1use std::sync::Arc;
17
18use anyhow::Context;
19use chrono::{DateTime, Utc};
20use nautilus_core::UnixNanos;
21use nautilus_model::{
22 data::{
23 Bar, BarType, BookOrder, DEPTH10_LEN, Data, FundingRateUpdate, IndexPriceUpdate,
24 MarkPriceUpdate, NULL_ORDER, OptionGreekValues, OptionGreeks, OrderBookDelta,
25 OrderBookDeltas, OrderBookDeltas_API, OrderBookDepth10, QuoteTick, TradeTick,
26 },
27 enums::{AggregationSource, BookAction, GreeksConvention, OrderSide, RecordFlag},
28 identifiers::{InstrumentId, TradeId},
29 types::{Price, Quantity},
30};
31
32use super::{
33 message::{
34 BarMsg, BookChangeMsg, BookLevel, BookSnapshotMsg, DerivativeTickerMsg, OptionSummaryMsg,
35 TradeMsg, WsMessage,
36 },
37 types::TardisInstrumentMiniInfo,
38};
39use crate::{
40 common::parse::{
41 derive_trade_id, normalize_amount, parse_aggressor_side, parse_bar_spec, parse_book_action,
42 },
43 config::BookSnapshotOutput,
44};
45
46#[must_use]
47pub fn parse_tardis_ws_message(
48 msg: WsMessage,
49 info: &Arc<TardisInstrumentMiniInfo>,
50 book_snapshot_output: &BookSnapshotOutput,
51) -> Option<Data> {
52 match msg {
53 WsMessage::BookChange(msg) => {
54 if msg.bids.is_empty() && msg.asks.is_empty() {
55 let exchange = msg.exchange;
56 let symbol = &msg.symbol;
57 log::error!("Invalid book change for {exchange} {symbol} (empty bids and asks)");
58 return None;
59 }
60
61 match parse_book_change_msg_as_deltas(
62 &msg,
63 info.price_precision,
64 info.size_precision,
65 info.instrument_id,
66 ) {
67 Ok(deltas) => Some(Data::Deltas(deltas)),
68 Err(e) => {
69 log::error!("Failed to parse book change message: {e}");
70 None
71 }
72 }
73 }
74 WsMessage::BookSnapshot(msg) => match msg.depth {
75 1 => {
76 match parse_book_snapshot_msg_as_quote(
77 &msg,
78 info.price_precision,
79 info.size_precision,
80 info.instrument_id,
81 ) {
82 Ok(quote) => Some(Data::Quote(quote)),
83 Err(e) => {
84 log::error!("Failed to parse book snapshot quote message: {e}");
85 None
86 }
87 }
88 }
89 _ => match book_snapshot_output {
90 BookSnapshotOutput::Depth10 => {
91 match parse_book_snapshot_msg_as_depth10(
92 &msg,
93 info.price_precision,
94 info.size_precision,
95 info.instrument_id,
96 ) {
97 Ok(depth10) => Some(Data::Depth10(Box::new(depth10))),
98 Err(e) => {
99 log::error!("Failed to parse book snapshot as depth10: {e}");
100 None
101 }
102 }
103 }
104 BookSnapshotOutput::Deltas => {
105 match parse_book_snapshot_msg_as_deltas(
106 &msg,
107 info.price_precision,
108 info.size_precision,
109 info.instrument_id,
110 ) {
111 Ok(deltas) => Some(Data::Deltas(deltas)),
112 Err(e) => {
113 log::error!("Failed to parse book snapshot as deltas: {e}");
114 None
115 }
116 }
117 }
118 },
119 },
120 WsMessage::Trade(msg) => {
121 match parse_trade_msg(
122 &msg,
123 info.price_precision,
124 info.size_precision,
125 info.instrument_id,
126 ) {
127 Ok(trade) => Some(Data::Trade(trade)),
128 Err(e) => {
129 log::error!("Failed to parse trade message: {e}");
130 None
131 }
132 }
133 }
134 WsMessage::TradeBar(msg) => {
135 match parse_bar_msg(
136 &msg,
137 info.price_precision,
138 info.size_precision,
139 info.instrument_id,
140 ) {
141 Ok(bar) => Some(Data::Bar(bar)),
142 Err(e) => {
143 log::error!("Failed to parse bar message: {e}");
144 None
145 }
146 }
147 }
148 WsMessage::OptionSummary(msg) => Some(Data::OptionGreeks(parse_option_summary_msg(
149 &msg,
150 info.instrument_id,
151 ))),
152 WsMessage::DerivativeTicker(_) => None,
155 WsMessage::Disconnect(_) => None,
156 }
157}
158
159#[must_use]
160pub fn parse_tardis_ws_message_data(
161 msg: WsMessage,
162 info: &Arc<TardisInstrumentMiniInfo>,
163 book_snapshot_output: &BookSnapshotOutput,
164 extract_bbo_as_quotes: bool,
165) -> Vec<Data> {
166 match msg {
167 WsMessage::OptionSummary(msg) if extract_bbo_as_quotes => {
168 let mut data = Vec::with_capacity(2);
169
170 match parse_option_summary_msg_as_quote(
171 &msg,
172 info.price_precision,
173 info.size_precision,
174 info.instrument_id,
175 ) {
176 Ok(Some(quote)) => data.push(Data::Quote(quote)),
177 Ok(None) => {}
178 Err(e) => {
179 log::error!("Failed to parse option summary quote message: {e}");
180 }
181 }
182
183 data.push(Data::OptionGreeks(parse_option_summary_msg(
184 &msg,
185 info.instrument_id,
186 )));
187 data
188 }
189 msg => parse_tardis_ws_message(msg, info, book_snapshot_output)
190 .into_iter()
191 .collect(),
192 }
193}
194
195#[must_use]
201pub fn parse_option_summary_msg(
202 msg: &OptionSummaryMsg,
203 instrument_id: InstrumentId,
204) -> OptionGreeks {
205 OptionGreeks {
206 instrument_id,
207 convention: GreeksConvention::BlackScholes,
208 greeks: OptionGreekValues {
209 delta: msg.delta.unwrap_or(0.0),
210 gamma: msg.gamma.unwrap_or(0.0),
211 vega: msg.vega.unwrap_or(0.0),
212 theta: msg.theta.unwrap_or(0.0),
213 rho: msg.rho.unwrap_or(0.0),
214 },
215 mark_iv: msg.mark_iv,
216 bid_iv: msg.best_bid_iv,
217 ask_iv: msg.best_ask_iv,
218 underlying_price: msg.underlying_price,
219 open_interest: msg.open_interest,
220 ts_event: UnixNanos::from(msg.timestamp),
221 ts_init: UnixNanos::from(msg.local_timestamp),
222 }
223}
224
225pub fn parse_option_summary_msg_as_quote(
233 msg: &OptionSummaryMsg,
234 price_precision: u8,
235 size_precision: u8,
236 instrument_id: InstrumentId,
237) -> anyhow::Result<Option<QuoteTick>> {
238 let (Some(best_bid_price), Some(best_bid_amount), Some(best_ask_price), Some(best_ask_amount)) = (
239 msg.best_bid_price,
240 msg.best_bid_amount,
241 msg.best_ask_price,
242 msg.best_ask_amount,
243 ) else {
244 return Ok(None);
245 };
246
247 let bid_price = Price::new_checked(best_bid_price, price_precision)
248 .with_context(|| format!("invalid option summary bid price for message: {msg:?}"))?;
249 let ask_price = Price::new_checked(best_ask_price, price_precision)
250 .with_context(|| format!("invalid option summary ask price for message: {msg:?}"))?;
251 let bid_size = Quantity::non_zero_checked(best_bid_amount, size_precision)
252 .with_context(|| format!("invalid option summary bid size for message: {msg:?}"))?;
253 let ask_size = Quantity::non_zero_checked(best_ask_amount, size_precision)
254 .with_context(|| format!("invalid option summary ask size for message: {msg:?}"))?;
255
256 Ok(Some(QuoteTick::new(
257 instrument_id,
258 bid_price,
259 ask_price,
260 bid_size,
261 ask_size,
262 UnixNanos::from(msg.timestamp),
263 UnixNanos::from(msg.local_timestamp),
264 )))
265}
266
267#[must_use]
270pub fn parse_tardis_ws_message_funding_rate(
271 msg: WsMessage,
272 info: &Arc<TardisInstrumentMiniInfo>,
273) -> Option<FundingRateUpdate> {
274 match msg {
275 WsMessage::DerivativeTicker(msg) => {
276 match parse_derivative_ticker_msg(&msg, info.instrument_id) {
277 Ok(funding_rate) => funding_rate,
278 Err(e) => {
279 log::error!("Failed to parse derivative ticker message for funding rate: {e}");
280 None
281 }
282 }
283 }
284 _ => None, }
286}
287
288pub fn parse_book_change_msg_as_deltas(
295 msg: &BookChangeMsg,
296 price_precision: u8,
297 size_precision: u8,
298 instrument_id: InstrumentId,
299) -> anyhow::Result<OrderBookDeltas_API> {
300 parse_book_msg_as_deltas(
301 &msg.bids,
302 &msg.asks,
303 msg.is_snapshot,
304 price_precision,
305 size_precision,
306 instrument_id,
307 msg.timestamp,
308 msg.local_timestamp,
309 )
310}
311
312pub fn parse_book_snapshot_msg_as_deltas(
319 msg: &BookSnapshotMsg,
320 price_precision: u8,
321 size_precision: u8,
322 instrument_id: InstrumentId,
323) -> anyhow::Result<OrderBookDeltas_API> {
324 parse_book_msg_as_deltas(
325 &msg.bids,
326 &msg.asks,
327 true,
328 price_precision,
329 size_precision,
330 instrument_id,
331 msg.timestamp,
332 msg.local_timestamp,
333 )
334}
335
336pub fn parse_book_snapshot_msg_as_depth10(
342 msg: &BookSnapshotMsg,
343 price_precision: u8,
344 size_precision: u8,
345 instrument_id: InstrumentId,
346) -> anyhow::Result<OrderBookDepth10> {
347 let ts_event_nanos = msg
348 .timestamp
349 .timestamp_nanos_opt()
350 .context("invalid timestamp: cannot extract event nanoseconds")?;
351 anyhow::ensure!(
352 ts_event_nanos >= 0,
353 "invalid timestamp: event nanoseconds {ts_event_nanos} is before UNIX epoch"
354 );
355 let ts_event = UnixNanos::from(ts_event_nanos as u64);
356
357 let ts_init_nanos = msg
358 .local_timestamp
359 .timestamp_nanos_opt()
360 .context("invalid timestamp: cannot extract init nanoseconds")?;
361 anyhow::ensure!(
362 ts_init_nanos >= 0,
363 "invalid timestamp: init nanoseconds {ts_init_nanos} is before UNIX epoch"
364 );
365 let ts_init = UnixNanos::from(ts_init_nanos as u64);
366
367 let mut bids = [NULL_ORDER; DEPTH10_LEN];
368 let mut asks = [NULL_ORDER; DEPTH10_LEN];
369 let mut bid_counts = [0u32; DEPTH10_LEN];
370 let mut ask_counts = [0u32; DEPTH10_LEN];
371
372 for (i, level) in msg.bids.iter().take(DEPTH10_LEN).enumerate() {
373 bids[i] = BookOrder::new(
374 OrderSide::Buy,
375 Price::new(level.price, price_precision),
376 Quantity::new(level.amount, size_precision),
377 0,
378 );
379 bid_counts[i] = 1;
380 }
381
382 for (i, level) in msg.asks.iter().take(DEPTH10_LEN).enumerate() {
383 asks[i] = BookOrder::new(
384 OrderSide::Sell,
385 Price::new(level.price, price_precision),
386 Quantity::new(level.amount, size_precision),
387 0,
388 );
389 ask_counts[i] = 1;
390 }
391
392 Ok(OrderBookDepth10::new(
393 instrument_id,
394 bids,
395 asks,
396 bid_counts,
397 ask_counts,
398 RecordFlag::F_SNAPSHOT as u8,
399 0, ts_event,
401 ts_init,
402 ))
403}
404
405#[expect(clippy::too_many_arguments)]
407pub fn parse_book_msg_as_deltas(
413 bids: &[BookLevel],
414 asks: &[BookLevel],
415 is_snapshot: bool,
416 price_precision: u8,
417 size_precision: u8,
418 instrument_id: InstrumentId,
419 timestamp: DateTime<Utc>,
420 local_timestamp: DateTime<Utc>,
421) -> anyhow::Result<OrderBookDeltas_API> {
422 let event_nanos = timestamp
423 .timestamp_nanos_opt()
424 .context("invalid timestamp: cannot extract event nanoseconds")?;
425 anyhow::ensure!(
426 event_nanos >= 0,
427 "invalid timestamp: event nanoseconds {event_nanos} is before UNIX epoch"
428 );
429 let ts_event = UnixNanos::from(event_nanos as u64);
430 let init_nanos = local_timestamp
431 .timestamp_nanos_opt()
432 .context("invalid timestamp: cannot extract init nanoseconds")?;
433 anyhow::ensure!(
434 init_nanos >= 0,
435 "invalid timestamp: init nanoseconds {init_nanos} is before UNIX epoch"
436 );
437 let ts_init = UnixNanos::from(init_nanos as u64);
438
439 let capacity = if is_snapshot {
440 bids.len() + asks.len() + 1
441 } else {
442 bids.len() + asks.len()
443 };
444 let mut deltas: Vec<OrderBookDelta> = Vec::with_capacity(capacity);
445
446 if is_snapshot {
447 deltas.push(OrderBookDelta::clear(instrument_id, 0, ts_event, ts_init));
448 }
449
450 for level in bids {
451 match parse_book_level(
452 instrument_id,
453 price_precision,
454 size_precision,
455 OrderSide::Buy,
456 level,
457 is_snapshot,
458 ts_event,
459 ts_init,
460 ) {
461 Ok(delta) => deltas.push(delta),
462 Err(e) => log::warn!("Skipping invalid bid level for {instrument_id}: {e}"),
463 }
464 }
465
466 for level in asks {
467 match parse_book_level(
468 instrument_id,
469 price_precision,
470 size_precision,
471 OrderSide::Sell,
472 level,
473 is_snapshot,
474 ts_event,
475 ts_init,
476 ) {
477 Ok(delta) => deltas.push(delta),
478 Err(e) => log::warn!("Skipping invalid ask level for {instrument_id}: {e}"),
479 }
480 }
481
482 if let Some(last_delta) = deltas.last_mut() {
483 last_delta.flags |= RecordFlag::F_LAST as u8;
484 }
485
486 Ok(OrderBookDeltas_API::new(OrderBookDeltas::new(
488 instrument_id,
489 deltas,
490 )))
491}
492
493#[expect(clippy::too_many_arguments)]
499pub fn parse_book_level(
500 instrument_id: InstrumentId,
501 price_precision: u8,
502 size_precision: u8,
503 side: OrderSide,
504 level: &BookLevel,
505 is_snapshot: bool,
506 ts_event: UnixNanos,
507 ts_init: UnixNanos,
508) -> anyhow::Result<OrderBookDelta> {
509 let amount = normalize_amount(level.amount, size_precision);
510 let action = parse_book_action(is_snapshot, amount);
511 let price = Price::new(level.price, price_precision);
512 let size = Quantity::new(amount, size_precision);
513 let order_id = 0; let order = BookOrder::new(side, price, size, order_id);
515 let flags = if is_snapshot {
516 RecordFlag::F_SNAPSHOT as u8
517 } else {
518 0
519 };
520 let sequence = 0; anyhow::ensure!(
523 !(action != BookAction::Delete && size.is_zero()),
524 "Invalid zero size for {action}"
525 );
526
527 Ok(OrderBookDelta::new(
528 instrument_id,
529 action,
530 order,
531 flags,
532 sequence,
533 ts_event,
534 ts_init,
535 ))
536}
537
538pub fn parse_book_snapshot_msg_as_quote(
545 msg: &BookSnapshotMsg,
546 price_precision: u8,
547 size_precision: u8,
548 instrument_id: InstrumentId,
549) -> anyhow::Result<QuoteTick> {
550 let ts_event = UnixNanos::from(msg.timestamp);
551 let ts_init = UnixNanos::from(msg.local_timestamp);
552
553 let best_bid = msg
554 .bids
555 .first()
556 .context("missing best bid level for quote message")?;
557 let bid_price = Price::new(best_bid.price, price_precision);
558 let bid_size = Quantity::non_zero_checked(best_bid.amount, size_precision)
559 .with_context(|| format!("Invalid bid size for message: {msg:?}"))?;
560
561 let best_ask = msg
562 .asks
563 .first()
564 .context("missing best ask level for quote message")?;
565 let ask_price = Price::new(best_ask.price, price_precision);
566 let ask_size = Quantity::non_zero_checked(best_ask.amount, size_precision)
567 .with_context(|| format!("Invalid ask size for message: {msg:?}"))?;
568
569 Ok(QuoteTick::new(
570 instrument_id,
571 bid_price,
572 ask_price,
573 bid_size,
574 ask_size,
575 ts_event,
576 ts_init,
577 ))
578}
579
580pub fn parse_trade_msg(
587 msg: &TradeMsg,
588 price_precision: u8,
589 size_precision: u8,
590 instrument_id: InstrumentId,
591) -> anyhow::Result<TradeTick> {
592 let price = Price::new(msg.price, price_precision);
593 let size = Quantity::non_zero_checked(msg.amount, size_precision)
594 .with_context(|| format!("Invalid trade size in message: {msg:?}"))?;
595 let aggressor_side = parse_aggressor_side(&msg.side);
596 let ts_event = UnixNanos::from(msg.timestamp);
597 let ts_init = UnixNanos::from(msg.local_timestamp);
598 let trade_id = match msg.id.as_deref() {
599 Some(id) if !id.is_empty() => TradeId::new(id),
600 _ => derive_trade_id(
601 msg.symbol,
602 ts_event.as_u64(),
603 msg.price,
604 msg.amount,
605 &msg.side,
606 ),
607 };
608
609 Ok(TradeTick::new(
610 instrument_id,
611 price,
612 size,
613 aggressor_side,
614 trade_id,
615 ts_event,
616 ts_init,
617 ))
618}
619
620pub fn parse_bar_msg(
626 msg: &BarMsg,
627 price_precision: u8,
628 size_precision: u8,
629 instrument_id: InstrumentId,
630) -> anyhow::Result<Bar> {
631 let spec = parse_bar_spec(&msg.name)?;
632 let bar_type = BarType::new(instrument_id, spec, AggregationSource::External);
633
634 let open = Price::new(msg.open, price_precision);
635 let high = Price::new(msg.high, price_precision);
636 let low = Price::new(msg.low, price_precision);
637 let close = Price::new(msg.close, price_precision);
638 let volume = Quantity::non_zero(msg.volume, size_precision);
639 let ts_event = UnixNanos::from(msg.timestamp);
640 let ts_init = UnixNanos::from(msg.local_timestamp);
641
642 Ok(Bar::new(
643 bar_type, open, high, low, close, volume, ts_event, ts_init,
644 ))
645}
646
647fn parse_derivative_ticker_timestamps(
649 msg: &DerivativeTickerMsg,
650) -> anyhow::Result<(UnixNanos, UnixNanos)> {
651 let ts_event_nanos = msg
652 .timestamp
653 .timestamp_nanos_opt()
654 .context("invalid timestamp: cannot extract event nanoseconds")?;
655 anyhow::ensure!(
656 ts_event_nanos >= 0,
657 "invalid timestamp: event nanoseconds {ts_event_nanos} is before UNIX epoch"
658 );
659
660 let ts_init_nanos = msg
661 .local_timestamp
662 .timestamp_nanos_opt()
663 .context("invalid timestamp: cannot extract init nanoseconds")?;
664 anyhow::ensure!(
665 ts_init_nanos >= 0,
666 "invalid timestamp: init nanoseconds {ts_init_nanos} is before UNIX epoch"
667 );
668
669 Ok((
670 UnixNanos::from(ts_event_nanos as u64),
671 UnixNanos::from(ts_init_nanos as u64),
672 ))
673}
674
675pub fn parse_derivative_ticker_msg(
681 msg: &DerivativeTickerMsg,
682 instrument_id: InstrumentId,
683) -> anyhow::Result<Option<FundingRateUpdate>> {
684 let funding_rate = match msg.funding_rate {
685 Some(rate) => rate,
686 None => return Ok(None),
687 };
688
689 let (ts_event, ts_init) = parse_derivative_ticker_timestamps(msg)?;
690 let rate = rust_decimal::Decimal::try_from(funding_rate)
691 .with_context(|| format!("failed to convert funding rate {funding_rate} to Decimal"))?;
692
693 Ok(Some(FundingRateUpdate::new(
694 instrument_id,
695 rate,
696 None,
697 None,
698 ts_event,
699 ts_init,
700 )))
701}
702
703pub fn parse_derivative_ticker_mark_price(
709 msg: &DerivativeTickerMsg,
710 instrument_id: InstrumentId,
711 price_precision: u8,
712) -> anyhow::Result<Option<MarkPriceUpdate>> {
713 let mark_price = match msg.mark_price {
714 Some(p) => p,
715 None => return Ok(None),
716 };
717
718 let (ts_event, ts_init) = parse_derivative_ticker_timestamps(msg)?;
719
720 Ok(Some(MarkPriceUpdate::new(
721 instrument_id,
722 Price::new(mark_price, price_precision),
723 ts_event,
724 ts_init,
725 )))
726}
727
728pub fn parse_derivative_ticker_index_price(
734 msg: &DerivativeTickerMsg,
735 instrument_id: InstrumentId,
736 price_precision: u8,
737) -> anyhow::Result<Option<IndexPriceUpdate>> {
738 let index_price = match msg.index_price {
739 Some(p) => p,
740 None => return Ok(None),
741 };
742
743 let (ts_event, ts_init) = parse_derivative_ticker_timestamps(msg)?;
744
745 Ok(Some(IndexPriceUpdate::new(
746 instrument_id,
747 Price::new(index_price, price_precision),
748 ts_event,
749 ts_init,
750 )))
751}
752
753#[cfg(test)]
754mod tests {
755 use nautilus_model::enums::AggressorSide;
756 use rstest::rstest;
757
758 use super::*;
759 use crate::common::{enums::TardisExchange, testing::load_test_json};
760
761 #[rstest]
762 fn test_parse_book_change_message() {
763 let json_data = load_test_json("book_change.json");
764 let msg: BookChangeMsg = serde_json::from_str(&json_data).unwrap();
765
766 let price_precision = 0;
767 let size_precision = 0;
768 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
769 let deltas =
770 parse_book_change_msg_as_deltas(&msg, price_precision, size_precision, instrument_id)
771 .unwrap();
772
773 assert_eq!(deltas.deltas.len(), 1);
774 assert_eq!(deltas.instrument_id, instrument_id);
775 assert_eq!(deltas.flags, RecordFlag::F_LAST as u8);
776 assert_eq!(deltas.sequence, 0);
777 assert_eq!(deltas.ts_event, UnixNanos::from(1571830193469000000));
778 assert_eq!(deltas.ts_init, UnixNanos::from(1571830193469000000));
779 assert_eq!(
780 deltas.deltas[0].instrument_id,
781 InstrumentId::from("XBTUSD.BITMEX")
782 );
783 assert_eq!(deltas.deltas[0].action, BookAction::Update);
784 assert_eq!(deltas.deltas[0].order.price, Price::from("7985"));
785 assert_eq!(deltas.deltas[0].order.size, Quantity::from(283318));
786 assert_eq!(deltas.deltas[0].order.order_id, 0);
787 assert_eq!(deltas.deltas[0].flags, RecordFlag::F_LAST as u8);
788 assert_eq!(deltas.deltas[0].sequence, 0);
789 assert_eq!(
790 deltas.deltas[0].ts_event,
791 UnixNanos::from(1571830193469000000)
792 );
793 assert_eq!(
794 deltas.deltas[0].ts_init,
795 UnixNanos::from(1571830193469000000)
796 );
797 }
798
799 #[rstest]
800 fn test_parse_book_snapshot_message_as_deltas() {
801 let json_data = load_test_json("book_snapshot.json");
802 let msg: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
803
804 let price_precision = 1;
805 let size_precision = 0;
806 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
807 let deltas =
808 parse_book_snapshot_msg_as_deltas(&msg, price_precision, size_precision, instrument_id)
809 .unwrap();
810
811 let clear_delta = deltas.deltas[0];
812 let bid_delta = deltas.deltas[1];
813 let ask_delta = deltas.deltas[3];
814
815 assert_eq!(deltas.deltas.len(), 5);
816 assert_eq!(deltas.instrument_id, instrument_id);
817 assert_eq!(
818 deltas.flags,
819 RecordFlag::F_LAST as u8 + RecordFlag::F_SNAPSHOT as u8
820 );
821 assert_eq!(deltas.sequence, 0);
822 assert_eq!(deltas.ts_event, UnixNanos::from(1572010786950000000));
823 assert_eq!(deltas.ts_init, UnixNanos::from(1572010786961000000));
824
825 assert_eq!(clear_delta.instrument_id, instrument_id);
827 assert_eq!(clear_delta.action, BookAction::Clear);
828 assert_eq!(clear_delta.flags, RecordFlag::F_SNAPSHOT as u8);
829 assert_eq!(clear_delta.sequence, 0);
830 assert_eq!(clear_delta.ts_event, UnixNanos::from(1572010786950000000));
831 assert_eq!(clear_delta.ts_init, UnixNanos::from(1572010786961000000));
832
833 assert_eq!(bid_delta.instrument_id, instrument_id);
835 assert_eq!(bid_delta.action, BookAction::Add);
836 assert_eq!(bid_delta.order.side, OrderSide::Buy);
837 assert_eq!(bid_delta.order.price, Price::from("7633.5"));
838 assert_eq!(bid_delta.order.size, Quantity::from(1906067));
839 assert_eq!(bid_delta.order.order_id, 0);
840 assert_eq!(bid_delta.flags, RecordFlag::F_SNAPSHOT as u8);
841 assert_eq!(bid_delta.sequence, 0);
842 assert_eq!(bid_delta.ts_event, UnixNanos::from(1572010786950000000));
843 assert_eq!(bid_delta.ts_init, UnixNanos::from(1572010786961000000));
844
845 assert_eq!(ask_delta.instrument_id, instrument_id);
847 assert_eq!(ask_delta.action, BookAction::Add);
848 assert_eq!(ask_delta.order.side, OrderSide::Sell);
849 assert_eq!(ask_delta.order.price, Price::from("7634.0"));
850 assert_eq!(ask_delta.order.size, Quantity::from(1467849));
851 assert_eq!(ask_delta.order.order_id, 0);
852 assert_eq!(ask_delta.flags, RecordFlag::F_SNAPSHOT as u8);
853 assert_eq!(ask_delta.sequence, 0);
854 assert_eq!(ask_delta.ts_event, UnixNanos::from(1572010786950000000));
855 assert_eq!(ask_delta.ts_init, UnixNanos::from(1572010786961000000));
856 }
857
858 #[rstest]
859 fn test_parse_book_snapshot_message_as_depth10() {
860 let json_data = load_test_json("book_snapshot.json");
861 let msg: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
862
863 let price_precision = 1;
864 let size_precision = 0;
865 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
866
867 let depth10 = parse_book_snapshot_msg_as_depth10(
868 &msg,
869 price_precision,
870 size_precision,
871 instrument_id,
872 )
873 .unwrap();
874
875 assert_eq!(depth10.instrument_id, instrument_id);
876 assert_eq!(depth10.flags, RecordFlag::F_SNAPSHOT as u8);
877 assert_eq!(depth10.sequence, 0);
878 assert_eq!(depth10.ts_event, UnixNanos::from(1572010786950000000));
879 assert_eq!(depth10.ts_init, UnixNanos::from(1572010786961000000));
880
881 assert_eq!(depth10.bids[0].side, OrderSide::Buy);
883 assert_eq!(depth10.bids[0].price, Price::from("7633.5"));
884 assert_eq!(depth10.bids[0].size, Quantity::from(1906067));
885 assert_eq!(depth10.bids[0].order_id, 0);
886 assert_eq!(depth10.bid_counts[0], 1);
887
888 assert_eq!(depth10.bids[1].side, OrderSide::Buy);
890 assert_eq!(depth10.bids[1].price, Price::from("7633.0"));
891 assert_eq!(depth10.bids[1].size, Quantity::from(65319));
892 assert_eq!(depth10.bid_counts[1], 1);
893
894 assert_eq!(depth10.asks[0].side, OrderSide::Sell);
896 assert_eq!(depth10.asks[0].price, Price::from("7634.0"));
897 assert_eq!(depth10.asks[0].size, Quantity::from(1467849));
898 assert_eq!(depth10.asks[0].order_id, 0);
899 assert_eq!(depth10.ask_counts[0], 1);
900
901 assert_eq!(depth10.asks[1].side, OrderSide::Sell);
903 assert_eq!(depth10.asks[1].price, Price::from("7634.5"));
904 assert_eq!(depth10.asks[1].size, Quantity::from(67939));
905 assert_eq!(depth10.ask_counts[1], 1);
906
907 assert_eq!(depth10.bids[2], NULL_ORDER);
909 assert_eq!(depth10.bid_counts[2], 0);
910 assert_eq!(depth10.asks[2], NULL_ORDER);
911 assert_eq!(depth10.ask_counts[2], 0);
912 }
913
914 #[rstest]
915 fn test_parse_book_snapshot_message_as_quote() {
916 let json_data = load_test_json("book_snapshot.json");
917 let msg: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
918
919 let price_precision = 1;
920 let size_precision = 0;
921 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
922 let quote =
923 parse_book_snapshot_msg_as_quote(&msg, price_precision, size_precision, instrument_id)
924 .expect("Failed to parse book snapshot quote message");
925
926 assert_eq!(quote.instrument_id, instrument_id);
927 assert_eq!(quote.bid_price, Price::from("7633.5"));
928 assert_eq!(quote.bid_size, Quantity::from(1906067));
929 assert_eq!(quote.ask_price, Price::from("7634.0"));
930 assert_eq!(quote.ask_size, Quantity::from(1467849));
931 assert_eq!(quote.ts_event, UnixNanos::from(1572010786950000000));
932 assert_eq!(quote.ts_init, UnixNanos::from(1572010786961000000));
933 }
934
935 #[rstest]
936 fn test_parse_trade_message() {
937 let json_data = load_test_json("trade.json");
938 let msg: TradeMsg = serde_json::from_str(&json_data).unwrap();
939
940 let price_precision = 0;
941 let size_precision = 0;
942 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
943 let trade = parse_trade_msg(&msg, price_precision, size_precision, instrument_id)
944 .expect("Failed to parse trade message");
945
946 assert_eq!(trade.instrument_id, instrument_id);
947 assert_eq!(trade.price, Price::from("7996"));
948 assert_eq!(trade.size, Quantity::from(50));
949 assert_eq!(trade.aggressor_side, AggressorSide::Seller);
950 assert_eq!(trade.ts_event, UnixNanos::from(1571826769669000000));
951 assert_eq!(trade.ts_init, UnixNanos::from(1571826769740000000));
952 }
953
954 fn build_trade_msg_without_id() -> TradeMsg {
955 let json_data = load_test_json("trade.json");
956 let mut msg: TradeMsg = serde_json::from_str(&json_data).unwrap();
957 msg.id = None;
958 msg
959 }
960
961 #[rstest]
962 fn test_parse_trade_message_derives_trade_id_when_missing() {
963 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
964
965 let first = parse_trade_msg(&build_trade_msg_without_id(), 0, 0, instrument_id).unwrap();
966 let second = parse_trade_msg(&build_trade_msg_without_id(), 0, 0, instrument_id).unwrap();
967
968 assert_eq!(first.trade_id, second.trade_id, "derivation must be stable");
969 assert_eq!(first.trade_id.as_str().len(), 16);
970
971 let mut altered = build_trade_msg_without_id();
972 altered.price = 7997.0;
973 let altered_trade = parse_trade_msg(&altered, 0, 0, instrument_id).unwrap();
974 assert_ne!(first.trade_id, altered_trade.trade_id);
975 }
976
977 #[rstest]
978 fn test_parse_trade_message_derives_trade_id_when_empty() {
979 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
980
981 let mut msg = build_trade_msg_without_id();
982 msg.id = Some(String::new());
983
984 let trade = parse_trade_msg(&msg, 0, 0, instrument_id).unwrap();
985 let fallback = parse_trade_msg(&build_trade_msg_without_id(), 0, 0, instrument_id).unwrap();
986 assert_eq!(trade.trade_id, fallback.trade_id);
987 }
988
989 #[rstest]
990 fn test_parse_bar_message() {
991 let json_data = load_test_json("bar.json");
992 let msg: BarMsg = serde_json::from_str(&json_data).unwrap();
993
994 let price_precision = 1;
995 let size_precision = 0;
996 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
997 let bar = parse_bar_msg(&msg, price_precision, size_precision, instrument_id).unwrap();
998
999 assert_eq!(
1000 bar.bar_type,
1001 BarType::from("XBTUSD.BITMEX-10-SECOND-LAST-EXTERNAL")
1002 );
1003 assert_eq!(bar.open, Price::from("7623.5"));
1004 assert_eq!(bar.high, Price::from("7623.5"));
1005 assert_eq!(bar.low, Price::from("7623"));
1006 assert_eq!(bar.close, Price::from("7623.5"));
1007 assert_eq!(bar.volume, Quantity::from(37034));
1008 assert_eq!(bar.ts_event, UnixNanos::from(1572009100000000000));
1009 assert_eq!(bar.ts_init, UnixNanos::from(1572009100369000000));
1010 }
1011
1012 #[rstest]
1013 fn test_parse_tardis_ws_message_book_snapshot_routes_to_depth10() {
1014 let json_data = load_test_json("book_snapshot.json");
1015 let msg: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
1016 let ws_msg = WsMessage::BookSnapshot(msg);
1017
1018 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1019 let info = Arc::new(TardisInstrumentMiniInfo::new(
1020 instrument_id,
1021 None,
1022 TardisExchange::Bitmex,
1023 1,
1024 0,
1025 ));
1026
1027 let result = parse_tardis_ws_message(ws_msg, &info, &BookSnapshotOutput::Depth10);
1028
1029 assert!(result.is_some());
1030 assert!(matches!(result.unwrap(), Data::Depth10(_)));
1031 }
1032
1033 #[rstest]
1034 fn test_parse_tardis_ws_message_sparse_book_snapshot_routes_to_depth10() {
1035 let json_data = r#"{
1036 "type": "book_snapshot",
1037 "symbol": "ETC",
1038 "exchange": "hyperliquid",
1039 "name": "book_snapshot_20_10s",
1040 "depth": 20,
1041 "interval": 10000,
1042 "bids": [{"price": 20.002, "amount": 5.81}],
1043 "asks": [{"price": 20.003, "amount": 162.45}, {}],
1044 "timestamp": "2025-03-03T10:48:10.000Z",
1045 "localTimestamp": "2025-03-03T10:48:10.596818Z"
1046 }"#;
1047 let msg: BookSnapshotMsg = serde_json::from_str(json_data).unwrap();
1048 let ws_msg = WsMessage::BookSnapshot(msg);
1049
1050 let instrument_id = InstrumentId::from("ETC.HYPERLIQUID");
1051 let info = Arc::new(TardisInstrumentMiniInfo::new(
1052 instrument_id,
1053 None,
1054 TardisExchange::Hyperliquid,
1055 3,
1056 2,
1057 ));
1058
1059 let result = parse_tardis_ws_message(ws_msg, &info, &BookSnapshotOutput::Depth10);
1060
1061 assert!(result.is_some());
1062 assert!(matches!(result.unwrap(), Data::Depth10(_)));
1063 }
1064
1065 #[rstest]
1066 fn test_parse_tardis_ws_message_book_snapshot_routes_to_deltas() {
1067 let json_data = load_test_json("book_snapshot.json");
1068 let msg: BookSnapshotMsg = serde_json::from_str(&json_data).unwrap();
1069 let ws_msg = WsMessage::BookSnapshot(msg);
1070
1071 let instrument_id = InstrumentId::from("XBTUSD.BITMEX");
1072 let info = Arc::new(TardisInstrumentMiniInfo::new(
1073 instrument_id,
1074 None,
1075 TardisExchange::Bitmex,
1076 1,
1077 0,
1078 ));
1079
1080 let result = parse_tardis_ws_message(ws_msg, &info, &BookSnapshotOutput::Deltas);
1081
1082 assert!(result.is_some());
1083 assert!(matches!(result.unwrap(), Data::Deltas(_)));
1084 }
1085
1086 #[rstest]
1087 fn test_parse_tardis_ws_message_option_summary_routes_to_option_greeks() {
1088 let json_data = load_test_json("option_summary.json");
1089 let msg: OptionSummaryMsg = serde_json::from_str(&json_data).unwrap();
1090 let ts_event = UnixNanos::from(msg.timestamp);
1091 let ts_init = UnixNanos::from(msg.local_timestamp);
1092 let ws_msg = WsMessage::OptionSummary(msg);
1093
1094 let instrument_id = InstrumentId::from("BTC-28JUN24-70000-C.DERIBIT");
1095 let info = Arc::new(TardisInstrumentMiniInfo::new(
1096 instrument_id,
1097 None,
1098 TardisExchange::Deribit,
1099 4,
1100 1,
1101 ));
1102
1103 let result = parse_tardis_ws_message(ws_msg, &info, &BookSnapshotOutput::Deltas);
1104
1105 let Some(Data::OptionGreeks(greeks)) = result else {
1106 panic!("Expected Data::OptionGreeks, was {result:?}");
1107 };
1108 assert_eq!(greeks.instrument_id, instrument_id);
1109 assert_eq!(greeks.convention, GreeksConvention::BlackScholes);
1110 assert_eq!(greeks.greeks.delta, 0.25);
1111 assert_eq!(greeks.greeks.gamma, 0.00002);
1112 assert_eq!(greeks.greeks.vega, 45.5);
1113 assert_eq!(greeks.greeks.theta, -15.2);
1114 assert_eq!(greeks.greeks.rho, 0.05);
1115 assert_eq!(greeks.mark_iv, Some(0.565));
1116 assert_eq!(greeks.bid_iv, Some(0.55));
1117 assert_eq!(greeks.ask_iv, Some(0.58));
1118 assert_eq!(greeks.underlying_price, Some(63_500.0));
1119 assert_eq!(greeks.open_interest, Some(150.0));
1120 assert_eq!(greeks.ts_event, ts_event);
1121 assert_eq!(greeks.ts_init, ts_init);
1122 }
1123
1124 #[rstest]
1125 fn test_parse_tardis_ws_message_data_extracts_option_summary_quote_when_enabled() {
1126 let json_data = load_test_json("option_summary.json");
1127 let msg: OptionSummaryMsg = serde_json::from_str(&json_data).unwrap();
1128 let ts_event = UnixNanos::from(msg.timestamp);
1129 let ts_init = UnixNanos::from(msg.local_timestamp);
1130 let ws_msg = WsMessage::OptionSummary(msg);
1131
1132 let instrument_id = InstrumentId::from("BTC-28JUN24-70000-C.DERIBIT");
1133 let info = Arc::new(TardisInstrumentMiniInfo::new(
1134 instrument_id,
1135 None,
1136 TardisExchange::Deribit,
1137 4,
1138 1,
1139 ));
1140
1141 let data = parse_tardis_ws_message_data(ws_msg, &info, &BookSnapshotOutput::Deltas, true);
1142
1143 assert_eq!(data.len(), 2);
1144 let Data::Quote(quote) = &data[0] else {
1145 panic!("Expected first data item to be QuoteTick");
1146 };
1147 let Data::OptionGreeks(greeks) = &data[1] else {
1148 panic!("Expected second data item to be OptionGreeks");
1149 };
1150 assert_eq!(quote.instrument_id, instrument_id);
1151 assert_eq!(quote.bid_price, Price::from("0.035"));
1152 assert_eq!(quote.bid_size, Quantity::from("5"));
1153 assert_eq!(quote.ask_price, Price::from("0.04"));
1154 assert_eq!(quote.ask_size, Quantity::from("10"));
1155 assert_eq!(quote.ts_event, ts_event);
1156 assert_eq!(quote.ts_init, ts_init);
1157 assert_eq!(greeks.instrument_id, instrument_id);
1158 assert_eq!(greeks.ts_event, ts_event);
1159 assert_eq!(greeks.ts_init, ts_init);
1160 }
1161
1162 #[rstest]
1163 fn test_parse_tardis_ws_message_data_skips_option_summary_quote_when_disabled() {
1164 let json_data = load_test_json("option_summary.json");
1165 let msg: OptionSummaryMsg = serde_json::from_str(&json_data).unwrap();
1166 let ws_msg = WsMessage::OptionSummary(msg);
1167
1168 let instrument_id = InstrumentId::from("BTC-28JUN24-70000-C.DERIBIT");
1169 let info = Arc::new(TardisInstrumentMiniInfo::new(
1170 instrument_id,
1171 None,
1172 TardisExchange::Deribit,
1173 4,
1174 1,
1175 ));
1176
1177 let data = parse_tardis_ws_message_data(ws_msg, &info, &BookSnapshotOutput::Deltas, false);
1178
1179 assert_eq!(data.len(), 1);
1180 assert!(
1181 matches!(data[0], Data::OptionGreeks(_)),
1182 "Expected OptionGreeks, was {:?}",
1183 data[0]
1184 );
1185 }
1186
1187 #[rstest]
1188 fn test_parse_tardis_ws_message_data_skips_option_summary_quote_when_bbo_missing() {
1189 let json_data = load_test_json("option_summary.json");
1190 let mut msg: OptionSummaryMsg = serde_json::from_str(&json_data).unwrap();
1191 msg.best_ask_price = None;
1192 let ws_msg = WsMessage::OptionSummary(msg);
1193
1194 let instrument_id = InstrumentId::from("BTC-28JUN24-70000-C.DERIBIT");
1195 let info = Arc::new(TardisInstrumentMiniInfo::new(
1196 instrument_id,
1197 None,
1198 TardisExchange::Deribit,
1199 4,
1200 1,
1201 ));
1202
1203 let data = parse_tardis_ws_message_data(ws_msg, &info, &BookSnapshotOutput::Deltas, true);
1204
1205 assert_eq!(data.len(), 1);
1206 assert!(
1207 matches!(data[0], Data::OptionGreeks(_)),
1208 "Expected OptionGreeks, was {:?}",
1209 data[0]
1210 );
1211 }
1212
1213 #[rstest]
1214 fn test_parse_tardis_ws_message_data_keeps_option_summary_when_bbo_size_invalid() {
1215 let json_data = load_test_json("option_summary.json");
1216 let mut msg: OptionSummaryMsg = serde_json::from_str(&json_data).unwrap();
1217 let ts_event = UnixNanos::from(msg.timestamp);
1218 let ts_init = UnixNanos::from(msg.local_timestamp);
1219 msg.best_bid_amount = Some(0.0);
1220 let ws_msg = WsMessage::OptionSummary(msg);
1221
1222 let instrument_id = InstrumentId::from("BTC-28JUN24-70000-C.DERIBIT");
1223 let info = Arc::new(TardisInstrumentMiniInfo::new(
1224 instrument_id,
1225 None,
1226 TardisExchange::Deribit,
1227 4,
1228 1,
1229 ));
1230
1231 let data = parse_tardis_ws_message_data(ws_msg, &info, &BookSnapshotOutput::Deltas, true);
1232
1233 assert_eq!(data.len(), 1);
1234 let Data::OptionGreeks(greeks) = &data[0] else {
1235 panic!("Expected OptionGreeks, was {:?}", data[0]);
1236 };
1237 assert_eq!(greeks.instrument_id, instrument_id);
1238 assert_eq!(greeks.ts_event, ts_event);
1239 assert_eq!(greeks.ts_init, ts_init);
1240 }
1241
1242 #[rstest]
1243 fn test_parse_tardis_ws_message_data_keeps_option_summary_when_bbo_price_invalid() {
1244 let json_data = load_test_json("option_summary.json");
1245 let mut msg: OptionSummaryMsg = serde_json::from_str(&json_data).unwrap();
1246 let ts_event = UnixNanos::from(msg.timestamp);
1247 let ts_init = UnixNanos::from(msg.local_timestamp);
1248 msg.best_bid_price = Some(f64::MAX);
1249 let ws_msg = WsMessage::OptionSummary(msg);
1250
1251 let instrument_id = InstrumentId::from("BTC-28JUN24-70000-C.DERIBIT");
1252 let info = Arc::new(TardisInstrumentMiniInfo::new(
1253 instrument_id,
1254 None,
1255 TardisExchange::Deribit,
1256 4,
1257 1,
1258 ));
1259
1260 let data = parse_tardis_ws_message_data(ws_msg, &info, &BookSnapshotOutput::Deltas, true);
1261
1262 assert_eq!(data.len(), 1);
1263 let Data::OptionGreeks(greeks) = &data[0] else {
1264 panic!("Expected OptionGreeks, was {:?}", data[0]);
1265 };
1266 assert_eq!(greeks.instrument_id, instrument_id);
1267 assert_eq!(greeks.ts_event, ts_event);
1268 assert_eq!(greeks.ts_init, ts_init);
1269 }
1270
1271 #[rstest]
1272 fn test_parse_option_summary_msg_defaults_absent_fields() {
1273 let ts = DateTime::parse_from_rfc3339("2024-01-15T10:30:00.123Z")
1274 .unwrap()
1275 .with_timezone(&Utc);
1276 let msg = OptionSummaryMsg {
1277 symbol: ustr::Ustr::from("BTC-28JUN24-70000-C"),
1278 exchange: TardisExchange::Deribit,
1279 option_type: "call".to_string(),
1280 strike_price: 70_000.0,
1281 expiration_date: ts,
1282 best_bid_price: None,
1283 best_bid_amount: None,
1284 best_bid_iv: None,
1285 best_ask_price: None,
1286 best_ask_amount: None,
1287 best_ask_iv: None,
1288 last_price: None,
1289 open_interest: None,
1290 mark_price: None,
1291 mark_iv: None,
1292 delta: None,
1293 gamma: None,
1294 vega: None,
1295 theta: None,
1296 rho: None,
1297 underlying_price: None,
1298 underlying_index: "BTC-USD".to_string(),
1299 timestamp: ts,
1300 local_timestamp: ts,
1301 };
1302
1303 let instrument_id = InstrumentId::from("BTC-28JUN24-70000-C.DERIBIT");
1304 let greeks = parse_option_summary_msg(&msg, instrument_id);
1305
1306 assert_eq!(greeks.greeks.delta, 0.0);
1308 assert_eq!(greeks.greeks.gamma, 0.0);
1309 assert_eq!(greeks.greeks.vega, 0.0);
1310 assert_eq!(greeks.greeks.theta, 0.0);
1311 assert_eq!(greeks.greeks.rho, 0.0);
1312 assert_eq!(greeks.mark_iv, None);
1313 assert_eq!(greeks.bid_iv, None);
1314 assert_eq!(greeks.ask_iv, None);
1315 assert_eq!(greeks.underlying_price, None);
1316 assert_eq!(greeks.open_interest, None);
1317 }
1318
1319 #[rstest]
1320 fn test_parse_derivative_ticker_funding_rate() {
1321 let json_data = load_test_json("derivative_ticker.json");
1322 let msg: DerivativeTickerMsg = serde_json::from_str(&json_data).unwrap();
1323
1324 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1325
1326 let result = parse_derivative_ticker_msg(&msg, instrument_id).unwrap();
1327 assert!(result.is_some());
1328
1329 let funding = result.unwrap();
1330 assert_eq!(funding.instrument_id, instrument_id);
1331 assert_eq!(funding.rate.to_string(), "-0.00001568");
1332 assert!(funding.ts_event.as_u64() > 0);
1333 assert!(funding.ts_init.as_u64() > 0);
1334 }
1335
1336 #[rstest]
1337 fn test_parse_derivative_ticker_mark_price() {
1338 let json_data = load_test_json("derivative_ticker.json");
1339 let msg: DerivativeTickerMsg = serde_json::from_str(&json_data).unwrap();
1340
1341 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1342 let price_precision = 2;
1343
1344 let result =
1345 parse_derivative_ticker_mark_price(&msg, instrument_id, price_precision).unwrap();
1346 assert!(result.is_some());
1347
1348 let mark = result.unwrap();
1349 assert_eq!(mark.instrument_id, instrument_id);
1350 assert_eq!(mark.value, Price::new(7987.56, price_precision));
1351 assert!(mark.ts_event.as_u64() > 0);
1352 assert!(mark.ts_init.as_u64() > 0);
1353 }
1354
1355 #[rstest]
1356 fn test_parse_derivative_ticker_index_price() {
1357 let json_data = load_test_json("derivative_ticker.json");
1358 let msg: DerivativeTickerMsg = serde_json::from_str(&json_data).unwrap();
1359
1360 let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT");
1361 let price_precision = 2;
1362
1363 let result =
1364 parse_derivative_ticker_index_price(&msg, instrument_id, price_precision).unwrap();
1365 assert!(result.is_some());
1366
1367 let index = result.unwrap();
1368 assert_eq!(index.instrument_id, instrument_id);
1369 assert_eq!(index.value, Price::new(7989.28, price_precision));
1370 assert!(index.ts_event.as_u64() > 0);
1371 assert!(index.ts_init.as_u64() > 0);
1372 }
1373
1374 #[rstest]
1375 fn test_parse_derivative_ticker_missing_fields() {
1376 let json = r#"{
1378 "type": "derivative_ticker",
1379 "symbol": "BTCUSD",
1380 "exchange": "bitmex",
1381 "lastPrice": null,
1382 "openInterest": null,
1383 "fundingRate": 0.0001,
1384 "indexPrice": null,
1385 "markPrice": null,
1386 "timestamp": "2024-01-01T00:00:00.000Z",
1387 "localTimestamp": "2024-01-01T00:00:00.100Z"
1388 }"#;
1389 let msg: DerivativeTickerMsg = serde_json::from_str(json).unwrap();
1390
1391 let instrument_id = InstrumentId::from("BTCUSD.BITMEX");
1392
1393 let funding = parse_derivative_ticker_msg(&msg, instrument_id).unwrap();
1394 assert!(funding.is_some());
1395
1396 let mark = parse_derivative_ticker_mark_price(&msg, instrument_id, 1).unwrap();
1397 assert!(mark.is_none());
1398
1399 let index = parse_derivative_ticker_index_price(&msg, instrument_id, 1).unwrap();
1400 assert!(index.is_none());
1401 }
1402}