1use nautilus_core::python::{serialization::from_dict_pyo3, to_pyvalue_err};
19use pyo3::{
20 IntoPyObjectExt, Py, PyAny, PyResult, Python,
21 types::{PyAnyMethods, PyDict, PyDictMethods},
22};
23use serde::de::DeserializeOwned;
24
25use crate::{
26 instruments::{
27 BettingInstrument, BinaryOption, Cfd, Commodity, CryptoFuture, CryptoFuturesSpread,
28 CryptoOptionSpread, CryptoPerpetual, CurrencyPair, Equity, FuturesContract, FuturesSpread,
29 IndexInstrument, Instrument, InstrumentAny, OptionContract, OptionSpread,
30 PerpetualContract, TokenizedAsset, crypto_option::CryptoOption,
31 },
32 types::{Currency, Money, Price, Quantity},
33};
34
35pub(crate) fn register_crypto_currencies_from_dict(
51 py: Python<'_>,
52 values: &Py<PyDict>,
53 fields: &[&str],
54) {
55 let dict = values.bind(py);
56 for field in fields {
57 if let Ok(Some(value)) = dict.get_item(field)
58 && let Ok(code) = value.extract::<String>()
59 {
60 let trimmed = code.trim();
61 if !trimmed.is_empty() {
62 let _ = Currency::get_or_create_crypto(trimmed);
63 }
64 }
65 }
66}
67
68pub(crate) fn tick_scheme_to_py(instrument: &impl Instrument) -> Option<String> {
69 instrument.tick_scheme().map(|name| name.to_string())
70}
71
72pub(crate) fn from_dict_instrument_pyo3<T>(py: Python<'_>, values: Py<PyDict>) -> PyResult<T>
73where
74 T: DeserializeOwned,
75{
76 let values = instrument_dict_with_tick_scheme_alias(py, values)?;
77 from_dict_pyo3(py, values)
78}
79
80fn instrument_dict_with_tick_scheme_alias(
81 py: Python<'_>,
82 values: Py<PyDict>,
83) -> PyResult<Py<PyDict>> {
84 let dict = values.bind(py);
85 if dict.contains("tick_scheme")? || !dict.contains("tick_scheme_name")? {
86 return Ok(values);
87 }
88
89 let dict = dict.copy()?;
90 if let Some(value) = dict.get_item("tick_scheme_name")? {
91 dict.set_item("tick_scheme", value)?;
92 }
93 Ok(dict.unbind())
94}
95
96macro_rules! impl_instrument_common_pymethods {
97 ($type:ty) => {
98 #[pyo3::pymethods]
99 impl $type {
100 fn __repr__(&self) -> String {
101 use crate::instruments::Instrument;
102 format!(
103 "{}(id={}, price_precision={}, size_precision={})",
104 stringify!($type),
105 self.id(),
106 self.price_precision(),
107 self.size_precision(),
108 )
109 }
110
111 #[getter]
112 #[pyo3(name = "tick_scheme")]
113 fn py_tick_scheme(&self) -> Option<String> {
114 use crate::instruments::Instrument;
115 self.tick_scheme().map(|name| name.to_string())
116 }
117
118 #[pyo3(name = "next_bid_price")]
120 #[pyo3(signature = (value, num_ticks=0))]
121 fn py_next_bid_price(&self, value: f64, num_ticks: i32) -> Option<Price> {
122 use crate::instruments::Instrument;
123 self.next_bid_price(value, num_ticks)
124 }
125
126 #[pyo3(name = "next_ask_price")]
128 #[pyo3(signature = (value, num_ticks=0))]
129 fn py_next_ask_price(&self, value: f64, num_ticks: i32) -> Option<Price> {
130 use crate::instruments::Instrument;
131 self.next_ask_price(value, num_ticks)
132 }
133
134 #[pyo3(name = "next_bid_prices")]
136 #[pyo3(signature = (value, num_ticks=100))]
137 fn py_next_bid_prices(
138 &self,
139 value: f64,
140 num_ticks: usize,
141 ) -> Vec<rust_decimal::Decimal> {
142 use crate::instruments::Instrument;
143 self.next_bid_prices(value, num_ticks)
144 .into_iter()
145 .map(|price| price.as_decimal())
146 .collect()
147 }
148
149 #[pyo3(name = "next_ask_prices")]
151 #[pyo3(signature = (value, num_ticks=100))]
152 fn py_next_ask_prices(
153 &self,
154 value: f64,
155 num_ticks: usize,
156 ) -> Vec<rust_decimal::Decimal> {
157 use crate::instruments::Instrument;
158 self.next_ask_prices(value, num_ticks)
159 .into_iter()
160 .map(|price| price.as_decimal())
161 .collect()
162 }
163
164 #[pyo3(name = "make_price")]
166 fn py_make_price(&self, value: f64) -> pyo3::PyResult<Price> {
167 use crate::instruments::Instrument;
168 self.try_make_price(value)
169 .map_err(nautilus_core::python::to_pyvalue_err)
170 }
171
172 #[pyo3(name = "make_qty")]
174 #[pyo3(signature = (value, round_down=false))]
175 fn py_make_qty(&self, value: f64, round_down: bool) -> pyo3::PyResult<Quantity> {
176 use crate::instruments::Instrument;
177 self.try_make_qty(value, Some(round_down))
178 .map_err(nautilus_core::python::to_pyvalue_err)
179 }
180
181 #[pyo3(name = "notional_value")]
183 #[pyo3(signature = (quantity, price, use_quote_for_inverse=false))]
184 fn py_notional_value(
185 &self,
186 quantity: Quantity,
187 price: Price,
188 use_quote_for_inverse: bool,
189 ) -> Money {
190 use crate::instruments::Instrument;
191 self.calculate_notional_value(quantity, price, Some(use_quote_for_inverse))
192 }
193 }
194 };
195}
196
197impl_instrument_common_pymethods!(BettingInstrument);
198impl_instrument_common_pymethods!(BinaryOption);
199impl_instrument_common_pymethods!(Cfd);
200impl_instrument_common_pymethods!(Commodity);
201impl_instrument_common_pymethods!(CryptoFuture);
202impl_instrument_common_pymethods!(CryptoFuturesSpread);
203impl_instrument_common_pymethods!(CryptoOption);
204impl_instrument_common_pymethods!(CryptoOptionSpread);
205impl_instrument_common_pymethods!(CryptoPerpetual);
206impl_instrument_common_pymethods!(CurrencyPair);
207impl_instrument_common_pymethods!(Equity);
208impl_instrument_common_pymethods!(FuturesContract);
209impl_instrument_common_pymethods!(FuturesSpread);
210impl_instrument_common_pymethods!(IndexInstrument);
211impl_instrument_common_pymethods!(OptionContract);
212impl_instrument_common_pymethods!(OptionSpread);
213impl_instrument_common_pymethods!(PerpetualContract);
214impl_instrument_common_pymethods!(TokenizedAsset);
215
216pub mod betting;
217pub mod binary_option;
218pub mod cfd;
219pub mod commodity;
220pub mod crypto_future;
221pub mod crypto_futures_spread;
222pub mod crypto_option;
223pub mod crypto_option_spread;
224pub mod crypto_perpetual;
225pub mod currency_pair;
226pub mod equity;
227pub mod futures_contract;
228pub mod futures_spread;
229pub mod index_instrument;
230pub mod option_contract;
231pub mod option_spread;
232pub mod perpetual_contract;
233pub mod synthetic;
234pub mod tokenized_asset;
235
236pub fn instrument_any_to_pyobject(py: Python, instrument: InstrumentAny) -> PyResult<Py<PyAny>> {
242 match instrument {
243 InstrumentAny::Betting(inst) => inst.into_py_any(py),
244 InstrumentAny::BinaryOption(inst) => inst.into_py_any(py),
245 InstrumentAny::Cfd(inst) => inst.into_py_any(py),
246 InstrumentAny::Commodity(inst) => inst.into_py_any(py),
247 InstrumentAny::CryptoFuture(inst) => inst.into_py_any(py),
248 InstrumentAny::CryptoFuturesSpread(inst) => inst.into_py_any(py),
249 InstrumentAny::CryptoOption(inst) => inst.into_py_any(py),
250 InstrumentAny::CryptoOptionSpread(inst) => inst.into_py_any(py),
251 InstrumentAny::CryptoPerpetual(inst) => inst.into_py_any(py),
252 InstrumentAny::CurrencyPair(inst) => inst.into_py_any(py),
253 InstrumentAny::Equity(inst) => inst.into_py_any(py),
254 InstrumentAny::FuturesContract(inst) => inst.into_py_any(py),
255 InstrumentAny::FuturesSpread(inst) => inst.into_py_any(py),
256 InstrumentAny::IndexInstrument(inst) => inst.into_py_any(py),
257 InstrumentAny::OptionContract(inst) => inst.into_py_any(py),
258 InstrumentAny::OptionSpread(inst) => inst.into_py_any(py),
259 InstrumentAny::PerpetualContract(inst) => inst.into_py_any(py),
260 InstrumentAny::TokenizedAsset(inst) => inst.into_py_any(py),
261 }
262}
263
264#[expect(clippy::needless_pass_by_value)]
270pub fn pyobject_to_instrument_any(py: Python, instrument: Py<PyAny>) -> PyResult<InstrumentAny> {
271 match instrument.getattr(py, "type_name")?.extract::<&str>(py)? {
272 stringify!(BettingInstrument) => Ok(InstrumentAny::Betting(
273 instrument.extract::<BettingInstrument>(py)?,
274 )),
275 stringify!(BinaryOption) => Ok(InstrumentAny::BinaryOption(
276 instrument.extract::<BinaryOption>(py)?,
277 )),
278 stringify!(Cfd) => Ok(InstrumentAny::Cfd(instrument.extract::<Cfd>(py)?)),
279 stringify!(Commodity) => Ok(InstrumentAny::Commodity(
280 instrument.extract::<Commodity>(py)?,
281 )),
282 stringify!(CryptoFuture) => Ok(InstrumentAny::CryptoFuture(
283 instrument.extract::<CryptoFuture>(py)?,
284 )),
285 stringify!(CryptoFuturesSpread) => Ok(InstrumentAny::CryptoFuturesSpread(
286 instrument.extract::<CryptoFuturesSpread>(py)?,
287 )),
288 stringify!(CryptoOption) => Ok(InstrumentAny::CryptoOption(
289 instrument.extract::<CryptoOption>(py)?,
290 )),
291 stringify!(CryptoOptionSpread) => Ok(InstrumentAny::CryptoOptionSpread(
292 instrument.extract::<CryptoOptionSpread>(py)?,
293 )),
294 stringify!(CryptoPerpetual) => Ok(InstrumentAny::CryptoPerpetual(
295 instrument.extract::<CryptoPerpetual>(py)?,
296 )),
297 stringify!(CurrencyPair) => Ok(InstrumentAny::CurrencyPair(
298 instrument.extract::<CurrencyPair>(py)?,
299 )),
300 stringify!(Equity) => Ok(InstrumentAny::Equity(instrument.extract::<Equity>(py)?)),
301 stringify!(FuturesContract) => Ok(InstrumentAny::FuturesContract(
302 instrument.extract::<FuturesContract>(py)?,
303 )),
304 stringify!(FuturesSpread) => Ok(InstrumentAny::FuturesSpread(
305 instrument.extract::<FuturesSpread>(py)?,
306 )),
307 stringify!(IndexInstrument) => Ok(InstrumentAny::IndexInstrument(
308 instrument.extract::<IndexInstrument>(py)?,
309 )),
310 stringify!(OptionContract) => Ok(InstrumentAny::OptionContract(
311 instrument.extract::<OptionContract>(py)?,
312 )),
313 stringify!(OptionSpread) => Ok(InstrumentAny::OptionSpread(
314 instrument.extract::<OptionSpread>(py)?,
315 )),
316 stringify!(PerpetualContract) => Ok(InstrumentAny::PerpetualContract(
317 instrument.extract::<PerpetualContract>(py)?,
318 )),
319 stringify!(TokenizedAsset) => Ok(InstrumentAny::TokenizedAsset(
320 instrument.extract::<TokenizedAsset>(py)?,
321 )),
322 _ => Err(to_pyvalue_err(
323 "Error in conversion from `Py<PyAny>` to `InstrumentAny`",
324 )),
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use pyo3::{prelude::*, types::PyDict};
331 use rstest::rstest;
332
333 use super::register_crypto_currencies_from_dict;
334 use crate::{enums::CurrencyType, types::Currency};
335
336 #[rstest]
337 fn test_register_crypto_currencies_from_dict_unknown_code() {
338 Python::initialize();
339 Python::attach(|py| {
340 let dict = PyDict::new(py);
341 dict.set_item("base_currency", "NEWHLP1").unwrap();
342 let values: Py<PyDict> = dict.unbind();
343
344 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
345
346 let created = Currency::try_from_str("NEWHLP1").unwrap();
347 assert_eq!(created.precision, 8);
348 assert_eq!(created.currency_type, CurrencyType::Crypto);
349 });
350 }
351
352 #[rstest]
353 fn test_register_crypto_currencies_from_dict_known_code_not_overwritten() {
354 Python::initialize();
355 Python::attach(|py| {
356 let dict = PyDict::new(py);
357 dict.set_item("quote_currency", "USD").unwrap();
358 let values: Py<PyDict> = dict.unbind();
359
360 register_crypto_currencies_from_dict(py, &values, &["quote_currency"]);
361
362 let usd = Currency::try_from_str("USD").unwrap();
363 assert_eq!(usd.precision, 2);
364 assert_eq!(usd.currency_type, CurrencyType::Fiat);
365 });
366 }
367
368 #[rstest]
369 fn test_register_crypto_currencies_from_dict_missing_key() {
370 Python::initialize();
371 Python::attach(|py| {
372 let dict = PyDict::new(py);
373 let values: Py<PyDict> = dict.unbind();
374
375 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
376
377 assert!(Currency::try_from_str("base_currency").is_none());
378 });
379 }
380
381 #[rstest]
382 fn test_register_crypto_currencies_from_dict_non_string_value() {
383 Python::initialize();
384 Python::attach(|py| {
385 let dict = PyDict::new(py);
386 dict.set_item("base_currency", 42).unwrap();
387 let values: Py<PyDict> = dict.unbind();
388
389 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
390
391 assert!(Currency::try_from_str("42").is_none());
392 });
393 }
394
395 #[rstest]
396 fn test_register_crypto_currencies_from_dict_trims_padding() {
397 Python::initialize();
400 Python::attach(|py| {
401 let dict = PyDict::new(py);
402 dict.set_item("base_currency", " NEWHLP2 ").unwrap();
403 let values: Py<PyDict> = dict.unbind();
404
405 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
406
407 assert!(Currency::try_from_str("NEWHLP2").is_some());
408 assert!(Currency::try_from_str(" NEWHLP2 ").is_none());
409 });
410 }
411
412 #[rstest]
413 fn test_register_crypto_currencies_from_dict_blank_code_skipped() {
414 Python::initialize();
417 Python::attach(|py| {
418 let dict = PyDict::new(py);
419 dict.set_item("base_currency", "").unwrap();
420 dict.set_item("quote_currency", " ").unwrap();
421 let values: Py<PyDict> = dict.unbind();
422
423 register_crypto_currencies_from_dict(py, &values, &["base_currency", "quote_currency"]);
424
425 assert!(Currency::try_from_str("").is_none());
426 assert!(Currency::try_from_str(" ").is_none());
427 });
428 }
429}