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, CryptoPerpetual,
27 CurrencyPair, Equity, FuturesContract, FuturesSpread, IndexInstrument, InstrumentAny,
28 OptionContract, OptionSpread, PerpetualContract, TokenizedAsset,
29 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!(CryptoOption);
121impl_instrument_common_pymethods!(CryptoPerpetual);
122impl_instrument_common_pymethods!(CurrencyPair);
123impl_instrument_common_pymethods!(Equity);
124impl_instrument_common_pymethods!(FuturesContract);
125impl_instrument_common_pymethods!(FuturesSpread);
126impl_instrument_common_pymethods!(IndexInstrument);
127impl_instrument_common_pymethods!(OptionContract);
128impl_instrument_common_pymethods!(OptionSpread);
129impl_instrument_common_pymethods!(PerpetualContract);
130impl_instrument_common_pymethods!(TokenizedAsset);
131
132pub mod betting;
133pub mod binary_option;
134pub mod cfd;
135pub mod commodity;
136pub mod crypto_future;
137pub mod crypto_option;
138pub mod crypto_perpetual;
139pub mod currency_pair;
140pub mod equity;
141pub mod futures_contract;
142pub mod futures_spread;
143pub mod index_instrument;
144pub mod option_contract;
145pub mod option_spread;
146pub mod perpetual_contract;
147pub mod synthetic;
148pub mod tokenized_asset;
149
150pub fn instrument_any_to_pyobject(py: Python, instrument: InstrumentAny) -> PyResult<Py<PyAny>> {
156 match instrument {
157 InstrumentAny::Betting(inst) => inst.into_py_any(py),
158 InstrumentAny::BinaryOption(inst) => inst.into_py_any(py),
159 InstrumentAny::Cfd(inst) => inst.into_py_any(py),
160 InstrumentAny::Commodity(inst) => inst.into_py_any(py),
161 InstrumentAny::CryptoFuture(inst) => inst.into_py_any(py),
162 InstrumentAny::CryptoOption(inst) => inst.into_py_any(py),
163 InstrumentAny::CryptoPerpetual(inst) => inst.into_py_any(py),
164 InstrumentAny::CurrencyPair(inst) => inst.into_py_any(py),
165 InstrumentAny::Equity(inst) => inst.into_py_any(py),
166 InstrumentAny::FuturesContract(inst) => inst.into_py_any(py),
167 InstrumentAny::FuturesSpread(inst) => inst.into_py_any(py),
168 InstrumentAny::IndexInstrument(inst) => inst.into_py_any(py),
169 InstrumentAny::OptionContract(inst) => inst.into_py_any(py),
170 InstrumentAny::OptionSpread(inst) => inst.into_py_any(py),
171 InstrumentAny::PerpetualContract(inst) => inst.into_py_any(py),
172 InstrumentAny::TokenizedAsset(inst) => inst.into_py_any(py),
173 }
174}
175
176#[expect(clippy::needless_pass_by_value)]
182pub fn pyobject_to_instrument_any(py: Python, instrument: Py<PyAny>) -> PyResult<InstrumentAny> {
183 match instrument.getattr(py, "type_name")?.extract::<&str>(py)? {
184 stringify!(BettingInstrument) => Ok(InstrumentAny::Betting(
185 instrument.extract::<BettingInstrument>(py)?,
186 )),
187 stringify!(BinaryOption) => Ok(InstrumentAny::BinaryOption(
188 instrument.extract::<BinaryOption>(py)?,
189 )),
190 stringify!(Cfd) => Ok(InstrumentAny::Cfd(instrument.extract::<Cfd>(py)?)),
191 stringify!(Commodity) => Ok(InstrumentAny::Commodity(
192 instrument.extract::<Commodity>(py)?,
193 )),
194 stringify!(CryptoFuture) => Ok(InstrumentAny::CryptoFuture(
195 instrument.extract::<CryptoFuture>(py)?,
196 )),
197 stringify!(CryptoOption) => Ok(InstrumentAny::CryptoOption(
198 instrument.extract::<CryptoOption>(py)?,
199 )),
200 stringify!(CryptoPerpetual) => Ok(InstrumentAny::CryptoPerpetual(
201 instrument.extract::<CryptoPerpetual>(py)?,
202 )),
203 stringify!(CurrencyPair) => Ok(InstrumentAny::CurrencyPair(
204 instrument.extract::<CurrencyPair>(py)?,
205 )),
206 stringify!(Equity) => Ok(InstrumentAny::Equity(instrument.extract::<Equity>(py)?)),
207 stringify!(FuturesContract) => Ok(InstrumentAny::FuturesContract(
208 instrument.extract::<FuturesContract>(py)?,
209 )),
210 stringify!(FuturesSpread) => Ok(InstrumentAny::FuturesSpread(
211 instrument.extract::<FuturesSpread>(py)?,
212 )),
213 stringify!(IndexInstrument) => Ok(InstrumentAny::IndexInstrument(
214 instrument.extract::<IndexInstrument>(py)?,
215 )),
216 stringify!(OptionContract) => Ok(InstrumentAny::OptionContract(
217 instrument.extract::<OptionContract>(py)?,
218 )),
219 stringify!(OptionSpread) => Ok(InstrumentAny::OptionSpread(
220 instrument.extract::<OptionSpread>(py)?,
221 )),
222 stringify!(PerpetualContract) => Ok(InstrumentAny::PerpetualContract(
223 instrument.extract::<PerpetualContract>(py)?,
224 )),
225 stringify!(TokenizedAsset) => Ok(InstrumentAny::TokenizedAsset(
226 instrument.extract::<TokenizedAsset>(py)?,
227 )),
228 _ => Err(to_pyvalue_err(
229 "Error in conversion from `Py<PyAny>` to `InstrumentAny`",
230 )),
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use pyo3::{prelude::*, types::PyDict};
237 use rstest::rstest;
238
239 use super::register_crypto_currencies_from_dict;
240 use crate::{enums::CurrencyType, types::Currency};
241
242 #[rstest]
243 fn test_register_crypto_currencies_from_dict_unknown_code() {
244 Python::initialize();
245 Python::attach(|py| {
246 let dict = PyDict::new(py);
247 dict.set_item("base_currency", "NEWHLP1").unwrap();
248 let values: Py<PyDict> = dict.unbind();
249
250 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
251
252 let created = Currency::try_from_str("NEWHLP1").unwrap();
253 assert_eq!(created.precision, 8);
254 assert_eq!(created.currency_type, CurrencyType::Crypto);
255 });
256 }
257
258 #[rstest]
259 fn test_register_crypto_currencies_from_dict_known_code_not_overwritten() {
260 Python::initialize();
261 Python::attach(|py| {
262 let dict = PyDict::new(py);
263 dict.set_item("quote_currency", "USD").unwrap();
264 let values: Py<PyDict> = dict.unbind();
265
266 register_crypto_currencies_from_dict(py, &values, &["quote_currency"]);
267
268 let usd = Currency::try_from_str("USD").unwrap();
269 assert_eq!(usd.precision, 2);
270 assert_eq!(usd.currency_type, CurrencyType::Fiat);
271 });
272 }
273
274 #[rstest]
275 fn test_register_crypto_currencies_from_dict_missing_key() {
276 Python::initialize();
277 Python::attach(|py| {
278 let dict = PyDict::new(py);
279 let values: Py<PyDict> = dict.unbind();
280
281 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
282
283 assert!(Currency::try_from_str("base_currency").is_none());
284 });
285 }
286
287 #[rstest]
288 fn test_register_crypto_currencies_from_dict_non_string_value() {
289 Python::initialize();
290 Python::attach(|py| {
291 let dict = PyDict::new(py);
292 dict.set_item("base_currency", 42).unwrap();
293 let values: Py<PyDict> = dict.unbind();
294
295 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
296
297 assert!(Currency::try_from_str("42").is_none());
298 });
299 }
300
301 #[rstest]
302 fn test_register_crypto_currencies_from_dict_trims_padding() {
303 Python::initialize();
306 Python::attach(|py| {
307 let dict = PyDict::new(py);
308 dict.set_item("base_currency", " NEWHLP2 ").unwrap();
309 let values: Py<PyDict> = dict.unbind();
310
311 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
312
313 assert!(Currency::try_from_str("NEWHLP2").is_some());
314 assert!(Currency::try_from_str(" NEWHLP2 ").is_none());
315 });
316 }
317
318 #[rstest]
319 fn test_register_crypto_currencies_from_dict_blank_code_skipped() {
320 Python::initialize();
323 Python::attach(|py| {
324 let dict = PyDict::new(py);
325 dict.set_item("base_currency", "").unwrap();
326 dict.set_item("quote_currency", " ").unwrap();
327 let values: Py<PyDict> = dict.unbind();
328
329 register_crypto_currencies_from_dict(py, &values, &["base_currency", "quote_currency"]);
330
331 assert!(Currency::try_from_str("").is_none());
332 assert!(Currency::try_from_str(" ").is_none());
333 });
334 }
335}