1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//! **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
//!
//! | 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:
//!
//! ```rust,no_run
//! 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>
pub use Metrics;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use BacktestError;
pub use RealisticFill;
pub use ;
pub use run_backtest;
pub use SimulatorSourceSpec;
/// The migrated OptionChain-Simulator session surface (feature `simulator`):
/// the async [`data::simulator::ApiClient`], the bug-fixed
/// [`data::simulator::MarketSimulator`] step wrapper, the local wire DTOs,
/// and the materialised-tape [`data::simulator::SimulatorFeed`] (#45).
pub use chain_response_to_snapshot;
pub use SimulatorFeed;
pub use ;