Files
chinese-equity-quant/docs/joinquant_cost_model_findings.md
T
2026-07-06 15:35:41 +08:00

5.8 KiB

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:

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:

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:

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.

  1. Add a JoinQuant-style cost model to the internal simulator:
commission = max(min_commission, turnover * commission_bps / 10000)
stamp_tax  = turnover * sell_stamp_tax_bps / 10000 for sells only
trade_cost = commission + stamp_tax
  1. Add a CLI option or preset for portfolio simulate, for example --cost-model joinquant-stock.

  2. Update JoinQuant reconciliation to parse fee values from the transaction table or order logs when CSV exports are unavailable.

  3. 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