diff --git a/docs/joinquant_cost_model_findings.md b/docs/joinquant_cost_model_findings.md new file mode 100644 index 0000000..301a1b2 --- /dev/null +++ b/docs/joinquant_cost_model_findings.md @@ -0,0 +1,167 @@ +# 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` +