# 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`. Both rates are **one-way, per-trade**: the combined `(cost_bps + slippage_bps)` is charged on the traded notional of *each* fill, buy and sell alike. A full round trip (enter then exit a position) is therefore charged twice — e.g. `5 + 5` bps becomes ~20 bps over a complete round trip, not 10. Quote any round-trip figure by doubling, or convert a round-trip budget to a per-trade rate by halving before passing it in. 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.