Transaction Cost Models for Options Backtests: The Fill Ladder | FlashAlpha

Transaction Cost Models for Options Backtests: The Fill Ladder

The fill model is the largest single free parameter in any options backtest - our published 8-year studies show the same signal swinging from -5% to +62% total return on the fill assumption alone. The four-rung fill ladder, how to calibrate alpha from replayed point-in-time spreads, and a recommended default per strategy family.

T
Tomasz Dobrowolski Quant Engineer
Jul 24, 2026
8 min read
Backtesting TransactionCosts FillModels Execution Methodology Quant

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):

RungFill priceWhat it assumesWhen it is defensible
MidmidSomeone always meets you in the middleBenchmarking the zero-cost upper bound. Never for reported performance.
Fractionalmid + α · s/2, α in [0, 1]You cross a fixed fraction of the half-spreadThe workhorse. Calibrate α from real spreads (below), report sensitivity across α = 0.3 to 0.7.
Touchask (α = 1)You always pay the full spreadLower bound; realistic for marketable orders in thin books and stressed tape.
Modeled MM fillconditioned on size vs top-of-book, quote stability, time-of-dayA market maker decides whether to improveWhen 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:

SessionContractBidAskSpread% of mid
2024-07-15 (calm)SPY 2024-08-16 563 C8.708.72$0.020.23%
2024-08-05 (carry unwind)SPY 2024-08-16 515 C12.2312.52$0.292.34%
2024-08-05, 0DTESPY 2024-08-05 515 C3.383.45$0.072.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 familyDefault fillNotes
ATM index singles, 7-45 DTE, small sizeα = 0.35-0.50Report α = 0.3 / 0.5 / 0.7 sensitivity; SPY/QQQ ATM books usually improve on touch.
0DTE and far-OTM wingsα = 0.70-1.00Percentage spreads are huge and quotes are unstable; touch is not paranoid here.
Single-stock weekliesα = 0.50-0.70 + size cap vs top-of-bookDepth binds before spread does; do not model fills bigger than displayed size without an MM model.
Multi-leg spreadsper-leg α with a 0.6-0.8 package discountComplex-order books net legs, but assuming full netting on every fill is mid-fill fantasy in two coats.
Event / stress tradesreplayed spreads at the actual timestampsThis 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.

Live Market Pulse

Get fast visibility into market shifts with full-chain analytics over low-latency REST and MCP polling.

Intelligent Screening

Screen millions of option pairs per second using your custom EV rules, filters, and setups.

Export-Ready

Export structured signals to your own execution stack or broker integration - FlashAlpha delivers the analytics, you keep control of order routing.

Join the Community

Discord

Engage in real time conversations with us!

Twitter / X

Follow us for real-time updates and insights!

GitHub

Explore our open-source SDK, examples, and analytics resources!