nautilus_model/python/
mod.rs1#![expect(
19 clippy::missing_errors_doc,
20 reason = "errors documented on underlying Rust methods"
21)]
22
23use nautilus_core::python::to_pyvalue_err;
24use pyo3::{PyErr, prelude::*};
25
26pub mod account;
27pub mod common;
28pub mod data;
29pub mod enums;
30pub mod events;
31pub mod identifiers;
32pub mod instruments;
33pub mod macros;
34pub mod orderbook;
35pub mod orders;
36pub mod position;
37pub mod reports;
38pub mod types;
39
40#[cfg(feature = "defi")]
41pub mod defi;
42
43use crate::{
44 data::{
45 CustomDataTrait, ensure_py_extractor_registered, ensure_rust_extractor_registered,
46 get_rust_extractor, greeks::YieldCurve,
47 },
48 identifiers::{InstrumentIdError, OptionSeriesIdError},
49 types::CurrencyLookupError,
50};
51
52#[must_use]
54#[allow(
55 clippy::needless_pass_by_value,
56 reason = "Result::map_err passes owned errors to conversion functions"
57)]
58pub fn instrument_id_error_to_pyvalue_err(e: InstrumentIdError) -> PyErr {
59 to_pyvalue_err(e)
60}
61
62#[must_use]
64#[allow(
65 clippy::needless_pass_by_value,
66 reason = "Result::map_err passes owned errors to conversion functions"
67)]
68pub fn option_series_id_error_to_pyvalue_err(e: OptionSeriesIdError) -> PyErr {
69 to_pyvalue_err(e)
70}
71
72#[must_use]
74#[allow(
75 clippy::needless_pass_by_value,
76 reason = "Result::map_err passes owned errors to conversion functions"
77)]
78pub fn currency_lookup_error_to_pyvalue_err(e: CurrencyLookupError) -> PyErr {
79 to_pyvalue_err(e)
80}
81
82#[pymodule]
88pub fn model(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
89 let _result = ensure_rust_extractor_registered::<YieldCurve>();
90 let type_name = YieldCurve::type_name_static();
91 if let Some(extractor) = get_rust_extractor(type_name) {
92 let _result = ensure_py_extractor_registered(type_name, extractor);
93 }
94
95 m.add("HIGH_PRECISION", crate::types::fixed::HIGH_PRECISION_MODE)?;
97 m.add("FIXED_SCALAR", crate::types::fixed::FIXED_SCALAR)?;
98 m.add("FIXED_PRECISION", crate::types::fixed::FIXED_PRECISION)?;
99 m.add("PRECISION_BYTES", crate::types::fixed::PRECISION_BYTES)?;
100 m.add_class::<crate::types::currency::Currency>()?;
101 m.add_class::<crate::types::money::Money>()?;
102 m.add_class::<crate::types::price::Price>()?;
103 m.add_class::<crate::types::quantity::Quantity>()?;
104 m.add_class::<crate::types::balance::AccountBalance>()?;
105 m.add_class::<crate::types::balance::MarginBalance>()?;
106 m.add_class::<crate::python::common::EnumIterator>()?;
107 m.add_class::<data::PyNautilusDataType>()?;
109 m.add_class::<data::PyNautilusRecordType>()?;
110 m.add_class::<instruments::PyNautilusInstrumentType>()?;
111 for record_type in <crate::data::NautilusRecordType as strum::IntoEnumIterator>::iter() {
112 m.getattr("NautilusRecordType")?.setattr(
113 record_type.to_string(),
114 Py::new(m.py(), data::PyNautilusRecordType::new(record_type))?,
115 )?;
116 }
117
118 for instrument_type in
119 <crate::instruments::NautilusInstrumentType as strum::IntoEnumIterator>::iter()
120 {
121 m.getattr("NautilusInstrumentType")?.setattr(
122 instrument_type.to_string(),
123 Py::new(
124 m.py(),
125 instruments::PyNautilusInstrumentType::new(instrument_type),
126 )?,
127 )?;
128 }
129 m.add_class::<crate::data::DataType>()?;
130 m.add_class::<crate::data::CustomData>()?;
131 m.add_function(pyo3::wrap_pyfunction!(
132 crate::python::data::deserialize_custom_from_json,
133 m
134 )?)?;
135 m.add_function(pyo3::wrap_pyfunction!(
136 crate::python::data::register_custom_data_class,
137 m
138 )?)?;
139 m.add_function(pyo3::wrap_pyfunction!(
140 crate::python::data::custom::custom_data_backend_kind,
141 m
142 )?)?;
143 m.add_class::<crate::data::bar::BarSpecification>()?;
144 m.add_class::<crate::data::bar::BarType>()?;
145 m.add_class::<crate::data::bar::Bar>()?;
146 m.add_class::<crate::data::bet::Bet>()?;
147 m.add_class::<crate::data::bet::BetPosition>()?;
148 m.add_class::<crate::data::order::BookOrder>()?;
149 m.add_class::<crate::data::prices::MarkPriceUpdate>()?;
150 m.add_class::<crate::data::prices::IndexPriceUpdate>()?;
151 m.add_class::<crate::data::delta::OrderBookDelta>()?;
152 m.add_class::<crate::data::deltas::OrderBookDeltas>()?;
153 m.add_class::<crate::data::depth::OrderBookDepth>()?;
154 m.add_class::<crate::data::quote::QuoteTick>()?;
155 m.add_class::<crate::data::status::InstrumentStatus>()?;
156 m.add_class::<crate::data::trade::TradeTick>()?;
157 m.add_class::<crate::data::close::InstrumentClose>()?;
158 m.add_class::<crate::data::funding::FundingRateUpdate>()?;
159 m.add_class::<crate::data::greeks::OptionGreekValues>()?;
160 m.add_class::<crate::data::greeks::BlackScholesGreeksResult>()?;
161 m.add_class::<crate::data::greeks::GreeksData>()?;
162 m.add_class::<crate::data::greeks::PortfolioGreeks>()?;
163 m.add_class::<crate::data::greeks::YieldCurve>()?;
164 m.add_class::<crate::data::option_chain::OptionGreeks>()?;
165 m.add_class::<crate::data::option_chain::OptionChainSlice>()?;
166 m.add_class::<crate::data::option_chain::OptionStrikeData>()?;
167 m.add_class::<crate::python::data::option_chain::PyStrikeRange>()?;
168 m.add_function(wrap_pyfunction!(
169 crate::python::data::greeks::py_black_scholes_greeks,
170 m
171 )?)?;
172 m.add_function(wrap_pyfunction!(
173 crate::python::data::greeks::py_imply_vol,
174 m
175 )?)?;
176 m.add_function(wrap_pyfunction!(
177 crate::python::data::greeks::py_imply_vol_and_greeks,
178 m
179 )?)?;
180 m.add_function(wrap_pyfunction!(
181 crate::python::data::greeks::py_refine_vol_and_greeks,
182 m
183 )?)?;
184 m.add_class::<crate::enums::AccountType>()?;
186 m.add_class::<crate::enums::AggregationSource>()?;
187 m.add_class::<crate::enums::AggressorSide>()?;
188 m.add_class::<crate::enums::AssetClass>()?;
189 m.add_class::<crate::enums::AvgPxReconciliation>()?;
190 m.add_class::<crate::enums::BarAggregation>()?;
191 m.add_class::<crate::enums::BarIntervalType>()?;
192 m.add_class::<crate::enums::BetSide>()?;
193 m.add_class::<crate::enums::BookAction>()?;
194 m.add_class::<crate::enums::BookType>()?;
195 m.add_class::<crate::enums::ContingencyType>()?;
196 m.add_class::<crate::enums::ContinuousFutureAdjustmentType>()?;
197 m.add_class::<crate::enums::CurrencyType>()?;
198 m.add_class::<crate::enums::GreeksConvention>()?;
199 m.add_class::<crate::enums::InstrumentClass>()?;
200 m.add_class::<crate::enums::InstrumentCloseType>()?;
201 m.add_class::<crate::enums::LiquiditySide>()?;
202 m.add_class::<crate::enums::MarketStatus>()?;
203 m.add_class::<crate::enums::MarketStatusAction>()?;
204 m.add_class::<crate::enums::OmsType>()?;
205 m.add_class::<crate::enums::OptionKind>()?;
206 m.add_class::<crate::enums::OptionSideFilter>()?;
207 m.add_class::<crate::enums::OtoTriggerMode>()?;
208 m.add_class::<crate::enums::OrderSide>()?;
209 m.add_class::<crate::enums::OrderStatus>()?;
210 m.add_class::<crate::enums::OrderType>()?;
211 m.add_class::<crate::enums::PositionAdjustmentType>()?;
212 m.add_class::<crate::enums::PositionSide>()?;
213 m.add_class::<crate::enums::PriceType>()?;
214 m.add_class::<crate::enums::RecordFlag>()?;
215 m.add_class::<crate::enums::TimeInForce>()?;
216 m.add_class::<crate::enums::TradingState>()?;
217 m.add_class::<crate::enums::TrailingOffsetType>()?;
218 m.add_class::<crate::enums::TriggerType>()?;
219 m.add_class::<crate::identifiers::AccountId>()?;
221 m.add_class::<crate::identifiers::ActorId>()?;
222 m.add_class::<crate::identifiers::ClientId>()?;
223 m.add_class::<crate::identifiers::ClientOrderId>()?;
224 m.add_class::<crate::identifiers::ComponentId>()?;
225 m.add_class::<crate::identifiers::ExecAlgorithmId>()?;
226 m.add_class::<crate::identifiers::InstrumentId>()?;
227 m.add_class::<crate::identifiers::OrderListId>()?;
228 m.add_class::<crate::identifiers::PositionId>()?;
229 m.add_class::<crate::identifiers::StrategyId>()?;
230 m.add_class::<crate::identifiers::Symbol>()?;
231 m.add_class::<crate::identifiers::TradeId>()?;
232 m.add_class::<crate::identifiers::TraderId>()?;
233 m.add_class::<crate::identifiers::Venue>()?;
234 m.add_class::<crate::identifiers::VenueOrderId>()?;
235 m.add_class::<crate::identifiers::OptionSeriesId>()?;
236 m.add_function(wrap_pyfunction!(
237 crate::python::identifiers::py_new_generic_spread_id,
238 m
239 )?)?;
240 m.add_function(wrap_pyfunction!(
241 crate::python::identifiers::py_generic_spread_id_to_list,
242 m
243 )?)?;
244 m.add_class::<crate::orders::LimitOrder>()?;
246 m.add_class::<crate::orders::LimitIfTouchedOrder>()?;
247 m.add_class::<crate::orders::MarketIfTouchedOrder>()?;
248 m.add_class::<crate::orders::MarketOrder>()?;
249 m.add_class::<crate::orders::MarketToLimitOrder>()?;
250 m.add_class::<crate::orders::OrderList>()?;
251 m.add_class::<crate::orders::StopLimitOrder>()?;
252 m.add_class::<crate::orders::StopMarketOrder>()?;
253 m.add_class::<crate::orders::TrailingStopLimitOrder>()?;
254 m.add_class::<crate::orders::TrailingStopMarketOrder>()?;
255 m.add_class::<crate::reports::fill::FillReport>()?;
257 m.add_class::<crate::reports::order::OrderStatusReport>()?;
258 m.add_class::<crate::reports::position::PositionStatusReport>()?;
259 m.add_class::<crate::reports::mass_status::ExecutionMassStatus>()?;
260 m.add_class::<crate::position::Position>()?;
262 m.add_function(wrap_pyfunction!(
263 crate::python::position::py_fold_net_position,
264 m
265 )?)?;
266 m.add_class::<instruments::tick_scheme::PyFixedTickScheme>()?;
268 m.add_class::<instruments::tick_scheme::PyTieredTickScheme>()?;
269 m.add_function(wrap_pyfunction!(
270 instruments::tick_scheme::py_register_tick_scheme,
271 m
272 )?)?;
273 m.add_function(wrap_pyfunction!(
274 instruments::tick_scheme::py_get_tick_scheme,
275 m
276 )?)?;
277 m.add_function(wrap_pyfunction!(
278 instruments::tick_scheme::py_list_tick_schemes,
279 m
280 )?)?;
281 m.add_class::<crate::instruments::BettingInstrument>()?;
282 m.add_class::<crate::instruments::BinaryOption>()?;
283 m.add_class::<crate::instruments::Cfd>()?;
284 m.add_class::<crate::instruments::Commodity>()?;
285 m.add_class::<crate::instruments::CryptoFuture>()?;
286 m.add_class::<crate::instruments::CryptoFuturesSpread>()?;
287 m.add_class::<crate::instruments::CryptoOption>()?;
288 m.add_class::<crate::instruments::CryptoOptionSpread>()?;
289 m.add_class::<crate::instruments::CryptoPerpetual>()?;
290 m.add_class::<crate::instruments::CurrencyPair>()?;
291 m.add_class::<crate::instruments::Equity>()?;
292 m.add_class::<crate::instruments::FuturesContract>()?;
293 m.add_class::<crate::instruments::FuturesSpread>()?;
294 m.add_class::<crate::instruments::IndexInstrument>()?;
295 m.add_class::<crate::instruments::OptionContract>()?;
296 m.add_class::<crate::instruments::OptionSpread>()?;
297 m.add_class::<crate::instruments::PerpetualContract>()?;
298 m.add_class::<crate::instruments::SyntheticInstrument>()?;
299 m.add_class::<crate::instruments::TokenizedAsset>()?;
300 m.add_class::<crate::orderbook::book::OrderBook>()?;
302 m.add_class::<crate::orderbook::level::BookLevel>()?;
303 m.add_function(wrap_pyfunction!(
304 crate::python::orderbook::book::py_update_book_with_quote_tick,
305 m
306 )?)?;
307 m.add_function(wrap_pyfunction!(
308 crate::python::orderbook::book::py_update_book_with_trade_tick,
309 m
310 )?)?;
311 m.add_class::<crate::orderbook::own::OwnOrderBook>()?;
312 m.add_class::<crate::orderbook::own::OwnBookOrder>()?;
313 m.add_class::<crate::events::AccountState>()?;
315 m.add_class::<crate::events::OrderDenied>()?;
316 m.add_class::<crate::events::OrderFilled>()?;
317 m.add_class::<crate::events::OrderFillVoided>()?;
318 m.add_class::<crate::events::OrderInitialized>()?;
319 m.add_class::<crate::events::OrderRejected>()?;
320 m.add_class::<crate::events::OrderTriggered>()?;
321 m.add_class::<crate::events::OrderSubmitted>()?;
322 m.add_class::<crate::events::OrderEmulated>()?;
323 m.add_class::<crate::events::OrderReleased>()?;
324 m.add_class::<crate::events::OrderUpdated>()?;
325 m.add_class::<crate::events::OrderPendingUpdate>()?;
326 m.add_class::<crate::events::OrderPendingCancel>()?;
327 m.add_class::<crate::events::OrderModifyRejected>()?;
328 m.add_class::<crate::events::OrderAccepted>()?;
329 m.add_class::<crate::events::OrderCancelRejected>()?;
330 m.add_class::<crate::events::OrderCanceled>()?;
331 m.add_class::<crate::events::OrderExpired>()?;
332 m.add_class::<crate::events::OrderSnapshot>()?;
333 m.add_class::<crate::events::PositionOpened>()?;
334 m.add_class::<crate::events::PositionChanged>()?;
335 m.add_class::<crate::events::PositionClosed>()?;
336 m.add_class::<crate::events::PositionAdjusted>()?;
337 m.add_class::<crate::events::PositionSnapshot>()?;
338 m.add_class::<crate::events::PortfolioSnapshot>()?;
339 m.add_class::<crate::accounts::BettingAccount>()?;
341 m.add_class::<crate::accounts::CashAccount>()?;
342 m.add_class::<crate::accounts::MarginAccount>()?;
343 m.add_class::<crate::accounts::WalletAccount>()?;
344 m.add_class::<account::margin_model::PyMarginModel>()?;
345 m.add_class::<crate::accounts::margin_model::StandardMarginModel>()?;
346 m.add_class::<crate::accounts::margin_model::LeveragedMarginModel>()?;
347 m.add_function(wrap_pyfunction!(
348 crate::python::account::transformer::betting_account_from_account_events,
349 m
350 )?)?;
351 m.add_function(wrap_pyfunction!(
352 crate::python::account::transformer::cash_account_from_account_events,
353 m
354 )?)?;
355 m.add_function(wrap_pyfunction!(
356 crate::python::account::transformer::margin_account_from_account_events,
357 m
358 )?)?;
359 m.add_function(wrap_pyfunction!(
360 crate::python::account::transformer::wallet_account_from_account_events,
361 m
362 )?)?;
363 m.add_function(wrap_pyfunction!(
364 crate::python::data::bet::py_calc_bets_pnl,
365 m
366 )?)?;
367 m.add_function(wrap_pyfunction!(
368 crate::python::data::bet::py_probability_to_bet,
369 m
370 )?)?;
371 m.add_function(wrap_pyfunction!(
372 crate::python::data::bet::py_inverse_probability_to_bet,
373 m
374 )?)?;
375 #[cfg(feature = "defi")]
377 {
378 m.add_class::<crate::defi::chain::Blockchain>()?;
379 m.add_class::<crate::defi::chain::Chain>()?;
380 m.add_class::<crate::defi::token::Token>()?;
381 m.add_class::<crate::defi::dex::AmmType>()?;
382 m.add_class::<crate::defi::dex::Dex>()?;
383 m.add_class::<crate::defi::amm::Pool>()?;
384 m.add_class::<crate::defi::data::PoolSwap>()?;
385 m.add_class::<crate::defi::data::PoolLiquidityUpdateType>()?;
386 m.add_class::<crate::defi::data::PoolLiquidityUpdate>()?;
387 m.add_class::<crate::defi::data::PoolFeeCollect>()?;
388 m.add_class::<crate::defi::data::PoolFeeProtocolUpdate>()?;
389 m.add_class::<crate::defi::data::PoolFeeProtocolCollect>()?;
390 m.add_class::<crate::defi::data::PoolFlash>()?;
391 m.add_class::<crate::defi::data::Transaction>()?;
392 m.add_class::<crate::defi::data::Block>()?;
393 m.add_class::<crate::defi::data::DefiData>()?;
394 m.add_class::<crate::defi::dex::DexType>()?;
395 m.add_class::<crate::defi::pool_analysis::PoolSnapshot>()?;
396 m.add_class::<crate::defi::pool_analysis::PoolProfiler>()?;
397 m.add_class::<crate::defi::pool_analysis::position::PoolPosition>()?;
398 m.add_class::<crate::defi::pool_analysis::quote::SwapQuote>()?;
399 m.add_class::<crate::defi::pool_analysis::size_estimator::SizeForImpactResult>()?;
400 m.add_class::<crate::defi::pool_analysis::snapshot::PoolAnalytics>()?;
401 m.add_class::<crate::defi::pool_analysis::snapshot::PoolState>()?;
402 m.add_class::<crate::defi::tick_map::tick::PoolTick>()?;
403 }
404 Ok(())
405}
406
407#[cfg(test)]
408mod tests {
409 use std::sync::Once;
410
411 use pyo3::{Python, exceptions::PyValueError};
412 use rstest::rstest;
413
414 use super::*;
415
416 fn ensure_python_initialized() {
417 static INIT: Once = Once::new();
418 INIT.call_once(|| {
419 Python::initialize();
420 });
421 }
422
423 #[rstest]
424 fn test_instrument_id_error_to_pyvalue_err_preserves_display_text() {
425 ensure_python_initialized();
426
427 let error = "BTCUSDT"
428 .parse::<crate::identifiers::InstrumentId>()
429 .unwrap_err();
430
431 Python::attach(|py| {
432 let py_err = instrument_id_error_to_pyvalue_err(error);
433
434 assert!(py_err.is_instance_of::<PyValueError>(py));
435 assert_eq!(
436 py_err.value(py).to_string(),
437 "invalid `InstrumentId` value 'BTCUSDT': missing '.' separator between symbol and venue components"
438 );
439 });
440 }
441
442 #[rstest]
443 fn test_option_series_id_error_to_pyvalue_err_preserves_display_text() {
444 ensure_python_initialized();
445
446 let error = "DERIBIT:BTC:USD"
447 .parse::<crate::identifiers::OptionSeriesId>()
448 .unwrap_err();
449
450 Python::attach(|py| {
451 let py_err = option_series_id_error_to_pyvalue_err(error);
452
453 assert!(py_err.is_instance_of::<PyValueError>(py));
454 assert_eq!(
455 py_err.value(py).to_string(),
456 "invalid `OptionSeriesId` value 'DERIBIT:BTC:USD': expected format 'VENUE:UNDERLYING:[REFERENCE:]SETTLEMENT:EXPIRY'"
457 );
458 });
459 }
460
461 #[rstest]
462 fn test_currency_lookup_error_to_pyvalue_err_preserves_display_text() {
463 ensure_python_initialized();
464
465 let error = "UNKNOWN_CURRENCY"
466 .parse::<crate::types::Currency>()
467 .unwrap_err();
468
469 Python::attach(|py| {
470 let py_err = currency_lookup_error_to_pyvalue_err(error);
471
472 assert!(py_err.is_instance_of::<PyValueError>(py));
473 assert_eq!(
474 py_err.value(py).to_string(),
475 "Unknown currency: UNKNOWN_CURRENCY"
476 );
477 });
478 }
479}