Transaction cost modeling — the edge that costs eat
▸ Pretest — guess, even if you don't know
A strategy earns 10 percent per year gross (before costs) and does 250 round trips per year (a round trip is a buy plus its later sell). At 5 bps of cost per round trip (bps = basis points; 1 bp is 0.01 percent), roughly what is its net annual return?
The cost stack, with numbers
Recall D4-01's list, now as a model you can compute with. First the vocabulary, then the numbers. Spread — the gap between the best bid (highest price anyone will pay) and the best ask (lowest price anyone will sell for); crossing it costs you money. Slippage — the difference between the price you expected and the price your order actually filled at. Borrow fees — the rent you pay to borrow shares for a short position. Market impact — your own order pushing the price against you as it fills. For a retail-scale trader in liquid US large-caps:
| Component | Typical size | Scales with |
|---|---|---|
| Spread | 1–10 bps quoted; you pay the half-spread per side | every trade |
| Slippage | 5–20 bps for medium orders | order size, urgency |
| Commission | ~0 at most retail brokers today | per trade |
| Borrow fees | 30–300 bps annualized | short positions held |
| Market impact | grows roughly like the square root of order size / ADV | size relative to liquidity |
The half-spread convention: suppose the quote is bid 100.00 / ask 100.04 — a 4 bps spread, since 0.04 on a price of 100 is four hundredths of a percent. A marketable buy (an order that crosses the spread to fill immediately) pays about 2 bps above mid — the midpoint between bid and ask. The later sell collects about 2 bps below mid. So you pay half the spread per side, the full spread per round trip. Market impact is the one that grows with you. ADV is average daily volume — how many shares typically trade in a day. Doubling your order size raises per-share impact by roughly the square root of two, about 1.4× (Almgren et al. 2005). That is why backtests of small accounts do not scale to large ones.
Cost-adjusted returns
From D4-02, turnover — the amount of trading you do — at time is . Read that as: "tau sub t" (τ is the Greek letter tau) equals the absolute value of today's position minus yesterday's position. Let be the one-way cost per unit of turnover, written as a decimal — for example, 5 bps = 0.0005. Then:
In words: the net return at time t equals the gross return minus c times that day's turnover. The superscripts "net" and "gross" are just labels — net means after costs, gross means before. Concrete numbers: you go from flat to fully long (turnover 1) on a day the strategy grosses 0.30%; at the net is , or 0.25%.
That's the whole model. Its honesty lives entirely in the value of — which is why must be stated, justified, and stress-tested, never defaulted to zero.
The sensitivity table
Never report one net number. Report net performance as a function of assumed cost. For a strategy earning 10% gross with 250 round trips per year:
| Round-trip cost | Annual cost drag | Net return |
|---|---|---|
| 2 bps | 250 × 2 bps = 5.0% | +5.0% |
| 5 bps | 250 × 5 bps = 12.5% | −2.5% |
| 10 bps | 250 × 10 bps = 25.0% | −15.0% |
The same backtest is a good strategy, a coin flip, or a disaster — depending on one assumption. A strategy whose sign flips inside the plausible cost range is not a strategy. It is a bet on your cost model. Contrast a 10%-gross strategy doing only 5 round trips per year: even at 10 bps, it loses just 0.5% to costs.
Document or it didn't happen
Any backtest claim must state three things: the assumed cost per unit of turnover, where that number came from (measured spreads? broker fill data? a guess?), and the sensitivity table. This is the "realistic transaction costs" checkbox from the D4-01 checklist made concrete. An undocumented cost assumption is indistinguishable from an optimistic one.
Try it
Implement net_returns(gross_returns, positions, cost_bps): turnover_t = abs(positions_t - positions_t-minus-1) with the position before the first day equal to 0, and net_t = gross_t - turnover_t * cost_bps / 10000. cost_bps is the one-way cost per unit of turnover, in basis points. Return a numpy array.
⧉ Review cardWhat is the half-spread convention for transaction costs?
⧉ Review cardHow do you compute cost-adjusted returns from a position series?
⧉ Review cardHow does market impact scale with order size?
⧉ Review cardWhy report a cost sensitivity table instead of one net return?
Explain it
Your generative activity: explain out loud, to an imaginary friend who just showed you a backtest with Sharpe 2 and daily trading, why their result might be an artifact of a zero-cost assumption. Use the 10-percent-gross, 250-round-trip example, and tell them exactly what two numbers you'd ask for (turnover and assumed cost per round trip).
Predict before the next lesson
D4-04 covers walk-forward testing and out-of-sample discipline. Predict:
- If you tune a parameter to maximize Sharpe on 2010–2020 data and then report the Sharpe on 2010–2020, what's wrong?
- How much of an in-sample Sharpe do you think typically survives out of sample: 100%, 70%, or less than half?
◈ Calibration check
Could you cost-adjust a backtest from its position series and defend your cost assumption with a sensitivity table?
1 = guessing · 5 = could teach it
⏻ End of lesson
Mark it read to book its 4 review cards into your deck.
Sources & further reading
- bookHarris (2003), Trading and Exchanges — §14
- bookChan (2009), Quantitative Trading — §3, 5
- paperAlmgren, Thum, Hauptmann, Li (2005), Direct Estimation of Equity Market Impact link