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