1use std::{
19 collections::hash_map::DefaultHasher,
20 hash::{Hash, Hasher},
21};
22
23use jiff::Timestamp;
24use nautilus_core::{
25 correctness::check_in_range_inclusive_usize,
26 python::{
27 IntoPyObjectNautilusExt, serialization::from_dict_pyo3, to_pyruntime_err, to_pyvalue_err,
28 },
29};
30use pyo3::{
31 IntoPyObjectExt, Py, PyAny, PyResult, Python,
32 types::{PyAnyMethods, PyDict, PyDictMethods},
33};
34use rust_decimal::Decimal;
35use serde::de::DeserializeOwned;
36
37use crate::{
38 enums::{AssetClass, InstrumentClass},
39 identifiers::{Symbol, Venue},
40 instruments::{
41 BettingInstrument, BinaryOption, Cfd, Commodity, CryptoFuture, CryptoFuturesSpread,
42 CryptoOptionSpread, CryptoPerpetual, CurrencyPair, Equity, FuturesContract, FuturesSpread,
43 IndexInstrument, Instrument, InstrumentAny, NautilusInstrumentType, OptionContract,
44 OptionSpread, PerpetualContract, TokenizedAsset, crypto_option::CryptoOption,
45 },
46 types::{Currency, Money, Price, Quantity},
47};
48
49#[derive(Clone, Debug, Eq, Hash, PartialEq)]
50#[pyo3::pyclass(
51 frozen,
52 name = "NautilusInstrumentType",
53 module = "nautilus_trader.model"
54)]
55#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")]
56pub struct PyNautilusInstrumentType {
57 inner: NautilusInstrumentType,
58}
59
60impl PyNautilusInstrumentType {
61 #[must_use]
62 pub const fn new(inner: NautilusInstrumentType) -> Self {
63 Self { inner }
64 }
65
66 #[must_use]
67 pub const fn inner(&self) -> NautilusInstrumentType {
68 self.inner
69 }
70}
71
72#[pyo3::pymethods]
73#[pyo3_stub_gen::derive::gen_stub_pymethods]
74impl PyNautilusInstrumentType {
75 #[new]
76 fn py_new(value: &str) -> PyResult<Self> {
77 value
78 .parse::<NautilusInstrumentType>()
79 .map(Self::new)
80 .map_err(to_pyruntime_err)
81 }
82
83 fn __str__(&self) -> String {
84 self.inner.to_string()
85 }
86
87 fn __repr__(&self) -> String {
88 format!("NautilusInstrumentType.{}", self.inner)
89 }
90
91 fn __richcmp__(&self, other: &Self, op: pyo3::pyclass::CompareOp, py: Python<'_>) -> Py<PyAny> {
92 match op {
93 pyo3::pyclass::CompareOp::Eq => (self.inner == other.inner).into_py_any_unwrap(py),
94 pyo3::pyclass::CompareOp::Ne => (self.inner != other.inner).into_py_any_unwrap(py),
95 _ => py.NotImplemented(),
96 }
97 }
98
99 fn __hash__(&self) -> isize {
100 let mut hasher = DefaultHasher::new();
101 self.inner.hash(&mut hasher);
102 hasher.finish() as isize
103 }
104}
105
106const MAX_PRICE_LIST_TICKS: usize = 100_000;
107
108pub(crate) fn register_crypto_currencies_from_dict(
124 py: Python<'_>,
125 values: &Py<PyDict>,
126 fields: &[&str],
127) {
128 let dict = values.bind(py);
129 for field in fields {
130 if let Ok(Some(value)) = dict.get_item(field)
131 && let Ok(code) = value.extract::<String>()
132 {
133 let trimmed = code.trim();
134 if !trimmed.is_empty() {
135 let _ = Currency::get_or_create_crypto(trimmed);
136 }
137 }
138 }
139}
140
141pub(crate) fn tick_scheme_to_py(instrument: &impl Instrument) -> Option<String> {
142 instrument.tick_scheme().map(|name| name.to_string())
143}
144
145pub(crate) fn from_dict_instrument_pyo3<T>(py: Python<'_>, values: Py<PyDict>) -> PyResult<T>
146where
147 T: DeserializeOwned,
148{
149 let values = instrument_dict_with_tick_scheme_alias(py, values)?;
150 from_dict_pyo3(py, values)
151}
152
153fn instrument_dict_with_tick_scheme_alias(
154 py: Python<'_>,
155 values: Py<PyDict>,
156) -> PyResult<Py<PyDict>> {
157 let dict = values.bind(py);
158 if dict.contains("tick_scheme")? || !dict.contains("tick_scheme_name")? {
159 return Ok(values);
160 }
161
162 let dict = dict.copy()?;
163 if let Some(value) = dict.get_item("tick_scheme_name")? {
164 dict.set_item("tick_scheme", value)?;
165 }
166 Ok(dict.unbind())
167}
168
169macro_rules! impl_instrument_common_pymethods {
170 ($type:ty) => {
171 #[pyo3_stub_gen::derive::gen_stub_pymethods]
172 #[pyo3::pymethods]
173 impl $type {
174 fn __repr__(&self) -> String {
175 format!(
176 "{}(id={}, price_precision={}, size_precision={})",
177 stringify!($type),
178 self.id(),
179 self.price_precision(),
180 self.size_precision(),
181 )
182 }
183
184 #[getter]
185 #[pyo3(name = "symbol")]
186 fn py_symbol(&self) -> Symbol {
187 self.id().symbol
188 }
189
190 #[getter]
191 #[pyo3(name = "venue")]
192 fn py_venue(&self) -> Venue {
193 self.id().venue
194 }
195
196 #[getter]
197 #[pyo3(name = "tick_scheme")]
198 fn py_tick_scheme(&self) -> Option<String> {
199 self.tick_scheme().map(|name| name.to_string())
200 }
201
202 #[pyo3(name = "next_bid_price")]
204 #[pyo3(signature = (value, num_ticks=0))]
205 fn py_next_bid_price(&self, value: f64, num_ticks: i32) -> Option<Price> {
206 self.next_bid_price(value, num_ticks)
207 }
208
209 #[pyo3(name = "next_ask_price")]
211 #[pyo3(signature = (value, num_ticks=0))]
212 fn py_next_ask_price(&self, value: f64, num_ticks: i32) -> Option<Price> {
213 self.next_ask_price(value, num_ticks)
214 }
215
216 #[pyo3(name = "next_bid_prices")]
224 #[pyo3(signature = (value, num_ticks=100))]
225 fn py_next_bid_prices(
226 &self,
227 value: f64,
228 num_ticks: usize,
229 ) -> PyResult<Vec<rust_decimal::Decimal>> {
230 validate_price_list_ticks(num_ticks)?;
231 Ok(self
232 .next_bid_prices(value, num_ticks)
233 .into_iter()
234 .map(|price| price.as_decimal())
235 .collect())
236 }
237
238 #[pyo3(name = "next_ask_prices")]
246 #[pyo3(signature = (value, num_ticks=100))]
247 fn py_next_ask_prices(
248 &self,
249 value: f64,
250 num_ticks: usize,
251 ) -> PyResult<Vec<rust_decimal::Decimal>> {
252 validate_price_list_ticks(num_ticks)?;
253 Ok(self
254 .next_ask_prices(value, num_ticks)
255 .into_iter()
256 .map(|price| price.as_decimal())
257 .collect())
258 }
259
260 #[pyo3(name = "make_price")]
262 fn py_make_price(&self, value: f64) -> PyResult<Price> {
263 self.try_make_price(value)
264 .map_err(nautilus_core::python::to_pyvalue_err)
265 }
266
267 #[pyo3(name = "make_qty")]
269 #[pyo3(signature = (value, round_down=false))]
270 fn py_make_qty(&self, value: f64, round_down: bool) -> PyResult<Quantity> {
271 self.try_make_qty(value, Some(round_down))
272 .map_err(nautilus_core::python::to_pyvalue_err)
273 }
274
275 #[pyo3(name = "notional_value")]
277 #[pyo3(signature = (quantity, price, use_quote_for_inverse=false))]
278 fn py_notional_value(
279 &self,
280 quantity: Quantity,
281 price: Price,
282 use_quote_for_inverse: bool,
283 ) -> PyResult<Money> {
284 self.try_calculate_notional_value(quantity, price, Some(use_quote_for_inverse))
285 .map_err(nautilus_core::python::to_pyvalue_err)
286 }
287 }
288 };
289}
290
291fn validate_price_list_ticks(num_ticks: usize) -> PyResult<()> {
292 check_in_range_inclusive_usize(num_ticks, 0, MAX_PRICE_LIST_TICKS, stringify!(num_ticks))
293 .map_err(to_pyvalue_err)
294}
295
296macro_rules! impl_instrument_getter {
297 ($name:literal, $getter:ident, $return_type:ty, $method:ident, $($type:ty),+ $(,)?) => {
298 $(
299 #[pyo3_stub_gen::derive::gen_stub_pymethods]
300 #[pyo3::pymethods]
301 impl $type {
302 #[getter]
303 #[pyo3(name = $name)]
304 fn $getter(&self) -> $return_type {
305 Instrument::$method(self)
306 }
307 }
308 )+
309 };
310}
311
312macro_rules! impl_instrument_isin_getter {
313 ($($type:ty),+ $(,)?) => {
314 $(
315 #[pyo3_stub_gen::derive::gen_stub_pymethods]
316 #[pyo3::pymethods]
317 impl $type {
318 #[getter]
319 #[pyo3(name = "isin")]
320 fn py_isin(&self) -> Option<String> {
321 Instrument::isin(self).map(|value| value.to_string())
322 }
323 }
324 )+
325 };
326}
327
328macro_rules! impl_instrument_utc_getters {
329 ($($type:ty),+ $(,)?) => {
330 $(
331 #[pyo3_stub_gen::derive::gen_stub_pymethods]
332 #[pyo3::pymethods]
333 impl $type {
334 #[getter]
335 #[pyo3(name = "activation_utc")]
336 fn py_activation_utc(&self) -> Timestamp {
337 self.activation_ns.to_datetime_utc()
338 }
339
340 #[getter]
341 #[pyo3(name = "expiration_utc")]
342 fn py_expiration_utc(&self) -> Timestamp {
343 self.expiration_ns.to_datetime_utc()
344 }
345 }
346 )+
347 };
348}
349
350impl_instrument_common_pymethods!(BettingInstrument);
351impl_instrument_common_pymethods!(BinaryOption);
352impl_instrument_common_pymethods!(Cfd);
353impl_instrument_common_pymethods!(Commodity);
354impl_instrument_common_pymethods!(CryptoFuture);
355impl_instrument_common_pymethods!(CryptoFuturesSpread);
356impl_instrument_common_pymethods!(CryptoOption);
357impl_instrument_common_pymethods!(CryptoOptionSpread);
358impl_instrument_common_pymethods!(CryptoPerpetual);
359impl_instrument_common_pymethods!(CurrencyPair);
360impl_instrument_common_pymethods!(Equity);
361impl_instrument_common_pymethods!(FuturesContract);
362impl_instrument_common_pymethods!(FuturesSpread);
363impl_instrument_common_pymethods!(IndexInstrument);
364impl_instrument_common_pymethods!(OptionContract);
365impl_instrument_common_pymethods!(OptionSpread);
366impl_instrument_common_pymethods!(PerpetualContract);
367impl_instrument_common_pymethods!(TokenizedAsset);
368
369impl_instrument_utc_getters!(
370 BinaryOption,
371 CryptoFuture,
372 CryptoFuturesSpread,
373 CryptoOption,
374 CryptoOptionSpread,
375 FuturesContract,
376 FuturesSpread,
377 OptionContract,
378 OptionSpread,
379);
380
381impl_instrument_getter!(
382 "asset_class",
383 py_asset_class,
384 AssetClass,
385 asset_class,
386 CryptoFuture,
387 CryptoFuturesSpread,
388 CryptoOption,
389 CryptoOptionSpread,
390 CryptoPerpetual,
391 CurrencyPair,
392 Equity,
393 IndexInstrument,
394);
395impl_instrument_getter!(
396 "instrument_class",
397 py_instrument_class,
398 InstrumentClass,
399 instrument_class,
400 BinaryOption,
401 Cfd,
402 Commodity,
403 CryptoFuture,
404 CryptoFuturesSpread,
405 CryptoOption,
406 CryptoOptionSpread,
407 CryptoPerpetual,
408 CurrencyPair,
409 Equity,
410 FuturesContract,
411 FuturesSpread,
412 IndexInstrument,
413 OptionContract,
414 OptionSpread,
415 PerpetualContract,
416 TokenizedAsset,
417);
418impl_instrument_getter!(
419 "is_inverse",
420 py_is_inverse,
421 bool,
422 is_inverse,
423 BettingInstrument,
424 BinaryOption,
425 Cfd,
426 Commodity,
427 CurrencyPair,
428 Equity,
429 FuturesContract,
430 FuturesSpread,
431 IndexInstrument,
432 OptionContract,
433 OptionSpread,
434 TokenizedAsset,
435);
436impl_instrument_getter!(
437 "is_quanto",
438 py_is_quanto,
439 bool,
440 is_quanto,
441 BettingInstrument,
442 BinaryOption,
443 Cfd,
444 Commodity,
445 CryptoFuture,
446 CryptoFuturesSpread,
447 CryptoOption,
448 CryptoOptionSpread,
449 CryptoPerpetual,
450 CurrencyPair,
451 Equity,
452 FuturesContract,
453 FuturesSpread,
454 IndexInstrument,
455 OptionContract,
456 OptionSpread,
457 PerpetualContract,
458 TokenizedAsset,
459);
460impl_instrument_isin_getter!(
461 BettingInstrument,
462 BinaryOption,
463 Cfd,
464 Commodity,
465 CryptoFuture,
466 CryptoFuturesSpread,
467 CryptoOption,
468 CryptoOptionSpread,
469 CryptoPerpetual,
470 CurrencyPair,
471 FuturesContract,
472 FuturesSpread,
473 IndexInstrument,
474 OptionContract,
475 OptionSpread,
476 PerpetualContract,
477);
478impl_instrument_getter!(
479 "lot_size",
480 py_lot_size,
481 Option<Quantity>,
482 lot_size,
483 BettingInstrument,
484 BinaryOption,
485 IndexInstrument,
486);
487impl_instrument_getter!(
488 "margin_init",
489 py_margin_init,
490 Decimal,
491 margin_init,
492 BettingInstrument,
493 IndexInstrument,
494);
495impl_instrument_getter!(
496 "margin_maint",
497 py_margin_maint,
498 Decimal,
499 margin_maint,
500 BettingInstrument,
501 IndexInstrument,
502);
503impl_instrument_getter!(
504 "max_notional",
505 py_max_notional,
506 Option<Money>,
507 max_notional,
508 Equity,
509 FuturesContract,
510 FuturesSpread,
511 IndexInstrument,
512 OptionContract,
513 OptionSpread,
514);
515impl_instrument_getter!(
516 "max_price",
517 py_max_price,
518 Option<Price>,
519 max_price,
520 IndexInstrument
521);
522impl_instrument_getter!(
523 "max_quantity",
524 py_max_quantity,
525 Option<Quantity>,
526 max_quantity,
527 IndexInstrument,
528);
529impl_instrument_getter!(
530 "min_notional",
531 py_min_notional,
532 Option<Money>,
533 min_notional,
534 Equity,
535 FuturesContract,
536 FuturesSpread,
537 IndexInstrument,
538 OptionContract,
539 OptionSpread,
540);
541impl_instrument_getter!(
542 "min_price",
543 py_min_price,
544 Option<Price>,
545 min_price,
546 IndexInstrument
547);
548impl_instrument_getter!(
549 "min_quantity",
550 py_min_quantity,
551 Option<Quantity>,
552 min_quantity,
553 IndexInstrument,
554);
555impl_instrument_getter!(
556 "multiplier",
557 py_multiplier,
558 Quantity,
559 multiplier,
560 BettingInstrument,
561 BinaryOption,
562 Cfd,
563 Commodity,
564 Equity,
565 IndexInstrument,
566);
567impl_instrument_getter!(
568 "quote_currency",
569 py_quote_currency,
570 Currency,
571 quote_currency,
572 BettingInstrument,
573 BinaryOption,
574 FuturesContract,
575 FuturesSpread,
576 OptionContract,
577 OptionSpread,
578);
579
580pub mod betting;
581pub mod binary_option;
582pub mod cfd;
583pub mod commodity;
584pub mod crypto_future;
585pub mod crypto_futures_spread;
586pub mod crypto_option;
587pub mod crypto_option_spread;
588pub mod crypto_perpetual;
589pub mod currency_pair;
590pub mod equity;
591pub mod futures_contract;
592pub mod futures_spread;
593pub mod index_instrument;
594pub mod option_contract;
595pub mod option_spread;
596pub mod perpetual_contract;
597pub mod synthetic;
598pub mod tick_scheme;
599pub mod tokenized_asset;
600
601pub fn instrument_any_to_pyobject(py: Python, instrument: InstrumentAny) -> PyResult<Py<PyAny>> {
607 match instrument {
608 InstrumentAny::Betting(inst) => inst.into_py_any(py),
609 InstrumentAny::BinaryOption(inst) => inst.into_py_any(py),
610 InstrumentAny::Cfd(inst) => inst.into_py_any(py),
611 InstrumentAny::Commodity(inst) => inst.into_py_any(py),
612 InstrumentAny::CryptoFuture(inst) => inst.into_py_any(py),
613 InstrumentAny::CryptoFuturesSpread(inst) => inst.into_py_any(py),
614 InstrumentAny::CryptoOption(inst) => inst.into_py_any(py),
615 InstrumentAny::CryptoOptionSpread(inst) => inst.into_py_any(py),
616 InstrumentAny::CryptoPerpetual(inst) => inst.into_py_any(py),
617 InstrumentAny::CurrencyPair(inst) => inst.into_py_any(py),
618 InstrumentAny::Equity(inst) => inst.into_py_any(py),
619 InstrumentAny::FuturesContract(inst) => inst.into_py_any(py),
620 InstrumentAny::FuturesSpread(inst) => inst.into_py_any(py),
621 InstrumentAny::IndexInstrument(inst) => inst.into_py_any(py),
622 InstrumentAny::OptionContract(inst) => inst.into_py_any(py),
623 InstrumentAny::OptionSpread(inst) => inst.into_py_any(py),
624 InstrumentAny::PerpetualContract(inst) => inst.into_py_any(py),
625 InstrumentAny::TokenizedAsset(inst) => inst.into_py_any(py),
626 }
627}
628
629#[expect(clippy::needless_pass_by_value)]
635pub fn pyobject_to_instrument_any(py: Python, instrument: Py<PyAny>) -> PyResult<InstrumentAny> {
636 match instrument.getattr(py, "type_name")?.extract::<&str>(py)? {
637 stringify!(BettingInstrument) => Ok(InstrumentAny::Betting(
638 instrument.extract::<BettingInstrument>(py)?,
639 )),
640 stringify!(BinaryOption) => Ok(InstrumentAny::BinaryOption(
641 instrument.extract::<BinaryOption>(py)?,
642 )),
643 stringify!(Cfd) => Ok(InstrumentAny::Cfd(instrument.extract::<Cfd>(py)?)),
644 stringify!(Commodity) => Ok(InstrumentAny::Commodity(
645 instrument.extract::<Commodity>(py)?,
646 )),
647 stringify!(CryptoFuture) => Ok(InstrumentAny::CryptoFuture(
648 instrument.extract::<CryptoFuture>(py)?,
649 )),
650 stringify!(CryptoFuturesSpread) => Ok(InstrumentAny::CryptoFuturesSpread(
651 instrument.extract::<CryptoFuturesSpread>(py)?,
652 )),
653 stringify!(CryptoOption) => Ok(InstrumentAny::CryptoOption(
654 instrument.extract::<CryptoOption>(py)?,
655 )),
656 stringify!(CryptoOptionSpread) => Ok(InstrumentAny::CryptoOptionSpread(
657 instrument.extract::<CryptoOptionSpread>(py)?,
658 )),
659 stringify!(CryptoPerpetual) => Ok(InstrumentAny::CryptoPerpetual(
660 instrument.extract::<CryptoPerpetual>(py)?,
661 )),
662 stringify!(CurrencyPair) => Ok(InstrumentAny::CurrencyPair(
663 instrument.extract::<CurrencyPair>(py)?,
664 )),
665 stringify!(Equity) => Ok(InstrumentAny::Equity(instrument.extract::<Equity>(py)?)),
666 stringify!(FuturesContract) => Ok(InstrumentAny::FuturesContract(
667 instrument.extract::<FuturesContract>(py)?,
668 )),
669 stringify!(FuturesSpread) => Ok(InstrumentAny::FuturesSpread(
670 instrument.extract::<FuturesSpread>(py)?,
671 )),
672 stringify!(IndexInstrument) => Ok(InstrumentAny::IndexInstrument(
673 instrument.extract::<IndexInstrument>(py)?,
674 )),
675 stringify!(OptionContract) => Ok(InstrumentAny::OptionContract(
676 instrument.extract::<OptionContract>(py)?,
677 )),
678 stringify!(OptionSpread) => Ok(InstrumentAny::OptionSpread(
679 instrument.extract::<OptionSpread>(py)?,
680 )),
681 stringify!(PerpetualContract) => Ok(InstrumentAny::PerpetualContract(
682 instrument.extract::<PerpetualContract>(py)?,
683 )),
684 stringify!(TokenizedAsset) => Ok(InstrumentAny::TokenizedAsset(
685 instrument.extract::<TokenizedAsset>(py)?,
686 )),
687 _ => Err(to_pyvalue_err(
688 "Error in conversion from `Py<PyAny>` to `InstrumentAny`",
689 )),
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use pyo3::{prelude::*, types::PyDict};
696 use rstest::rstest;
697
698 use super::{
699 MAX_PRICE_LIST_TICKS, register_crypto_currencies_from_dict, validate_price_list_ticks,
700 };
701 use crate::{enums::CurrencyType, instruments::stubs::audusd_sim, types::Currency};
702
703 #[rstest]
704 #[case(0)]
705 #[case(100)]
706 #[case(MAX_PRICE_LIST_TICKS)]
707 fn test_validate_price_list_ticks_accepts_supported_values(#[case] num_ticks: usize) {
708 assert!(validate_price_list_ticks(num_ticks).is_ok());
709 }
710
711 #[rstest]
712 #[case(MAX_PRICE_LIST_TICKS + 1)]
713 #[case(usize::MAX)]
714 fn test_validate_price_list_ticks_rejects_oversized_values(#[case] num_ticks: usize) {
715 assert!(validate_price_list_ticks(num_ticks).is_err());
716 }
717
718 #[rstest]
719 fn test_next_bid_prices_materializes_maximum_supported_list() {
720 let prices = audusd_sim()
721 .py_next_bid_prices(1.0, MAX_PRICE_LIST_TICKS)
722 .unwrap();
723
724 assert_eq!(prices.len(), MAX_PRICE_LIST_TICKS);
725 }
726
727 #[rstest]
728 fn test_register_crypto_currencies_from_dict_unknown_code() {
729 Python::initialize();
730 Python::attach(|py| {
731 let dict = PyDict::new(py);
732 dict.set_item("base_currency", "NEWHLP1").unwrap();
733 let values: Py<PyDict> = dict.unbind();
734
735 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
736
737 let created = Currency::try_from_str("NEWHLP1").unwrap();
738 assert_eq!(created.precision, 8);
739 assert_eq!(created.currency_type, CurrencyType::Crypto);
740 });
741 }
742
743 #[rstest]
744 fn test_register_crypto_currencies_from_dict_known_code_not_overwritten() {
745 Python::initialize();
746 Python::attach(|py| {
747 let dict = PyDict::new(py);
748 dict.set_item("quote_currency", "USD").unwrap();
749 let values: Py<PyDict> = dict.unbind();
750
751 register_crypto_currencies_from_dict(py, &values, &["quote_currency"]);
752
753 let usd = Currency::try_from_str("USD").unwrap();
754 assert_eq!(usd.precision, 2);
755 assert_eq!(usd.currency_type, CurrencyType::Fiat);
756 });
757 }
758
759 #[rstest]
760 fn test_register_crypto_currencies_from_dict_missing_key() {
761 Python::initialize();
762 Python::attach(|py| {
763 let dict = PyDict::new(py);
764 let values: Py<PyDict> = dict.unbind();
765
766 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
767
768 assert!(Currency::try_from_str("base_currency").is_none());
769 });
770 }
771
772 #[rstest]
773 fn test_register_crypto_currencies_from_dict_non_string_value() {
774 Python::initialize();
775 Python::attach(|py| {
776 let dict = PyDict::new(py);
777 dict.set_item("base_currency", 42).unwrap();
778 let values: Py<PyDict> = dict.unbind();
779
780 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
781
782 assert!(Currency::try_from_str("42").is_none());
783 });
784 }
785
786 #[rstest]
787 fn test_register_crypto_currencies_from_dict_trims_padding() {
788 Python::initialize();
791 Python::attach(|py| {
792 let dict = PyDict::new(py);
793 dict.set_item("base_currency", " NEWHLP2 ").unwrap();
794 let values: Py<PyDict> = dict.unbind();
795
796 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
797
798 assert!(Currency::try_from_str("NEWHLP2").is_some());
799 assert!(Currency::try_from_str(" NEWHLP2 ").is_none());
800 });
801 }
802
803 #[rstest]
804 fn test_register_crypto_currencies_from_dict_blank_code_skipped() {
805 Python::initialize();
808 Python::attach(|py| {
809 let dict = PyDict::new(py);
810 dict.set_item("base_currency", "").unwrap();
811 dict.set_item("quote_currency", " ").unwrap();
812 let values: Py<PyDict> = dict.unbind();
813
814 register_crypto_currencies_from_dict(py, &values, &["base_currency", "quote_currency"]);
815
816 assert!(Currency::try_from_str("").is_none());
817 assert!(Currency::try_from_str(" ").is_none());
818 });
819 }
820}