Skip to main content

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

Project description

flashalpha-fill-simulator

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

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flashalpha_fill_simulator-0.2.1.tar.gz (48.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flashalpha_fill_simulator-0.2.1-py3-none-any.whl (22.2 kB view details)

Uploaded Python 3

File details

Details for the file flashalpha_fill_simulator-0.2.1.tar.gz.

File metadata

File hashes

Hashes for flashalpha_fill_simulator-0.2.1.tar.gz
Algorithm Hash digest
SHA256 779fd082daeabe2e04b1c70b1190ef1105037e967fb2e3f0d62b6d32736a1bf5
MD5 9c257cf77f299c87588d17be128b5fd6
BLAKE2b-256 388f0e95fb4c6488966e4b2cd7c9d946c7028b9b743feb6b160c14502c5ea89f

See more details on using hashes here.

File details

Details for the file flashalpha_fill_simulator-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for flashalpha_fill_simulator-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 950177cc32faa3140a96166a2d4693d7d72c4403a44a109b726e3e089b3d6e11
MD5 8c466cec3ba2d0979cacbc7ce443678d
BLAKE2b-256 76edcf538a34a5d3a916b0964bc0a0b9459dddf56769fbf2b48dd082a8d6a426

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page