# JoinQuant Cost Model Findings Generated: 2026-07-06 This report summarizes the JoinQuant trading-cost behavior observed from the browser-automated real-data backtests and compares it with the current internal simulator model. The JoinQuant cost formula below is inferred from rendered transaction tables and strategy logs for this account. Treat it as an observed platform default, not as a guaranteed external contract. ## Runs Used ### Longer Comparison Run - Portfolio: `jq_long_one_stock_long` - Window: `2024-01-11` to `2024-02-29` - Booksize: `1,000,000 CNY` - Instrument: `600000.XSHG` - Target: buy and hold `1,000` shares - JoinQuant rendered result: completed - Local total PnL: `546.22 CNY` - JoinQuant total PnL from positions tab: `575.00 CNY` - Difference: `28.78 CNY`, or `0.002878` percentage points on the book The first JoinQuant transaction was: | Date | Side | Shares | Price | Turnover | Fee | |---|---:|---:|---:|---:|---:| | `2024-01-11` | Buy | `1,000` | `6.57` | `6,570.00` | `5.00` | That trade hit a minimum fee. The local simulator charged `6.1415 CNY` because the smoke runner used a flat `10 bps` cash cost on adjusted-price turnover. ### Cost Probe Run - Portfolio: `jq_cost_probe_buy_sell` - Window: `2024-01-11` to `2024-01-12` - Booksize: `1,000,000 CNY` - Instrument: `600000.XSHG` - Targets: - `2024-01-11`: buy `100,000` shares - `2024-01-12`: sell to `0` shares Observed JoinQuant transactions: | Date | Side | Shares | Price | Turnover | Fee | Implied Fee | |---|---:|---:|---:|---:|---:|---:| | `2024-01-11` | Buy | `100,000` | `6.57` | `657,000.00` | `197.10` | `3.0000 bps` | | `2024-01-12` | Sell | `-100,000` | `6.51` | `651,000.00` | `846.30` | `13.0000 bps` | The transaction-table numbers match this formula exactly: ```text buy fee = max(5 CNY, turnover * 0.0003) sell fee = max(5 CNY, turnover * 0.0003) + turnover * 0.001 ``` In basis points: - Buy commission: `3 bps` - Sell commission: `3 bps` - Sell stamp tax: `10 bps` - Minimum commission: `5 CNY` No separate transfer fee was visible in this probe. If a separate transfer fee was present as an additional charge, the observed fees would not match the formula above exactly. It may still be folded into JoinQuant's displayed commission field, so this finding should be read as "not separately observable" rather than "impossible". No slippage was visible. The wrapper submitted open-time market orders with `run_daily(..., time="open")`, and JoinQuant filled them at the displayed open prices. ## Evidence From Strategy Logs The JoinQuant order log for the cost probe showed: ```text 2024-01-11 ... trade price: 6.57, amount:100000, commission: 197.1 2024-01-12 ... trade price: 6.51, amount:100000, commission: 846.3 ``` The normal transaction tab showed the same fee values. However, the generated wrapper's `JOINQUANT_FILL` log records had `trade_cost: 0.0`, even for those same fills. That means `get_trades()` did not expose the usable commission value through the field the wrapper currently reads. For reconciliation, use the transaction table or JoinQuant order logs for fee details. Do not rely on the wrapper's current `JOINQUANT_FILL.trade_cost`. ## Difference From The Internal Simulator The current internal simulator cost model is `SimpleProportionalCostModel` in `pipeline/portfolio/costs.py`: ```text trade_cost = abs(traded_shares * execution_price) * (cost_bps + slippage_bps) / 10000 ``` The smoke runner used: - `cost_bps = 5` - `slippage_bps = 5` - combined one-way cash cost: `10 bps` Important differences: - The internal simulator uses the same rate for buys and sells. - It has no minimum commission. - It has no sell-only stamp tax. - Slippage is modeled as an extra cash cost. - JoinQuant did not show slippage in the observed open-time fills. - The local smoke download used `adjust="qfq"`, while the JoinQuant wrapper set `set_option("use_real_price", True)`. That price-scale mismatch also affects PnL and cost comparisons. ## Practical Implications For a buy-only smoke test, JoinQuant may charge less than the local model when the trade is large enough for `3 bps` to apply, but it may charge more on small orders because of the `5 CNY` minimum. For any test with sells, JoinQuant's default sell fee is materially higher than the current local flat model because of the inferred `10 bps` stamp tax. The earlier 30-day buy-and-hold discrepancy was small because only one buy was executed. A rebalancing strategy with many sells will show a larger cost-model difference unless the local simulator is configured to match JoinQuant. ## Recommended Follow-Ups 1. Add a JoinQuant-style cost model to the internal simulator: ```text commission = max(min_commission, turnover * commission_bps / 10000) stamp_tax = turnover * sell_stamp_tax_bps / 10000 for sells only trade_cost = commission + stamp_tax ``` 2. Add a CLI option or preset for `portfolio simulate`, for example `--cost-model joinquant-stock`. 3. Update JoinQuant reconciliation to parse fee values from the transaction table or order logs when CSV exports are unavailable. 4. Run a second local-vs-JoinQuant comparison with: - raw or real-price local bars, not adjusted-price bars - JoinQuant-style costs - slippage disabled locally That test should isolate remaining differences to data alignment, price source, rounding, and JoinQuant internal execution behavior. ## Local Artifacts The temporary artifacts from the investigation are: - `/tmp/chinese-equity-quant-jq-long/comparison_report.md` - `/tmp/chinese-equity-quant-jq-long/parsed_joinquant/daily_pnl_compare_from_positions_tab.csv` - `/tmp/chinese-equity-quant-jq-cost-probe/jq_cost_analysis_report.md` - `/tmp/chinese-equity-quant-jq-cost-probe/jq_cost_analysis_summary.json` - `/tmp/chinese-equity-quant-jq-cost-probe/jq_cost_probe_transactions_parsed.csv` - `/tmp/chinese-equity-quant-jq-cost-probe/detail_tabs/transactions.txt`