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
//! Python bindings (feature `python`).
//!
//! A thin PyO3 layer over the **public Rust API** and nothing more. The Rust
//! core never imports from this module; the dependency arrow points one way
//! ([CLAUDE.md](../../CLAUDE.md) module boundaries,
//! [docs/06 §2](../../docs/06-python-bindings.md)). This scaffold (#38) wires
//! the *shape*, not the API: the `#[pymodule]` entry point plus stub submodules
//! ([`config`], [`run`], [`bundle`], [`errors`]) that #39/#40 fill in.
//!
//! ## What the `python` feature gates
//!
//! The `python` feature gates the **PyO3 dependency and this code**, **not the
//! crate-type**. Cargo resolves `[lib] crate-type` *before* feature
//! unification, so a feature cannot toggle it — toggling the crate-type by
//! feature is **unrepresentable in Cargo**. The crate therefore declares
//! `crate-type = ["rlib", "cdylib"]` **unconditionally**: without `python`
//! the `cdylib` links no PyO3 and exports no Python module (an inert artifact,
//! and the crates.io build carries zero Python dependencies); with `python`
//! that same `cdylib` becomes the importable extension module maturin packages.
//! Do not try to move `crate-type` behind a feature — it will not work.
//!
//! ## `__version__` parity
//!
//! The module reports [`crate_version`] (`env!("CARGO_PKG_VERSION")`) as
//! `ironcondor.__version__`, so the Python surface and the crate share **one**
//! source of truth for the version — parity is structural, never hand-copied
//! ([docs/06 §9](../../docs/06-python-bindings.md)).
//!
//! ## No panic across the FFI boundary (#40)
//!
//! Every fallible boundary entry point ([`run::run`], [`bundle::load_bundle`],
//! the [`bundle::Bundle`] accessors, the fallible [`config`] builders) drives
//! the pure-Rust API — which returns [`Result`]s — and maps every error to its
//! **typed** Python exception through [`errors::to_pyerr`] (the single mapping
//! seam) with `?` / `map_err`, never `.unwrap()` / `.expect()`. Each boundary
//! body is additionally wrapped in [`errors::guard_boundary`], a `catch_unwind`
//! that converts an *unexpected* Rust panic into `ic.EngineError` — so no panic
//! crosses the boundary, whether foreseen (typed) or not (a bug).
//!
//! ## Doc-table parity (docs/06 §5)
//!
//! The mapping is verified against the **actual** [`crate::error::BacktestError`]
//! enum, and the docs/06 §5 table now carries all 15 variants (architect synced
//! it during the #40 review — including `PriceNotTickAligned` → `ic.ConfigError`
//! and `TapeTooLarge` / `Data` → `ic.DataError`, the rows the earlier draft
//! omitted). This exhaustive, wildcard-free `match` is the source of truth; a
//! new variant is a compile error here until it is mapped.
//!
//! ## What is exposed (and what is deferred)
//!
//! #39 fills the working surface: [`config::PyBacktestConfig`] (Python
//! `BacktestConfig`) with integer-cents builders, [`run::run`], and
//! [`bundle::Bundle`] with lazy DataFrame accessors + [`bundle::load_bundle`].
//! Only what [`crate::run_backtest`] genuinely wires end to end is reachable: a
//! **Parquet** source and the **iron condor** strategy (a CSV source or a short
//! strangle spec is a deferred composition-root dispatch, so no `data_csv` /
//! `strategy_short_strangle` builder exists). Both execution modes work (#26).
use *;
/// The crate version reported to Python as `ironcondor.__version__`.
///
/// Sourced from `env!("CARGO_PKG_VERSION")` so the Python surface and the
/// crate version can never drift.
pub
/// The `ironcondor` Python extension module.
///
/// The function name matches the crate's `[lib] name`, so PyO3 emits the
/// `PyInit_ironcondor` symbol maturin packages as an importable module. It
/// registers `__version__`, the `BacktestConfig` builder and `Bundle` handle
/// classes, the `run` / `load_bundle` functions (#39), and the typed exception
/// hierarchy rooted at `ic.IronCondorError` (#40).
///
/// # Why `pub`
///
/// The `#[pymodule]` macro derives a hidden companion module from this
/// function's visibility; making the function `pub` therefore also makes that
/// companion `pub`, which is what lets an **embedding** caller register the
/// module in the interpreter's init table via
/// [`pyo3::append_to_inittab!`](pyo3::append_to_inittab) before
/// `Python::initialize`. The determinism-parity integration test
/// (`tests/python_parity.rs`, #42) uses exactly that to drive the *real*
/// registered module in-process. This is a Rust-visibility change only: it does
/// **not** alter what the Python module exposes (the same classes / functions
/// are registered either way), and maturin still packages the identical
/// `PyInit_ironcondor` symbol.
///
/// # Errors
///
/// Returns a [`PyErr`] if registering an attribute, class, or function on the
/// module fails (the interpreter is out of memory or the module object is
/// invalid).