Files
chinese-equity-quant/docs/portfolio_trading_cost_model.md
T
2026-06-10 15:41:38 +08:00

134 lines
3.6 KiB
Markdown

# Portfolio Trading Cost Model
This document describes the trading cost model used by `portfolio simulate`.
The current implementation is a simplified open-execution proportional cost
model. It is intentionally small, explicit, and easy to audit.
## Open-Execution Timeline
The simulator runs once per trading day:
1. A constructed portfolio row provides the target book for an execution date.
In the current file layout, a target dated `t` is executed at the next
available market date `d = next(t)`.
2. Trades are executed at `open[d]`.
3. Realized positions are held during the trading day.
4. Daily PnL is marked from `open[d]` to `close[d]` on the newly realized book,
plus any overnight gap from the previous realized holdings.
5. Trading cost is charged only on actually realized `traded_shares`, after all
constraints have clipped the desired trade.
This means a fully blocked order has `traded_shares = 0` and therefore zero
trading cost.
## Current Formula
For each symbol:
```text
trade_value_i = abs(traded_shares_i * execution_price_i)
trade_cost_i = trade_value_i * (cost_bps + slippage_bps) / 10000
```
where:
```text
execution_price_i = open_price_i
```
`cost_bps` is the proportional explicit trading-cost rate in basis points.
`slippage_bps` is modeled as an additional cash cost in basis points. The two
rates are added linearly. The CLI options `--cost-bps` and `--slippage-bps`
both default to `0.0`.
Example:
```text
traded_shares = 1000
execution_price = 20 yuan
cost_bps = 10
slippage_bps = 5
abs(1000 * 20) * 15 / 10000 = 30 yuan
```
## Slippage Convention
Slippage is not applied by changing the execution price. It is charged only as
a cash cost through `trade_cost`.
Do not double-count slippage by doing both:
```text
execution_price = open * (1 +/- slippage_bps / 10000)
trade_cost += trade_value * slippage_bps / 10000
```
The simulator should execute at the open price and subtract the slippage cash
cost from PnL.
## Relationship To The Simulator
`ReferenceSimulator.fill()` clips desired trades through constraints first, then
passes the actual `traded_shares` to the cost model. The per-name result is
stored in the fills parquet as `trade_cost`.
`ReferenceSimulator.run()` sums per-name `trade_cost` into the daily PnL row's
`cost` column and subtracts that total from daily PnL:
```text
pnl = overnight + intraday - cost_total
```
## What This Model Does Not Cover
The current model intentionally does not model:
- Minimum commissions.
- Buy/sell asymmetric fees.
- Sell-side stamp duty.
- Exchange handling fees.
- Regulatory fees.
- Transfer fees.
- Date-aware fee schedule changes.
- Nonlinear price impact.
- Auction liquidity / queue effects.
- Partial fills caused by open auction depth.
These omissions are deliberate. The current model is the default reference
model, not a detailed brokerage fee simulator.
## Future Extension
The simulator is structured around a cost model abstraction:
```python
class CostModel:
def compute(
self,
traded_shares,
execution_price,
side,
date,
metadata,
):
...
```
The current implementation is `SimpleProportionalCostModel`.
A future `AShareDetailedCostModel` can add:
- Commission, optionally subject to minimum commission.
- Sell-side stamp duty.
- Transfer fee.
- Exchange handling fee.
- Regulatory fee.
- Date-aware fee rates.
- Separate buy-side and sell-side rates.
- Optional nonlinear slippage / market-impact model.
Any future model must preserve the same high-level simulator contract: costs
are computed from realized trades after constraints, and slippage must not be
counted both through execution-price adjustment and cash cost.