nautilus_model/python/instruments/
mod.rs1use nautilus_core::python::to_pyvalue_err;
19use pyo3::{
20 IntoPyObjectExt, Py, PyAny, PyResult, Python,
21 types::{PyAnyMethods, PyDict, PyDictMethods},
22};
23
24use crate::{
25 instruments::{
26 BettingInstrument, BinaryOption, Cfd, Commodity, CryptoFuture, CryptoFuturesSpread,
27 CryptoOptionSpread, CryptoPerpetual, CurrencyPair, Equity, FuturesContract, FuturesSpread,
28 IndexInstrument, InstrumentAny, OptionContract, OptionSpread, PerpetualContract,
29 TokenizedAsset, crypto_option::CryptoOption,
30 },
31 types::{Currency, Money, Price, Quantity},
32};
33
34pub(crate) fn register_crypto_currencies_from_dict(
50 py: Python<'_>,
51 values: &Py<PyDict>,
52 fields: &[&str],
53) {
54 let dict = values.bind(py);
55 for field in fields {
56 if let Ok(Some(value)) = dict.get_item(field)
57 && let Ok(code) = value.extract::<String>()
58 {
59 let trimmed = code.trim();
60 if !trimmed.is_empty() {
61 let _ = Currency::get_or_create_crypto(trimmed);
62 }
63 }
64 }
65}
66
67macro_rules! impl_instrument_common_pymethods {
68 ($type:ty) => {
69 #[pyo3::pymethods]
70 impl $type {
71 fn __repr__(&self) -> String {
72 use crate::instruments::Instrument;
73 format!(
74 "{}(id={}, price_precision={}, size_precision={})",
75 stringify!($type),
76 self.id(),
77 self.price_precision(),
78 self.size_precision(),
79 )
80 }
81
82 #[pyo3(name = "make_price")]
84 fn py_make_price(&self, value: f64) -> pyo3::PyResult<Price> {
85 use crate::instruments::Instrument;
86 self.try_make_price(value)
87 .map_err(nautilus_core::python::to_pyvalue_err)
88 }
89
90 #[pyo3(name = "make_qty")]
92 #[pyo3(signature = (value, round_down=false))]
93 fn py_make_qty(&self, value: f64, round_down: bool) -> pyo3::PyResult<Quantity> {
94 use crate::instruments::Instrument;
95 self.try_make_qty(value, Some(round_down))
96 .map_err(nautilus_core::python::to_pyvalue_err)
97 }
98
99 #[pyo3(name = "notional_value")]
101 #[pyo3(signature = (quantity, price, use_quote_for_inverse=false))]
102 fn py_notional_value(
103 &self,
104 quantity: Quantity,
105 price: Price,
106 use_quote_for_inverse: bool,
107 ) -> Money {
108 use crate::instruments::Instrument;
109 self.calculate_notional_value(quantity, price, Some(use_quote_for_inverse))
110 }
111 }
112 };
113}
114
115impl_instrument_common_pymethods!(BettingInstrument);
116impl_instrument_common_pymethods!(BinaryOption);
117impl_instrument_common_pymethods!(Cfd);
118impl_instrument_common_pymethods!(Commodity);
119impl_instrument_common_pymethods!(CryptoFuture);
120impl_instrument_common_pymethods!(CryptoFuturesSpread);
121impl_instrument_common_pymethods!(CryptoOption);
122impl_instrument_common_pymethods!(CryptoOptionSpread);
123impl_instrument_common_pymethods!(CryptoPerpetual);
124impl_instrument_common_pymethods!(CurrencyPair);
125impl_instrument_common_pymethods!(Equity);
126impl_instrument_common_pymethods!(FuturesContract);
127impl_instrument_common_pymethods!(FuturesSpread);
128impl_instrument_common_pymethods!(IndexInstrument);
129impl_instrument_common_pymethods!(OptionContract);
130impl_instrument_common_pymethods!(OptionSpread);
131impl_instrument_common_pymethods!(PerpetualContract);
132impl_instrument_common_pymethods!(TokenizedAsset);
133
134pub mod betting;
135pub mod binary_option;
136pub mod cfd;
137pub mod commodity;
138pub mod crypto_future;
139pub mod crypto_futures_spread;
140pub mod crypto_option;
141pub mod crypto_option_spread;
142pub mod crypto_perpetual;
143pub mod currency_pair;
144pub mod equity;
145pub mod futures_contract;
146pub mod futures_spread;
147pub mod index_instrument;
148pub mod option_contract;
149pub mod option_spread;
150pub mod perpetual_contract;
151pub mod synthetic;
152pub mod tokenized_asset;
153
154pub fn instrument_any_to_pyobject(py: Python, instrument: InstrumentAny) -> PyResult<Py<PyAny>> {
160 match instrument {
161 InstrumentAny::Betting(inst) => inst.into_py_any(py),
162 InstrumentAny::BinaryOption(inst) => inst.into_py_any(py),
163 InstrumentAny::Cfd(inst) => inst.into_py_any(py),
164 InstrumentAny::Commodity(inst) => inst.into_py_any(py),
165 InstrumentAny::CryptoFuture(inst) => inst.into_py_any(py),
166 InstrumentAny::CryptoFuturesSpread(inst) => inst.into_py_any(py),
167 InstrumentAny::CryptoOption(inst) => inst.into_py_any(py),
168 InstrumentAny::CryptoOptionSpread(inst) => inst.into_py_any(py),
169 InstrumentAny::CryptoPerpetual(inst) => inst.into_py_any(py),
170 InstrumentAny::CurrencyPair(inst) => inst.into_py_any(py),
171 InstrumentAny::Equity(inst) => inst.into_py_any(py),
172 InstrumentAny::FuturesContract(inst) => inst.into_py_any(py),
173 InstrumentAny::FuturesSpread(inst) => inst.into_py_any(py),
174 InstrumentAny::IndexInstrument(inst) => inst.into_py_any(py),
175 InstrumentAny::OptionContract(inst) => inst.into_py_any(py),
176 InstrumentAny::OptionSpread(inst) => inst.into_py_any(py),
177 InstrumentAny::PerpetualContract(inst) => inst.into_py_any(py),
178 InstrumentAny::TokenizedAsset(inst) => inst.into_py_any(py),
179 }
180}
181
182#[expect(clippy::needless_pass_by_value)]
188pub fn pyobject_to_instrument_any(py: Python, instrument: Py<PyAny>) -> PyResult<InstrumentAny> {
189 match instrument.getattr(py, "type_name")?.extract::<&str>(py)? {
190 stringify!(BettingInstrument) => Ok(InstrumentAny::Betting(
191 instrument.extract::<BettingInstrument>(py)?,
192 )),
193 stringify!(BinaryOption) => Ok(InstrumentAny::BinaryOption(
194 instrument.extract::<BinaryOption>(py)?,
195 )),
196 stringify!(Cfd) => Ok(InstrumentAny::Cfd(instrument.extract::<Cfd>(py)?)),
197 stringify!(Commodity) => Ok(InstrumentAny::Commodity(
198 instrument.extract::<Commodity>(py)?,
199 )),
200 stringify!(CryptoFuture) => Ok(InstrumentAny::CryptoFuture(
201 instrument.extract::<CryptoFuture>(py)?,
202 )),
203 stringify!(CryptoFuturesSpread) => Ok(InstrumentAny::CryptoFuturesSpread(
204 instrument.extract::<CryptoFuturesSpread>(py)?,
205 )),
206 stringify!(CryptoOption) => Ok(InstrumentAny::CryptoOption(
207 instrument.extract::<CryptoOption>(py)?,
208 )),
209 stringify!(CryptoOptionSpread) => Ok(InstrumentAny::CryptoOptionSpread(
210 instrument.extract::<CryptoOptionSpread>(py)?,
211 )),
212 stringify!(CryptoPerpetual) => Ok(InstrumentAny::CryptoPerpetual(
213 instrument.extract::<CryptoPerpetual>(py)?,
214 )),
215 stringify!(CurrencyPair) => Ok(InstrumentAny::CurrencyPair(
216 instrument.extract::<CurrencyPair>(py)?,
217 )),
218 stringify!(Equity) => Ok(InstrumentAny::Equity(instrument.extract::<Equity>(py)?)),
219 stringify!(FuturesContract) => Ok(InstrumentAny::FuturesContract(
220 instrument.extract::<FuturesContract>(py)?,
221 )),
222 stringify!(FuturesSpread) => Ok(InstrumentAny::FuturesSpread(
223 instrument.extract::<FuturesSpread>(py)?,
224 )),
225 stringify!(IndexInstrument) => Ok(InstrumentAny::IndexInstrument(
226 instrument.extract::<IndexInstrument>(py)?,
227 )),
228 stringify!(OptionContract) => Ok(InstrumentAny::OptionContract(
229 instrument.extract::<OptionContract>(py)?,
230 )),
231 stringify!(OptionSpread) => Ok(InstrumentAny::OptionSpread(
232 instrument.extract::<OptionSpread>(py)?,
233 )),
234 stringify!(PerpetualContract) => Ok(InstrumentAny::PerpetualContract(
235 instrument.extract::<PerpetualContract>(py)?,
236 )),
237 stringify!(TokenizedAsset) => Ok(InstrumentAny::TokenizedAsset(
238 instrument.extract::<TokenizedAsset>(py)?,
239 )),
240 _ => Err(to_pyvalue_err(
241 "Error in conversion from `Py<PyAny>` to `InstrumentAny`",
242 )),
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use pyo3::{prelude::*, types::PyDict};
249 use rstest::rstest;
250
251 use super::register_crypto_currencies_from_dict;
252 use crate::{enums::CurrencyType, types::Currency};
253
254 #[rstest]
255 fn test_register_crypto_currencies_from_dict_unknown_code() {
256 Python::initialize();
257 Python::attach(|py| {
258 let dict = PyDict::new(py);
259 dict.set_item("base_currency", "NEWHLP1").unwrap();
260 let values: Py<PyDict> = dict.unbind();
261
262 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
263
264 let created = Currency::try_from_str("NEWHLP1").unwrap();
265 assert_eq!(created.precision, 8);
266 assert_eq!(created.currency_type, CurrencyType::Crypto);
267 });
268 }
269
270 #[rstest]
271 fn test_register_crypto_currencies_from_dict_known_code_not_overwritten() {
272 Python::initialize();
273 Python::attach(|py| {
274 let dict = PyDict::new(py);
275 dict.set_item("quote_currency", "USD").unwrap();
276 let values: Py<PyDict> = dict.unbind();
277
278 register_crypto_currencies_from_dict(py, &values, &["quote_currency"]);
279
280 let usd = Currency::try_from_str("USD").unwrap();
281 assert_eq!(usd.precision, 2);
282 assert_eq!(usd.currency_type, CurrencyType::Fiat);
283 });
284 }
285
286 #[rstest]
287 fn test_register_crypto_currencies_from_dict_missing_key() {
288 Python::initialize();
289 Python::attach(|py| {
290 let dict = PyDict::new(py);
291 let values: Py<PyDict> = dict.unbind();
292
293 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
294
295 assert!(Currency::try_from_str("base_currency").is_none());
296 });
297 }
298
299 #[rstest]
300 fn test_register_crypto_currencies_from_dict_non_string_value() {
301 Python::initialize();
302 Python::attach(|py| {
303 let dict = PyDict::new(py);
304 dict.set_item("base_currency", 42).unwrap();
305 let values: Py<PyDict> = dict.unbind();
306
307 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
308
309 assert!(Currency::try_from_str("42").is_none());
310 });
311 }
312
313 #[rstest]
314 fn test_register_crypto_currencies_from_dict_trims_padding() {
315 Python::initialize();
318 Python::attach(|py| {
319 let dict = PyDict::new(py);
320 dict.set_item("base_currency", " NEWHLP2 ").unwrap();
321 let values: Py<PyDict> = dict.unbind();
322
323 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
324
325 assert!(Currency::try_from_str("NEWHLP2").is_some());
326 assert!(Currency::try_from_str(" NEWHLP2 ").is_none());
327 });
328 }
329
330 #[rstest]
331 fn test_register_crypto_currencies_from_dict_blank_code_skipped() {
332 Python::initialize();
335 Python::attach(|py| {
336 let dict = PyDict::new(py);
337 dict.set_item("base_currency", "").unwrap();
338 dict.set_item("quote_currency", " ").unwrap();
339 let values: Py<PyDict> = dict.unbind();
340
341 register_crypto_currencies_from_dict(py, &values, &["base_currency", "quote_currency"]);
342
343 assert!(Currency::try_from_str("").is_none());
344 assert!(Currency::try_from_str(" ").is_none());
345 });
346 }
347}