1use jiff::Timestamp;
17use nautilus_core::UnixNanos;
18use nautilus_model::{
19 identifiers::Symbol,
20 instruments::InstrumentAny,
21 types::{Currency, Price, Quantity},
22};
23use rust_decimal_macros::dec;
24
25use super::{
26 instruments::{
27 create_crypto_future, create_crypto_option, create_crypto_perpetual, create_currency_pair,
28 get_currency,
29 },
30 models::TardisInstrumentInfo,
31};
32use crate::common::{
33 enums::TardisInstrumentType,
34 parse::{normalize_instrument_id, parse_instrument_id},
35};
36
37#[must_use]
38pub fn parse_instrument_any(
39 info: &TardisInstrumentInfo,
40 effective: Option<UnixNanos>,
41 ts_init: Option<UnixNanos>,
42 normalize_symbols: bool,
43) -> Vec<InstrumentAny> {
44 match info.instrument_type {
45 TardisInstrumentType::Spot => {
46 parse_spot_instrument(info, effective, ts_init, normalize_symbols)
47 }
48 TardisInstrumentType::Perpetual => {
49 parse_perp_instrument(info, effective, ts_init, normalize_symbols)
50 }
51 TardisInstrumentType::Future | TardisInstrumentType::Combo => {
52 parse_future_instrument(info, effective, ts_init, normalize_symbols)
53 }
54 TardisInstrumentType::Option => {
55 match parse_option_instrument(info, effective, ts_init, normalize_symbols) {
56 Ok(instruments) => instruments,
57 Err(e) => {
58 log::error!("Failed to parse option instrument: {e}");
59 vec![]
60 }
61 }
62 }
63 }
64}
65
66fn parse_spot_instrument(
67 info: &TardisInstrumentInfo,
68 effective: Option<UnixNanos>,
69 ts_init: Option<UnixNanos>,
70 normalize_symbols: bool,
71) -> Vec<InstrumentAny> {
72 let instrument_id = if normalize_symbols {
73 normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
74 } else {
75 parse_instrument_id(&info.exchange, info.id)
76 };
77 let raw_symbol = Symbol::new(info.id);
78 let margin_init = dec!(0); let margin_maint = dec!(0); let mut price_increment = parse_price_increment(info.price_increment);
82 let base_currency = get_currency(info.base_currency.to_uppercase().as_str());
83 let mut size_increment = parse_spot_size_increment(info.amount_increment, base_currency);
84 let mut multiplier = parse_multiplier(info.contract_multiplier);
85 let mut ts_event = info
86 .changes
87 .as_ref()
88 .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
89 .unwrap_or_else(|| UnixNanos::from(info.available_since));
90
91 let mut instruments = vec![create_currency_pair(
93 info,
94 instrument_id,
95 raw_symbol,
96 price_increment,
97 size_increment,
98 multiplier,
99 margin_init,
100 margin_maint,
101 ts_event,
102 ts_init.unwrap_or(ts_event),
103 )];
104
105 if let Some(changes) = &info.changes {
106 let mut sorted_changes = changes.clone();
108 sorted_changes.sort_by_key(|b| std::cmp::Reverse(b.until));
109
110 if let Some(effective_time) = effective {
111 for (i, change) in sorted_changes.iter().enumerate() {
113 if change.price_increment.is_none()
114 && change.amount_increment.is_none()
115 && change.contract_multiplier.is_none()
116 {
117 continue; }
119
120 ts_event = UnixNanos::from(change.until);
121
122 if ts_event < effective_time {
123 break; } else if i == sorted_changes.len() - 1 {
125 ts_event = UnixNanos::from(info.available_since);
126 }
127
128 price_increment = change
129 .price_increment
130 .map_or(price_increment, parse_price_increment);
131 size_increment = change.amount_increment.map_or(size_increment, |value| {
132 parse_spot_size_increment(value, base_currency)
133 });
134 multiplier = match change.contract_multiplier {
135 Some(value) => Some(Quantity::from(value.to_string())),
136 None => multiplier,
137 };
138 }
139
140 instruments = vec![create_currency_pair(
142 info,
143 instrument_id,
144 raw_symbol,
145 price_increment,
146 size_increment,
147 multiplier,
148 margin_init,
149 margin_maint,
150 ts_event,
151 ts_init.unwrap_or(ts_event),
152 )];
153 } else {
154 for (i, change) in sorted_changes.iter().enumerate() {
156 if change.price_increment.is_none()
157 && change.amount_increment.is_none()
158 && change.contract_multiplier.is_none()
159 {
160 continue; }
162
163 price_increment = change
164 .price_increment
165 .map_or(price_increment, parse_price_increment);
166 size_increment = change.amount_increment.map_or(size_increment, |value| {
167 parse_spot_size_increment(value, base_currency)
168 });
169 multiplier = match change.contract_multiplier {
170 Some(value) => Some(Quantity::from(value.to_string())),
171 None => multiplier,
172 };
173
174 ts_event = if i == sorted_changes.len() - 1 {
176 UnixNanos::from(info.available_since)
177 } else {
178 UnixNanos::from(change.until)
179 };
180
181 instruments.push(create_currency_pair(
182 info,
183 instrument_id,
184 raw_symbol,
185 price_increment,
186 size_increment,
187 multiplier,
188 margin_init,
189 margin_maint,
190 ts_event,
191 ts_init.unwrap_or(ts_event),
192 ));
193 }
194
195 instruments.reverse();
197 }
198 }
199
200 instruments
201}
202
203fn parse_perp_instrument(
204 info: &TardisInstrumentInfo,
205 effective: Option<UnixNanos>,
206 ts_init: Option<UnixNanos>,
207 normalize_symbols: bool,
208) -> Vec<InstrumentAny> {
209 let instrument_id = if normalize_symbols {
210 normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
211 } else {
212 parse_instrument_id(&info.exchange, info.id)
213 };
214 let raw_symbol = Symbol::new(info.id);
215 let margin_init = dec!(0); let margin_maint = dec!(0); let mut price_increment = parse_price_increment(info.price_increment);
219 let mut size_increment = parse_size_increment(info.amount_increment);
220 let mut multiplier = parse_multiplier(info.contract_multiplier);
221 let mut ts_event = info
222 .changes
223 .as_ref()
224 .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
225 .unwrap_or_else(|| UnixNanos::from(info.available_since));
226
227 let mut instruments = vec![create_crypto_perpetual(
229 info,
230 instrument_id,
231 raw_symbol,
232 price_increment,
233 size_increment,
234 multiplier,
235 margin_init,
236 margin_maint,
237 ts_event,
238 ts_init.unwrap_or(ts_event),
239 )];
240
241 if let Some(changes) = &info.changes {
242 let mut sorted_changes = changes.clone();
244 sorted_changes.sort_by_key(|b| std::cmp::Reverse(b.until));
245
246 if let Some(effective_time) = effective {
247 for (i, change) in sorted_changes.iter().enumerate() {
249 if change.price_increment.is_none()
250 && change.amount_increment.is_none()
251 && change.contract_multiplier.is_none()
252 {
253 continue; }
255
256 ts_event = UnixNanos::from(change.until);
257
258 if ts_event < effective_time {
259 break; } else if i == sorted_changes.len() - 1 {
261 ts_event = UnixNanos::from(info.available_since);
262 }
263
264 price_increment = change
265 .price_increment
266 .map_or(price_increment, parse_price_increment);
267 size_increment = change
268 .amount_increment
269 .map_or(size_increment, parse_size_increment);
270 multiplier = match change.contract_multiplier {
271 Some(value) => Some(Quantity::from(value.to_string())),
272 None => multiplier,
273 };
274 }
275
276 instruments = vec![create_crypto_perpetual(
278 info,
279 instrument_id,
280 raw_symbol,
281 price_increment,
282 size_increment,
283 multiplier,
284 margin_init,
285 margin_maint,
286 ts_event,
287 ts_init.unwrap_or(ts_event),
288 )];
289 } else {
290 for (i, change) in sorted_changes.iter().enumerate() {
292 if change.price_increment.is_none()
293 && change.amount_increment.is_none()
294 && change.contract_multiplier.is_none()
295 {
296 continue; }
298
299 price_increment = change
300 .price_increment
301 .map_or(price_increment, parse_price_increment);
302 size_increment = change
303 .amount_increment
304 .map_or(size_increment, parse_size_increment);
305 multiplier = match change.contract_multiplier {
306 Some(value) => Some(Quantity::from(value.to_string())),
307 None => multiplier,
308 };
309
310 ts_event = if i == sorted_changes.len() - 1 {
312 UnixNanos::from(info.available_since)
313 } else {
314 UnixNanos::from(change.until)
315 };
316
317 instruments.push(create_crypto_perpetual(
318 info,
319 instrument_id,
320 raw_symbol,
321 price_increment,
322 size_increment,
323 multiplier,
324 margin_init,
325 margin_maint,
326 ts_event,
327 ts_init.unwrap_or(ts_event),
328 ));
329 }
330
331 instruments.reverse();
333 }
334 }
335
336 instruments
337}
338
339fn parse_future_instrument(
340 info: &TardisInstrumentInfo,
341 effective: Option<UnixNanos>,
342 ts_init: Option<UnixNanos>,
343 normalize_symbols: bool,
344) -> Vec<InstrumentAny> {
345 let instrument_id = if normalize_symbols {
346 normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
347 } else {
348 parse_instrument_id(&info.exchange, info.id)
349 };
350 let raw_symbol = Symbol::new(info.id);
351 let activation = parse_datetime_to_unix_nanos(Some(info.available_since));
352 let expiration = parse_datetime_to_unix_nanos(info.expiry);
353 let margin_init = dec!(0); let margin_maint = dec!(0); let mut price_increment = parse_price_increment(info.price_increment);
357 let mut size_increment = parse_size_increment(info.amount_increment);
358 let mut multiplier = parse_multiplier(info.contract_multiplier);
359 let mut ts_event = info
360 .changes
361 .as_ref()
362 .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
363 .unwrap_or_else(|| UnixNanos::from(info.available_since));
364
365 let mut instruments = vec![create_crypto_future(
367 info,
368 instrument_id,
369 raw_symbol,
370 activation,
371 expiration,
372 price_increment,
373 size_increment,
374 multiplier,
375 margin_init,
376 margin_maint,
377 ts_event,
378 ts_init.unwrap_or(ts_event),
379 )];
380
381 if let Some(changes) = &info.changes {
382 let mut sorted_changes = changes.clone();
384 sorted_changes.sort_by_key(|b| std::cmp::Reverse(b.until));
385
386 if let Some(effective_time) = effective {
387 for (i, change) in sorted_changes.iter().enumerate() {
389 if change.price_increment.is_none()
390 && change.amount_increment.is_none()
391 && change.contract_multiplier.is_none()
392 {
393 continue; }
395
396 ts_event = UnixNanos::from(change.until);
397
398 if ts_event < effective_time {
399 break; } else if i == sorted_changes.len() - 1 {
401 ts_event = UnixNanos::from(info.available_since);
402 }
403
404 price_increment = change
405 .price_increment
406 .map_or(price_increment, parse_price_increment);
407 size_increment = change
408 .amount_increment
409 .map_or(size_increment, parse_size_increment);
410 multiplier = match change.contract_multiplier {
411 Some(value) => Some(Quantity::from(value.to_string())),
412 None => multiplier,
413 };
414 }
415
416 instruments = vec![create_crypto_future(
418 info,
419 instrument_id,
420 raw_symbol,
421 activation,
422 expiration,
423 price_increment,
424 size_increment,
425 multiplier,
426 margin_init,
427 margin_maint,
428 ts_event,
429 ts_init.unwrap_or(ts_event),
430 )];
431 } else {
432 for (i, change) in sorted_changes.iter().enumerate() {
434 if change.price_increment.is_none()
435 && change.amount_increment.is_none()
436 && change.contract_multiplier.is_none()
437 {
438 continue; }
440
441 price_increment = change
442 .price_increment
443 .map_or(price_increment, parse_price_increment);
444 size_increment = change
445 .amount_increment
446 .map_or(size_increment, parse_size_increment);
447 multiplier = match change.contract_multiplier {
448 Some(value) => Some(Quantity::from(value.to_string())),
449 None => multiplier,
450 };
451
452 ts_event = if i == sorted_changes.len() - 1 {
454 UnixNanos::from(info.available_since)
455 } else {
456 UnixNanos::from(change.until)
457 };
458
459 instruments.push(create_crypto_future(
460 info,
461 instrument_id,
462 raw_symbol,
463 activation,
464 expiration,
465 price_increment,
466 size_increment,
467 multiplier,
468 margin_init,
469 margin_maint,
470 ts_event,
471 ts_init.unwrap_or(ts_event),
472 ));
473 }
474
475 instruments.reverse();
477 }
478 }
479
480 instruments
481}
482
483fn parse_option_instrument(
484 info: &TardisInstrumentInfo,
485 effective: Option<UnixNanos>,
486 ts_init: Option<UnixNanos>,
487 normalize_symbols: bool,
488) -> anyhow::Result<Vec<InstrumentAny>> {
489 let instrument_id = if normalize_symbols {
490 normalize_instrument_id(&info.exchange, info.id, &info.instrument_type, info.inverse)
491 } else {
492 parse_instrument_id(&info.exchange, info.id)
493 };
494 let raw_symbol = Symbol::new(info.id);
495 let activation = parse_datetime_to_unix_nanos(Some(info.available_since));
496 let expiration = parse_datetime_to_unix_nanos(info.expiry);
497 let margin_init = dec!(0); let margin_maint = dec!(0); let mut price_increment = parse_price_increment(info.price_increment);
501 let mut size_increment = parse_size_increment(info.amount_increment);
502 let mut multiplier = parse_multiplier(info.contract_multiplier);
503 let mut ts_event = info
504 .changes
505 .as_ref()
506 .and_then(|changes| changes.last().map(|c| UnixNanos::from(c.until)))
507 .unwrap_or_else(|| UnixNanos::from(info.available_since));
508
509 let mut instruments = vec![create_crypto_option(
511 info,
512 instrument_id,
513 raw_symbol,
514 activation,
515 expiration,
516 price_increment,
517 size_increment,
518 multiplier,
519 margin_init,
520 margin_maint,
521 ts_event,
522 ts_init.unwrap_or(ts_event),
523 )?];
524
525 if let Some(changes) = &info.changes {
526 let mut sorted_changes = changes.clone();
528 sorted_changes.sort_by_key(|b| std::cmp::Reverse(b.until));
529
530 if let Some(effective_time) = effective {
531 for (i, change) in sorted_changes.iter().enumerate() {
533 if change.price_increment.is_none()
534 && change.amount_increment.is_none()
535 && change.contract_multiplier.is_none()
536 {
537 continue; }
539
540 ts_event = UnixNanos::from(change.until);
541
542 if ts_event < effective_time {
543 break; } else if i == sorted_changes.len() - 1 {
545 ts_event = UnixNanos::from(info.available_since);
546 }
547
548 price_increment = change
549 .price_increment
550 .map_or(price_increment, parse_price_increment);
551 size_increment = change
552 .amount_increment
553 .map_or(size_increment, parse_size_increment);
554 multiplier = match change.contract_multiplier {
555 Some(value) => Some(Quantity::from(value.to_string())),
556 None => multiplier,
557 };
558 }
559
560 instruments = vec![create_crypto_option(
562 info,
563 instrument_id,
564 raw_symbol,
565 activation,
566 expiration,
567 price_increment,
568 size_increment,
569 multiplier,
570 margin_init,
571 margin_maint,
572 ts_event,
573 ts_init.unwrap_or(ts_event),
574 )?];
575 } else {
576 for (i, change) in sorted_changes.iter().enumerate() {
578 if change.price_increment.is_none()
579 && change.amount_increment.is_none()
580 && change.contract_multiplier.is_none()
581 {
582 continue; }
584
585 price_increment = change
586 .price_increment
587 .map_or(price_increment, parse_price_increment);
588 size_increment = change
589 .amount_increment
590 .map_or(size_increment, parse_size_increment);
591 multiplier = match change.contract_multiplier {
592 Some(value) => Some(Quantity::from(value.to_string())),
593 None => multiplier,
594 };
595
596 ts_event = if i == sorted_changes.len() - 1 {
598 UnixNanos::from(info.available_since)
599 } else {
600 UnixNanos::from(change.until)
601 };
602
603 instruments.push(create_crypto_option(
604 info,
605 instrument_id,
606 raw_symbol,
607 activation,
608 expiration,
609 price_increment,
610 size_increment,
611 multiplier,
612 margin_init,
613 margin_maint,
614 ts_event,
615 ts_init.unwrap_or(ts_event),
616 )?);
617 }
618
619 instruments.reverse();
621 }
622 }
623
624 Ok(instruments)
625}
626
627fn parse_price_increment(value: f64) -> Price {
629 Price::from(value.to_string())
630}
631
632fn parse_size_increment(value: f64) -> Quantity {
634 Quantity::from(value.to_string())
635}
636
637fn parse_spot_size_increment(value: f64, currency: Currency) -> Quantity {
639 if value == 0.0 {
640 let exponent = -i32::from(currency.precision);
641 Quantity::from(format!("{}", 10.0_f64.powi(exponent)))
642 } else {
643 Quantity::from(value.to_string())
644 }
645}
646
647fn parse_multiplier(value: Option<f64>) -> Option<Quantity> {
649 value.map(|x| Quantity::from(x.to_string()))
650}
651
652fn parse_datetime_to_unix_nanos(value: Option<Timestamp>) -> UnixNanos {
656 value
657 .map(|dt| match u64::try_from(dt.as_nanosecond()) {
658 Ok(nanos) => UnixNanos::from(nanos),
659 Err(_) => {
660 log::warn!("Timestamp {dt} is outside the UnixNanos range, defaulting to 0");
661 UnixNanos::default()
662 }
663 })
664 .unwrap_or_default()
665}
666
667#[must_use]
669pub fn parse_settlement_currency(info: &TardisInstrumentInfo, is_inverse: bool) -> String {
670 info.settlement_currency
671 .unwrap_or({
672 if is_inverse {
673 info.base_currency
674 } else {
675 info.quote_currency
676 }
677 })
678 .to_uppercase()
679}
680
681#[cfg(test)]
682mod tests {
683 use nautilus_model::{identifiers::InstrumentId, instruments::Instrument};
684 use rstest::rstest;
685
686 use super::*;
687 use crate::common::testing::load_test_json;
688
689 #[rstest]
690 fn test_parse_instrument_spot() {
691 let json_data = load_test_json("instrument_spot.json");
692 let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
693
694 let instruments = parse_instrument_any(&info, None, None, false);
695 let inst0 = instruments[0].clone();
696 let inst1 = instruments[1].clone();
697
698 assert_eq!(inst0.id(), InstrumentId::from("BTC_USDC.DERIBIT"));
699 assert_eq!(inst0.raw_symbol(), Symbol::from("BTC_USDC"));
700 assert_eq!(inst0.underlying(), None);
701 assert_eq!(inst0.base_currency(), Some(Currency::BTC()));
702 assert_eq!(inst0.quote_currency(), Currency::USDC());
703 assert_eq!(inst0.settlement_currency(), Currency::USDC());
704 assert!(!inst0.is_inverse());
705 assert_eq!(inst0.price_precision(), 2);
706 assert_eq!(inst0.size_precision(), 4);
707 assert_eq!(inst0.price_increment(), Price::from("0.01"));
708 assert_eq!(inst0.size_increment(), Quantity::from("0.0001"));
709 assert_eq!(inst0.multiplier(), Quantity::from(1));
710 assert_eq!(inst0.activation_ns(), None);
711 assert_eq!(inst0.expiration_ns(), None);
712 assert_eq!(inst0.lot_size(), Some(Quantity::from("0.0001")));
713 assert_eq!(inst0.min_quantity(), Some(Quantity::from("0.0001")));
714 assert_eq!(inst0.max_quantity(), None);
715 assert_eq!(inst0.min_notional(), None);
716 assert_eq!(inst0.max_notional(), None);
717 assert_eq!(inst0.ts_event().to_rfc3339(), "2023-04-24T00:00:00+00:00");
718 assert_eq!(inst0.ts_init().to_rfc3339(), "2023-04-24T00:00:00+00:00");
719
720 assert_eq!(inst1.id(), InstrumentId::from("BTC_USDC.DERIBIT"));
721 assert_eq!(inst1.raw_symbol(), Symbol::from("BTC_USDC"));
722 assert_eq!(inst1.underlying(), None);
723 assert_eq!(inst1.base_currency(), Some(Currency::BTC()));
724 assert_eq!(inst1.quote_currency(), Currency::USDC());
725 assert_eq!(inst1.settlement_currency(), Currency::USDC());
726 assert!(!inst1.is_inverse());
727 assert_eq!(inst1.price_precision(), 0); assert_eq!(inst1.size_precision(), 4);
729 assert_eq!(inst1.price_increment(), Price::from("1")); assert_eq!(inst1.size_increment(), Quantity::from("0.0001"));
731 assert_eq!(inst1.multiplier(), Quantity::from(1));
732 assert_eq!(inst1.activation_ns(), None);
733 assert_eq!(inst1.expiration_ns(), None);
734 assert_eq!(inst1.lot_size(), Some(Quantity::from("0.0001")));
735 assert_eq!(inst1.min_quantity(), Some(Quantity::from("0.0001")));
736 assert_eq!(inst1.max_quantity(), None);
737 assert_eq!(inst1.min_notional(), None);
738 assert_eq!(inst1.max_notional(), None);
739 assert_eq!(inst1.ts_event().to_rfc3339(), "2024-04-02T12:10:00+00:00");
740 assert_eq!(inst1.ts_init().to_rfc3339(), "2024-04-02T12:10:00+00:00");
741 }
742
743 #[rstest]
744 fn test_parse_instrument_perpetual() {
745 let json_data = load_test_json("instrument_perpetual.json");
746 let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
747
748 let effective = UnixNanos::from("2020-08-01T08:00:00+00:00");
749 let instrument =
750 parse_instrument_any(&info, Some(effective), Some(UnixNanos::default()), false)
751 .first()
752 .unwrap()
753 .clone();
754
755 assert_eq!(instrument.id(), InstrumentId::from("XBTUSD.BITMEX"));
756 assert_eq!(instrument.raw_symbol(), Symbol::from("XBTUSD"));
757 assert_eq!(instrument.underlying(), None);
758 assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
759 assert_eq!(instrument.quote_currency(), Currency::USD());
760 assert_eq!(instrument.settlement_currency(), Currency::BTC());
761 assert!(instrument.is_inverse());
762 assert_eq!(instrument.price_precision(), 1);
763 assert_eq!(instrument.size_precision(), 0);
764 assert_eq!(instrument.price_increment(), Price::from("0.5"));
765 assert_eq!(instrument.size_increment(), Quantity::from(1));
766 assert_eq!(instrument.multiplier(), Quantity::from(1));
767 assert_eq!(instrument.activation_ns(), None);
768 assert_eq!(instrument.expiration_ns(), None);
769 assert_eq!(instrument.lot_size(), Some(Quantity::from(1)));
770 assert_eq!(instrument.min_quantity(), Some(Quantity::from(100)));
771 assert_eq!(instrument.max_quantity(), None);
772 assert_eq!(instrument.min_notional(), None);
773 assert_eq!(instrument.max_notional(), None);
774 }
775
776 #[rstest]
777 fn test_parse_instrument_future() {
778 let json_data = load_test_json("instrument_future.json");
779 let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
780
781 let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
782 .first()
783 .unwrap()
784 .clone();
785
786 assert_eq!(instrument.id(), InstrumentId::from("BTC-14FEB25.DERIBIT"));
787 assert_eq!(instrument.raw_symbol(), Symbol::from("BTC-14FEB25"));
788 assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
789 assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
790 assert_eq!(instrument.quote_currency(), Currency::USD());
791 assert_eq!(instrument.settlement_currency(), Currency::BTC());
792 assert!(instrument.is_inverse());
793 assert_eq!(instrument.price_precision(), 1); assert_eq!(instrument.size_precision(), 0); assert_eq!(instrument.price_increment(), Price::from("2.5"));
796 assert_eq!(instrument.size_increment(), Quantity::from(10));
797 assert_eq!(instrument.multiplier(), Quantity::from(1));
798 assert_eq!(
799 instrument.activation_ns(),
800 Some(UnixNanos::from(1_738_281_600_000_000_000))
801 );
802 assert_eq!(
803 instrument.expiration_ns(),
804 Some(UnixNanos::from(1_739_520_000_000_000_000))
805 );
806 assert_eq!(instrument.lot_size(), Some(Quantity::from(10)));
807 assert_eq!(instrument.min_quantity(), Some(Quantity::from(10)));
808 assert_eq!(instrument.max_quantity(), None);
809 assert_eq!(instrument.min_notional(), None);
810 assert_eq!(instrument.max_notional(), None);
811 }
812
813 #[rstest]
814 fn test_parse_instrument_perpetual_current() {
815 let json_data = load_test_json("instrument_perpetual.json");
816 let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
817
818 let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
819 .last()
820 .unwrap()
821 .clone();
822
823 assert_eq!(instrument.id(), InstrumentId::from("XBTUSD.BITMEX"));
824 assert_eq!(instrument.raw_symbol(), Symbol::from("XBTUSD"));
825 assert_eq!(instrument.size_increment(), Quantity::from(100));
826 assert_eq!(instrument.lot_size(), Some(Quantity::from(100)));
827 assert_eq!(instrument.min_quantity(), Some(Quantity::from(100)));
828 }
829
830 #[rstest]
831 fn test_parse_instrument_combo() {
832 let json_data = load_test_json("instrument_combo.json");
833 let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
834
835 let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
836 .first()
837 .unwrap()
838 .clone();
839
840 assert_eq!(
841 instrument.id(),
842 InstrumentId::from("BTC-FS-28MAR25_PERP.DERIBIT")
843 );
844 assert_eq!(instrument.raw_symbol(), Symbol::from("BTC-FS-28MAR25_PERP"));
845 assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
846 assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
847 assert_eq!(instrument.quote_currency(), Currency::USD());
848 assert_eq!(instrument.settlement_currency(), Currency::BTC());
849 assert!(instrument.is_inverse());
850 assert_eq!(instrument.price_precision(), 1); assert_eq!(instrument.size_precision(), 0); assert_eq!(instrument.price_increment(), Price::from("0.5"));
853 assert_eq!(instrument.size_increment(), Quantity::from(10));
854 assert_eq!(instrument.multiplier(), Quantity::from(1));
855 assert_eq!(
856 instrument.activation_ns(),
857 Some(UnixNanos::from(1_711_670_400_000_000_000))
858 );
859 assert_eq!(
860 instrument.expiration_ns(),
861 Some(UnixNanos::from(1_743_148_800_000_000_000))
862 );
863 assert_eq!(instrument.lot_size(), Some(Quantity::from(10)));
864 assert_eq!(instrument.min_quantity(), Some(Quantity::from(10)));
865 assert_eq!(instrument.max_quantity(), None);
866 assert_eq!(instrument.min_notional(), None);
867 assert_eq!(instrument.max_notional(), None);
868 }
869
870 #[rstest]
871 fn test_parse_instrument_option() {
872 let json_data = load_test_json("instrument_option.json");
873 let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
874
875 let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
876 .first()
877 .unwrap()
878 .clone();
879
880 assert_eq!(
881 instrument.id(),
882 InstrumentId::from("BTC-25APR25-200000-P.DERIBIT")
883 );
884 assert_eq!(
885 instrument.raw_symbol(),
886 Symbol::from("BTC-25APR25-200000-P")
887 );
888 assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
889 assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
890 assert_eq!(instrument.quote_currency(), Currency::BTC());
891 assert_eq!(instrument.settlement_currency(), Currency::BTC());
892 assert!(instrument.is_inverse());
893 assert_eq!(instrument.price_precision(), 4);
894 assert_eq!(instrument.size_precision(), 1); assert_eq!(instrument.price_increment(), Price::from("0.0001"));
896 assert_eq!(instrument.size_increment(), Quantity::from("0.1"));
897 assert_eq!(instrument.multiplier(), Quantity::from(1));
898 assert_eq!(
899 instrument.activation_ns(),
900 Some(UnixNanos::from(1_738_281_600_000_000_000))
901 );
902 assert_eq!(
903 instrument.expiration_ns(),
904 Some(UnixNanos::from(1_745_568_000_000_000_000))
905 );
906 assert_eq!(instrument.lot_size(), Some(Quantity::from("0.1")));
907 assert_eq!(instrument.min_quantity(), Some(Quantity::from("0.1")));
908 assert_eq!(instrument.max_quantity(), None);
909 assert_eq!(instrument.min_notional(), None);
910 assert_eq!(instrument.max_notional(), None);
911 }
912
913 #[rstest]
914 fn test_parse_instrument_info_populated() {
915 let json_data = load_test_json("instrument_perpetual.json");
916 let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
917
918 let instrument = parse_instrument_any(&info, None, Some(UnixNanos::default()), false)
919 .last()
920 .unwrap()
921 .clone();
922
923 let InstrumentAny::CryptoPerpetual(perp) = instrument else {
924 panic!("Expected CryptoPerpetual variant");
925 };
926
927 let info_params = perp.info.expect("info should be populated");
928
929 assert!(info_params.get("exchange").is_some());
931 assert!(info_params.get("baseCurrency").is_some());
932 assert!(info_params.get("quoteCurrency").is_some());
933
934 assert_eq!(info_params.get_str("datasetId"), Some("XBTUSD"),);
936 assert_eq!(
937 info_params.get_str("contractType"),
938 Some("inverse_perpetual"),
939 );
940 assert_eq!(info_params.get_str("underlyingIndex"), Some(".BXBT"),);
941
942 assert!(info_params.get("listing").is_none());
944 assert!(info_params.get("expiry").is_none());
945 }
946
947 #[rstest]
948 fn test_parse_okex_xperp_instrument() {
949 let json_data = load_test_json("okex_futures_xperp_instrument.json");
950 let info: TardisInstrumentInfo = serde_json::from_str(&json_data).unwrap();
951
952 let instruments = parse_instrument_any(&info, None, Some(UnixNanos::default()), false);
953 let instrument = instruments.first().unwrap().clone();
954
955 assert_eq!(instruments.len(), 1);
956 assert_eq!(info.instrument_type, TardisInstrumentType::Future);
957 assert_eq!(
958 instrument.id(),
959 InstrumentId::from("BTC-USD_UM_XPERP-310404.OKEX")
960 );
961 assert_eq!(
962 instrument.raw_symbol(),
963 Symbol::from("BTC-USD_UM_XPERP-310404")
964 );
965 assert_eq!(instrument.underlying().unwrap().as_str(), "BTC");
966 assert_eq!(instrument.base_currency(), Some(Currency::BTC()));
967 assert_eq!(instrument.quote_currency().code, "USD_UM_XPERP");
968 assert_eq!(instrument.settlement_currency().code, "USD_UM_XPERP");
969 assert!(!instrument.is_inverse());
970 assert_eq!(instrument.price_precision(), 1);
971 assert_eq!(instrument.size_precision(), 0);
972 assert_eq!(instrument.price_increment(), Price::from("0.1"));
973 assert_eq!(instrument.size_increment(), Quantity::from(1));
974 assert_eq!(instrument.multiplier(), Quantity::from("0.0001"));
975 assert_eq!(
976 instrument.activation_ns(),
977 Some(UnixNanos::from("2026-03-30T00:00:00Z"))
978 );
979 assert_eq!(
980 instrument.expiration_ns(),
981 Some(UnixNanos::from("2031-04-04T08:00:00Z"))
982 );
983 assert_eq!(instrument.lot_size(), Some(Quantity::from(1)));
984 assert_eq!(instrument.min_quantity(), Some(Quantity::from(1)));
985 assert_eq!(instrument.max_quantity(), None);
986 assert_eq!(
987 instrument.ts_event(),
988 UnixNanos::from("2026-03-30T00:00:00Z")
989 );
990
991 let InstrumentAny::CryptoFuture(future) = instrument else {
992 panic!("Expected CryptoFuture variant");
993 };
994
995 let info_params = future.info.expect("info should be populated");
996
997 assert_eq!(info_params.get_str("contractType"), Some("linear_xperp"));
999 assert_eq!(info_params.get_str("underlyingType"), Some("native"));
1000 assert_eq!(info_params.get_str("underlyingIndex"), Some("BTC-USD"));
1001 }
1002}