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 rust_decimal::Decimal;
24use serde::de::DeserializeOwned;
25
26use crate::{
27 enums::{AssetClass, InstrumentClass},
28 instruments::{
29 BettingInstrument, BinaryOption, Cfd, Commodity, CryptoFuture, CryptoFuturesSpread,
30 CryptoOptionSpread, CryptoPerpetual, CurrencyPair, Equity, FuturesContract, FuturesSpread,
31 IndexInstrument, Instrument, InstrumentAny, OptionContract, OptionSpread,
32 PerpetualContract, TokenizedAsset, crypto_option::CryptoOption,
33 },
34 types::{Currency, Money, Price, Quantity},
35};
36
37pub(crate) fn register_crypto_currencies_from_dict(
53 py: Python<'_>,
54 values: &Py<PyDict>,
55 fields: &[&str],
56) {
57 let dict = values.bind(py);
58 for field in fields {
59 if let Ok(Some(value)) = dict.get_item(field)
60 && let Ok(code) = value.extract::<String>()
61 {
62 let trimmed = code.trim();
63 if !trimmed.is_empty() {
64 let _ = Currency::get_or_create_crypto(trimmed);
65 }
66 }
67 }
68}
69
70pub(crate) fn tick_scheme_to_py(instrument: &impl Instrument) -> Option<String> {
71 instrument.tick_scheme().map(|name| name.to_string())
72}
73
74pub(crate) fn from_dict_instrument_pyo3<T>(py: Python<'_>, values: Py<PyDict>) -> PyResult<T>
75where
76 T: DeserializeOwned,
77{
78 let values = instrument_dict_with_tick_scheme_alias(py, values)?;
79 from_dict_pyo3(py, values)
80}
81
82fn instrument_dict_with_tick_scheme_alias(
83 py: Python<'_>,
84 values: Py<PyDict>,
85) -> PyResult<Py<PyDict>> {
86 let dict = values.bind(py);
87 if dict.contains("tick_scheme")? || !dict.contains("tick_scheme_name")? {
88 return Ok(values);
89 }
90
91 let dict = dict.copy()?;
92 if let Some(value) = dict.get_item("tick_scheme_name")? {
93 dict.set_item("tick_scheme", value)?;
94 }
95 Ok(dict.unbind())
96}
97
98macro_rules! impl_instrument_common_pymethods {
99 ($type:ty) => {
100 #[pyo3_stub_gen::derive::gen_stub_pymethods]
101 #[pyo3::pymethods]
102 impl $type {
103 fn __repr__(&self) -> String {
104 format!(
105 "{}(id={}, price_precision={}, size_precision={})",
106 stringify!($type),
107 self.id(),
108 self.price_precision(),
109 self.size_precision(),
110 )
111 }
112
113 #[getter]
114 #[pyo3(name = "tick_scheme")]
115 fn py_tick_scheme(&self) -> Option<String> {
116 self.tick_scheme().map(|name| name.to_string())
117 }
118
119 #[pyo3(name = "next_bid_price")]
121 #[pyo3(signature = (value, num_ticks=0))]
122 fn py_next_bid_price(&self, value: f64, num_ticks: i32) -> Option<Price> {
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 self.next_ask_price(value, num_ticks)
131 }
132
133 #[pyo3(name = "next_bid_prices")]
135 #[pyo3(signature = (value, num_ticks=100))]
136 fn py_next_bid_prices(
137 &self,
138 value: f64,
139 num_ticks: usize,
140 ) -> Vec<rust_decimal::Decimal> {
141 self.next_bid_prices(value, num_ticks)
142 .into_iter()
143 .map(|price| price.as_decimal())
144 .collect()
145 }
146
147 #[pyo3(name = "next_ask_prices")]
149 #[pyo3(signature = (value, num_ticks=100))]
150 fn py_next_ask_prices(
151 &self,
152 value: f64,
153 num_ticks: usize,
154 ) -> Vec<rust_decimal::Decimal> {
155 self.next_ask_prices(value, num_ticks)
156 .into_iter()
157 .map(|price| price.as_decimal())
158 .collect()
159 }
160
161 #[pyo3(name = "make_price")]
163 fn py_make_price(&self, value: f64) -> PyResult<Price> {
164 self.try_make_price(value)
165 .map_err(nautilus_core::python::to_pyvalue_err)
166 }
167
168 #[pyo3(name = "make_qty")]
170 #[pyo3(signature = (value, round_down=false))]
171 fn py_make_qty(&self, value: f64, round_down: bool) -> PyResult<Quantity> {
172 self.try_make_qty(value, Some(round_down))
173 .map_err(nautilus_core::python::to_pyvalue_err)
174 }
175
176 #[pyo3(name = "notional_value")]
178 #[pyo3(signature = (quantity, price, use_quote_for_inverse=false))]
179 fn py_notional_value(
180 &self,
181 quantity: Quantity,
182 price: Price,
183 use_quote_for_inverse: bool,
184 ) -> PyResult<Money> {
185 self.try_calculate_notional_value(quantity, price, Some(use_quote_for_inverse))
186 .map_err(nautilus_core::python::to_pyvalue_err)
187 }
188 }
189 };
190}
191
192macro_rules! impl_instrument_getter {
193 ($name:literal, $getter:ident, $return_type:ty, $method:ident, $($type:ty),+ $(,)?) => {
194 $(
195 #[pyo3_stub_gen::derive::gen_stub_pymethods]
196 #[pyo3::pymethods]
197 impl $type {
198 #[getter]
199 #[pyo3(name = $name)]
200 fn $getter(&self) -> $return_type {
201 Instrument::$method(self)
202 }
203 }
204 )+
205 };
206}
207
208macro_rules! impl_instrument_isin_getter {
209 ($($type:ty),+ $(,)?) => {
210 $(
211 #[pyo3_stub_gen::derive::gen_stub_pymethods]
212 #[pyo3::pymethods]
213 impl $type {
214 #[getter]
215 #[pyo3(name = "isin")]
216 fn py_isin(&self) -> Option<String> {
217 Instrument::isin(self).map(|value| value.to_string())
218 }
219 }
220 )+
221 };
222}
223
224impl_instrument_common_pymethods!(BettingInstrument);
225impl_instrument_common_pymethods!(BinaryOption);
226impl_instrument_common_pymethods!(Cfd);
227impl_instrument_common_pymethods!(Commodity);
228impl_instrument_common_pymethods!(CryptoFuture);
229impl_instrument_common_pymethods!(CryptoFuturesSpread);
230impl_instrument_common_pymethods!(CryptoOption);
231impl_instrument_common_pymethods!(CryptoOptionSpread);
232impl_instrument_common_pymethods!(CryptoPerpetual);
233impl_instrument_common_pymethods!(CurrencyPair);
234impl_instrument_common_pymethods!(Equity);
235impl_instrument_common_pymethods!(FuturesContract);
236impl_instrument_common_pymethods!(FuturesSpread);
237impl_instrument_common_pymethods!(IndexInstrument);
238impl_instrument_common_pymethods!(OptionContract);
239impl_instrument_common_pymethods!(OptionSpread);
240impl_instrument_common_pymethods!(PerpetualContract);
241impl_instrument_common_pymethods!(TokenizedAsset);
242
243impl_instrument_getter!(
244 "asset_class",
245 py_asset_class,
246 AssetClass,
247 asset_class,
248 CryptoFuture,
249 CryptoFuturesSpread,
250 CryptoOption,
251 CryptoOptionSpread,
252 CryptoPerpetual,
253 CurrencyPair,
254 Equity,
255 IndexInstrument,
256);
257impl_instrument_getter!(
258 "instrument_class",
259 py_instrument_class,
260 InstrumentClass,
261 instrument_class,
262 BinaryOption,
263 Cfd,
264 Commodity,
265 CryptoFuture,
266 CryptoFuturesSpread,
267 CryptoOption,
268 CryptoOptionSpread,
269 CryptoPerpetual,
270 CurrencyPair,
271 Equity,
272 FuturesContract,
273 FuturesSpread,
274 IndexInstrument,
275 OptionContract,
276 OptionSpread,
277 PerpetualContract,
278 TokenizedAsset,
279);
280impl_instrument_getter!(
281 "is_inverse",
282 py_is_inverse,
283 bool,
284 is_inverse,
285 BettingInstrument,
286 BinaryOption,
287 Cfd,
288 Commodity,
289 CurrencyPair,
290 Equity,
291 FuturesContract,
292 FuturesSpread,
293 IndexInstrument,
294 OptionContract,
295 OptionSpread,
296 TokenizedAsset,
297);
298impl_instrument_getter!(
299 "is_quanto",
300 py_is_quanto,
301 bool,
302 is_quanto,
303 BettingInstrument,
304 BinaryOption,
305 Cfd,
306 Commodity,
307 CryptoFuture,
308 CryptoFuturesSpread,
309 CryptoOption,
310 CryptoOptionSpread,
311 CryptoPerpetual,
312 CurrencyPair,
313 Equity,
314 FuturesContract,
315 FuturesSpread,
316 IndexInstrument,
317 OptionContract,
318 OptionSpread,
319 PerpetualContract,
320 TokenizedAsset,
321);
322impl_instrument_isin_getter!(
323 BettingInstrument,
324 BinaryOption,
325 Cfd,
326 Commodity,
327 CryptoFuture,
328 CryptoFuturesSpread,
329 CryptoOption,
330 CryptoOptionSpread,
331 CryptoPerpetual,
332 CurrencyPair,
333 FuturesContract,
334 FuturesSpread,
335 IndexInstrument,
336 OptionContract,
337 OptionSpread,
338 PerpetualContract,
339);
340impl_instrument_getter!(
341 "lot_size",
342 py_lot_size,
343 Option<Quantity>,
344 lot_size,
345 BettingInstrument,
346 BinaryOption,
347 IndexInstrument,
348);
349impl_instrument_getter!(
350 "maker_fee",
351 py_maker_fee,
352 Decimal,
353 maker_fee,
354 IndexInstrument
355);
356impl_instrument_getter!(
357 "margin_init",
358 py_margin_init,
359 Decimal,
360 margin_init,
361 BettingInstrument,
362 IndexInstrument,
363);
364impl_instrument_getter!(
365 "margin_maint",
366 py_margin_maint,
367 Decimal,
368 margin_maint,
369 BettingInstrument,
370 IndexInstrument,
371);
372impl_instrument_getter!(
373 "max_notional",
374 py_max_notional,
375 Option<Money>,
376 max_notional,
377 Equity,
378 FuturesContract,
379 FuturesSpread,
380 IndexInstrument,
381 OptionContract,
382 OptionSpread,
383);
384impl_instrument_getter!(
385 "max_price",
386 py_max_price,
387 Option<Price>,
388 max_price,
389 IndexInstrument
390);
391impl_instrument_getter!(
392 "max_quantity",
393 py_max_quantity,
394 Option<Quantity>,
395 max_quantity,
396 IndexInstrument,
397);
398impl_instrument_getter!(
399 "min_notional",
400 py_min_notional,
401 Option<Money>,
402 min_notional,
403 Equity,
404 FuturesContract,
405 FuturesSpread,
406 IndexInstrument,
407 OptionContract,
408 OptionSpread,
409);
410impl_instrument_getter!(
411 "min_price",
412 py_min_price,
413 Option<Price>,
414 min_price,
415 IndexInstrument
416);
417impl_instrument_getter!(
418 "min_quantity",
419 py_min_quantity,
420 Option<Quantity>,
421 min_quantity,
422 IndexInstrument,
423);
424impl_instrument_getter!(
425 "multiplier",
426 py_multiplier,
427 Quantity,
428 multiplier,
429 BettingInstrument,
430 BinaryOption,
431 Cfd,
432 Commodity,
433 Equity,
434 IndexInstrument,
435);
436impl_instrument_getter!(
437 "quote_currency",
438 py_quote_currency,
439 Currency,
440 quote_currency,
441 BettingInstrument,
442 BinaryOption,
443 FuturesContract,
444 FuturesSpread,
445 OptionContract,
446 OptionSpread,
447);
448impl_instrument_getter!(
449 "taker_fee",
450 py_taker_fee,
451 Decimal,
452 taker_fee,
453 IndexInstrument
454);
455
456pub mod betting;
457pub mod binary_option;
458pub mod cfd;
459pub mod commodity;
460pub mod crypto_future;
461pub mod crypto_futures_spread;
462pub mod crypto_option;
463pub mod crypto_option_spread;
464pub mod crypto_perpetual;
465pub mod currency_pair;
466pub mod equity;
467pub mod futures_contract;
468pub mod futures_spread;
469pub mod index_instrument;
470pub mod option_contract;
471pub mod option_spread;
472pub mod perpetual_contract;
473pub mod synthetic;
474pub mod tokenized_asset;
475
476pub fn instrument_any_to_pyobject(py: Python, instrument: InstrumentAny) -> PyResult<Py<PyAny>> {
482 match instrument {
483 InstrumentAny::Betting(inst) => inst.into_py_any(py),
484 InstrumentAny::BinaryOption(inst) => inst.into_py_any(py),
485 InstrumentAny::Cfd(inst) => inst.into_py_any(py),
486 InstrumentAny::Commodity(inst) => inst.into_py_any(py),
487 InstrumentAny::CryptoFuture(inst) => inst.into_py_any(py),
488 InstrumentAny::CryptoFuturesSpread(inst) => inst.into_py_any(py),
489 InstrumentAny::CryptoOption(inst) => inst.into_py_any(py),
490 InstrumentAny::CryptoOptionSpread(inst) => inst.into_py_any(py),
491 InstrumentAny::CryptoPerpetual(inst) => inst.into_py_any(py),
492 InstrumentAny::CurrencyPair(inst) => inst.into_py_any(py),
493 InstrumentAny::Equity(inst) => inst.into_py_any(py),
494 InstrumentAny::FuturesContract(inst) => inst.into_py_any(py),
495 InstrumentAny::FuturesSpread(inst) => inst.into_py_any(py),
496 InstrumentAny::IndexInstrument(inst) => inst.into_py_any(py),
497 InstrumentAny::OptionContract(inst) => inst.into_py_any(py),
498 InstrumentAny::OptionSpread(inst) => inst.into_py_any(py),
499 InstrumentAny::PerpetualContract(inst) => inst.into_py_any(py),
500 InstrumentAny::TokenizedAsset(inst) => inst.into_py_any(py),
501 }
502}
503
504#[expect(clippy::needless_pass_by_value)]
510pub fn pyobject_to_instrument_any(py: Python, instrument: Py<PyAny>) -> PyResult<InstrumentAny> {
511 match instrument.getattr(py, "type_name")?.extract::<&str>(py)? {
512 stringify!(BettingInstrument) => Ok(InstrumentAny::Betting(
513 instrument.extract::<BettingInstrument>(py)?,
514 )),
515 stringify!(BinaryOption) => Ok(InstrumentAny::BinaryOption(
516 instrument.extract::<BinaryOption>(py)?,
517 )),
518 stringify!(Cfd) => Ok(InstrumentAny::Cfd(instrument.extract::<Cfd>(py)?)),
519 stringify!(Commodity) => Ok(InstrumentAny::Commodity(
520 instrument.extract::<Commodity>(py)?,
521 )),
522 stringify!(CryptoFuture) => Ok(InstrumentAny::CryptoFuture(
523 instrument.extract::<CryptoFuture>(py)?,
524 )),
525 stringify!(CryptoFuturesSpread) => Ok(InstrumentAny::CryptoFuturesSpread(
526 instrument.extract::<CryptoFuturesSpread>(py)?,
527 )),
528 stringify!(CryptoOption) => Ok(InstrumentAny::CryptoOption(
529 instrument.extract::<CryptoOption>(py)?,
530 )),
531 stringify!(CryptoOptionSpread) => Ok(InstrumentAny::CryptoOptionSpread(
532 instrument.extract::<CryptoOptionSpread>(py)?,
533 )),
534 stringify!(CryptoPerpetual) => Ok(InstrumentAny::CryptoPerpetual(
535 instrument.extract::<CryptoPerpetual>(py)?,
536 )),
537 stringify!(CurrencyPair) => Ok(InstrumentAny::CurrencyPair(
538 instrument.extract::<CurrencyPair>(py)?,
539 )),
540 stringify!(Equity) => Ok(InstrumentAny::Equity(instrument.extract::<Equity>(py)?)),
541 stringify!(FuturesContract) => Ok(InstrumentAny::FuturesContract(
542 instrument.extract::<FuturesContract>(py)?,
543 )),
544 stringify!(FuturesSpread) => Ok(InstrumentAny::FuturesSpread(
545 instrument.extract::<FuturesSpread>(py)?,
546 )),
547 stringify!(IndexInstrument) => Ok(InstrumentAny::IndexInstrument(
548 instrument.extract::<IndexInstrument>(py)?,
549 )),
550 stringify!(OptionContract) => Ok(InstrumentAny::OptionContract(
551 instrument.extract::<OptionContract>(py)?,
552 )),
553 stringify!(OptionSpread) => Ok(InstrumentAny::OptionSpread(
554 instrument.extract::<OptionSpread>(py)?,
555 )),
556 stringify!(PerpetualContract) => Ok(InstrumentAny::PerpetualContract(
557 instrument.extract::<PerpetualContract>(py)?,
558 )),
559 stringify!(TokenizedAsset) => Ok(InstrumentAny::TokenizedAsset(
560 instrument.extract::<TokenizedAsset>(py)?,
561 )),
562 _ => Err(to_pyvalue_err(
563 "Error in conversion from `Py<PyAny>` to `InstrumentAny`",
564 )),
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use pyo3::{prelude::*, types::PyDict};
571 use rstest::rstest;
572
573 use super::register_crypto_currencies_from_dict;
574 use crate::{enums::CurrencyType, types::Currency};
575
576 #[rstest]
577 fn test_register_crypto_currencies_from_dict_unknown_code() {
578 Python::initialize();
579 Python::attach(|py| {
580 let dict = PyDict::new(py);
581 dict.set_item("base_currency", "NEWHLP1").unwrap();
582 let values: Py<PyDict> = dict.unbind();
583
584 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
585
586 let created = Currency::try_from_str("NEWHLP1").unwrap();
587 assert_eq!(created.precision, 8);
588 assert_eq!(created.currency_type, CurrencyType::Crypto);
589 });
590 }
591
592 #[rstest]
593 fn test_register_crypto_currencies_from_dict_known_code_not_overwritten() {
594 Python::initialize();
595 Python::attach(|py| {
596 let dict = PyDict::new(py);
597 dict.set_item("quote_currency", "USD").unwrap();
598 let values: Py<PyDict> = dict.unbind();
599
600 register_crypto_currencies_from_dict(py, &values, &["quote_currency"]);
601
602 let usd = Currency::try_from_str("USD").unwrap();
603 assert_eq!(usd.precision, 2);
604 assert_eq!(usd.currency_type, CurrencyType::Fiat);
605 });
606 }
607
608 #[rstest]
609 fn test_register_crypto_currencies_from_dict_missing_key() {
610 Python::initialize();
611 Python::attach(|py| {
612 let dict = PyDict::new(py);
613 let values: Py<PyDict> = dict.unbind();
614
615 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
616
617 assert!(Currency::try_from_str("base_currency").is_none());
618 });
619 }
620
621 #[rstest]
622 fn test_register_crypto_currencies_from_dict_non_string_value() {
623 Python::initialize();
624 Python::attach(|py| {
625 let dict = PyDict::new(py);
626 dict.set_item("base_currency", 42).unwrap();
627 let values: Py<PyDict> = dict.unbind();
628
629 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
630
631 assert!(Currency::try_from_str("42").is_none());
632 });
633 }
634
635 #[rstest]
636 fn test_register_crypto_currencies_from_dict_trims_padding() {
637 Python::initialize();
640 Python::attach(|py| {
641 let dict = PyDict::new(py);
642 dict.set_item("base_currency", " NEWHLP2 ").unwrap();
643 let values: Py<PyDict> = dict.unbind();
644
645 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
646
647 assert!(Currency::try_from_str("NEWHLP2").is_some());
648 assert!(Currency::try_from_str(" NEWHLP2 ").is_none());
649 });
650 }
651
652 #[rstest]
653 fn test_register_crypto_currencies_from_dict_blank_code_skipped() {
654 Python::initialize();
657 Python::attach(|py| {
658 let dict = PyDict::new(py);
659 dict.set_item("base_currency", "").unwrap();
660 dict.set_item("quote_currency", " ").unwrap();
661 let values: Py<PyDict> = dict.unbind();
662
663 register_crypto_currencies_from_dict(py, &values, &["base_currency", "quote_currency"]);
664
665 assert!(Currency::try_from_str("").is_none());
666 assert!(Currency::try_from_str(" ").is_none());
667 });
668 }
669}