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 "maker_fee",
490 py_maker_fee,
491 Decimal,
492 maker_fee,
493 IndexInstrument
494);
495impl_instrument_getter!(
496 "margin_init",
497 py_margin_init,
498 Decimal,
499 margin_init,
500 BettingInstrument,
501 IndexInstrument,
502);
503impl_instrument_getter!(
504 "margin_maint",
505 py_margin_maint,
506 Decimal,
507 margin_maint,
508 BettingInstrument,
509 IndexInstrument,
510);
511impl_instrument_getter!(
512 "max_notional",
513 py_max_notional,
514 Option<Money>,
515 max_notional,
516 Equity,
517 FuturesContract,
518 FuturesSpread,
519 IndexInstrument,
520 OptionContract,
521 OptionSpread,
522);
523impl_instrument_getter!(
524 "max_price",
525 py_max_price,
526 Option<Price>,
527 max_price,
528 IndexInstrument
529);
530impl_instrument_getter!(
531 "max_quantity",
532 py_max_quantity,
533 Option<Quantity>,
534 max_quantity,
535 IndexInstrument,
536);
537impl_instrument_getter!(
538 "min_notional",
539 py_min_notional,
540 Option<Money>,
541 min_notional,
542 Equity,
543 FuturesContract,
544 FuturesSpread,
545 IndexInstrument,
546 OptionContract,
547 OptionSpread,
548);
549impl_instrument_getter!(
550 "min_price",
551 py_min_price,
552 Option<Price>,
553 min_price,
554 IndexInstrument
555);
556impl_instrument_getter!(
557 "min_quantity",
558 py_min_quantity,
559 Option<Quantity>,
560 min_quantity,
561 IndexInstrument,
562);
563impl_instrument_getter!(
564 "multiplier",
565 py_multiplier,
566 Quantity,
567 multiplier,
568 BettingInstrument,
569 BinaryOption,
570 Cfd,
571 Commodity,
572 Equity,
573 IndexInstrument,
574);
575impl_instrument_getter!(
576 "quote_currency",
577 py_quote_currency,
578 Currency,
579 quote_currency,
580 BettingInstrument,
581 BinaryOption,
582 FuturesContract,
583 FuturesSpread,
584 OptionContract,
585 OptionSpread,
586);
587impl_instrument_getter!(
588 "taker_fee",
589 py_taker_fee,
590 Decimal,
591 taker_fee,
592 IndexInstrument
593);
594
595pub mod betting;
596pub mod binary_option;
597pub mod cfd;
598pub mod commodity;
599pub mod crypto_future;
600pub mod crypto_futures_spread;
601pub mod crypto_option;
602pub mod crypto_option_spread;
603pub mod crypto_perpetual;
604pub mod currency_pair;
605pub mod equity;
606pub mod futures_contract;
607pub mod futures_spread;
608pub mod index_instrument;
609pub mod option_contract;
610pub mod option_spread;
611pub mod perpetual_contract;
612pub mod synthetic;
613pub mod tokenized_asset;
614
615pub fn instrument_any_to_pyobject(py: Python, instrument: InstrumentAny) -> PyResult<Py<PyAny>> {
621 match instrument {
622 InstrumentAny::Betting(inst) => inst.into_py_any(py),
623 InstrumentAny::BinaryOption(inst) => inst.into_py_any(py),
624 InstrumentAny::Cfd(inst) => inst.into_py_any(py),
625 InstrumentAny::Commodity(inst) => inst.into_py_any(py),
626 InstrumentAny::CryptoFuture(inst) => inst.into_py_any(py),
627 InstrumentAny::CryptoFuturesSpread(inst) => inst.into_py_any(py),
628 InstrumentAny::CryptoOption(inst) => inst.into_py_any(py),
629 InstrumentAny::CryptoOptionSpread(inst) => inst.into_py_any(py),
630 InstrumentAny::CryptoPerpetual(inst) => inst.into_py_any(py),
631 InstrumentAny::CurrencyPair(inst) => inst.into_py_any(py),
632 InstrumentAny::Equity(inst) => inst.into_py_any(py),
633 InstrumentAny::FuturesContract(inst) => inst.into_py_any(py),
634 InstrumentAny::FuturesSpread(inst) => inst.into_py_any(py),
635 InstrumentAny::IndexInstrument(inst) => inst.into_py_any(py),
636 InstrumentAny::OptionContract(inst) => inst.into_py_any(py),
637 InstrumentAny::OptionSpread(inst) => inst.into_py_any(py),
638 InstrumentAny::PerpetualContract(inst) => inst.into_py_any(py),
639 InstrumentAny::TokenizedAsset(inst) => inst.into_py_any(py),
640 }
641}
642
643#[expect(clippy::needless_pass_by_value)]
649pub fn pyobject_to_instrument_any(py: Python, instrument: Py<PyAny>) -> PyResult<InstrumentAny> {
650 match instrument.getattr(py, "type_name")?.extract::<&str>(py)? {
651 stringify!(BettingInstrument) => Ok(InstrumentAny::Betting(
652 instrument.extract::<BettingInstrument>(py)?,
653 )),
654 stringify!(BinaryOption) => Ok(InstrumentAny::BinaryOption(
655 instrument.extract::<BinaryOption>(py)?,
656 )),
657 stringify!(Cfd) => Ok(InstrumentAny::Cfd(instrument.extract::<Cfd>(py)?)),
658 stringify!(Commodity) => Ok(InstrumentAny::Commodity(
659 instrument.extract::<Commodity>(py)?,
660 )),
661 stringify!(CryptoFuture) => Ok(InstrumentAny::CryptoFuture(
662 instrument.extract::<CryptoFuture>(py)?,
663 )),
664 stringify!(CryptoFuturesSpread) => Ok(InstrumentAny::CryptoFuturesSpread(
665 instrument.extract::<CryptoFuturesSpread>(py)?,
666 )),
667 stringify!(CryptoOption) => Ok(InstrumentAny::CryptoOption(
668 instrument.extract::<CryptoOption>(py)?,
669 )),
670 stringify!(CryptoOptionSpread) => Ok(InstrumentAny::CryptoOptionSpread(
671 instrument.extract::<CryptoOptionSpread>(py)?,
672 )),
673 stringify!(CryptoPerpetual) => Ok(InstrumentAny::CryptoPerpetual(
674 instrument.extract::<CryptoPerpetual>(py)?,
675 )),
676 stringify!(CurrencyPair) => Ok(InstrumentAny::CurrencyPair(
677 instrument.extract::<CurrencyPair>(py)?,
678 )),
679 stringify!(Equity) => Ok(InstrumentAny::Equity(instrument.extract::<Equity>(py)?)),
680 stringify!(FuturesContract) => Ok(InstrumentAny::FuturesContract(
681 instrument.extract::<FuturesContract>(py)?,
682 )),
683 stringify!(FuturesSpread) => Ok(InstrumentAny::FuturesSpread(
684 instrument.extract::<FuturesSpread>(py)?,
685 )),
686 stringify!(IndexInstrument) => Ok(InstrumentAny::IndexInstrument(
687 instrument.extract::<IndexInstrument>(py)?,
688 )),
689 stringify!(OptionContract) => Ok(InstrumentAny::OptionContract(
690 instrument.extract::<OptionContract>(py)?,
691 )),
692 stringify!(OptionSpread) => Ok(InstrumentAny::OptionSpread(
693 instrument.extract::<OptionSpread>(py)?,
694 )),
695 stringify!(PerpetualContract) => Ok(InstrumentAny::PerpetualContract(
696 instrument.extract::<PerpetualContract>(py)?,
697 )),
698 stringify!(TokenizedAsset) => Ok(InstrumentAny::TokenizedAsset(
699 instrument.extract::<TokenizedAsset>(py)?,
700 )),
701 _ => Err(to_pyvalue_err(
702 "Error in conversion from `Py<PyAny>` to `InstrumentAny`",
703 )),
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use pyo3::{prelude::*, types::PyDict};
710 use rstest::rstest;
711
712 use super::{
713 MAX_PRICE_LIST_TICKS, register_crypto_currencies_from_dict, validate_price_list_ticks,
714 };
715 use crate::{enums::CurrencyType, instruments::stubs::audusd_sim, types::Currency};
716
717 #[rstest]
718 #[case(0)]
719 #[case(100)]
720 #[case(MAX_PRICE_LIST_TICKS)]
721 fn test_validate_price_list_ticks_accepts_supported_values(#[case] num_ticks: usize) {
722 assert!(validate_price_list_ticks(num_ticks).is_ok());
723 }
724
725 #[rstest]
726 #[case(MAX_PRICE_LIST_TICKS + 1)]
727 #[case(usize::MAX)]
728 fn test_validate_price_list_ticks_rejects_oversized_values(#[case] num_ticks: usize) {
729 assert!(validate_price_list_ticks(num_ticks).is_err());
730 }
731
732 #[rstest]
733 fn test_next_bid_prices_materializes_maximum_supported_list() {
734 let prices = audusd_sim()
735 .py_next_bid_prices(1.0, MAX_PRICE_LIST_TICKS)
736 .unwrap();
737
738 assert_eq!(prices.len(), MAX_PRICE_LIST_TICKS);
739 }
740
741 #[rstest]
742 fn test_register_crypto_currencies_from_dict_unknown_code() {
743 Python::initialize();
744 Python::attach(|py| {
745 let dict = PyDict::new(py);
746 dict.set_item("base_currency", "NEWHLP1").unwrap();
747 let values: Py<PyDict> = dict.unbind();
748
749 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
750
751 let created = Currency::try_from_str("NEWHLP1").unwrap();
752 assert_eq!(created.precision, 8);
753 assert_eq!(created.currency_type, CurrencyType::Crypto);
754 });
755 }
756
757 #[rstest]
758 fn test_register_crypto_currencies_from_dict_known_code_not_overwritten() {
759 Python::initialize();
760 Python::attach(|py| {
761 let dict = PyDict::new(py);
762 dict.set_item("quote_currency", "USD").unwrap();
763 let values: Py<PyDict> = dict.unbind();
764
765 register_crypto_currencies_from_dict(py, &values, &["quote_currency"]);
766
767 let usd = Currency::try_from_str("USD").unwrap();
768 assert_eq!(usd.precision, 2);
769 assert_eq!(usd.currency_type, CurrencyType::Fiat);
770 });
771 }
772
773 #[rstest]
774 fn test_register_crypto_currencies_from_dict_missing_key() {
775 Python::initialize();
776 Python::attach(|py| {
777 let dict = PyDict::new(py);
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("base_currency").is_none());
783 });
784 }
785
786 #[rstest]
787 fn test_register_crypto_currencies_from_dict_non_string_value() {
788 Python::initialize();
789 Python::attach(|py| {
790 let dict = PyDict::new(py);
791 dict.set_item("base_currency", 42).unwrap();
792 let values: Py<PyDict> = dict.unbind();
793
794 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
795
796 assert!(Currency::try_from_str("42").is_none());
797 });
798 }
799
800 #[rstest]
801 fn test_register_crypto_currencies_from_dict_trims_padding() {
802 Python::initialize();
805 Python::attach(|py| {
806 let dict = PyDict::new(py);
807 dict.set_item("base_currency", " NEWHLP2 ").unwrap();
808 let values: Py<PyDict> = dict.unbind();
809
810 register_crypto_currencies_from_dict(py, &values, &["base_currency"]);
811
812 assert!(Currency::try_from_str("NEWHLP2").is_some());
813 assert!(Currency::try_from_str(" NEWHLP2 ").is_none());
814 });
815 }
816
817 #[rstest]
818 fn test_register_crypto_currencies_from_dict_blank_code_skipped() {
819 Python::initialize();
822 Python::attach(|py| {
823 let dict = PyDict::new(py);
824 dict.set_item("base_currency", "").unwrap();
825 dict.set_item("quote_currency", " ").unwrap();
826 let values: Py<PyDict> = dict.unbind();
827
828 register_crypto_currencies_from_dict(py, &values, &["base_currency", "quote_currency"]);
829
830 assert!(Currency::try_from_str("").is_none());
831 assert!(Currency::try_from_str(" ").is_none());
832 });
833 }
834}