1use nautilus_core::nanos::UnixNanos;
19use nautilus_model::{
20 data::{BookOrder, Data, OrderBookDelta, OrderBookDeltas, QuoteTick, TradeTick},
21 enums::{AggressorSide, BookAction, OrderSide, RecordFlag},
22 identifiers::TradeId,
23 instruments::{Instrument, InstrumentAny},
24 types::{Price, Quantity},
25};
26
27use crate::{
28 common::parse::parse_micros_or_init,
29 spot::sbe::stream::{
30 BestBidAskStreamEvent, DepthDiffStreamEvent, DepthSnapshotStreamEvent, MessageHeader,
31 StreamDecodeError, TradesStreamEvent, template_id,
32 },
33};
34
35#[derive(Debug)]
37pub enum MarketDataMessage {
38 Trades(TradesStreamEvent),
40 BestBidAsk(BestBidAskStreamEvent),
42 DepthSnapshot(DepthSnapshotStreamEvent),
44 DepthDiff(DepthDiffStreamEvent),
46}
47
48pub fn decode_market_data(buf: &[u8]) -> Result<MarketDataMessage, StreamDecodeError> {
58 let header = MessageHeader::decode(buf)?;
59 header.validate_schema()?;
60
61 match header.template_id {
62 template_id::TRADES_STREAM_EVENT => Ok(MarketDataMessage::Trades(
63 TradesStreamEvent::decode_validated(buf)?,
64 )),
65 template_id::BEST_BID_ASK_STREAM_EVENT => Ok(MarketDataMessage::BestBidAsk(
66 BestBidAskStreamEvent::decode_validated(buf)?,
67 )),
68 template_id::DEPTH_SNAPSHOT_STREAM_EVENT => Ok(MarketDataMessage::DepthSnapshot(
69 DepthSnapshotStreamEvent::decode_validated(buf)?,
70 )),
71 template_id::DEPTH_DIFF_STREAM_EVENT => Ok(MarketDataMessage::DepthDiff(
72 DepthDiffStreamEvent::decode_validated(buf)?,
73 )),
74 _ => Err(StreamDecodeError::UnknownTemplateId(header.template_id)),
75 }
76}
77
78pub fn parse_trades_event(
87 event: &TradesStreamEvent,
88 instrument: &InstrumentAny,
89 ts_init: UnixNanos,
90) -> anyhow::Result<Vec<Data>> {
91 let instrument_id = instrument.id();
92 let price_precision = instrument.price_precision();
93 let size_precision = instrument.size_precision();
94
95 let mut trades = Vec::with_capacity(event.trades.len());
96 for t in &event.trades {
97 let price = parse_price(t.price_mantissa, event.price_exponent, price_precision)?;
98 let size = parse_quantity(t.qty_mantissa, event.qty_exponent, size_precision)?;
99 let ts_event = parse_micros_or_init(
100 event.transact_time_us,
101 "Spot SBE stream transaction time",
102 ts_init,
103 );
104
105 let trade = TradeTick::new_checked(
106 instrument_id,
107 price,
108 size,
109 if t.is_buyer_maker {
110 AggressorSide::Sell
111 } else {
112 AggressorSide::Buy
113 },
114 TradeId::new(t.id.to_string()),
115 ts_event,
116 ts_init,
117 )?;
118
119 trades.push(Data::from(trade));
120 }
121
122 Ok(trades)
123}
124
125pub fn parse_bbo_event(
134 event: &BestBidAskStreamEvent,
135 instrument: &InstrumentAny,
136 ts_init: UnixNanos,
137) -> anyhow::Result<QuoteTick> {
138 let instrument_id = instrument.id();
139 let price_precision = instrument.price_precision();
140 let size_precision = instrument.size_precision();
141
142 let bid_price = parse_price(
143 event.bid_price_mantissa,
144 event.price_exponent,
145 price_precision,
146 )?;
147 let bid_size = parse_quantity(event.bid_qty_mantissa, event.qty_exponent, size_precision)?;
148 let ask_price = parse_price(
149 event.ask_price_mantissa,
150 event.price_exponent,
151 price_precision,
152 )?;
153 let ask_size = parse_quantity(event.ask_qty_mantissa, event.qty_exponent, size_precision)?;
154 let ts_event = parse_micros_or_init(event.event_time_us, "Spot SBE BBO event time", ts_init);
155
156 Ok(QuoteTick::new(
157 instrument_id,
158 bid_price,
159 ask_price,
160 bid_size,
161 ask_size,
162 ts_event,
163 ts_init,
164 ))
165}
166
167pub fn parse_depth_snapshot(
178 event: &DepthSnapshotStreamEvent,
179 instrument: &InstrumentAny,
180 ts_init: UnixNanos,
181) -> anyhow::Result<OrderBookDeltas> {
182 let instrument_id = instrument.id();
183 let price_precision = instrument.price_precision();
184 let size_precision = instrument.size_precision();
185 let ts_event = parse_micros_or_init(
186 event.event_time_us,
187 "Spot SBE depth snapshot event time",
188 ts_init,
189 );
190 let sequence = event.book_update_id as u64;
191
192 let mut deltas = Vec::with_capacity(event.bids.len() + event.asks.len() + 1);
193
194 deltas.push(OrderBookDelta::clear(
196 instrument_id,
197 sequence,
198 ts_event,
199 ts_init,
200 ));
201
202 for (side, levels) in [
203 (OrderSide::Buy, &event.bids),
204 (OrderSide::Sell, &event.asks),
205 ] {
206 for level in levels {
207 let price = parse_price(level.price_mantissa, event.price_exponent, price_precision)?;
208 let size = parse_quantity(level.qty_mantissa, event.qty_exponent, size_precision)?;
209 let order = BookOrder::new(side, price, size, 0);
210
211 deltas.push(OrderBookDelta::new_checked(
212 instrument_id,
213 BookAction::Add,
214 order,
215 RecordFlag::F_SNAPSHOT as u8,
216 sequence,
217 ts_event,
218 ts_init,
219 )?);
220 }
221 }
222
223 if let Some(last) = deltas.last_mut() {
224 last.flags |= RecordFlag::F_LAST as u8;
225 }
226
227 Ok(OrderBookDeltas::new(instrument_id, deltas))
228}
229
230pub fn parse_depth_diff(
242 event: &DepthDiffStreamEvent,
243 instrument: &InstrumentAny,
244 ts_init: UnixNanos,
245) -> anyhow::Result<Option<OrderBookDeltas>> {
246 let instrument_id = instrument.id();
247 let price_precision = instrument.price_precision();
248 let size_precision = instrument.size_precision();
249 let ts_event = parse_micros_or_init(
250 event.event_time_us,
251 "Spot SBE depth diff event time",
252 ts_init,
253 );
254 let sequence = event.last_book_update_id as u64;
255
256 let mut deltas = Vec::with_capacity(event.bids.len() + event.asks.len());
257
258 for (i, level) in event.bids.iter().enumerate() {
260 let price = parse_price(level.price_mantissa, event.price_exponent, price_precision)?;
261 let size = parse_quantity(level.qty_mantissa, event.qty_exponent, size_precision)?;
262
263 let action = if level.qty_mantissa == 0 {
265 BookAction::Delete
266 } else {
267 BookAction::Update
268 };
269
270 let flags = if i == event.bids.len() - 1 && event.asks.is_empty() {
271 RecordFlag::F_LAST as u8
272 } else {
273 0
274 };
275
276 let order = BookOrder::new(OrderSide::Buy, price, size, 0);
277
278 deltas.push(OrderBookDelta::new_checked(
279 instrument_id,
280 action,
281 order,
282 flags,
283 sequence,
284 ts_event,
285 ts_init,
286 )?);
287 }
288
289 for (i, level) in event.asks.iter().enumerate() {
291 let price = parse_price(level.price_mantissa, event.price_exponent, price_precision)?;
292 let size = parse_quantity(level.qty_mantissa, event.qty_exponent, size_precision)?;
293
294 let action = if level.qty_mantissa == 0 {
295 BookAction::Delete
296 } else {
297 BookAction::Update
298 };
299
300 let flags = if i == event.asks.len() - 1 {
301 RecordFlag::F_LAST as u8
302 } else {
303 0
304 };
305
306 let order = BookOrder::new(OrderSide::Sell, price, size, 0);
307
308 deltas.push(OrderBookDelta::new_checked(
309 instrument_id,
310 action,
311 order,
312 flags,
313 sequence,
314 ts_event,
315 ts_init,
316 )?);
317 }
318
319 if deltas.is_empty() {
320 return Ok(None);
321 }
322
323 Ok(Some(OrderBookDeltas::new(instrument_id, deltas)))
324}
325
326fn parse_price(mantissa: i64, exponent: i8, precision: u8) -> anyhow::Result<Price> {
327 let price = Price::from_mantissa_exponent_checked(mantissa, exponent, precision)?;
328 anyhow::ensure!(
329 !price.is_error() && !price.is_undefined(),
330 "invalid SBE price sentinel"
331 );
332 Ok(price)
333}
334
335fn parse_quantity(mantissa: i64, exponent: i8, precision: u8) -> anyhow::Result<Quantity> {
336 let mantissa =
337 u64::try_from(mantissa).map_err(|_| anyhow::anyhow!("invalid negative SBE quantity"))?;
338 Ok(Quantity::from_mantissa_exponent_checked(
339 mantissa, exponent, precision,
340 )?)
341}
342
343#[cfg(test)]
344mod tests {
345 use rstest::rstest;
346 use ustr::Ustr;
347
348 use super::*;
349 use crate::{
350 common::parse::parse_spot_instrument_sbe,
351 spot::{
352 http::models::{
353 BinanceLotSizeFilterSbe, BinancePriceFilterSbe, BinanceSymbolFiltersSbe,
354 BinanceSymbolSbe,
355 },
356 sbe::stream::{PriceLevel, STREAM_SCHEMA_ID, Trade},
357 },
358 };
359
360 fn make_bbo_buffer() -> Vec<u8> {
361 let mut buf = vec![0u8; 70];
362
363 buf[0..2].copy_from_slice(&50u16.to_le_bytes()); buf[2..4].copy_from_slice(&template_id::BEST_BID_ASK_STREAM_EVENT.to_le_bytes());
366 buf[4..6].copy_from_slice(&STREAM_SCHEMA_ID.to_le_bytes());
367 buf[6..8].copy_from_slice(&0u16.to_le_bytes()); let body = &mut buf[8..];
371 body[0..8].copy_from_slice(&1000000i64.to_le_bytes()); body[8..16].copy_from_slice(&12345i64.to_le_bytes()); body[16] = (-2i8) as u8; body[17] = (-8i8) as u8; body[18..26].copy_from_slice(&4200000i64.to_le_bytes()); body[26..34].copy_from_slice(&100000000i64.to_le_bytes()); body[34..42].copy_from_slice(&4200100i64.to_le_bytes()); body[42..50].copy_from_slice(&200000000i64.to_le_bytes()); body[50] = 7;
382 body[51..58].copy_from_slice(b"BTCUSDT");
383
384 buf
385 }
386
387 fn sample_instrument() -> InstrumentAny {
388 let symbol = BinanceSymbolSbe {
389 symbol: "ETHUSDT".to_string(),
390 base_asset: "ETH".to_string(),
391 quote_asset: "USDT".to_string(),
392 base_asset_precision: 8,
393 quote_asset_precision: 8,
394 status: 0,
395 order_types: 0,
396 iceberg_allowed: true,
397 oco_allowed: true,
398 oto_allowed: false,
399 quote_order_qty_market_allowed: true,
400 allow_trailing_stop: true,
401 cancel_replace_allowed: true,
402 amend_allowed: true,
403 is_spot_trading_allowed: true,
404 is_margin_trading_allowed: false,
405 filters: BinanceSymbolFiltersSbe {
406 notional_filters: Vec::new(),
407 price_filter: Some(BinancePriceFilterSbe {
408 price_exponent: -8,
409 min_price: 1_000_000,
410 max_price: 100_000_000_000_000,
411 tick_size: 1_000_000,
412 }),
413 lot_size_filter: Some(BinanceLotSizeFilterSbe {
414 qty_exponent: -8,
415 min_qty: 10_000,
416 max_qty: 900_000_000_000,
417 step_size: 10_000,
418 }),
419 },
420 permissions: vec![vec!["SPOT".to_string()]],
421 };
422
423 let ts = UnixNanos::from(1_700_000_000_000_000_000u64);
424 parse_spot_instrument_sbe(&symbol, ts, ts).unwrap()
425 }
426
427 #[rstest]
428 fn test_decode_empty_buffer() {
429 let err = decode_market_data(&[]).unwrap_err();
430 assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
431 }
432
433 #[rstest]
434 fn test_decode_short_buffer() {
435 let buf = [0u8; 5];
436 let err = decode_market_data(&buf).unwrap_err();
437 assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
438 }
439
440 #[rstest]
441 fn test_decode_wrong_schema() {
442 let mut buf = [0u8; 100];
443 buf[0..2].copy_from_slice(&50u16.to_le_bytes()); buf[2..4].copy_from_slice(&template_id::BEST_BID_ASK_STREAM_EVENT.to_le_bytes());
445 buf[4..6].copy_from_slice(&99u16.to_le_bytes()); buf[6..8].copy_from_slice(&0u16.to_le_bytes()); let err = decode_market_data(&buf).unwrap_err();
449 assert!(matches!(err, StreamDecodeError::SchemaMismatch { .. }));
450 }
451
452 #[rstest]
453 fn test_decode_unknown_template() {
454 let mut buf = [0u8; 100];
455 buf[0..2].copy_from_slice(&50u16.to_le_bytes()); buf[2..4].copy_from_slice(&9999u16.to_le_bytes()); buf[4..6].copy_from_slice(&STREAM_SCHEMA_ID.to_le_bytes());
458 buf[6..8].copy_from_slice(&0u16.to_le_bytes()); let err = decode_market_data(&buf).unwrap_err();
461 assert!(matches!(err, StreamDecodeError::UnknownTemplateId(9999)));
462 }
463
464 #[rstest]
465 fn test_decode_valid_best_bid_ask() {
466 let buf = make_bbo_buffer();
467 let msg = decode_market_data(&buf).unwrap();
468
469 match msg {
470 MarketDataMessage::BestBidAsk(event) => {
471 assert_eq!(event.event_time_us, 1_000_000);
472 assert_eq!(event.symbol, Ustr::from("BTCUSDT"));
473 }
474 _ => panic!("Expected BestBidAsk"),
475 }
476 }
477
478 #[rstest]
479 fn test_parse_trades_event() {
480 let instrument = sample_instrument();
481 let ts_init = UnixNanos::from(1_800_000_000_000_000_000u64);
482 let event = TradesStreamEvent {
483 event_time_us: 1_700_000_000_000_000,
484 transact_time_us: 1_700_000_000_100_000,
485 price_exponent: -2,
486 qty_exponent: -4,
487 trades: vec![
488 Trade {
489 id: 1,
490 price_mantissa: 12_345,
491 qty_mantissa: 25_000,
492 is_buyer_maker: false,
493 },
494 Trade {
495 id: 2,
496 price_mantissa: 12_340,
497 qty_mantissa: 10_000,
498 is_buyer_maker: true,
499 },
500 ],
501 symbol: Ustr::from("ETHUSDT"),
502 };
503
504 let data = parse_trades_event(&event, &instrument, ts_init).unwrap();
505
506 assert_eq!(data.len(), 2);
507 match &data[0] {
508 Data::Trade(trade) => {
509 assert_eq!(trade.instrument_id, instrument.id());
510 assert_eq!(trade.price, Price::from("123.45"));
511 assert_eq!(trade.size, Quantity::from("2.5000"));
512 assert_eq!(trade.aggressor_side, AggressorSide::Buy);
513 assert_eq!(trade.trade_id, TradeId::new("1"));
514 assert_eq!(
515 trade.ts_event,
516 UnixNanos::from(1_700_000_000_100_000_000u64)
517 );
518 assert_eq!(trade.ts_init, ts_init);
519 }
520 other => panic!("Expected trade data, was {other:?}"),
521 }
522
523 match &data[1] {
524 Data::Trade(trade) => {
525 assert_eq!(trade.instrument_id, instrument.id());
526 assert_eq!(trade.price, Price::from("123.40"));
527 assert_eq!(trade.size, Quantity::from("1.0000"));
528 assert_eq!(trade.aggressor_side, AggressorSide::Sell);
529 assert_eq!(trade.trade_id, TradeId::new("2"));
530 assert_eq!(
531 trade.ts_event,
532 UnixNanos::from(1_700_000_000_100_000_000u64)
533 );
534 assert_eq!(trade.ts_init, ts_init);
535 }
536 other => panic!("Expected trade data, was {other:?}"),
537 }
538 }
539
540 #[rstest]
541 fn test_parse_bbo_event() {
542 let instrument = sample_instrument();
543 let ts_init = UnixNanos::from(1_800_000_000_000_000_000u64);
544 let event = BestBidAskStreamEvent {
545 event_time_us: 1_700_000_000_000_000,
546 book_update_id: 123,
547 price_exponent: -2,
548 qty_exponent: -4,
549 bid_price_mantissa: 12_345,
550 bid_qty_mantissa: 25_000,
551 ask_price_mantissa: 12_350,
552 ask_qty_mantissa: 30_000,
553 symbol: Ustr::from("ETHUSDT"),
554 };
555
556 let quote = parse_bbo_event(&event, &instrument, ts_init).unwrap();
557
558 assert_eq!(quote.instrument_id, instrument.id());
559 assert_eq!(quote.bid_price, Price::from("123.45"));
560 assert_eq!(quote.ask_price, Price::from("123.50"));
561 assert_eq!(quote.bid_size, Quantity::from("2.5000"));
562 assert_eq!(quote.ask_size, Quantity::from("3.0000"));
563 assert_eq!(
564 quote.ts_event,
565 UnixNanos::from(1_700_000_000_000_000_000u64)
566 );
567 assert_eq!(quote.ts_init, ts_init);
568 }
569
570 #[rstest]
571 #[case::negative(-1)]
572 #[case::overflow(i64::MAX)]
573 fn test_parse_bbo_event_falls_back_for_invalid_timestamp(#[case] event_time_us: i64) {
574 let instrument = sample_instrument();
575 let event = BestBidAskStreamEvent {
576 event_time_us,
577 book_update_id: 123,
578 price_exponent: -2,
579 qty_exponent: -4,
580 bid_price_mantissa: 12_345,
581 bid_qty_mantissa: 25_000,
582 ask_price_mantissa: 12_350,
583 ask_qty_mantissa: 30_000,
584 symbol: Ustr::from("ETHUSDT"),
585 };
586
587 let ts_init = UnixNanos::from(1);
588 let quote = parse_bbo_event(&event, &instrument, ts_init).unwrap();
589
590 assert_eq!(quote.ts_event, ts_init);
591 assert_eq!(quote.ts_init, ts_init);
592 }
593
594 #[rstest]
595 fn test_parse_depth_snapshot() {
596 let instrument = sample_instrument();
597 let ts_init = UnixNanos::from(1_800_000_000_000_000_000u64);
598 let event = DepthSnapshotStreamEvent {
599 event_time_us: 1_700_000_000_000_000,
600 book_update_id: 123,
601 price_exponent: -2,
602 qty_exponent: -4,
603 bids: vec![PriceLevel {
604 price_mantissa: 12_345,
605 qty_mantissa: 25_000,
606 }],
607 asks: vec![PriceLevel {
608 price_mantissa: 12_350,
609 qty_mantissa: 30_000,
610 }],
611 symbol: Ustr::from("ETHUSDT"),
612 };
613
614 let deltas = parse_depth_snapshot(&event, &instrument, ts_init).unwrap();
615
616 assert_eq!(deltas.instrument_id, instrument.id());
617 assert_eq!(deltas.deltas.len(), 3);
618 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
619 assert_eq!(deltas.deltas[1].action, BookAction::Add);
620 assert_eq!(deltas.deltas[1].order.side, OrderSide::Buy.into());
621 assert_eq!(deltas.deltas[1].order.price, Price::from("123.45"));
622 assert_eq!(deltas.deltas[1].order.size, Quantity::from("2.5000"));
623 assert_eq!(deltas.deltas[2].action, BookAction::Add);
624 assert_eq!(deltas.deltas[2].order.side, OrderSide::Sell.into());
625 assert_eq!(deltas.deltas[2].order.price, Price::from("123.50"));
626 assert_eq!(deltas.deltas[2].order.size, Quantity::from("3.0000"));
627 assert_eq!(deltas.deltas[1].flags, RecordFlag::F_SNAPSHOT as u8);
628 assert_eq!(
629 deltas.deltas[2].flags,
630 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
631 );
632 assert_eq!(deltas.deltas[0].sequence, 123);
633 assert_eq!(deltas.deltas[2].sequence, 123);
634 assert_eq!(
635 deltas.ts_event,
636 UnixNanos::from(1_700_000_000_000_000_000u64)
637 );
638 assert_eq!(deltas.ts_init, ts_init);
639 assert!(deltas.deltas.iter().all(|delta| delta.ts_init == ts_init));
640 }
641
642 #[rstest]
643 fn test_parse_depth_snapshot_empty_clears_book() {
644 let instrument = sample_instrument();
645 let event = DepthSnapshotStreamEvent {
646 event_time_us: 1_700_000_000_000_000,
647 book_update_id: 123,
648 price_exponent: -2,
649 qty_exponent: -4,
650 bids: vec![],
651 asks: vec![],
652 symbol: Ustr::from("ETHUSDT"),
653 };
654
655 let deltas = parse_depth_snapshot(&event, &instrument, UnixNanos::from(2)).unwrap();
656
657 assert_eq!(deltas.deltas.len(), 1);
658 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
659 assert_eq!(deltas.deltas[0].sequence, 123);
660 assert_eq!(
661 deltas.deltas[0].flags,
662 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
663 );
664 }
665
666 #[rstest]
667 fn test_parse_depth_diff() {
668 let instrument = sample_instrument();
669 let ts_init = UnixNanos::from(1_800_000_000_000_000_000u64);
670 let event = DepthDiffStreamEvent {
671 event_time_us: 1_700_000_000_000_000,
672 first_book_update_id: 100,
673 last_book_update_id: 101,
674 price_exponent: -2,
675 qty_exponent: -4,
676 bids: vec![
677 PriceLevel {
678 price_mantissa: 12_345,
679 qty_mantissa: 25_000,
680 },
681 PriceLevel {
682 price_mantissa: 12_340,
683 qty_mantissa: 0,
684 },
685 ],
686 asks: vec![PriceLevel {
687 price_mantissa: 12_350,
688 qty_mantissa: 30_000,
689 }],
690 symbol: Ustr::from("ETHUSDT"),
691 };
692
693 let deltas = parse_depth_diff(&event, &instrument, ts_init)
694 .unwrap()
695 .unwrap();
696
697 assert_eq!(deltas.instrument_id, instrument.id());
698 assert_eq!(deltas.deltas.len(), 3);
699 assert_eq!(deltas.deltas[0].order.price, Price::from("123.45"));
700 assert_eq!(deltas.deltas[0].order.size, Quantity::from("2.5000"));
701 assert_eq!(deltas.deltas[1].order.price, Price::from("123.40"));
702 assert_eq!(deltas.deltas[1].order.size, Quantity::from("0.0000"));
703 assert_eq!(deltas.deltas[2].order.price, Price::from("123.50"));
704 assert_eq!(deltas.deltas[2].order.size, Quantity::from("3.0000"));
705 assert_eq!(deltas.deltas[0].action, BookAction::Update);
706 assert_eq!(deltas.deltas[0].order.side, OrderSide::Buy.into());
707 assert_eq!(deltas.deltas[1].action, BookAction::Delete);
708 assert_eq!(deltas.deltas[1].order.side, OrderSide::Buy.into());
709 assert_eq!(deltas.deltas[2].action, BookAction::Update);
710 assert_eq!(deltas.deltas[2].order.side, OrderSide::Sell.into());
711 assert_eq!(deltas.deltas[2].flags, RecordFlag::F_LAST as u8);
712 assert_eq!(deltas.deltas[0].flags, 0);
713 assert_eq!(deltas.deltas[1].flags, 0);
714
715 for delta in &deltas.deltas {
716 assert_eq!(delta.instrument_id, instrument.id());
717 assert_eq!(delta.order.order_id, 0);
718 assert_eq!(delta.sequence, 101);
719 assert_eq!(
720 delta.ts_event,
721 UnixNanos::from(1_700_000_000_000_000_000u64)
722 );
723 assert_eq!(delta.ts_init, ts_init);
724 }
725
726 assert_eq!(
727 deltas.ts_event,
728 UnixNanos::from(1_700_000_000_000_000_000u64)
729 );
730 assert_eq!(deltas.ts_init, ts_init);
731 assert!(deltas.deltas.iter().all(|delta| delta.ts_init == ts_init));
732 }
733
734 #[rstest]
735 fn test_parse_depth_diff_empty_returns_none() {
736 let instrument = sample_instrument();
737 let event = DepthDiffStreamEvent {
738 event_time_us: 1_700_000_000_000_000,
739 first_book_update_id: 100,
740 last_book_update_id: 101,
741 price_exponent: -2,
742 qty_exponent: -4,
743 bids: vec![],
744 asks: vec![],
745 symbol: Ustr::from("ETHUSDT"),
746 };
747
748 let deltas = parse_depth_diff(&event, &instrument, UnixNanos::from(2)).unwrap();
749
750 assert!(deltas.is_none());
751 }
752
753 #[rstest]
754 #[case::price_exponent(1, 1, i8::MAX, 0)]
755 #[case::qty_exponent(1, 1, 0, i8::MAX)]
756 #[case::price_above_range(i64::MAX, 1, 0, 0)]
757 #[case::price_below_range(i64::MIN, 1, 0, 0)]
758 #[case::qty_above_range(1, i64::MAX, 0, 0)]
759 #[case::negative_qty(1, -1, 0, 0)]
760 #[case::minimum_qty(1, i64::MIN, 0, 0)]
761 #[case::negative_qty_rounds_to_zero(1, -1, 0, i8::MIN)]
762 fn test_parse_events_reject_invalid_numbers(
763 #[case] price_mantissa: i64,
764 #[case] qty_mantissa: i64,
765 #[case] price_exponent: i8,
766 #[case] qty_exponent: i8,
767 #[values(false, true)] ask: bool,
768 ) {
769 let instrument = sample_instrument();
770 let ts_init = UnixNanos::from(2);
771
772 let level = PriceLevel {
773 price_mantissa,
774 qty_mantissa,
775 };
776
777 let valid = PriceLevel {
778 price_mantissa: 12_345,
779 qty_mantissa: 25_000,
780 };
781
782 let (bid, ask) = if ask { (valid, level) } else { (level, valid) };
783
784 let trades = TradesStreamEvent {
785 event_time_us: 1,
786 transact_time_us: 1,
787 price_exponent,
788 qty_exponent,
789 trades: vec![
790 Trade {
791 id: 1,
792 price_mantissa: 0,
793 qty_mantissa: 1,
794 is_buyer_maker: false,
795 },
796 Trade {
797 id: 2,
798 price_mantissa,
799 qty_mantissa,
800 is_buyer_maker: true,
801 },
802 ],
803 symbol: Ustr::from("ETHUSDT"),
804 };
805
806 let bbo = BestBidAskStreamEvent {
807 event_time_us: 1,
808 book_update_id: 123,
809 price_exponent,
810 qty_exponent,
811 bid_price_mantissa: bid.price_mantissa,
812 bid_qty_mantissa: bid.qty_mantissa,
813 ask_price_mantissa: ask.price_mantissa,
814 ask_qty_mantissa: ask.qty_mantissa,
815 symbol: trades.symbol,
816 };
817
818 let snapshot = DepthSnapshotStreamEvent {
819 event_time_us: 1,
820 book_update_id: 123,
821 price_exponent,
822 qty_exponent,
823 bids: vec![bid],
824 asks: vec![ask],
825 symbol: trades.symbol,
826 };
827
828 let diff = DepthDiffStreamEvent {
829 event_time_us: 1,
830 first_book_update_id: 123,
831 last_book_update_id: 124,
832 price_exponent,
833 qty_exponent,
834 bids: snapshot.bids.clone(),
835 asks: snapshot.asks.clone(),
836 symbol: trades.symbol,
837 };
838
839 assert!(parse_trades_event(&trades, &instrument, ts_init).is_err());
840 assert!(parse_bbo_event(&bbo, &instrument, ts_init).is_err());
841 assert!(parse_depth_snapshot(&snapshot, &instrument, ts_init).is_err());
842 assert!(parse_depth_diff(&diff, &instrument, ts_init).is_err());
843 }
844
845 #[rstest]
846 #[case::zero(0, -4)]
847 #[case::rounded_to_zero(1, i8::MIN)]
848 fn test_parse_events_handle_zero_size(#[case] qty_mantissa: i64, #[case] qty_exponent: i8) {
849 let instrument = sample_instrument();
850 let ts_init = UnixNanos::from(2);
851
852 let trades = TradesStreamEvent {
853 event_time_us: 1,
854 transact_time_us: 1,
855 price_exponent: -2,
856 qty_exponent,
857 trades: vec![Trade {
858 id: 1,
859 price_mantissa: 12_345,
860 qty_mantissa,
861 is_buyer_maker: false,
862 }],
863 symbol: Ustr::from("ETHUSDT"),
864 };
865
866 let snapshot = DepthSnapshotStreamEvent {
867 event_time_us: 1,
868 book_update_id: 123,
869 price_exponent: -2,
870 qty_exponent,
871 bids: vec![PriceLevel {
872 price_mantissa: 12_345,
873 qty_mantissa,
874 }],
875 asks: vec![],
876 symbol: trades.symbol,
877 };
878
879 let diff = DepthDiffStreamEvent {
880 event_time_us: 1,
881 first_book_update_id: 123,
882 last_book_update_id: 124,
883 price_exponent: -2,
884 qty_exponent,
885 bids: snapshot.bids.clone(),
886 asks: vec![],
887 symbol: trades.symbol,
888 };
889
890 let bbo = BestBidAskStreamEvent {
891 event_time_us: 1,
892 book_update_id: 123,
893 price_exponent: -2,
894 qty_exponent,
895 bid_price_mantissa: 12_345,
896 bid_qty_mantissa: qty_mantissa,
897 ask_price_mantissa: 12_350,
898 ask_qty_mantissa: qty_mantissa,
899 symbol: trades.symbol,
900 };
901
902 assert!(parse_trades_event(&trades, &instrument, ts_init).is_err());
903 assert!(parse_depth_snapshot(&snapshot, &instrument, ts_init).is_err());
904 let result = parse_depth_diff(&diff, &instrument, ts_init);
905 if qty_mantissa == 0 {
906 let deltas = result.unwrap().unwrap();
907 assert_eq!(deltas.deltas[0].action, BookAction::Delete);
908 assert_eq!(
909 deltas.deltas[0].order.size,
910 Quantity::zero(instrument.size_precision())
911 );
912 } else {
913 assert!(result.is_err());
914 }
915
916 let quote = parse_bbo_event(&bbo, &instrument, ts_init).unwrap();
917 assert_eq!(quote.bid_size, Quantity::zero(instrument.size_precision()));
918 assert_eq!(quote.ask_size, Quantity::zero(instrument.size_precision()));
919 }
920
921 #[cfg(not(feature = "high-precision"))]
922 #[rstest]
923 fn test_parse_price_rejects_sentinel() {
924 assert!(parse_price(i64::MIN, -9, 9).is_err());
925 }
926}