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::spot::sbe::stream::{
28 BestBidAskStreamEvent, DepthDiffStreamEvent, DepthSnapshotStreamEvent, MessageHeader,
29 StreamDecodeError, TradesStreamEvent, template_id,
30};
31
32#[derive(Debug)]
34pub enum MarketDataMessage {
35 Trades(TradesStreamEvent),
37 BestBidAsk(BestBidAskStreamEvent),
39 DepthSnapshot(DepthSnapshotStreamEvent),
41 DepthDiff(DepthDiffStreamEvent),
43}
44
45pub fn decode_market_data(buf: &[u8]) -> Result<MarketDataMessage, StreamDecodeError> {
55 let header = MessageHeader::decode(buf)?;
56 header.validate_schema()?;
57
58 match header.template_id {
59 template_id::TRADES_STREAM_EVENT => Ok(MarketDataMessage::Trades(
60 TradesStreamEvent::decode_validated(buf)?,
61 )),
62 template_id::BEST_BID_ASK_STREAM_EVENT => Ok(MarketDataMessage::BestBidAsk(
63 BestBidAskStreamEvent::decode_validated(buf)?,
64 )),
65 template_id::DEPTH_SNAPSHOT_STREAM_EVENT => Ok(MarketDataMessage::DepthSnapshot(
66 DepthSnapshotStreamEvent::decode_validated(buf)?,
67 )),
68 template_id::DEPTH_DIFF_STREAM_EVENT => Ok(MarketDataMessage::DepthDiff(
69 DepthDiffStreamEvent::decode_validated(buf)?,
70 )),
71 _ => Err(StreamDecodeError::UnknownTemplateId(header.template_id)),
72 }
73}
74
75pub fn parse_trades_event(event: &TradesStreamEvent, instrument: &InstrumentAny) -> Vec<Data> {
77 let instrument_id = instrument.id();
78 let price_precision = instrument.price_precision();
79 let size_precision = instrument.size_precision();
80
81 event
82 .trades
83 .iter()
84 .map(|t| {
85 let price = Price::from_mantissa_exponent(
86 t.price_mantissa,
87 event.price_exponent,
88 price_precision,
89 );
90 let size = Quantity::from_mantissa_exponent(
91 t.qty_mantissa as u64,
92 event.qty_exponent,
93 size_precision,
94 );
95 let ts_event = UnixNanos::from_micros(event.transact_time_us as u64);
96
97 let trade = TradeTick::new(
98 instrument_id,
99 price,
100 size,
101 if t.is_buyer_maker {
102 AggressorSide::Seller
103 } else {
104 AggressorSide::Buyer
105 },
106 TradeId::new(t.id.to_string()),
107 ts_event,
108 ts_event,
109 );
110 Data::from(trade)
111 })
112 .collect()
113}
114
115pub fn parse_bbo_event(event: &BestBidAskStreamEvent, instrument: &InstrumentAny) -> QuoteTick {
117 let instrument_id = instrument.id();
118 let price_precision = instrument.price_precision();
119 let size_precision = instrument.size_precision();
120
121 let bid_price = Price::from_mantissa_exponent(
122 event.bid_price_mantissa,
123 event.price_exponent,
124 price_precision,
125 );
126 let bid_size = Quantity::from_mantissa_exponent(
127 event.bid_qty_mantissa as u64,
128 event.qty_exponent,
129 size_precision,
130 );
131 let ask_price = Price::from_mantissa_exponent(
132 event.ask_price_mantissa,
133 event.price_exponent,
134 price_precision,
135 );
136 let ask_size = Quantity::from_mantissa_exponent(
137 event.ask_qty_mantissa as u64,
138 event.qty_exponent,
139 size_precision,
140 );
141 let ts_event = UnixNanos::from_micros(event.event_time_us as u64);
142
143 QuoteTick::new(
144 instrument_id,
145 bid_price,
146 ask_price,
147 bid_size,
148 ask_size,
149 ts_event,
150 ts_event,
151 )
152}
153
154pub fn parse_depth_snapshot(
158 event: &DepthSnapshotStreamEvent,
159 instrument: &InstrumentAny,
160) -> Option<OrderBookDeltas> {
161 let instrument_id = instrument.id();
162 let price_precision = instrument.price_precision();
163 let size_precision = instrument.size_precision();
164 let ts_event = UnixNanos::from_micros(event.event_time_us as u64);
165 let sequence = event.book_update_id as u64;
166
167 let mut deltas = Vec::with_capacity(event.bids.len() + event.asks.len() + 1);
168
169 deltas.push(OrderBookDelta::clear(
171 instrument_id,
172 sequence,
173 ts_event,
174 ts_event,
175 ));
176
177 for (i, level) in event.bids.iter().enumerate() {
179 let price = Price::from_mantissa_exponent(
180 level.price_mantissa,
181 event.price_exponent,
182 price_precision,
183 );
184 let size = Quantity::from_mantissa_exponent(
185 level.qty_mantissa as u64,
186 event.qty_exponent,
187 size_precision,
188 );
189 let flags = if i == event.bids.len() - 1 && event.asks.is_empty() {
190 RecordFlag::F_LAST as u8
191 } else {
192 0
193 };
194
195 let order = BookOrder::new(OrderSide::Buy, price, size, 0);
196
197 deltas.push(OrderBookDelta::new(
198 instrument_id,
199 BookAction::Add,
200 order,
201 flags,
202 sequence,
203 ts_event,
204 ts_event,
205 ));
206 }
207
208 for (i, level) in event.asks.iter().enumerate() {
210 let price = Price::from_mantissa_exponent(
211 level.price_mantissa,
212 event.price_exponent,
213 price_precision,
214 );
215 let size = Quantity::from_mantissa_exponent(
216 level.qty_mantissa as u64,
217 event.qty_exponent,
218 size_precision,
219 );
220 let flags = if i == event.asks.len() - 1 {
221 RecordFlag::F_LAST as u8
222 } else {
223 0
224 };
225
226 let order = BookOrder::new(OrderSide::Sell, price, size, 0);
227
228 deltas.push(OrderBookDelta::new(
229 instrument_id,
230 BookAction::Add,
231 order,
232 flags,
233 sequence,
234 ts_event,
235 ts_event,
236 ));
237 }
238
239 if deltas.len() <= 1 {
242 return None;
243 }
244
245 Some(OrderBookDeltas::new(instrument_id, deltas))
246}
247
248pub fn parse_depth_diff(
252 event: &DepthDiffStreamEvent,
253 instrument: &InstrumentAny,
254) -> Option<OrderBookDeltas> {
255 let instrument_id = instrument.id();
256 let price_precision = instrument.price_precision();
257 let size_precision = instrument.size_precision();
258 let ts_event = UnixNanos::from_micros(event.event_time_us as u64);
259 let sequence = event.last_book_update_id as u64;
260
261 let mut deltas = Vec::with_capacity(event.bids.len() + event.asks.len());
262
263 for (i, level) in event.bids.iter().enumerate() {
265 let price = Price::from_mantissa_exponent(
266 level.price_mantissa,
267 event.price_exponent,
268 price_precision,
269 );
270 let size = Quantity::from_mantissa_exponent(
271 level.qty_mantissa as u64,
272 event.qty_exponent,
273 size_precision,
274 );
275
276 let action = if level.qty_mantissa == 0 {
278 BookAction::Delete
279 } else {
280 BookAction::Update
281 };
282
283 let flags = if i == event.bids.len() - 1 && event.asks.is_empty() {
284 RecordFlag::F_LAST as u8
285 } else {
286 0
287 };
288
289 let order = BookOrder::new(OrderSide::Buy, price, size, 0);
290
291 deltas.push(OrderBookDelta::new(
292 instrument_id,
293 action,
294 order,
295 flags,
296 sequence,
297 ts_event,
298 ts_event,
299 ));
300 }
301
302 for (i, level) in event.asks.iter().enumerate() {
304 let price = Price::from_mantissa_exponent(
305 level.price_mantissa,
306 event.price_exponent,
307 price_precision,
308 );
309 let size = Quantity::from_mantissa_exponent(
310 level.qty_mantissa as u64,
311 event.qty_exponent,
312 size_precision,
313 );
314
315 let action = if level.qty_mantissa == 0 {
316 BookAction::Delete
317 } else {
318 BookAction::Update
319 };
320
321 let flags = if i == event.asks.len() - 1 {
322 RecordFlag::F_LAST as u8
323 } else {
324 0
325 };
326
327 let order = BookOrder::new(OrderSide::Sell, price, size, 0);
328
329 deltas.push(OrderBookDelta::new(
330 instrument_id,
331 action,
332 order,
333 flags,
334 sequence,
335 ts_event,
336 ts_event,
337 ));
338 }
339
340 if deltas.is_empty() {
341 return None;
342 }
343
344 Some(OrderBookDeltas::new(instrument_id, deltas))
345}
346
347#[cfg(test)]
348mod tests {
349 use rstest::rstest;
350 use ustr::Ustr;
351
352 use super::*;
353 use crate::{
354 common::parse::parse_spot_instrument_sbe,
355 spot::{
356 http::models::{
357 BinanceLotSizeFilterSbe, BinancePriceFilterSbe, BinanceSymbolFiltersSbe,
358 BinanceSymbolSbe,
359 },
360 sbe::stream::{PriceLevel, STREAM_SCHEMA_ID, Trade},
361 },
362 };
363
364 fn make_bbo_buffer() -> Vec<u8> {
365 let mut buf = vec![0u8; 70];
366
367 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());
370 buf[4..6].copy_from_slice(&STREAM_SCHEMA_ID.to_le_bytes());
371 buf[6..8].copy_from_slice(&0u16.to_le_bytes()); let body = &mut buf[8..];
375 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;
386 body[51..58].copy_from_slice(b"BTCUSDT");
387
388 buf
389 }
390
391 fn sample_instrument() -> InstrumentAny {
392 let symbol = BinanceSymbolSbe {
393 symbol: "ETHUSDT".to_string(),
394 base_asset: "ETH".to_string(),
395 quote_asset: "USDT".to_string(),
396 base_asset_precision: 8,
397 quote_asset_precision: 8,
398 status: 0,
399 order_types: 0,
400 iceberg_allowed: true,
401 oco_allowed: true,
402 oto_allowed: false,
403 quote_order_qty_market_allowed: true,
404 allow_trailing_stop: true,
405 cancel_replace_allowed: true,
406 amend_allowed: true,
407 is_spot_trading_allowed: true,
408 is_margin_trading_allowed: false,
409 filters: BinanceSymbolFiltersSbe {
410 price_filter: Some(BinancePriceFilterSbe {
411 price_exponent: -8,
412 min_price: 1_000_000,
413 max_price: 100_000_000_000_000,
414 tick_size: 1_000_000,
415 }),
416 lot_size_filter: Some(BinanceLotSizeFilterSbe {
417 qty_exponent: -8,
418 min_qty: 10_000,
419 max_qty: 900_000_000_000,
420 step_size: 10_000,
421 }),
422 },
423 permissions: vec![vec!["SPOT".to_string()]],
424 };
425
426 let ts = UnixNanos::from(1_700_000_000_000_000_000u64);
427 parse_spot_instrument_sbe(&symbol, ts, ts).unwrap()
428 }
429
430 #[rstest]
431 fn test_decode_empty_buffer() {
432 let err = decode_market_data(&[]).unwrap_err();
433 assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
434 }
435
436 #[rstest]
437 fn test_decode_short_buffer() {
438 let buf = [0u8; 5];
439 let err = decode_market_data(&buf).unwrap_err();
440 assert!(matches!(err, StreamDecodeError::BufferTooShort { .. }));
441 }
442
443 #[rstest]
444 fn test_decode_wrong_schema() {
445 let mut buf = [0u8; 100];
446 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());
448 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();
452 assert!(matches!(err, StreamDecodeError::SchemaMismatch { .. }));
453 }
454
455 #[rstest]
456 fn test_decode_unknown_template() {
457 let mut buf = [0u8; 100];
458 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());
461 buf[6..8].copy_from_slice(&0u16.to_le_bytes()); let err = decode_market_data(&buf).unwrap_err();
464 assert!(matches!(err, StreamDecodeError::UnknownTemplateId(9999)));
465 }
466
467 #[rstest]
468 fn test_decode_valid_best_bid_ask() {
469 let buf = make_bbo_buffer();
470 let msg = decode_market_data(&buf).unwrap();
471
472 match msg {
473 MarketDataMessage::BestBidAsk(event) => {
474 assert_eq!(event.event_time_us, 1_000_000);
475 assert_eq!(event.symbol, Ustr::from("BTCUSDT"));
476 }
477 _ => panic!("Expected BestBidAsk"),
478 }
479 }
480
481 #[rstest]
482 fn test_parse_trades_event() {
483 let instrument = sample_instrument();
484 let event = TradesStreamEvent {
485 event_time_us: 1_700_000_000_000_000,
486 transact_time_us: 1_700_000_000_100_000,
487 price_exponent: -2,
488 qty_exponent: -4,
489 trades: vec![
490 Trade {
491 id: 1,
492 price_mantissa: 12_345,
493 qty_mantissa: 25_000,
494 is_buyer_maker: false,
495 },
496 Trade {
497 id: 2,
498 price_mantissa: 12_340,
499 qty_mantissa: 10_000,
500 is_buyer_maker: true,
501 },
502 ],
503 symbol: Ustr::from("ETHUSDT"),
504 };
505
506 let data = parse_trades_event(&event, &instrument);
507
508 assert_eq!(data.len(), 2);
509 match &data[0] {
510 Data::Trade(trade) => {
511 assert_eq!(trade.instrument_id, instrument.id());
512 assert_eq!(trade.price, Price::new(123.45, 2));
513 assert_eq!(trade.size, Quantity::new(2.5, 4));
514 assert_eq!(trade.aggressor_side, AggressorSide::Buyer);
515 assert_eq!(trade.trade_id, TradeId::new("1"));
516 assert_eq!(
517 trade.ts_event,
518 UnixNanos::from(1_700_000_000_100_000_000u64)
519 );
520 assert_eq!(trade.ts_init, UnixNanos::from(1_700_000_000_100_000_000u64));
521 }
522 other => panic!("Expected trade data, was {other:?}"),
523 }
524
525 match &data[1] {
526 Data::Trade(trade) => assert_eq!(trade.aggressor_side, AggressorSide::Seller),
527 other => panic!("Expected trade data, was {other:?}"),
528 }
529 }
530
531 #[rstest]
532 fn test_parse_bbo_event() {
533 let instrument = sample_instrument();
534 let event = BestBidAskStreamEvent {
535 event_time_us: 1_700_000_000_000_000,
536 book_update_id: 123,
537 price_exponent: -2,
538 qty_exponent: -4,
539 bid_price_mantissa: 12_345,
540 bid_qty_mantissa: 25_000,
541 ask_price_mantissa: 12_350,
542 ask_qty_mantissa: 30_000,
543 symbol: Ustr::from("ETHUSDT"),
544 };
545
546 let quote = parse_bbo_event(&event, &instrument);
547
548 assert_eq!(quote.instrument_id, instrument.id());
549 assert_eq!(quote.bid_price, Price::new(123.45, 2));
550 assert_eq!(quote.ask_price, Price::new(123.50, 2));
551 assert_eq!(quote.bid_size, Quantity::new(2.5, 4));
552 assert_eq!(quote.ask_size, Quantity::new(3.0, 4));
553 assert_eq!(
554 quote.ts_event,
555 UnixNanos::from(1_700_000_000_000_000_000u64)
556 );
557 assert_eq!(quote.ts_init, UnixNanos::from(1_700_000_000_000_000_000u64));
558 }
559
560 #[rstest]
561 fn test_parse_depth_snapshot() {
562 let instrument = sample_instrument();
563 let event = DepthSnapshotStreamEvent {
564 event_time_us: 1_700_000_000_000_000,
565 book_update_id: 123,
566 price_exponent: -2,
567 qty_exponent: -4,
568 bids: vec![PriceLevel {
569 price_mantissa: 12_345,
570 qty_mantissa: 25_000,
571 }],
572 asks: vec![PriceLevel {
573 price_mantissa: 12_350,
574 qty_mantissa: 30_000,
575 }],
576 symbol: Ustr::from("ETHUSDT"),
577 };
578
579 let deltas = parse_depth_snapshot(&event, &instrument).unwrap();
580
581 assert_eq!(deltas.instrument_id, instrument.id());
582 assert_eq!(deltas.deltas.len(), 3);
583 assert_eq!(deltas.deltas[0].action, BookAction::Clear);
584 assert_eq!(deltas.deltas[1].action, BookAction::Add);
585 assert_eq!(deltas.deltas[1].order.side, OrderSide::Buy);
586 assert_eq!(deltas.deltas[1].order.price, Price::new(123.45, 2));
587 assert_eq!(deltas.deltas[1].order.size, Quantity::new(2.5, 4));
588 assert_eq!(deltas.deltas[2].action, BookAction::Add);
589 assert_eq!(deltas.deltas[2].order.side, OrderSide::Sell);
590 assert_eq!(deltas.deltas[2].order.price, Price::new(123.50, 2));
591 assert_eq!(deltas.deltas[2].order.size, Quantity::new(3.0, 4));
592 assert_eq!(deltas.deltas[2].flags, RecordFlag::F_LAST as u8);
593 assert_eq!(deltas.deltas[0].sequence, 123);
594 assert_eq!(deltas.deltas[2].sequence, 123);
595 assert_eq!(
596 deltas.ts_event,
597 UnixNanos::from(1_700_000_000_000_000_000u64)
598 );
599 }
600
601 #[rstest]
602 fn test_parse_depth_snapshot_empty_returns_none() {
603 let instrument = sample_instrument();
604 let event = DepthSnapshotStreamEvent {
605 event_time_us: 1_700_000_000_000_000,
606 book_update_id: 123,
607 price_exponent: -2,
608 qty_exponent: -4,
609 bids: vec![],
610 asks: vec![],
611 symbol: Ustr::from("ETHUSDT"),
612 };
613
614 let deltas = parse_depth_snapshot(&event, &instrument);
615
616 assert!(deltas.is_none());
617 }
618
619 #[rstest]
620 fn test_parse_depth_diff() {
621 let instrument = sample_instrument();
622 let event = DepthDiffStreamEvent {
623 event_time_us: 1_700_000_000_000_000,
624 first_book_update_id: 100,
625 last_book_update_id: 101,
626 price_exponent: -2,
627 qty_exponent: -4,
628 bids: vec![
629 PriceLevel {
630 price_mantissa: 12_345,
631 qty_mantissa: 25_000,
632 },
633 PriceLevel {
634 price_mantissa: 12_340,
635 qty_mantissa: 0,
636 },
637 ],
638 asks: vec![PriceLevel {
639 price_mantissa: 12_350,
640 qty_mantissa: 30_000,
641 }],
642 symbol: Ustr::from("ETHUSDT"),
643 };
644
645 let deltas = parse_depth_diff(&event, &instrument).unwrap();
646
647 assert_eq!(deltas.instrument_id, instrument.id());
648 assert_eq!(deltas.deltas.len(), 3);
649 assert_eq!(deltas.deltas[0].action, BookAction::Update);
650 assert_eq!(deltas.deltas[0].order.side, OrderSide::Buy);
651 assert_eq!(deltas.deltas[1].action, BookAction::Delete);
652 assert_eq!(deltas.deltas[1].order.side, OrderSide::Buy);
653 assert_eq!(deltas.deltas[2].action, BookAction::Update);
654 assert_eq!(deltas.deltas[2].order.side, OrderSide::Sell);
655 assert_eq!(deltas.deltas[2].flags, RecordFlag::F_LAST as u8);
656 assert_eq!(deltas.deltas[0].sequence, 101);
657 assert_eq!(deltas.deltas[2].sequence, 101);
658 assert_eq!(
659 deltas.ts_event,
660 UnixNanos::from(1_700_000_000_000_000_000u64)
661 );
662 }
663
664 #[rstest]
665 fn test_parse_depth_diff_empty_returns_none() {
666 let instrument = sample_instrument();
667 let event = DepthDiffStreamEvent {
668 event_time_us: 1_700_000_000_000_000,
669 first_book_update_id: 100,
670 last_book_update_id: 101,
671 price_exponent: -2,
672 qty_exponent: -4,
673 bids: vec![],
674 asks: vec![],
675 symbol: Ustr::from("ETHUSDT"),
676 };
677
678 let deltas = parse_depth_diff(&event, &instrument);
679
680 assert!(deltas.is_none());
681 }
682}