flashalpha-fill-simulator

June 16, 2026 · View on GitHub

Realistic limit-order fill simulator for options credit/debit spreads.

Realistic fills are where most options backtests lie to you. This simulator is calibrated against the FlashAlpha Historical API (Alpha tier) so your backtested fills match what actually traded. Background: Fill model is the edge

Engine-agnostic. Data-source-agnostic. Zero runtime dependencies.

Most options-credit-spread backtests fill at mid (or at bid/ask without queueing). Both lie. This library models what actually happens when you post a limit at MM-edge against a 1-min option chain (or any tick stream): you sit on the book until someone else's order crosses your price, with stale-quote guards, deterministic tiebreaking, and a patient-then-cross exit. It's the substrate, not a strategy.

from datetime import date, datetime
from fillsim import simulate_fill, Spread, Leg, Config

# A vertical credit spread you've decided to post
spread = Spread(
    short=Leg(strike=440, bid=1.30, ask=1.30),
    long=Leg(strike=435, bid=0.86, ask=0.88),
    limit_credit=0.40,
    width=5.0,
    expiry=date(2026, 5, 15),
)

# The chain at the bar you're checking
chain_at_bar = {
    (date(2026, 5, 15), 440.0): (1.30, 1.30),
    (date(2026, 5, 15), 435.0): (0.86, 0.88),
}

bar = simulate_fill(
    bar_ts=datetime(2026, 4, 15, 10, 5),
    chain=chain_at_bar,
    candidates=[spread],
)
if bar.fill is not None:
    print(f"filled at {bar.fill.fill_price:.2f}, edge_captured={bar.fill.edge_captured:+.2f}")
else:
    print(f"no fill, near_misses={bar.near_misses}")

Why this exists

