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::<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    // DeFi
376    #[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}