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