168 lines
5.8 KiB
Markdown
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`
|
|
|