1use std::collections::HashMap;
23
24use arrow::{datatypes::Schema, error::ArrowError, record_batch::RecordBatch};
25use nautilus_model::{
26 instruments::{
27 Instrument, InstrumentAny, betting::BettingInstrument, binary_option::BinaryOption,
28 cfd::Cfd, commodity::Commodity, crypto_future::CryptoFuture, crypto_option::CryptoOption,
29 crypto_perpetual::CryptoPerpetual, currency_pair::CurrencyPair, equity::Equity,
30 futures_contract::FuturesContract, futures_spread::FuturesSpread,
31 index_instrument::IndexInstrument, option_contract::OptionContract,
32 option_spread::OptionSpread, perpetual_contract::PerpetualContract,
33 tokenized_asset::TokenizedAsset,
34 },
35 types::Currency,
36};
37
38#[allow(unused)]
39use crate::arrow::{
40 ArrowSchemaProvider, Data, DecodeDataFromRecordBatch, DecodeFromRecordBatch,
41 EncodeToRecordBatch, EncodingError, KEY_INSTRUMENT_ID,
42};
43
44pub mod betting;
45pub mod binary_option;
46pub mod cfd;
47pub mod commodity;
48pub mod crypto_future;
49pub mod crypto_option;
50pub mod crypto_perpetual;
51pub mod currency_pair;
52pub mod equity;
53pub mod futures_contract;
54pub mod futures_spread;
55pub mod index_instrument;
56pub mod option_contract;
57pub mod option_spread;
58pub mod perpetual_contract;
59pub mod tokenized_asset;
60
61pub(crate) fn decode_currency(
66 value: &str,
67 field: &'static str,
68 context: &'static str,
69 row: usize,
70) -> Result<Currency, EncodingError> {
71 let trimmed = value.trim();
72 if trimmed.is_empty() {
73 return Err(EncodingError::ParseError(
74 field,
75 format!("row {row}: empty currency code"),
76 ));
77 }
78
79 Ok(Currency::get_or_create_crypto_with_context(
80 trimmed,
81 Some(context),
82 ))
83}
84
85impl ArrowSchemaProvider for InstrumentAny {
86 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
87 let instrument_type = metadata
88 .as_ref()
89 .and_then(|m| m.get("class"))
90 .map_or("CurrencyPair", |s| s.as_str());
91
92 match instrument_type {
93 "BettingInstrument" => BettingInstrument::get_schema(metadata),
94 "BinaryOption" => BinaryOption::get_schema(metadata),
95 "Cfd" => Cfd::get_schema(metadata),
96 "Commodity" => Commodity::get_schema(metadata),
97 "CryptoFuture" => CryptoFuture::get_schema(metadata),
98 "CryptoOption" => CryptoOption::get_schema(metadata),
99 "CryptoPerpetual" => CryptoPerpetual::get_schema(metadata),
100 "CurrencyPair" => CurrencyPair::get_schema(metadata),
101 "Equity" => Equity::get_schema(metadata),
102 "FuturesContract" => FuturesContract::get_schema(metadata),
103 "FuturesSpread" => FuturesSpread::get_schema(metadata),
104 "IndexInstrument" => IndexInstrument::get_schema(metadata),
105 "OptionContract" => OptionContract::get_schema(metadata),
106 "OptionSpread" => OptionSpread::get_schema(metadata),
107 "PerpetualContract" => PerpetualContract::get_schema(metadata),
108 "TokenizedAsset" => TokenizedAsset::get_schema(metadata),
109 _ => {
110 CurrencyPair::get_schema(metadata)
112 }
113 }
114 }
115}
116
117impl EncodeToRecordBatch for InstrumentAny {
118 fn encode_batch(
119 #[allow(unused)] metadata: &HashMap<String, String>,
120 data: &[Self],
121 ) -> Result<RecordBatch, ArrowError> {
122 if data.is_empty() {
123 return Err(ArrowError::InvalidArgumentError(
124 "Cannot encode empty instrument batch".to_string(),
125 ));
126 }
127
128 let mut by_type: HashMap<String, Vec<&Self>> = HashMap::new();
129
130 for instrument in data {
131 let type_name = match instrument {
132 Self::Cfd(_) => "Cfd",
133 Self::Commodity(_) => "Commodity",
134 Self::CurrencyPair(_) => "CurrencyPair",
135 Self::Equity(_) => "Equity",
136 Self::CryptoFuture(_) => "CryptoFuture",
137 Self::CryptoPerpetual(_) => "CryptoPerpetual",
138 Self::CryptoOption(_) => "CryptoOption",
139 Self::FuturesContract(_) => "FuturesContract",
140 Self::FuturesSpread(_) => "FuturesSpread",
141 Self::IndexInstrument(_) => "IndexInstrument",
142 Self::OptionContract(_) => "OptionContract",
143 Self::OptionSpread(_) => "OptionSpread",
144 Self::BinaryOption(_) => "BinaryOption",
145 Self::Betting(_) => "BettingInstrument",
146 Self::PerpetualContract(_) => "PerpetualContract",
147 Self::TokenizedAsset(_) => "TokenizedAsset",
148 };
149 by_type
150 .entry(type_name.to_string())
151 .or_default()
152 .push(instrument);
153 }
154
155 if by_type.len() > 1 {
156 return Err(ArrowError::InvalidArgumentError(
157 "Cannot encode mixed instrument types in a single batch. Use separate batches for each type.".to_string(),
158 ));
159 }
160
161 let (type_name, instruments) = by_type.iter().next().unwrap();
162 match type_name.as_str() {
163 "Cfd" => {
164 let cfds: Vec<_> = instruments
165 .iter()
166 .map(|i| {
167 if let Self::Cfd(c) = i {
168 c
169 } else {
170 unreachable!()
171 }
172 })
173 .cloned()
174 .collect();
175 Cfd::encode_batch(metadata, &cfds)
176 }
177 "Commodity" => {
178 let commodities: Vec<_> = instruments
179 .iter()
180 .map(|i| {
181 if let Self::Commodity(c) = i {
182 c
183 } else {
184 unreachable!()
185 }
186 })
187 .cloned()
188 .collect();
189 Commodity::encode_batch(metadata, &commodities)
190 }
191 "BettingInstrument" => {
192 let betting: Vec<_> = instruments
193 .iter()
194 .map(|i| {
195 if let Self::Betting(b) = i {
196 b
197 } else {
198 unreachable!()
199 }
200 })
201 .cloned()
202 .collect();
203 BettingInstrument::encode_batch(metadata, &betting)
204 }
205 "BinaryOption" => {
206 let binary_options: Vec<_> = instruments
207 .iter()
208 .map(|i| {
209 if let Self::BinaryOption(bo) = i {
210 bo
211 } else {
212 unreachable!()
213 }
214 })
215 .cloned()
216 .collect();
217 BinaryOption::encode_batch(metadata, &binary_options)
218 }
219 "CryptoFuture" => {
220 let crypto_futures: Vec<_> = instruments
221 .iter()
222 .map(|i| {
223 if let Self::CryptoFuture(cf) = i {
224 cf
225 } else {
226 unreachable!()
227 }
228 })
229 .cloned()
230 .collect();
231 CryptoFuture::encode_batch(metadata, &crypto_futures)
232 }
233 "CryptoOption" => {
234 let crypto_options: Vec<_> = instruments
235 .iter()
236 .map(|i| {
237 if let Self::CryptoOption(co) = i {
238 co
239 } else {
240 unreachable!()
241 }
242 })
243 .cloned()
244 .collect();
245 CryptoOption::encode_batch(metadata, &crypto_options)
246 }
247 "CryptoPerpetual" => {
248 let crypto_perps: Vec<_> = instruments
249 .iter()
250 .map(|i| {
251 if let Self::CryptoPerpetual(cp) = i {
252 cp
253 } else {
254 unreachable!()
255 }
256 })
257 .cloned()
258 .collect();
259 CryptoPerpetual::encode_batch(metadata, &crypto_perps)
260 }
261 "CurrencyPair" => {
262 let currency_pairs: Vec<_> = instruments
263 .iter()
264 .map(|i| {
265 if let Self::CurrencyPair(cp) = i {
266 cp
267 } else {
268 unreachable!()
269 }
270 })
271 .cloned()
272 .collect();
273 CurrencyPair::encode_batch(metadata, ¤cy_pairs)
274 }
275 "Equity" => {
276 let equities: Vec<_> = instruments
277 .iter()
278 .map(|i| {
279 if let Self::Equity(e) = i {
280 e
281 } else {
282 unreachable!()
283 }
284 })
285 .cloned()
286 .collect();
287 Equity::encode_batch(metadata, &equities)
288 }
289 "FuturesContract" => {
290 let futures_contracts: Vec<_> = instruments
291 .iter()
292 .map(|i| {
293 if let Self::FuturesContract(fc) = i {
294 fc
295 } else {
296 unreachable!()
297 }
298 })
299 .cloned()
300 .collect();
301 FuturesContract::encode_batch(metadata, &futures_contracts)
302 }
303 "FuturesSpread" => {
304 let futures_spreads: Vec<_> = instruments
305 .iter()
306 .map(|i| {
307 if let Self::FuturesSpread(fs) = i {
308 fs
309 } else {
310 unreachable!()
311 }
312 })
313 .cloned()
314 .collect();
315 FuturesSpread::encode_batch(metadata, &futures_spreads)
316 }
317 "IndexInstrument" => {
318 let index_instruments: Vec<_> = instruments
319 .iter()
320 .map(|i| {
321 if let Self::IndexInstrument(ii) = i {
322 ii
323 } else {
324 unreachable!()
325 }
326 })
327 .cloned()
328 .collect();
329 IndexInstrument::encode_batch(metadata, &index_instruments)
330 }
331 "OptionContract" => {
332 let option_contracts: Vec<_> = instruments
333 .iter()
334 .map(|i| {
335 if let Self::OptionContract(oc) = i {
336 oc
337 } else {
338 unreachable!()
339 }
340 })
341 .cloned()
342 .collect();
343 OptionContract::encode_batch(metadata, &option_contracts)
344 }
345 "OptionSpread" => {
346 let option_spreads: Vec<_> = instruments
347 .iter()
348 .map(|i| {
349 if let Self::OptionSpread(os) = i {
350 os
351 } else {
352 unreachable!()
353 }
354 })
355 .cloned()
356 .collect();
357 OptionSpread::encode_batch(metadata, &option_spreads)
358 }
359 "PerpetualContract" => {
360 let perpetual_contracts: Vec<_> = instruments
361 .iter()
362 .map(|i| {
363 if let Self::PerpetualContract(pc) = i {
364 pc
365 } else {
366 unreachable!()
367 }
368 })
369 .cloned()
370 .collect();
371 PerpetualContract::encode_batch(metadata, &perpetual_contracts)
372 }
373 "TokenizedAsset" => {
374 let tokenized_assets: Vec<_> = instruments
375 .iter()
376 .map(|i| {
377 if let Self::TokenizedAsset(ta) = i {
378 ta
379 } else {
380 unreachable!()
381 }
382 })
383 .cloned()
384 .collect();
385 TokenizedAsset::encode_batch(metadata, &tokenized_assets)
386 }
387 _ => Err(ArrowError::InvalidArgumentError(format!(
388 "Instrument type {type_name} serialization not yet implemented"
389 ))),
390 }
391 }
392
393 fn metadata(&self) -> HashMap<String, String> {
394 let mut metadata = HashMap::new();
395 metadata.insert(
396 KEY_INSTRUMENT_ID.to_string(),
397 Instrument::id(self).to_string(),
398 );
399
400 let type_name = match self {
401 Self::Cfd(_) => "Cfd",
402 Self::Commodity(_) => "Commodity",
403 Self::CurrencyPair(_) => "CurrencyPair",
404 Self::Equity(_) => "Equity",
405 Self::CryptoFuture(_) => "CryptoFuture",
406 Self::CryptoPerpetual(_) => "CryptoPerpetual",
407 Self::CryptoOption(_) => "CryptoOption",
408 Self::FuturesContract(_) => "FuturesContract",
409 Self::FuturesSpread(_) => "FuturesSpread",
410 Self::IndexInstrument(_) => "IndexInstrument",
411 Self::OptionContract(_) => "OptionContract",
412 Self::OptionSpread(_) => "OptionSpread",
413 Self::BinaryOption(_) => "BinaryOption",
414 Self::Betting(_) => "BettingInstrument",
415 Self::PerpetualContract(_) => "PerpetualContract",
416 Self::TokenizedAsset(_) => "TokenizedAsset",
417 };
418 metadata.insert("class".to_string(), type_name.to_string());
419 metadata
420 }
421}
422
423pub fn decode_instrument_any_batch(
430 #[allow(unused)] metadata: &HashMap<String, String>,
431 record_batch: &RecordBatch,
432) -> Result<Vec<InstrumentAny>, EncodingError> {
433 let type_name = metadata
434 .get("class")
435 .map(String::as_str)
436 .ok_or_else(|| EncodingError::MissingMetadata("class"))?;
437
438 match type_name {
439 "Cfd" => {
440 let cfds = cfd::decode_cfd_batch(metadata, record_batch)?;
441 Ok(cfds.into_iter().map(InstrumentAny::Cfd).collect())
442 }
443 "Commodity" => {
444 let commodities = commodity::decode_commodity_batch(metadata, record_batch)?;
445 Ok(commodities
446 .into_iter()
447 .map(InstrumentAny::Commodity)
448 .collect())
449 }
450 "BettingInstrument" => {
451 let betting = betting::decode_betting_instrument_batch(metadata, record_batch)?;
452 Ok(betting.into_iter().map(InstrumentAny::Betting).collect())
453 }
454 "BinaryOption" => {
455 let binary_options = binary_option::decode_binary_option_batch(metadata, record_batch)?;
456 Ok(binary_options
457 .into_iter()
458 .map(InstrumentAny::BinaryOption)
459 .collect())
460 }
461 "CryptoFuture" => {
462 let crypto_futures = crypto_future::decode_crypto_future_batch(metadata, record_batch)?;
463 Ok(crypto_futures
464 .into_iter()
465 .map(InstrumentAny::CryptoFuture)
466 .collect())
467 }
468 "CryptoOption" => {
469 let crypto_options = crypto_option::decode_crypto_option_batch(metadata, record_batch)?;
470 Ok(crypto_options
471 .into_iter()
472 .map(InstrumentAny::CryptoOption)
473 .collect())
474 }
475 "CryptoPerpetual" => {
476 let crypto_perps =
477 crypto_perpetual::decode_crypto_perpetual_batch(metadata, record_batch)?;
478 Ok(crypto_perps
479 .into_iter()
480 .map(InstrumentAny::CryptoPerpetual)
481 .collect())
482 }
483 "CurrencyPair" => {
484 let currency_pairs = currency_pair::decode_currency_pair_batch(metadata, record_batch)?;
485 Ok(currency_pairs
486 .into_iter()
487 .map(InstrumentAny::CurrencyPair)
488 .collect())
489 }
490 "Equity" => {
491 let equities = equity::decode_equity_batch(metadata, record_batch)?;
492 Ok(equities.into_iter().map(InstrumentAny::Equity).collect())
493 }
494 "FuturesContract" => {
495 let futures_contracts =
496 futures_contract::decode_futures_contract_batch(metadata, record_batch)?;
497 Ok(futures_contracts
498 .into_iter()
499 .map(InstrumentAny::FuturesContract)
500 .collect())
501 }
502 "FuturesSpread" => {
503 let futures_spreads =
504 futures_spread::decode_futures_spread_batch(metadata, record_batch)?;
505 Ok(futures_spreads
506 .into_iter()
507 .map(InstrumentAny::FuturesSpread)
508 .collect())
509 }
510 "IndexInstrument" => {
511 let index_instruments =
512 index_instrument::decode_index_instrument_batch(metadata, record_batch)?;
513 Ok(index_instruments
514 .into_iter()
515 .map(InstrumentAny::IndexInstrument)
516 .collect())
517 }
518 "OptionContract" => {
519 let option_contracts =
520 option_contract::decode_option_contract_batch(metadata, record_batch)?;
521 Ok(option_contracts
522 .into_iter()
523 .map(InstrumentAny::OptionContract)
524 .collect())
525 }
526 "OptionSpread" => {
527 let option_spreads = option_spread::decode_option_spread_batch(metadata, record_batch)?;
528 Ok(option_spreads
529 .into_iter()
530 .map(InstrumentAny::OptionSpread)
531 .collect())
532 }
533 "PerpetualContract" => {
534 let perpetual_contracts =
535 perpetual_contract::decode_perpetual_contract_batch(metadata, record_batch)?;
536 Ok(perpetual_contracts
537 .into_iter()
538 .map(InstrumentAny::PerpetualContract)
539 .collect())
540 }
541 "TokenizedAsset" => {
542 let tokenized_assets =
543 tokenized_asset::decode_tokenized_asset_batch(metadata, record_batch)?;
544 Ok(tokenized_assets
545 .into_iter()
546 .map(InstrumentAny::TokenizedAsset)
547 .collect())
548 }
549 _ => Err(EncodingError::ParseError(
550 "class",
551 format!("Unknown instrument type: {type_name}"),
552 )),
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use nautilus_core::UnixNanos;
559 use nautilus_model::{
560 enums::CurrencyType,
561 identifiers::{InstrumentId, Symbol},
562 instruments::{InstrumentAny, currency_pair::CurrencyPair},
563 types::{Currency, Price, Quantity},
564 };
565 use rstest::rstest;
566
567 use super::*;
568
569 #[rstest]
570 fn test_get_schema() {
571 let mut metadata = HashMap::new();
572 metadata.insert("class".to_string(), "CurrencyPair".to_string());
573 let schema = InstrumentAny::get_schema(Some(metadata));
574 assert!(schema.fields().len() >= 20);
575 assert_eq!(schema.field(0).name(), "id");
576 }
577
578 #[rstest]
579 #[case("")]
580 #[case(" ")]
581 #[case("\t\n")]
582 fn test_decode_currency_empty_or_whitespace_errors(#[case] value: &str) {
583 let result = decode_currency(value, "currency", "test.currency", 7);
584 let err = result.expect_err("empty code must surface EncodingError");
585 match err {
586 EncodingError::ParseError(field, msg) => {
587 assert_eq!(field, "currency");
588 assert!(
589 msg.contains("row 7"),
590 "message should include row index, found: {msg}",
591 );
592 assert!(
593 msg.contains("empty currency code"),
594 "message should describe empty code, found: {msg}",
595 );
596 }
597 other => panic!("unexpected error variant: {other:?}"),
598 }
599 assert!(Currency::try_from_str(value.trim()).is_none());
601 }
602
603 #[rstest]
604 #[case("USD", CurrencyType::Fiat, 2)]
605 #[case("BTC", CurrencyType::Crypto, 8)]
606 #[case("XAU", CurrencyType::CommodityBacked, 2)]
607 fn test_decode_currency_known_code_preserves_metadata(
608 #[case] code: &str,
609 #[case] expected_type: CurrencyType,
610 #[case] expected_precision: u8,
611 ) {
612 let currency = decode_currency(code, "currency", "test.currency", 0).unwrap();
613 assert_eq!(currency.code.as_str(), code);
614 assert_eq!(currency.currency_type, expected_type);
615 assert_eq!(currency.precision, expected_precision);
616 }
617
618 #[rstest]
619 fn test_decode_currency_unknown_code_registers_as_crypto() {
620 let code = "XDECTEST";
621 assert!(
622 Currency::try_from_str(code).is_none(),
623 "test precondition: '{code}' must not be pre-registered",
624 );
625
626 let currency = decode_currency(code, "base_currency", "test.base_currency", 0).unwrap();
627 assert_eq!(currency.code.as_str(), code);
628 assert_eq!(currency.currency_type, CurrencyType::Crypto);
629 assert_eq!(currency.precision, 8);
630 assert_eq!(currency.iso4217, 0);
631
632 let registered = Currency::try_from_str(code).expect("unknown code must be registered");
633 assert_eq!(registered, currency);
634 }
635
636 #[rstest]
637 fn test_encode_decode_round_trip() {
638 use nautilus_model::instruments::Instrument;
639 let instrument_id = InstrumentId::from("EUR/USD.SIM");
640 let currency_pair = CurrencyPair::new(
641 instrument_id,
642 Symbol::from("EUR/USD"),
643 Currency::from("EUR"),
644 Currency::from("USD"),
645 5,
646 0, Price::new(0.00001, 5),
648 Quantity::new(1.0, 0), None, None, None, None, None, None, None, None, None, None, None, None, None, UnixNanos::default(),
663 UnixNanos::default(),
664 );
665 let instrument = InstrumentAny::CurrencyPair(currency_pair);
666
667 let metadata = instrument.metadata();
668 let record_batch =
669 InstrumentAny::encode_batch(&metadata, std::slice::from_ref(&instrument)).unwrap();
670 let decoded = decode_instrument_any_batch(&metadata, &record_batch).unwrap();
671
672 assert_eq!(decoded.len(), 1);
673 assert_eq!(Instrument::id(&decoded[0]), Instrument::id(&instrument));
674 assert_eq!(
675 Instrument::raw_symbol(&decoded[0]),
676 Instrument::raw_symbol(&instrument)
677 );
678 assert_eq!(
679 Instrument::asset_class(&decoded[0]),
680 Instrument::asset_class(&instrument)
681 );
682
683 match (&decoded[0], &instrument) {
684 (InstrumentAny::CurrencyPair(decoded_cp), InstrumentAny::CurrencyPair(original_cp)) => {
685 assert_eq!(decoded_cp.id, original_cp.id);
686 assert_eq!(decoded_cp.base_currency, original_cp.base_currency);
687 assert_eq!(decoded_cp.quote_currency, original_cp.quote_currency);
688 assert_eq!(decoded_cp.price_precision, original_cp.price_precision);
689 assert_eq!(decoded_cp.size_precision, original_cp.size_precision);
690 }
691 _ => panic!("Decoded instrument type mismatch"),
692 }
693 }
694
695 #[rstest]
696 fn test_encode_decode_round_trip_equity() {
697 use nautilus_model::instruments::{Instrument, equity::Equity};
698
699 let instrument_id = InstrumentId::from("AAPL.NASDAQ");
700 let equity = Equity::new(
701 instrument_id,
702 Symbol::from("AAPL"),
703 None, Currency::from("USD"),
705 2,
706 Price::new(0.01, 2),
707 None, None, None, None, None, None, None, None, None, None, UnixNanos::default(),
718 UnixNanos::default(),
719 );
720 let instrument = InstrumentAny::Equity(equity);
721
722 let metadata = instrument.metadata();
723 let record_batch =
724 InstrumentAny::encode_batch(&metadata, std::slice::from_ref(&instrument)).unwrap();
725 let decoded = decode_instrument_any_batch(&metadata, &record_batch).unwrap();
726 assert_eq!(decoded.len(), 1);
727 assert_eq!(Instrument::id(&decoded[0]), Instrument::id(&instrument));
728 assert_eq!(
729 Instrument::raw_symbol(&decoded[0]),
730 Instrument::raw_symbol(&instrument)
731 );
732 assert_eq!(
733 Instrument::asset_class(&decoded[0]),
734 Instrument::asset_class(&instrument)
735 );
736
737 match (&decoded[0], &instrument) {
738 (InstrumentAny::Equity(decoded_eq), InstrumentAny::Equity(original_eq)) => {
739 assert_eq!(decoded_eq.id, original_eq.id);
740 assert_eq!(decoded_eq.currency, original_eq.currency);
741 assert_eq!(decoded_eq.price_precision, original_eq.price_precision);
742 }
743 _ => panic!("Decoded instrument type mismatch"),
744 }
745 }
746
747 fn roundtrip_case(instrument: &InstrumentAny) {
748 use nautilus_model::instruments::Instrument;
749
750 let metadata = instrument.metadata();
751 let record_batch =
752 InstrumentAny::encode_batch(&metadata, std::slice::from_ref(instrument)).unwrap();
753 let decoded = decode_instrument_any_batch(&metadata, &record_batch).unwrap();
754
755 assert_eq!(decoded.len(), 1);
756 assert_eq!(Instrument::id(&decoded[0]), Instrument::id(instrument));
757 assert_eq!(
758 Instrument::raw_symbol(&decoded[0]),
759 Instrument::raw_symbol(instrument)
760 );
761 assert_eq!(
762 Instrument::asset_class(&decoded[0]),
763 Instrument::asset_class(instrument)
764 );
765 assert_eq!(
766 Instrument::instrument_class(&decoded[0]),
767 Instrument::instrument_class(instrument)
768 );
769 assert_eq!(
770 Instrument::price_precision(&decoded[0]),
771 Instrument::price_precision(instrument)
772 );
773 assert_eq!(
774 Instrument::size_precision(&decoded[0]),
775 Instrument::size_precision(instrument)
776 );
777 assert_eq!(
778 Instrument::quote_currency(&decoded[0]),
779 Instrument::quote_currency(instrument)
780 );
781 assert_eq!(
782 std::mem::discriminant(&decoded[0]),
783 std::mem::discriminant(instrument),
784 "decoded variant must match encoded variant"
785 );
786 }
787
788 #[rstest]
789 fn test_roundtrip_betting() {
790 use nautilus_model::instruments::stubs::betting;
791 roundtrip_case(&InstrumentAny::Betting(betting()));
792 }
793
794 #[rstest]
795 fn test_roundtrip_binary_option() {
796 use nautilus_model::instruments::stubs::binary_option;
797 roundtrip_case(&InstrumentAny::BinaryOption(binary_option()));
798 }
799
800 #[rstest]
801 fn test_roundtrip_cfd() {
802 use nautilus_model::instruments::stubs::cfd_gold;
803 roundtrip_case(&InstrumentAny::Cfd(cfd_gold()));
804 }
805
806 #[rstest]
807 fn test_roundtrip_commodity() {
808 use nautilus_model::instruments::stubs::commodity_gold;
809 roundtrip_case(&InstrumentAny::Commodity(commodity_gold()));
810 }
811
812 #[rstest]
813 fn test_roundtrip_crypto_future() {
814 use nautilus_model::instruments::stubs::crypto_future_btcusdt;
815 roundtrip_case(&InstrumentAny::CryptoFuture(crypto_future_btcusdt(
816 2,
817 6,
818 Price::from("0.01"),
819 Quantity::from("0.000001"),
820 )));
821 }
822
823 #[rstest]
824 fn test_roundtrip_crypto_option() {
825 use nautilus_model::instruments::stubs::crypto_option_btc_deribit;
826 roundtrip_case(&InstrumentAny::CryptoOption(crypto_option_btc_deribit(
827 3,
828 1,
829 Price::from("0.001"),
830 Quantity::from("0.1"),
831 )));
832 }
833
834 #[rstest]
835 fn test_roundtrip_crypto_perpetual_inverse() {
836 use nautilus_model::instruments::stubs::xbtusd_bitmex;
837 roundtrip_case(&InstrumentAny::CryptoPerpetual(xbtusd_bitmex()));
838 }
839
840 #[rstest]
841 fn test_roundtrip_crypto_perpetual_linear() {
842 use nautilus_model::instruments::stubs::crypto_perpetual_ethusdt;
843 roundtrip_case(&InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt()));
844 }
845
846 #[rstest]
847 fn test_roundtrip_futures_contract() {
848 use nautilus_model::instruments::stubs::futures_contract_es;
849 roundtrip_case(&InstrumentAny::FuturesContract(futures_contract_es(
850 None, None,
851 )));
852 }
853
854 #[rstest]
855 fn test_roundtrip_futures_spread() {
856 use nautilus_model::instruments::stubs::futures_spread_es;
857 roundtrip_case(&InstrumentAny::FuturesSpread(futures_spread_es()));
858 }
859
860 #[rstest]
861 fn test_roundtrip_index_instrument() {
862 use nautilus_model::instruments::stubs::index_instrument_spx;
863 roundtrip_case(&InstrumentAny::IndexInstrument(index_instrument_spx()));
864 }
865
866 #[rstest]
867 fn test_roundtrip_option_contract() {
868 use nautilus_model::instruments::stubs::option_contract_appl;
869 roundtrip_case(&InstrumentAny::OptionContract(option_contract_appl()));
870 }
871
872 #[rstest]
873 fn test_roundtrip_option_spread() {
874 use nautilus_model::instruments::stubs::option_spread;
875 roundtrip_case(&InstrumentAny::OptionSpread(option_spread()));
876 }
877
878 #[rstest]
879 fn test_roundtrip_perpetual_contract() {
880 use nautilus_model::instruments::stubs::perpetual_contract_eurusd;
881 roundtrip_case(&InstrumentAny::PerpetualContract(
882 perpetual_contract_eurusd(),
883 ));
884 }
885
886 #[rstest]
887 fn test_roundtrip_tokenized_asset() {
888 use nautilus_model::instruments::stubs::tokenized_asset_aaplx;
889 roundtrip_case(&InstrumentAny::TokenizedAsset(tokenized_asset_aaplx()));
890 }
891}