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.