Skip to main content

nautilus_model/python/
mod.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings from [PyO3](https://pyo3.rs).
17
18#![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/// Converts an instrument ID validation failure to a Python `ValueError`.
53#[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/// Converts an option series ID validation failure to a Python `ValueError`.
63#[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/// Converts a currency lookup failure to a Python `ValueError`.
73#[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/// Exposed through `nautilus_trader.model`.
83///
84/// # Errors
85///
86/// Returns an error if registering any module components fails.
87#[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    // Types
96    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    // Data
108    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    // Enums
185    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    // Identifiers
220    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    // Orders
245    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    // Reports
256    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    // Position
261    m.add_class::<crate::position::Position>()?;
262    m.add_function(wrap_pyfunction!(
263        crate::python::position::py_fold_net_position,
264        m
265    )?)?;
266    // Instruments
267    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    // Order book
301    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    // Events
314    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    // Accounts
340    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::<crate::accounts::margin_model::StandardMarginModel>()?;
345    m.add_class::<crate::accounts::margin_model::LeveragedMarginModel>()?;
346    m.add_function(wrap_pyfunction!(
347        crate::python::account::transformer::betting_account_from_account_events,
348        m
349    )?)?;
350    m.add_function(wrap_pyfunction!(
351        crate::python::account::transformer::cash_account_from_account_events,
352        m
353    )?)?;
354    m.add_function(wrap_pyfunction!(
355        crate::python::account::transformer::margin_account_from_account_events,
356        m
357    )?)?;
358    m.add_function(wrap_pyfunction!(
359        crate::python::account::transformer::wallet_account_from_account_events,
360        m
361    )?)?;
362    m.add_function(wrap_pyfunction!(
363        crate::python::data::bet::py_calc_bets_pnl,
364        m
365    )?)?;
366    m.add_function(wrap_pyfunction!(
367        crate::python::data::bet::py_probability_to_bet,
368        m
369    )?)?;
370    m.add_function(wrap_pyfunction!(
371        crate::python::data::bet::py_inverse_probability_to_bet,
372        m
373    )?)?;
374    // DeFi
375    #[cfg(feature = "defi")]
376    {
377        m.add_class::<crate::defi::chain::Blockchain>()?;
378        m.add_class::<crate::defi::chain::Chain>()?;
379        m.add_class::<crate::defi::token::Token>()?;
380        m.add_class::<crate::defi::dex::AmmType>()?;
381        m.add_class::<crate::defi::dex::Dex>()?;
382        m.add_class::<crate::defi::amm::Pool>()?;
383        m.add_class::<crate::defi::data::PoolSwap>()?;
384        m.add_class::<crate::defi::data::PoolLiquidityUpdateType>()?;
385        m.add_class::<crate::defi::data::PoolLiquidityUpdate>()?;
386        m.add_class::<crate::defi::data::PoolFeeCollect>()?;
387        m.add_class::<crate::defi::data::PoolFeeProtocolUpdate>()?;
388        m.add_class::<crate::defi::data::PoolFeeProtocolCollect>()?;
389        m.add_class::<crate::defi::data::PoolFlash>()?;
390        m.add_class::<crate::defi::data::Transaction>()?;
391        m.add_class::<crate::defi::data::Block>()?;
392        m.add_class::<crate::defi::data::DefiData>()?;
393        m.add_class::<crate::defi::dex::DexType>()?;
394        m.add_class::<crate::defi::pool_analysis::PoolSnapshot>()?;
395        m.add_class::<crate::defi::pool_analysis::PoolProfiler>()?;
396        m.add_class::<crate::defi::pool_analysis::position::PoolPosition>()?;
397        m.add_class::<crate::defi::pool_analysis::quote::SwapQuote>()?;
398        m.add_class::<crate::defi::pool_analysis::size_estimator::SizeForImpactResult>()?;
399        m.add_class::<crate::defi::pool_analysis::snapshot::PoolAnalytics>()?;
400        m.add_class::<crate::defi::pool_analysis::snapshot::PoolState>()?;
401        m.add_class::<crate::defi::tick_map::tick::PoolTick>()?;
402    }
403    Ok(())
404}
405
406#[cfg(test)]
407mod tests {
408    use std::sync::Once;
409
410    use pyo3::{Python, exceptions::PyValueError};
411    use rstest::rstest;
412
413    use super::*;
414
415    fn ensure_python_initialized() {
416        static INIT: Once = Once::new();
417        INIT.call_once(|| {
418            Python::initialize();
419        });
420    }
421
422    #[rstest]
423    fn test_instrument_id_error_to_pyvalue_err_preserves_display_text() {
424        ensure_python_initialized();
425
426        let error = "BTCUSDT"
427            .parse::<crate::identifiers::InstrumentId>()
428            .unwrap_err();
429
430        Python::attach(|py| {
431            let py_err = instrument_id_error_to_pyvalue_err(error);
432
433            assert!(py_err.is_instance_of::<PyValueError>(py));
434            assert_eq!(
435                py_err.value(py).to_string(),
436                "invalid `InstrumentId` value 'BTCUSDT': missing '.' separator between symbol and venue components"
437            );
438        });
439    }
440
441    #[rstest]
442    fn test_option_series_id_error_to_pyvalue_err_preserves_display_text() {
443        ensure_python_initialized();
444
445        let error = "DERIBIT:BTC:USD"
446            .parse::<crate::identifiers::OptionSeriesId>()
447            .unwrap_err();
448
449        Python::attach(|py| {
450            let py_err = option_series_id_error_to_pyvalue_err(error);
451
452            assert!(py_err.is_instance_of::<PyValueError>(py));
453            assert_eq!(
454                py_err.value(py).to_string(),
455                "invalid `OptionSeriesId` value 'DERIBIT:BTC:USD': expected format 'VENUE:UNDERLYING:[REFERENCE:]SETTLEMENT:EXPIRY'"
456            );
457        });
458    }
459
460    #[rstest]
461    fn test_currency_lookup_error_to_pyvalue_err_preserves_display_text() {
462        ensure_python_initialized();
463
464        let error = "UNKNOWN_CURRENCY"
465            .parse::<crate::types::Currency>()
466            .unwrap_err();
467
468        Python::attach(|py| {
469            let py_err = currency_lookup_error_to_pyvalue_err(error);
470
471            assert!(py_err.is_instance_of::<PyValueError>(py));
472            assert_eq!(
473                py_err.value(py).to_string(),
474                "Unknown currency: UNKNOWN_CURRENCY"
475            );
476        });
477    }
478}