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