Expand description
IronCondor is a high-performance backtester for options-trading strategies with order-book-level fill simulation, written in Rust.
It is the deterministic replay engine around an upstream options stack,
not a re-implementation of it. Pricing, Greeks, multi-leg strategies, and
exit policies come from
optionstratlib; order matching
comes from
option-chain-orderbook
and orderbook-rs;
synthetic option chains come from
OptionChain-Simulator.
This crate contributes the replay loop, the dual fill models, P&L
attribution by Greek, the result bundle, and the Python bindings.
§Why order-book-level fills
Most backtesters fill option orders at mid or bid/ask with a fixed slippage assumption. IronCondor adds a realistic mode that routes every order through a real options matching engine — queue position, partial fills, multi-level book walks, and a resting GTC lifecycle — so fill risk is an emergent property of the book rather than a guess. A fast naive mode (mid/spread plus configurable slippage) stays available for quick iteration, and both modes emit the identical fill-report shape, so downstream analytics is mode-agnostic.
§Key properties
- Deterministic replay. For a fixed
(seed, config, data, crate version, Rust toolchain, lockfile)the four Parquet tables are byte-identical and the manifest is identical minus its one wall-clock provenance field. Across environments the guarantee is logical equivalence under a documented normalization (canonical row ordering, canonical JSON). No wall clock, no unseeded RNG, and no map-iteration order reaches a result. - Money as integer cents. Every execution and result boundary carries
money as integer cents; order-book prices are
u128ticks.f64is confined to the upstream pricing/Greeks kernel and the documented derived-analytics columns. - Dual execution modes.
naive(mid/spread plus slippage) andrealistic(a real order book: queue position, partial fills, multi-level walks, resting GTC orders), selected once from config with no per-step dynamic dispatch. - P&L attribution by Greek. Each step’s mark-to-market change is
decomposed into theta, delta, vega, and spread capture minus fees, closed
by an exact integer-cents residual: for every step,
theta + delta + vega + spread − fees + residualequals the equity change by construction. A large residual is an advisory model-quality signal, never a run failure. - A frozen result bundle. Every run publishes an
ironcondor.bundle.v1directory (a manifest plus four Parquet tables) consumed by ChainView. - Hardened against untrusted input. The engine parses untrusted files
and runs inside CI, so every external input pairs a validation with a
resource ceiling and a typed error — no panic, hang, or OOM on malformed
input. Parsers are fuzzed, the crate is
#![forbid(unsafe_code)], errors are the typedBacktestError, and none cross the Python boundary as a panic. - Performance as an acceptance criterion. The replay loop holds zero
steady-state allocation, and the hot paths (loop, fill models, conversion,
bundle writer, PyO3 boundary) carry CI-gated budgets measured with
criterionandhdrhistogram. As a recorded baseline (seeBENCH.md), a fullrun_backtestover a 2048-step, four-leg iron-condor Parquet chain runs at a p50 of about 2.35 µs/step (about 425k steps/sec/core) in naive mode on an Apple M4 Max — a measurement, not a guarantee.
§Feature flags
| Feature | Default | What it adds |
|---|---|---|
| (none) | yes | Replay engine, naive execution, Parquet/CSV historical feeds, and the result bundle. |
orderbook | Realistic, liquidity-aware fills routed through option-chain-orderbook. | |
simulator | Synthetic chain sessions from OptionChain-Simulator over HTTP. | |
python | PyO3 bindings, built as a wheel with maturin (PyPI publication planned). |
§Quick start (Rust)
Drive one backtest end to end — a Parquet chain in, an equity curve out:
use ironcondor::{
BacktestConfig, DataSourceSpec, ExecutionMode, FeeSchedule, IronCondorSpec,
LiquidityProfile, PriceCents, Quantity, ResourceLimits, SlippageModel,
StrategySpec, Underlying, run_backtest,
};
use optionstratlib::ExpirationDate;
use optionstratlib::simulation::ExitPolicy;
use rust_decimal::Decimal;
fn main() -> Result<(), ironcondor::BacktestError> {
let config = BacktestConfig {
data_source: DataSourceSpec::Parquet {
path: "chains/spx.parquet".to_string(),
sha256: String::new(),
},
mode: ExecutionMode::Naive,
seed: 42,
initial_capital: 10_000_000, // $100,000, in cents
fees: FeeSchedule { per_contract_cents: 65, per_order_cents: 100 },
slippage: SlippageModel::None,
marketable_cap_ticks: 10,
liquidity_profile: LiquidityProfile::default(),
limits: ResourceLimits::default(),
output_dir: "runs".into(),
overwrite: false,
};
let strategy = StrategySpec::IronCondor(IronCondorSpec {
underlying: Underlying::new("SPX")?,
underlying_price: PriceCents::new(500_000),
short_call_strike: PriceCents::new(510_000),
short_put_strike: PriceCents::new(490_000),
long_call_strike: PriceCents::new(520_000),
long_put_strike: PriceCents::new(480_000),
expiration: ExpirationDate::DateTime(
chrono::DateTime::from_timestamp_nanos(1_750_291_200_000_000_000),
),
implied_volatility: Decimal::new(20, 2), // 0.20
risk_free_rate: Decimal::new(5, 2), // 0.05
dividend_yield: Decimal::ZERO,
quantity: Quantity::new(1)?,
premium_short_call: PriceCents::new(2_000),
premium_short_put: PriceCents::new(1_800),
premium_long_call: PriceCents::new(800),
premium_long_put: PriceCents::new(700),
open_fee: PriceCents::new(65),
close_fee: PriceCents::new(65),
});
// A non-triggering exit so the run marks every step and closes at the end.
let run = run_backtest(&config, &strategy, ExitPolicy::TimeSteps(1_000_000))?;
println!(
"{}: {} equity points",
run.result.strategy_name,
run.equity_curve.len(),
);
Ok(())
}§Quick start (Python)
The python feature builds a PyO3 extension module. Wheels are not yet on
PyPI; build one locally with maturin:
maturin develop --release --features python,orderbook,simulatorimport ironcondor as ic
config = (
ic.BacktestConfig(seed=42, capital_cents=10_000_000)
.data_parquet("chains/spx.parquet")
.strategy_iron_condor(
underlying="SPX",
underlying_price_cents=500_000,
short_call_strike_cents=510_000,
short_put_strike_cents=490_000,
long_call_strike_cents=520_000,
long_put_strike_cents=480_000,
expiration_ns=1_750_291_200_000_000_000,
quantity=1,
premium_short_call_cents=2_000,
premium_short_put_cents=1_800,
premium_long_call_cents=800,
premium_long_put_cents=700,
implied_volatility=0.20,
risk_free_rate=0.05,
dividend_yield=0.0,
open_fee_cents=65,
close_fee_cents=65,
)
.execution_naive()
.fees(per_contract_cents=65, per_order_cents=100)
.exit_time_steps(1_000_000)
.output_dir("runs")
)
bundle = ic.run(config) # publishes an ironcondor.bundle.v1 directory
print(bundle.metrics()) # summary metrics as a dict
equity = bundle.equity_curve() # a pandas DataFrame with integer-cents columns§The result bundle
Every run publishes an ironcondor.bundle.v1 directory: a manifest.json
(run metadata, strategy, config, data source, code version) plus four Parquet
tables — fills.parquet, equity_curve.parquet, positions.parquet, and
greeks_attribution.parquet. Writes are atomic (temp file plus rename). The
schema tag is frozen and its lineage is coordinated with
ChainView, which consumes the
bundle in replay mode — so a schema change is a deliberate SemVer event, not
an accident.
§Status and versioning
0.5.0 completes the v0.1–v0.5 roadmap — the engine, both fill models, the
full analytics and result bundle, and the Python bindings — with the v1.0
stability gates wired: the Rust public surface, the configuration surface,
and the bundle schema are each pinned by a committed snapshot that fails CI
on drift. Under SemVer 0.x, breaking changes may still land in minor
releases; the 1.0 cut follows the documented one-quarter stability window.
Documentation states present-tense claims only for behaviour that exists,
and no benchmark number is written before it is measured.
§Ecosystem
Part of a family of Rust crates for options-trading infrastructure: OptionStratLib · Option-Chain-OrderBook · OrderBook-rs · OptionChain-Simulator · ChainView
§Contact
Joaquin Bejar — jb@taunais.com
Re-exports§
pub use analytics::Metrics;pub use batch::BATCH_INDEX_SCHEMA;pub use batch::BatchIndex;pub use batch::BatchRunEntry;pub use batch::BatchRunOutcome;pub use batch::SimulatorMaterialisation;pub use batch::run_scenario_batch;pub use bundle::BUNDLE_SCHEMA;pub use bundle::EQUITY_CURVE_SORT_COLUMNS;pub use bundle::FILLS_SORT_COLUMNS;pub use bundle::GREEKS_ATTRIBUTION_SORT_COLUMNS;pub use bundle::Manifest;pub use bundle::POSITIONS_SORT_COLUMNS;pub use bundle::RowCounts;pub use bundle::RunId;pub use bundle::ValidatedBundle;pub use bundle::ValidatedManifest;pub use bundle::equity_sort_key;pub use bundle::fill_sort_key;pub use bundle::greeks_sort_key;pub use bundle::position_sort_key;pub use bundle::read_bundle;pub use bundle::write_bundle;pub use config::BacktestConfig;pub use config::FeeSchedule;pub use config::LiquidityProfile;pub use config::ResourceLimits;pub use config::SlippageModel;pub use config::TouchSize;pub use data::CsvFeed;pub use data::DataFeed;pub use data::DataSourceSpec;pub use data::FeedKind;pub use data::ParquetFeed;pub use data::RawQuote;pub use data::SnapshotMeta;pub use data::TapeMeta;pub use data::feed_catalogue;pub use data::raw_quotes_to_snapshot;pub use data::snapshot_to_option_chain;pub use domain::Cents;pub use domain::ChainSnapshot;pub use domain::ContractKey;pub use domain::EquityPoint;pub use domain::ExecutionMode;pub use domain::Fill;pub use domain::FillRow;pub use domain::GreeksAttributionRow;pub use domain::InstrumentSpec;pub use domain::IronCondorSpec;pub use domain::OpenPosition;pub use domain::OrderCommand;pub use domain::OrderId;pub use domain::OrderIntent;pub use domain::PendingOrder;pub use domain::PositionAction;pub use domain::PositionId;pub use domain::PositionRow;pub use domain::PriceCents;pub use domain::Quantity;pub use domain::QuoteView;pub use domain::ShortStrangleSpec;pub use domain::SimTime;pub use domain::StepIndex;pub use domain::StrategySpec;pub use domain::Ticks;pub use domain::TimeInForce;pub use domain::TradeId;pub use domain::Underlying;pub use engine::AttributionSubstrate;pub use engine::BacktestEngine;pub use engine::BacktestRun;pub use engine::ChainContext;pub use engine::ConfigOverride;pub use engine::Event;pub use engine::FillRecord;pub use engine::Ledger;pub use engine::LegAttributionSample;pub use engine::OptStratAdapter;pub use engine::PositionMark;pub use engine::PositionSnapshot;pub use engine::PositionableStrategy;pub use engine::ScenarioParams;pub use engine::ScenarioType;pub use engine::SimClock;pub use engine::StepAttributionScalars;pub use engine::Strategy;pub use engine::UnitGreeks;pub use engine::WalkPreset;pub use engine::child_data_seed;pub use engine::child_seed;pub use engine::expand;pub use error::BacktestError;pub use execution::ExecutionModel;pub use execution::FillGroup;pub use execution::NaiveFill;pub use run::run_backtest;
Modules§
- analytics
- Post-run analytics.
- batch
- Scenario batch runner — the multi-run composition root.
- bundle
- The result bundle.
- config
- Backtest configuration.
- data
- Chain data feeds.
- domain
- Canonical domain types.
- engine
- The deterministic replay engine.
- error
- The typed error boundary of the crate.
- execution
- Fill simulation: the
ExecutionModelseam and the sharedFillassembly both fill models emit through. - run
- The end-to-end entry: Parquet in, equity curve out.