Lab notebook · chapter 2 of 7 · page 2 of 3

Backtesting and costs continued

Measured, not assumed

python spreads.py --port 4002 --rounds 3

Samples real quoted bid/ask across the universe and writes data/spreads.json. Reads only. Falls back to the delayed tier, which does expose two-sided quotes.

Measured 2026-09-08, 112 names, 3 rounds, delayed feed:

half-spread
universe median 1.91 bps
p25 / p75 1.12 / 3.12
min (SPY) / max (DE) 0.20 / 9.16
trade-weighted for momentum 2.40 bps

The 1.5bps default was too low. Momentum shows no tilt toward wide or tight names — its 20 most-traded average 2.04bps against 2.12 for its least-traded — so the trade-weighted figure is close to the simple mean of what it touches.

Three limits on what this proves. These are quoted spreads, an upper bound on the effective cost since real fills often land inside the quote. They are today's spreads applied to a nine-year backtest, and spreads have generally narrowed, so historical cost was probably higher. And the feed is delayed by 15-20 minutes, which perturbs a snapshot more than a level.

Cost sensitivity without re-running

Re-running a backtest at a different spread does not measure the spread. One different fill changes the position, which changes every later proposal. On real data, raising the half-spread from 1.91 to 2.40bps increased the measured excess — path divergence, not a cost effect — so the rows of such a sweep are not comparable and the sweep answers nothing.

labtrader/costs.py holds the order sequence fixed and charges it differently. sensitivity() is anchored so the reference assumption reproduces the realised return exactly (verified: +367.94% against a realised +367.90%), and only fills with Fill.crossed pay — 70% of momentum's notional crosses, 30% rests.

half-spread drag/yr return vs equal-weight
0.00 0.00% +381.6% +57.2%
1.91 (universe median) 0.40% +364.3% +40.0%
2.40 (measured) 0.50% +359.9% +35.6%
6.00 1.25% +329.1% +4.8%
8.00 1.66% +312.8% −11.5%

breakeven_bps() gives the number directly: momentum stops beating equal-weight at a 6.59bps half-spread, and SPY at 14.63bps, against a measured 2.40. So the edge survives roughly a 2.7x error in the spread estimate.

That is a real margin, and it does not make the edge significant — the information ratio is still 0.14 (t=0.42 over 9.2 years). Cost is no longer the reason to doubt it; sample size is.

On synthetic bars, through-vs-touch moved only +0.4% of ~3,000 passive orders — much less than the framing "paper trading flatters passive strategies" would suggest. It should bite harder on real data, where limits cluster at round numbers that get touched exactly and not traded through, but that is unmeasured. Treat the conservative default as cheap insurance, not a proven correction.

--optimistic relaxes all three. Note that comparing a run against its own optimistic twin is confounded: once one fill differs, positions differ and all subsequent proposals diverge. To measure the fill model itself, hold the order flow fixed and re-simulate it under both.