Skip to main content

Transaction costs

Until a cost model charges trading, every Sharpe and equity curve is gross — and gross results are the most reliable way to fool yourself, because the strategies that look best in-sample are very often the highest-turnover ones that costs destroy. tradeflow/costs/ prices the cost of changing a position so research metrics are net by default.

Research clock only

This models cost for simulation. The live path gets real fills from the broker; it imports no cost model.

The decomposition

For a trade of q shares at price p in a name with average daily volume ADV and quoted spread s:

cost($) = commission + (s/2)·|q|·p + impact_rate·|q|·p
  • Commission — a flat per-notional broker fee (default 1 bp).

  • Half-spread(s/2)·|q|·p, the immediate cost of crossing (default 5 bp spread).

  • Market impact — the square-root law (Almgren et al., the robust default):

    impact_rate = η · σ_daily · √(|q| / ADV)

    |q|/ADV is participation (the fraction of a day's volume you demand), σ_daily the name's daily volatility, η a coefficient (~0.3 default). Impact is concave per share (√) but convex in total cost (|q|·impact_rate ∝ |q|^{3/2}) — exactly the property that makes spreading a target across names cheaper than dumping it on one. A linear-participation fallback is offered for a convex-quadratic optimizer.

As an alpha haircut

Amortizing the round-trip cost over the holding period gives a cost rate per unit time, which is the honest input to portfolio construction:

α_net = α − round_trip_cost_rate / holding_period_years

A +4%/yr alpha with a 2% round-trip held for a month is not a +4% opportunity — it's deeply negative. This is what stops a high-IC, high-turnover signal from looking profitable when it isn't.

Integration

  • Backtest (engine/backtest.py): every fill is charged on both legs (entry and exit), using a trailing (as-of) ADV and volatility so a past trade's cost never depends on future volume. Metrics become net; total_cost and the gross final capital are reported alongside for the haircut attribution. Net is the default at the service/CLI layer; --gross disables the charge.
  • Portfolio construction (engineering): the cost is priced inside the optimizer's objective, not just as an ex-post drag — turnover_cost_rate (the linear cᵢ) and impact_coefficient (the conic kᵢ) are the same functions the objective's proximal solve uses, so the optimizer trades a name's alpha against that name's cost and a no-trade band emerges from it. This needed no new solver dependency: the cost term's exact proximal operator is closed-form, solved by the same 1-D budget bisection the cost-free projection already used.
  • Participation cap: a trade demanding more than ~10% of a day's volume is flagged — the seed of a capacity analysis (how much capital before cost eats the edge).

Borrow (short financing)

Holding a short accrues a borrow cost over time: borrow_rate · notional · holding_years (default 50 bp/yr, --borrow-bps). The backtest charges it per short position by how long it was held, so a short-heavy strategy isn't silently flattered. Long-side margin financing and leverage costs remain out of scope.

Where it runs

ParametricCostModel in tradeflow/costs/ — the single source of the √-impact coefficient, shared by the backtest and the cost-aware optimizer so the two price the same model. Surfaced as backtest flags (--gross, --commission-bps, --impact-eta), allocate --objective utility's --gross-objective/--holding-period/--capital, and folded into compute_risk-adjacent flows; the backtest report and the MCP run_backtest tool both return net metrics + total_cost.

Live runs use it too, to record rather than to charge. The venue prices the fill; the model says what the fill was expected to cost, and tradeflow execution-report shows the two side by side. That comparison is only meaningful if both came from the same parameters, so live takes the same cost flags and a saved config's cost block fills them the same way — a second, live-only cost formula would make the number look comparable while meaning something else.

The live estimate is commission + half-spread only, and says that it excludes impact: impact is a function of how much of a day's volume the order demands, and the trade clock has no ADV for a symbol at the moment it sizes one. Reporting the two components as though they were the whole cost would understate it silently. The modelled figure is never added to the venue's own fee — one is a prediction, the other an observation, and a paper account reports no fee at all.