Skip to content

FlashAlpha-lab/flashalpha-fill-simulator

Repository files navigation

flashalpha-fill-simulator

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

feature configurable via
post-and-wait limit fills Config.fill_max_wait_bars
stale-quote guard at fill Config.min_edge_floor
epsilon over limit required to count as a fill Config.fill_epsilon
relative-spread quote-quality filter Config.fill_max_rel_spread
same-bar tiebreak (deterministic, EV-blind) seeded by bar timestamp
multi-expiry candidate pools per-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 window Config.exit_max_wait_bars
at-expiry intrinsic settlement expiry_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

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 Alpha tier unlocks

Free and entry tiers cover live exposure analytics. The Alpha tier ($1,499/mo) adds the data you cannot get anywhere else:

  • Aggregate vanna and charm exposure. FlashAlpha is the only public source for these dealer-positioning aggregates.
  • Point-in-time replay since 2018. Backtest and trade the same code, with no look-ahead and no training-serving skew.
  • SVI vol surfaces, VRP analytics, higher-order Greeks, uncached and unlimited.

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

About

Realistic limit-order fill simulator for options credit/debit spreads. Engine-agnostic, data-source-agnostic.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages