If you searched for options backtest fill model or options transaction costs, here is the uncomfortable version first: for most retail-visible options strategies, the fill model is not a refinement of the backtest - it IS the backtest. Get it wrong by half a spread per leg and a losing strategy prints a Sharpe above 1.
-5% to +62%
Same SPY VRP signal, 8 years, varying only the fill model
10x
ATM SPY spread widening, calm session vs 2024-08-05
-0.08 to +1.05
QQQ Sharpe range on the fill assumption alone
The fill ladder
Write s = ask - bid and mid = (bid + ask) / 2. Every fill model in common use is a point on one ladder. For a BUY (mirror for sells):
| Rung | Fill price | What it assumes | When it is defensible |
| Mid | mid | Someone always meets you in the middle | Benchmarking the zero-cost upper bound. Never for reported performance. |
| Fractional | mid + α · s/2, α in [0, 1] | You cross a fixed fraction of the half-spread | The workhorse. Calibrate α from real spreads (below), report sensitivity across α = 0.3 to 0.7. |
| Touch | ask (α = 1) | You always pay the full spread | Lower bound; realistic for marketable orders in thin books and stressed tape. |
| Modeled MM fill | conditioned on size vs top-of-book, quote stability, time-of-day | A market maker decides whether to improve | When you have the book context to support it - see the 18.3M-row spread matrix study for our implementation, and the open-source flashalpha-fill-simulator for an engine-agnostic version. |
The evidence: the edge lives in the fill gap
Two published studies on this site quantify it, both with downloadable artifacts:
- VRP backtest: the fill model is the edge - identical VRP signal and trade list, eight years (2018-2026), fill model varied rung by rung. SPY total return spans -5% to +62%; across underlyings the spread is -5% to +71%; QQQ Sharpe runs from -0.08 to +1.05. Nothing about the signal changed.
- Every SPY put credit spread, 2017-2026 - 18.3 million spread permutations priced with SVI + Black-Scholes and joined to realized expiry outcomes. Average theoretical EV is minus $0.48 per spread; average realized P&L is plus $0.58. The gap between model and outcome is exactly the territory where fill assumptions decide the sign of your backtest.
Calibrating α from point-in-time spreads
The wrong way to pick α is by feel. The right way is to measure the spreads your strategy would actually have crossed, at the timestamps it would have traded. The Historical API replays the full option chain - bid, ask, sizes, greeks - at any minute since 2017, so the measurement is two calls:
import requests
BASE = "https://historical.flashalpha.com"
H = {"X-Api-Key": "YOUR_ALPHA_KEY"}
def atm_spread(at, expiry):
spot = requests.get(f"{BASE}/v1/stockquote/SPY", params={"at": at}, headers=H).json()["mid"]
chain = requests.get(f"{BASE}/v1/optionquote/SPY", params={"at": at}, headers=H).json()
calls = [q for q in chain if q["type"] == "C" and q["expiry"] == expiry and q["bid"] and q["ask"]]
q = min(calls, key=lambda q: abs(q["strike"] - spot))
return q["strike"], q["bid"], q["ask"]
Run on a calm session and on the most stressed session in the window, same expiry held fixed (2024-08-16: 32 DTE on the calm day, 11 DTE on the stressed day - so part of the widening is tenor; the 0DTE row shows the stress effect at matched-day tenor). Production responses, 10:30 ET:
| Session | Contract | Bid | Ask | Spread | % of mid |
| 2024-07-15 (calm) | SPY 2024-08-16 563 C | 8.70 | 8.72 | $0.02 | 0.23% |
| 2024-08-05 (carry unwind) | SPY 2024-08-16 515 C | 12.23 | 12.52 | $0.29 | 2.34% |
| 2024-08-05, 0DTE | SPY 2024-08-05 515 C | 3.38 | 3.45 | $0.07 | 2.05% |
That is a 10x widening in the single most liquid options market in the world - on exactly the day a short-vol strategy is stopped out or rolling. A constant-α model calibrated on calm tape understates stress costs by an order of magnitude, which is why the honest procedure is: measure spreads at YOUR trade timestamps via replay, use the distribution, and stress-test with the worst decile. Point-in-time chain replay is an Alpha tier capability; the two calls above are the entire integration.
Sizing reality: read the book before you assume the fill
Spread percentage is half the story; depth is the other half. The live /v1/liquidity/{symbol} endpoint scores it per expiry - ATM spread, OI-weighted spread across the chain, ATM open interest, and a 0-100 execution score:
{
"symbol": "SPY",
"chain_execution_score": 40,
"best_expiry": "2026-08-07",
"thin_expiry_count": 7,
"expiries": [
{"expiration": "2026-07-24", "dte": 0, "atm_spread_pct": 0.735,
"weighted_spread_pct": 21.697, "atm_oi": 5083,
"execution_score": 32, "label": "wide"},
{"expiration": "2026-07-27", "dte": 3, "atm_spread_pct": 0.775,
"weighted_spread_pct": 12.227, "atm_oi": 1097,
"execution_score": 15, "label": "illiquid"}
]
}
Note the gap between atm_spread_pct (under 1%) and weighted_spread_pct (12-22%) on the same expiries: ATM looks tradeable while the wings are a desert. A backtest that trades 10-delta wings at mid is fiction, and this endpoint is the cheapest way to catch that before the backtest does.
Recommended defaults per strategy family
| Strategy family | Default fill | Notes |
| ATM index singles, 7-45 DTE, small size | α = 0.35-0.50 | Report α = 0.3 / 0.5 / 0.7 sensitivity; SPY/QQQ ATM books usually improve on touch. |
| 0DTE and far-OTM wings | α = 0.70-1.00 | Percentage spreads are huge and quotes are unstable; touch is not paranoid here. |
| Single-stock weeklies | α = 0.50-0.70 + size cap vs top-of-book | Depth binds before spread does; do not model fills bigger than displayed size without an MM model. |
| Multi-leg spreads | per-leg α with a 0.6-0.8 package discount | Complex-order books net legs, but assuming full netting on every fill is mid-fill fantasy in two coats. |
| Event / stress trades | replayed spreads at the actual timestamps | This is the case where averages lie the most - measure, don't assume. |
Checklist before you trust a backtest
- Is the reported result mid-fill? Then it is an upper bound, not a result.
- Is α constant across regimes? Re-run with stress-decile spreads from replay.
- Does modeled size exceed displayed depth? Cap it or model the improvement explicitly.
- Are commissions and fees per contract included? At high trade counts they rival the spread cost.
- Does the strategy still clear its hurdle at α = 0.7? If not, you have a fill-model artifact, not an edge.
The fill model deserves the same rigor as the signal, because in most published options backtests it carries more of the P&L than the signal does. Use the ladder, calibrate α from point-in-time spreads instead of vibes, read the book depth with the /v1/liquidity endpoint, and publish the sensitivity table. Point-in-time spread replay to calibrate it is an Alpha-tier capability. For the deeper treatments with full artifacts, see the fill-model VRP study, the credit-spread matrix, and the complete quant backtesting guide.