# IronCondor
**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`](https://crates.io/crates/optionstratlib); order matching
comes from
[`option-chain-orderbook`](https://crates.io/crates/option-chain-orderbook)
and [`orderbook-rs`](https://github.com/joaquinbejar/OrderBook-rs);
synthetic option chains come from
[OptionChain-Simulator](https://github.com/joaquinbejar/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 `u128` ticks. `f64` is
confined to the upstream pricing/Greeks kernel and the documented
derived-analytics columns.
- **Dual execution modes.** `naive` (mid/spread plus slippage) and
`realistic` (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 + residual` equals 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.v1`
directory (a manifest plus four Parquet tables) consumed by
[ChainView](https://github.com/joaquinbejar/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 typed `BacktestError`, 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
`criterion` and `hdrhistogram`. As a recorded baseline (see `BENCH.md`), a
full `run_backtest` over 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
| *(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:
```rust
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](https://www.maturin.rs):
```bash
maturin develop --release --features python,orderbook,simulator
```
```python
import 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](https://github.com/joaquinbejar/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](https://github.com/joaquinbejar/OptionStratLib) ·
[Option-Chain-OrderBook](https://github.com/joaquinbejar/Option-Chain-OrderBook) ·
[OrderBook-rs](https://github.com/joaquinbejar/OrderBook-rs) ·
[OptionChain-Simulator](https://github.com/joaquinbejar/OptionChain-Simulator) ·
[ChainView](https://github.com/joaquinbejar/ChainView)
### Contact
Joaquin Bejar — <jb@taunais.com>
## License
MIT — see [LICENSE](./LICENSE).