Modelling Transaction Costs in a Backtest

A gross-return backtest describes a strategy nobody can run. Every trade pays a fee, crosses a spread, and fills at a price other than the one on your chart, and those frictions scale with how often you trade. Modelling them is not a refinement applied at the end — it changes which strategies look good, because cost is roughly proportional to turnover while edge is not.

Educational material, not trading advice. Algorithmic crypto trading is high-risk and most retail algo traders lose money.

The four components

Costs decompose into four distinct things, and conflating them produces a model that is wrong in a direction you can’t predict.

Fees are charged by the venue per execution, typically as a percentage of notional, and usually differ depending on whether your order provided liquidity or took it. They are the most predictable component and the easiest to model.

Spread is the gap between the best bid and the best ask. Taking liquidity means buying at the ask and selling at the bid, so a round trip costs the spread. Because a chart’s “price” is usually the last trade or a mid, a backtest that buys and sells at that single price has omitted the spread entirely.

Slippage is the difference between the price you expected and the price you got, for any reason — the market moved between decision and execution, the queue moved, the book was thinner than modelled.

Holding costs are recurring rather than per-trade: financing on leveraged or short exposure, and any periodic payment attached to a derivative position. A strategy that holds for weeks may pay more in holding costs than in trade costs, and an engine with nowhere to represent them overstates returns for the entire holding period.

Modelling them

A serviceable model applies a percentage per trade plus a spread charge on every crossing, both scaled by traded notional. In its simplest vectorized form:

turnover = position.diff().abs()          # fraction of capital traded per bar
cost     = turnover * (fee_rate + spread_cost + slip_rate)
net      = gross_returns - cost

Three details that matter more than the constants:

Charge the change, not the position. Rebalancing from 0.9 to 1.0 of capital trades 0.1, and paying a fee on 1.0 overstates cost as badly as paying nothing understates it. position.diff().abs() is the quantity that costs money.

Model slippage as proportional to volatility, not as a constant. A fixed number of basis points is wrong in both directions — too punitive in calm conditions, far too generous in violent ones, which is exactly when many strategies trade most. Scaling the slippage assumption by a rolling volatility estimate is a small change that removes a large systematic error.

Scale with size where size matters. For small orders in liquid pairs, assuming no size effect is tolerable. Above that, a larger order fills across worse levels, and a cost model insensitive to size will make a strategy look scalable when it isn’t. A square-root-of-size relationship is the conventional shape for this effect; the exact form matters less than having some size dependence rather than none.

Turnover is the whole story

Cost is proportional to turnover, so turnover determines how much edge a strategy needs to survive. This is the single most useful reframing available, and it’s worth doing arithmetic on rather than intuiting.

The comparison to make: per-trade edge against per-trade cost. A strategy that trades many times per day needs its average per-trade gross edge to exceed the round-trip cost by a comfortable margin, and higher-frequency ideas fail this test constantly — the edge is real but smaller than the fees, and no amount of parameter tuning fixes a subtraction. A strategy that trades monthly can survive on a much smaller per-trade edge because it pays the cost far less often.

The practical consequences:

  • Report turnover next to every performance metric. A Sharpe ratio without a turnover figure is uninterpretable, because you cannot tell how sensitive it is to the cost assumption.
  • Compute the break-even cost. Solve for the cost rate at which the strategy’s net return reaches zero. That single number tells you how much cost the edge can absorb, and it is far more informative than a result at one assumed rate.
  • Optimize on net returns. Tune parameters against cost-adjusted performance or the optimizer will select high-turnover configurations whose gross edge is illusory — see how walk-forward validation works.

Which strategies costs destroy

Cost sensitivity is a property of a strategy family, not an accident of implementation. Short-horizon mean-reversion trades frequently by design and is therefore the most cost-exposed family there is; the effect it captures is often genuinely smaller than the round trip. Trend-following trades rarely and is far more tolerant of frictions, which is one of the underappreciated reasons the family survives — see momentum vs. mean-reversion.

Anything with tight stops trades often by construction, since every stop-out is a round trip. And any strategy concentrated in illiquid pairs pays a spread that can dwarf the modelled fee, because in thin markets the spread is the dominant cost by a wide margin.

Pessimism as a test

Re-run every candidate at deliberately punitive costs — several times your best estimate — and see what survives. This is the cheapest robustness test in the toolkit and one of the most decisive. A strategy whose edge persists under pessimistic assumptions is more likely real. A strategy that is only profitable at zero cost is not profitable.

Related habits worth adopting:

  • Never report a gross figure without the net one beside it. Gross performance is a diagnostic, not a result.
  • Sweep the cost assumption and plot performance against it. A curve that collapses just past your assumed rate means the conclusion rests on a constant you guessed.
  • Assume you take liquidity unless the strategy genuinely rests passive orders and you have modelled the risk of those not filling — which is not free, just a different risk you have not priced.

Calibrating against reality

Modelled costs are guesses until you measure them. The measurement is per-order: expected price at decision time against realized fill price, logged for every trade. The distribution of that difference is your empirical slippage, and it will usually be worse than your model in volatile conditions and better in calm ones.

That feedback loop is the main argument for trading the smallest real size before any meaningful size — see what paper trading catches, and what it misses, where the key point is that a simulated fill reuses your assumption rather than testing it. Once you have real fills, the cost model stops being a parameter you chose and becomes a measurement, and every backtest run afterwards is more honest than every one before.

The framing to keep: cost modelling is not a haircut applied to a result. It is part of the hypothesis. “This relationship exists” and “this relationship is larger than the cost of exploiting it” are different claims, and only the second one matters — a point that runs through what backtesting is, and why naive backtests lie.