Pick any "this strategy returned 5,000% in backtest" credit-spread post and check the fill model. It's almost always implicit mid-fills. Returns drop dramatically the moment you model:

  • Post-and-wait limits (you don't fill until someone crosses your price)
  • Stale-quote crosses (a one-tick blip in bid doesn't mean you'd really get filled)
  • Random tiebreak when multiple candidates cross the same bar (any EV-aware tiebreak is a forward-looking oracle)
  • Exit limits that don't walk down (your stop-loss has to actually fill at a real ask)

This library models all of those. None of the magic numbers are tuned to make a specific strategy look good — they were calibrated against the edge_captured distribution of an early permissive run, then frozen.

Use it from anywhere

The headline API is a per-bar primitive — one stateless function that takes a bar's quotes and a list of open limit candidates, returns whether any fill happened on that bar:

def simulate_fill(
    bar_ts: datetime,
    chain: dict[tuple[date, float], tuple[float, float]],   # (expiry, strike) → (bid, ask)
    candidates: list[Spread],
    config: Config = Config(),
) -> BarResult: ...

This makes the simulator embed in:

  • QuantConnect — call it from your OnData handler
  • Backtrader — call it from next()
  • Live trading bots — call it on each market-data update
  • Custom backtesters — drop-in replacement for naive if combo_mid <= limit: fill logic
  • EOD strategies — works the same way; the simulator doesn't assume any specific bar resolution

For offline backtests with all the data up-front, loop-driving convenience wrappers are also shipped. right defaults to "PUT" and can be set to "CALL" for call-spread chains:

from fillsim import InMemoryChainProvider, simulate_fills

provider = InMemoryChainProvider(quotes=[...])
result = simulate_fills(posted_ts, candidates, provider, right="PUT")
if result.filled:
    print(f"filled in {result.bars_waited} bars; saw {result.near_misses} near-misses")

CSVChainProvider is available for tidy CSV exports with ts, expiry, strike, right, bid, and ask columns.

Install

pip install flashalpha-fill-simulator

Zero runtime dependencies. Python 3.10+.

What's modeled

featureconfigurable via
post-and-wait limit fillsConfig.fill_max_wait_bars
stale-quote guard at fillConfig.min_edge_floor
epsilon over limit required to count as a fillConfig.fill_epsilon
relative-spread quote-quality filterConfig.fill_max_rel_spread
same-bar tiebreak (deterministic, EV-blind)seeded by bar timestamp
multi-expiry candidate poolsper-candidate expiry field
patient exit (limit-then-market-out)Config.exit_mode = "patient"
simpler exit modes (mid / ask)Config.exit_mode = "mid" | "ask"
exit wait windowConfig.exit_max_wait_bars
at-expiry intrinsic settlementexpiry_settlement_pnl(...)

What's NOT modeled

These are intentional simplifications. See docs/SPEC.md §7 for the full list.

  • Queue position / size impact (works for retail/prop scale, breaks down at institutional size)
  • Commissions / fees (caller subtracts them)
  • Borrow/financing on cash collateral
  • Early assignment risk
  • Pin risk at expiry (linear interpolation only)
  • Hard exchange halts

Futures (CME equity-index)

The simulator is symbol-agnostic — feed it any option chain, including CME equity-index futures. FlashAlpha serves the full options-analytics stack for ES=F (E-mini S&P 500) and NQ=F (E-mini Nasdaq-100); options-on-futures are priced with Black-76 (forward-priced) using the correct CME contract multipliers. Everything that works for an equity works for futures: gamma exposure (GEX), DEX, VEX, CHEX, key levels, max pain, the IV surface, exposure summary, narrative, and live flow.

Pull a futures option chain to feed simulate_fill(...) straight from the FlashAlpha API:

# Option quotes for the E-mini S&P 500 future (note the %3D-encoded '=')
curl -H "X-Api-Key: $FLASHALPHA_API_KEY" \
  "https://lab.flashalpha.com/optionquote/ES%3DF"

Use the =F suffix — bare ES/NQ are equities, not futures. In raw REST paths URL-encode the = as %3D (e.g. GET /v1/exposure/gex/ES%3DF); SDK methods take the plain string "ES=F". Historical replay for futures is coming; live analytics are available now.

Documentation

  • docs/SPEC.md — full behavioural contract. Read this before relying on any number the simulator produces.
  • docs/examples/ — runnable examples, no broker/data feed required.
  • CHANGELOG.md — version history.

Tests

pip install -e ".[test]"
pytest

60+ tests, <2s wall time. CI enforces ruff, formatting, coverage, and type checks. The mandatory regression tests cover:

  1. EV-oracle: same-bar tiebreak never reverts to EV/rank ordering
  2. Stale-quote: invalid wide/crossed quotes cannot create fills
  3. Exit realism: patient exit does not walk the limit down
  4. Boundary: every threshold (fill_epsilon, min_edge_floor, exit_max_wait_bars) has a test asserting the correct boundary semantics

Real-data integration tests

Beyond the synthetic-chain unit tests, the suite includes 11 integration scenarios driven by real SPY put-chain data (tests/fixtures/real_data/spy_2024_06_03.json). The fixture is checked in so the suite runs offline, but it was pulled minute-by-minute from the FlashAlpha Historical Options API — the same data product the simulator was originally tuned against:

FA_API_KEY=... python scripts/fetch_real_data.py

If you want to run the simulator against your own quotes, historical.flashalpha.com covers SPY at 1-min resolution since 2018 plus 6,000+ US equities/ETFs, with greeks, IV surfaces, and dealer exposure pre-computed. Free for evaluation; paid plans for production. The fetch script is self-contained — adapt it to any chain provider you prefer.

Contributing

PRs welcome. See CONTRIBUTING.md. For behavioral changes, update docs/SPEC.md and add a synthetic-chain regression test.

Particularly wanted:

  • Additional ChainProvider adapters (Polygon, Tradier, IBKR, dxFeed, ...)
  • Property-based tests via Hypothesis
  • A quantconnect-fillsim companion package showing how to wire it into a QC algorithm

License

MIT. See LICENSE.

Provenance

Extracted from FlashAlpha's internal SPY VRP-harvest backtester. The simulator was built specifically because every off-the-shelf options backtest framework we evaluated assumed mid-fills, and our strategy returns flipped from "+5,400%" to "ambiguous" the moment we modeled execution honestly. Open-sourcing the substrate so others don't have to relearn that lesson the hard way.

What the paid tiers unlock

The free tier covers single-expiry GEX on equities, key levels, the BSM Greeks/IV calculator and stock quotes. Paid tiers add:

  • DEX, VEX (vanna) and CHEX (charm) exposure, plus max pain — from the Basic tier ($79/mo), with ETF and index symbols.
  • Full-chain GEX, 0DTE and flow analytics — from the Growth tier ($299/mo).
  • Point-in-time replay since 2018, SVI vol surfaces, VRP analytics, higher-order Greeks, uncached and unlimited — the Alpha tier ($1,499/mo). FlashAlpha is one of the only public APIs publishing aggregate vanna and charm exposure across the full universe, with no look-ahead and no training-serving skew.

Built for quants, prop desks, and vol funds. See the full picture and get a key: flashalpha.com/for-quant-teams