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
//! The end-to-end entry: **Parquet in, equity curve out**.
//!
//! [`run_backtest`] is the thin composition root that ties the vertical
//! slice together — the Parquet historical feed (#9), the `IronCondor` strategy
//! adapter (#11), the config-selected fill model (naive #13 / realistic #24,
//! selected by `config.mode` at startup #26), the replay loop + mark-to-market
//! ledger (#14/#15), and the minimal summary metrics (#16) — into one call.
//!
//! # Layering: why this lives above both `engine` and `analytics`
//!
//! `analytics` **consumes** engine output; the engine must therefore **not**
//! import `analytics` (that would invert the `analytics → engine output`
//! dependency, [CLAUDE.md](../../CLAUDE.md) Module Boundaries). So the "run,
//! then compute analytics" orchestration cannot live inside
//! [`crate::engine::BacktestEngine::run`]. It lives **here**, at the crate top
//! level — a composition root that sits *above* both layers, calls
//! [`crate::engine::BacktestEngine::run`] first, then
//! [`crate::analytics::metrics::populate`] and
//! [`crate::analytics::attribution::attribute`] on its output (the P&L
//! attribution by Greek, #31, reads the engine's owned attribution substrate
//! and fills `run.greeks_attribution`). The engine stays analytics-free;
//! analytics stays engine-loop-free; this module depends on both.
//!
//! # Determinism
//!
//! `run_backtest` adds no wall clock and no RNG of its own — the engine owns
//! the sole seeded RNG and the metrics are a pure function of the equity
//! series, so `(seed, config, data)` is byte-reproducible
//! ([docs/02 §7](../../docs/02-engine-architecture.md#7-determinism-and-reproducibility)).
use ExitPolicy;
use IronCondor;
use crate;
use crateBacktestConfig;
use crate;
use crate;
use crate;
use crateBacktestError;
use crateNaiveFill;
use crateRealisticFill;
/// Run one backtest end to end and return the populated [`BacktestRun`] — the
/// ordered `equity_curve` plus the upstream
/// [`optionstratlib::backtesting::BacktestResult`] with the minimal summary
/// metrics filled in ([`metrics::populate`]).
///
/// The feed and execution model are built **from `config`**: a
/// [`DataSourceSpec::Parquet`] source opens a [`ParquetFeed`] under
/// `config.limits`, and `config.mode` selects the [`crate::execution::ExecutionModel`]
/// once at startup ([docs/04 §2](../../docs/04-execution-models.md#2-the-executionmodel-trait-and-the-shared-fill-report)):
///
/// - [`ExecutionMode::Naive`] builds a [`NaiveFill`] from `config.slippage` and
/// `config.fees`;
/// - [`ExecutionMode::Realistic`] (feature `orderbook`) builds a `RealisticFill`
/// from `config.fees`, `config.marketable_cap_ticks`, `config.seed`, and
/// `config.liquidity_profile`.
///
/// The selected model fixes the concrete `X: ExecutionModel` of the
/// monomorphised [`BacktestEngine::run`], so the loop has **no per-step `dyn`
/// dispatch**; the strategy runs unchanged under either mode (the mode is
/// configuration, not a code path the strategy sees). This composition root
/// wires the [`StrategySpec::IronCondor`] kind, wrapping `strategy_spec` with
/// `exit` through `OptStratAdapter::<IronCondor>::from_spec`; a
/// [`StrategySpec::ShortStrangle`] (v0.2 breadth, #28) is a typed
/// [`BacktestError::Strategy`] here rather than being wired into `run_backtest`
/// (it is driven directly through the same generic adapter and engine — see
/// `OptStratAdapter::<ShortStrangle>::from_spec` and the `short_strangle_naive`
/// golden).
///
/// The primary artifact is the ordered `run.equity_curve`
/// (`Vec<EquityPoint>`, integer cents + the one drawdown float); the result
/// bundle (`manifest.json` + Parquet tables) is v0.3 and is **not** written
/// here.
///
/// # Errors
///
/// - [`BacktestError::Config`] if the config fails [`BacktestConfig::validate`],
/// or the data source is not a Parquet feed (the sole v0.1 feed), or
/// `config.mode` is [`ExecutionMode::Realistic`] but the crate was built
/// without the `orderbook` feature (realistic execution is unavailable), or
/// the initial capital exceeds the `i64` cents range.
/// - [`BacktestError::Data`] / [`BacktestError::DataIo`] if the Parquet feed
/// cannot be opened or its tape is malformed.
/// - [`BacktestError::Strategy`] / [`BacktestError::Conversion`] if the strategy
/// spec cannot be constructed.
/// - Any [`BacktestError`] the replay loop or the metrics pass raises
/// (including [`BacktestError::ArithmeticOverflow`]).
/// Run one backtest end to end over an **already-opened** feed `F`, returning
/// the populated [`BacktestRun`] — the feed-agnostic core shared by
/// [`run_backtest`] (Parquet) and the scenario batch runner
/// ([`crate::batch::run_scenario_batch`], which opens a [`ParquetFeed`] or a
/// simulator [`SimulatorFeed`](crate::data::SimulatorFeed) per run).
///
/// This is the consolidated composition core: it wires the
/// [`StrategySpec::IronCondor`] adapter, selects the [`ExecutionModel`] once
/// from `config.mode` (no per-step `dyn`), drives [`BacktestEngine::run`], then
/// runs the post-run analytics ([`metrics::populate`] and
/// [`attribution::attribute`]). It sits **above** both the engine and analytics
/// layers exactly as [`run_backtest`] does, so the engine stays analytics-free.
///
/// `config` MUST already have passed [`BacktestConfig::validate`] — the feed
/// (which reads `config.limits`) is opened by the caller before this runs, so
/// validation happens there, once, and this core does not re-validate.
///
/// Determinism is unchanged from [`run_backtest`]: no wall clock and no RNG of
/// its own; `(seed, config, data)` is byte-reproducible.
///
/// # Errors
///
/// - [`BacktestError::Config`] if `config.mode` is [`ExecutionMode::Realistic`]
/// but the crate was built without the `orderbook` feature, or the initial
/// capital exceeds the `i64` cents range.
/// - [`BacktestError::Strategy`] / [`BacktestError::Conversion`] if the strategy
/// spec cannot be constructed.
/// - Any [`BacktestError`] the replay loop or the metrics pass raises
/// (including [`BacktestError::ArithmeticOverflow`]).
pub