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

168 lines
5.8 KiB
Markdown

# 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`