3.6 KiB
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:
- A constructed portfolio row provides the target book for an execution date.
In the current file layout, a target dated
tis executed at the next available market dated = next(t). - Trades are executed at
open[d]. - Realized positions are held during the trading day.
- Daily PnL is marked from
open[d]toclose[d]on the newly realized book, plus any overnight gap from the previous realized holdings. - 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:
trade_value_i = abs(traded_shares_i * execution_price_i)
trade_cost_i = trade_value_i * (cost_bps + slippage_bps) / 10000
where:
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:
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:
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:
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:
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.