1use std::fmt::Display;
19
20use ahash::AHashSet;
21use indexmap::IndexMap;
22use nautilus_core::{
23 UnixNanos,
24 correctness::{CorrectnessResult, CorrectnessResultExt, FAILED},
25};
26use rust_decimal::Decimal;
27
28use super::{
29 BookViewError, aggregation::pre_process_order, analysis, display::pprint_book,
30 level::BookLevel, own::OwnOrderBook,
31};
32use crate::{
33 data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick},
34 enums::{BookAction, BookType, OrderSide, OrderStatus, RecordFlag},
35 identifiers::InstrumentId,
36 orderbook::{
37 BookIntegrityError, InvalidBookOperation,
38 ladder::{BookLadder, BookPrice},
39 },
40 types::{Price, Quantity, fixed::check_fixed_precision},
41};
42
43#[derive(Clone, Debug)]
51#[cfg_attr(
52 feature = "python",
53 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
54)]
55#[cfg_attr(
56 feature = "python",
57 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
58)]
59pub struct OrderBook {
60 pub instrument_id: InstrumentId,
62 pub book_type: BookType,
64 pub sequence: u64,
66 pub ts_last: UnixNanos,
68 pub update_count: u64,
70 pub(crate) bids: BookLadder,
71 pub(crate) asks: BookLadder,
72}
73
74impl PartialEq for OrderBook {
75 fn eq(&self, other: &Self) -> bool {
76 self.instrument_id == other.instrument_id && self.book_type == other.book_type
77 }
78}
79
80impl Eq for OrderBook {}
81
82impl Display for OrderBook {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(
85 f,
86 "{}(instrument_id={}, book_type={}, update_count={})",
87 stringify!(OrderBook),
88 self.instrument_id,
89 self.book_type,
90 self.update_count,
91 )
92 }
93}
94
95impl OrderBook {
96 #[must_use]
98 pub fn new(instrument_id: InstrumentId, book_type: BookType) -> Self {
99 Self {
100 instrument_id,
101 book_type,
102 sequence: 0,
103 ts_last: UnixNanos::default(),
104 update_count: 0,
105 bids: BookLadder::new(OrderSide::Buy, book_type),
106 asks: BookLadder::new(OrderSide::Sell, book_type),
107 }
108 }
109
110 #[must_use]
114 pub fn batch_state_code(&self, side: OrderSide) -> u8 {
115 match side {
116 OrderSide::Buy => self.bids.batch_state_code(),
117 OrderSide::Sell => self.asks.batch_state_code(),
118 }
119 }
120
121 pub fn set_batch_state_code(&mut self, side: OrderSide, code: u8) -> CorrectnessResult<()> {
127 match side {
128 OrderSide::Buy => self.bids.set_batch_state_code(code),
129 OrderSide::Sell => self.asks.set_batch_state_code(code),
130 }
131 }
132
133 pub fn reset(&mut self) {
135 self.bids.clear();
136 self.asks.clear();
137 self.sequence = 0;
138 self.ts_last = UnixNanos::default();
139 self.update_count = 0;
140 }
141
142 pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
148 let order = pre_process_order(self.book_type, order, flags);
149 match order.side.expect("BookOrder side must be Buy or Sell") {
150 OrderSide::Buy => self.bids.add(order, flags),
151 OrderSide::Sell => self.asks.add(order, flags),
152 }
153
154 self.increment(sequence, ts_event, flags);
155 }
156
157 pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
163 let order = pre_process_order(self.book_type, order, flags);
164 match order.side.expect("BookOrder side must be Buy or Sell") {
165 OrderSide::Buy => self.bids.update(order, flags),
166 OrderSide::Sell => self.asks.update(order, flags),
167 }
168
169 self.increment(sequence, ts_event, flags);
170 }
171
172 pub fn delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
178 let order = pre_process_order(self.book_type, order, flags);
179 match order.side.expect("BookOrder side must be Buy or Sell") {
180 OrderSide::Buy => self.bids.delete(order, sequence, ts_event),
181 OrderSide::Sell => self.asks.delete(order, sequence, ts_event),
182 }
183
184 self.increment(sequence, ts_event, flags);
185 }
186
187 pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
192 self.clear_with_flags(sequence, ts_event, 0);
193 }
194
195 pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
197 self.bids.clear();
198 self.increment(sequence, ts_event, 0);
199 }
200
201 pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
203 self.asks.clear();
204 self.increment(sequence, ts_event, 0);
205 }
206
207 fn clear_with_flags(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
208 self.bids.clear();
209 self.asks.clear();
210 self.increment(sequence, ts_event, flags);
211
212 if !RecordFlag::F_SNAPSHOT.matches(flags) {
214 self.sequence = sequence;
215 }
216 }
217
218 pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
226 if self.book_type == BookType::L1_MBP {
227 return None;
229 }
230
231 let (Some(best_bid), Some(best_ask)) = (self.best_bid_price(), self.best_ask_price())
232 else {
233 return None;
234 };
235
236 if best_bid <= best_ask {
237 return None;
238 }
239
240 let mut removed_levels = Vec::new();
241 let (clear_bids, clear_asks) = match side {
242 Some(OrderSide::Buy) => (true, false),
243 Some(OrderSide::Sell) => (false, true),
244 None => (true, true),
245 };
246
247 let mut ask_prices_to_remove = Vec::new();
249
250 if clear_asks {
251 for bp in self.asks.levels.keys() {
252 if bp.value <= best_bid {
253 ask_prices_to_remove.push(*bp);
254 } else {
255 break;
256 }
257 }
258 }
259
260 let mut bid_prices_to_remove = Vec::new();
262
263 if clear_bids {
264 for bp in self.bids.levels.keys() {
265 if bp.value >= best_ask {
266 bid_prices_to_remove.push(*bp);
267 } else {
268 break;
269 }
270 }
271 }
272
273 if ask_prices_to_remove.is_empty() && bid_prices_to_remove.is_empty() {
274 return None;
275 }
276
277 let bid_count = bid_prices_to_remove.len();
278 let ask_count = ask_prices_to_remove.len();
279
280 for price in bid_prices_to_remove {
282 if let Some(level) = self.bids.remove_level(price) {
283 removed_levels.push(level);
284 }
285 }
286
287 for price in ask_prices_to_remove {
289 if let Some(level) = self.asks.remove_level(price) {
290 removed_levels.push(level);
291 }
292 }
293
294 self.increment(self.sequence, self.ts_last, 0);
295
296 if removed_levels.is_empty() {
297 None
298 } else {
299 let total_orders: usize = removed_levels.iter().map(|level| level.orders.len()).sum();
300
301 log::warn!(
302 "Removed {} stale/crossed levels (instrument_id={}, bid_levels={}, ask_levels={}, total_orders={}), book was crossed with best_bid={} > best_ask={}",
303 removed_levels.len(),
304 self.instrument_id,
305 bid_count,
306 ask_count,
307 total_orders,
308 best_bid,
309 best_ask
310 );
311
312 Some(removed_levels)
313 }
314 }
315
316 pub fn apply_delta(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
330 if delta.instrument_id != self.instrument_id {
331 return Err(BookIntegrityError::InstrumentMismatch(
332 self.instrument_id,
333 delta.instrument_id,
334 ));
335 }
336 self.apply_delta_unchecked(delta)
337 }
338
339 pub fn apply_delta_unchecked(
357 &mut self,
358 delta: &OrderBookDelta,
359 ) -> Result<(), BookIntegrityError> {
360 self.report_out_of_order_snapshot(delta.flags, delta.sequence, delta.ts_event, 1);
362 self.apply_delta_inner(delta)
363 }
364
365 fn apply_delta_inner(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
366 let mut order = delta.order;
367
368 if order.side.is_none() && order.order_id != 0 {
369 match self.resolve_no_side_order(order) {
370 Ok(resolved) => order = resolved,
371 Err(BookIntegrityError::OrderNotFoundForSideResolution(order_id)) => {
372 match delta.action {
373 BookAction::Add => return Err(BookIntegrityError::NoOrderSide),
374 BookAction::Update | BookAction::Delete => {
375 log::debug!(
377 "Skipping {:?} for unknown order_id={order_id}",
378 delta.action
379 );
380 return Ok(());
381 }
382 BookAction::Clear => {} }
384 }
385 Err(BookIntegrityError::AmbiguousOrderSide(order_id)) => {
386 match delta.action {
387 BookAction::Add => {
388 return Err(BookIntegrityError::AmbiguousOrderSide(order_id));
389 }
390 BookAction::Update | BookAction::Delete => {
391 log::warn!(
392 "Skipping {:?} for order_id={order_id} found on both book sides",
393 delta.action
394 );
395 return Ok(());
396 }
397 BookAction::Clear => {} }
399 }
400 Err(e) => return Err(e),
401 }
402 }
403
404 if order.side.is_none() && delta.action != BookAction::Clear {
405 return Err(BookIntegrityError::NoOrderSide);
406 }
407
408 let flags = delta.flags;
409 let sequence = delta.sequence;
410 let ts_event = delta.ts_event;
411
412 match delta.action {
413 BookAction::Add => self.add(order, flags, sequence, ts_event),
414 BookAction::Update => self.update(order, flags, sequence, ts_event),
415 BookAction::Delete => self.delete(order, flags, sequence, ts_event),
416 BookAction::Clear => self.clear_with_flags(sequence, ts_event, flags),
417 }
418
419 Ok(())
420 }
421
422 pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) -> Result<(), BookIntegrityError> {
430 if deltas.instrument_id != self.instrument_id {
431 return Err(BookIntegrityError::InstrumentMismatch(
432 self.instrument_id,
433 deltas.instrument_id,
434 ));
435 }
436 self.apply_deltas_unchecked(deltas)
437 }
438
439 pub fn apply_deltas_unchecked(
453 &mut self,
454 deltas: &OrderBookDeltas,
455 ) -> Result<(), BookIntegrityError> {
456 self.report_out_of_order_snapshot(
457 deltas.flags,
458 deltas.sequence,
459 deltas.ts_event,
460 deltas.deltas.len(),
461 );
462
463 for delta in &deltas.deltas {
464 self.apply_delta_inner(delta)?;
465 }
466
467 Ok(())
468 }
469
470 fn report_out_of_order_snapshot(
473 &self,
474 flags: u8,
475 sequence: u64,
476 ts_event: UnixNanos,
477 count: usize,
478 ) {
479 if !RecordFlag::F_SNAPSHOT.matches(flags) {
480 return;
481 }
482
483 if sequence > 0 && sequence < self.sequence {
484 log::warn!(
485 "Out-of-order snapshot: sequence {} < {} (deltas={}, instrument_id={})",
486 sequence,
487 self.sequence,
488 count,
489 self.instrument_id
490 );
491 }
492
493 if ts_event < self.ts_last {
494 log::warn!(
495 "Out-of-order snapshot: ts_event {} < {} (deltas={}, instrument_id={})",
496 ts_event,
497 self.ts_last,
498 count,
499 self.instrument_id
500 );
501 }
502 }
503
504 #[must_use]
518 pub fn to_deltas(&self, ts_event: UnixNanos, ts_init: UnixNanos) -> OrderBookDeltas {
519 let mut deltas = Vec::new();
520
521 let total_orders = self.bids(None).map(BookLevel::len).sum::<usize>()
522 + self.asks(None).map(BookLevel::len).sum::<usize>();
523
524 let mut clear = OrderBookDelta::clear(self.instrument_id, self.sequence, ts_event, ts_init);
526
527 if total_orders == 0 {
528 clear.flags |= RecordFlag::F_LAST as u8;
529 }
530 deltas.push(clear);
531
532 let mut order_count = 0;
533
534 for level in self.bids(None).chain(self.asks(None)) {
535 for order in level.iter() {
536 order_count += 1;
537 let flags = if order_count == total_orders {
538 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
539 } else {
540 RecordFlag::F_SNAPSHOT as u8
541 };
542
543 deltas.push(OrderBookDelta::new(
544 self.instrument_id,
545 BookAction::Add,
546 *order,
547 flags,
548 self.sequence,
549 ts_event,
550 ts_init,
551 ));
552 }
553 }
554
555 OrderBookDeltas::new(self.instrument_id, deltas)
556 }
557
558 pub fn apply_depth(&mut self, depth: &OrderBookDepth) -> Result<(), BookIntegrityError> {
564 if depth.instrument_id != self.instrument_id {
565 return Err(BookIntegrityError::InstrumentMismatch(
566 self.instrument_id,
567 depth.instrument_id,
568 ));
569 }
570 self.apply_depth_unchecked(depth)
571 }
572
573 #[allow(
579 clippy::missing_errors_doc,
580 reason = "unchecked depth application returns no integrity errors"
581 )]
582 pub fn apply_depth_unchecked(
583 &mut self,
584 depth: &OrderBookDepth,
585 ) -> Result<(), BookIntegrityError> {
586 self.bids.clear();
587 self.asks.clear();
588
589 for &order in &depth.bids {
590 if order.side.is_none() || !order.size.is_positive() {
592 continue;
593 }
594
595 if order.side != Some(OrderSide::Buy) {
596 debug_assert_eq!(
597 order.side,
598 Some(OrderSide::Buy),
599 "Bid order must have Buy side, was {:?}",
600 order.side
601 );
602 log::warn!(
603 "Skipping bid order with wrong side {:?} (instrument_id={})",
604 order.side,
605 self.instrument_id
606 );
607 continue;
608 }
609
610 let order = pre_process_order(self.book_type, order, depth.flags);
611 self.bids.add(order, depth.flags);
612 }
613
614 for &order in &depth.asks {
615 if order.side.is_none() || !order.size.is_positive() {
617 continue;
618 }
619
620 if order.side != Some(OrderSide::Sell) {
621 debug_assert_eq!(
622 order.side,
623 Some(OrderSide::Sell),
624 "Ask order must have Sell side, was {:?}",
625 order.side
626 );
627 log::warn!(
628 "Skipping ask order with wrong side {:?} (instrument_id={})",
629 order.side,
630 self.instrument_id
631 );
632 continue;
633 }
634
635 let order = pre_process_order(self.book_type, order, depth.flags);
636 self.asks.add(order, depth.flags);
637 }
638
639 self.increment(depth.sequence, depth.ts_event, 0);
642
643 Ok(())
644 }
645
646 fn resolve_no_side_order(&self, mut order: BookOrder) -> Result<BookOrder, BookIntegrityError> {
647 let bid_price = self.bids.cache.get(&order.order_id);
648 let ask_price = self.asks.cache.get(&order.order_id);
649
650 let book_price = match (bid_price, ask_price) {
653 (Some(_), Some(_)) => {
654 return Err(BookIntegrityError::AmbiguousOrderSide(order.order_id));
655 }
656 (Some(book_price), None) | (None, Some(book_price)) => book_price,
657 (None, None) => {
658 return Err(BookIntegrityError::OrderNotFoundForSideResolution(
659 order.order_id,
660 ));
661 }
662 };
663
664 order.side = book_price.side.into();
665
666 Ok(order)
667 }
668
669 pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
671 self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
672 }
673
674 pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
676 self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
677 }
678
679 #[must_use]
681 pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
682 self.bids(depth)
683 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
684 .collect()
685 }
686
687 #[must_use]
689 pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
690 self.asks(depth)
691 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
692 .collect()
693 }
694
695 #[must_use]
697 pub fn group_bids(
698 &self,
699 group_size: Decimal,
700 depth: Option<usize>,
701 ) -> IndexMap<Decimal, Decimal> {
702 group_levels(self.bids(None), group_size, depth, true)
703 }
704
705 #[must_use]
707 pub fn group_asks(
708 &self,
709 group_size: Decimal,
710 depth: Option<usize>,
711 ) -> IndexMap<Decimal, Decimal> {
712 group_levels(self.asks(None), group_size, depth, false)
713 }
714
715 #[must_use]
726 pub fn bids_filtered_as_map(
727 &self,
728 depth: Option<usize>,
729 own_book: Option<&OwnOrderBook>,
730 status: Option<&AHashSet<OrderStatus>>,
731 accepted_buffer_ns: Option<u64>,
732 now: Option<u64>,
733 ) -> IndexMap<Decimal, Decimal> {
734 let mut public_map = self
735 .bids(depth)
736 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
737 .collect::<IndexMap<Decimal, Decimal>>();
738
739 if let Some(own_book) = own_book {
740 filter_quantities(
741 &mut public_map,
742 own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
743 );
744 }
745
746 public_map
747 }
748
749 #[must_use]
760 pub fn asks_filtered_as_map(
761 &self,
762 depth: Option<usize>,
763 own_book: Option<&OwnOrderBook>,
764 status: Option<&AHashSet<OrderStatus>>,
765 accepted_buffer_ns: Option<u64>,
766 now: Option<u64>,
767 ) -> IndexMap<Decimal, Decimal> {
768 let mut public_map = self
769 .asks(depth)
770 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
771 .collect::<IndexMap<Decimal, Decimal>>();
772
773 if let Some(own_book) = own_book {
774 filter_quantities(
775 &mut public_map,
776 own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
777 );
778 }
779
780 public_map
781 }
782
783 #[must_use]
792 pub fn filtered_view(
793 &self,
794 own_book: Option<&OwnOrderBook>,
795 depth: Option<usize>,
796 status: Option<&AHashSet<OrderStatus>>,
797 accepted_buffer_ns: Option<u64>,
798 now: Option<u64>,
799 ) -> Self {
800 self.filtered_view_checked(own_book, depth, status, accepted_buffer_ns, now)
801 .expect(FAILED)
802 }
803
804 pub fn filtered_view_checked(
816 &self,
817 own_book: Option<&OwnOrderBook>,
818 depth: Option<usize>,
819 status: Option<&AHashSet<OrderStatus>>,
820 accepted_buffer_ns: Option<u64>,
821 now: Option<u64>,
822 ) -> Result<Self, BookViewError> {
823 if let Some(own_book) = own_book
824 && self.instrument_id != own_book.instrument_id
825 {
826 return Err(BookViewError::InstrumentMismatch(
827 self.instrument_id,
828 own_book.instrument_id,
829 ));
830 }
831
832 let bids_map = self.bids_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
833 let asks_map = self.asks_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
834
835 let mut filtered_book = Self::new(self.instrument_id, self.book_type);
836 filtered_book.sequence = self.sequence;
837 filtered_book.ts_last = self.ts_last;
838
839 let sequence = self.sequence;
840 let ts_event = self.ts_last;
841
842 let mut order_id = 1_u64;
843
844 for (price, quantity) in bids_map {
845 if quantity <= Decimal::ZERO {
846 continue;
847 }
848
849 let order = BookOrder::new(
850 OrderSide::Buy,
851 Price::from_decimal(price).expect("Invalid bid price for OrderBook::filtered_view"),
852 Quantity::from_decimal(quantity)
853 .expect("Invalid bid quantity for OrderBook::filtered_view"),
854 order_id,
855 );
856 order_id += 1;
857 filtered_book.add(order, 0, sequence, ts_event);
858 }
859
860 for (price, quantity) in asks_map {
861 if quantity <= Decimal::ZERO {
862 continue;
863 }
864
865 let order = BookOrder::new(
866 OrderSide::Sell,
867 Price::from_decimal(price).expect("Invalid ask price for OrderBook::filtered_view"),
868 Quantity::from_decimal(quantity)
869 .expect("Invalid ask quantity for OrderBook::filtered_view"),
870 order_id,
871 );
872 order_id += 1;
873 filtered_book.add(order, 0, sequence, ts_event);
874 }
875
876 Ok(filtered_book)
877 }
878
879 #[must_use]
890 pub fn group_bids_filtered(
891 &self,
892 group_size: Decimal,
893 depth: Option<usize>,
894 own_book: Option<&OwnOrderBook>,
895 status: Option<&AHashSet<OrderStatus>>,
896 accepted_buffer_ns: Option<u64>,
897 now: Option<u64>,
898 ) -> IndexMap<Decimal, Decimal> {
899 let mut public_map = group_levels(self.bids(None), group_size, depth, true);
900
901 if let Some(own_book) = own_book {
902 filter_quantities(
904 &mut public_map,
905 own_book.bid_quantity(status, None, Some(group_size), accepted_buffer_ns, now),
906 );
907 }
908
909 public_map
910 }
911
912 #[must_use]
923 pub fn group_asks_filtered(
924 &self,
925 group_size: Decimal,
926 depth: Option<usize>,
927 own_book: Option<&OwnOrderBook>,
928 status: Option<&AHashSet<OrderStatus>>,
929 accepted_buffer_ns: Option<u64>,
930 now: Option<u64>,
931 ) -> IndexMap<Decimal, Decimal> {
932 let mut public_map = group_levels(self.asks(None), group_size, depth, false);
933
934 if let Some(own_book) = own_book {
935 filter_quantities(
937 &mut public_map,
938 own_book.ask_quantity(status, None, Some(group_size), accepted_buffer_ns, now),
939 );
940 }
941
942 public_map
943 }
944
945 #[must_use]
947 pub fn has_bid(&self) -> bool {
948 self.bids.top().is_some_and(|top| !top.orders.is_empty())
949 }
950
951 #[must_use]
953 pub fn has_ask(&self) -> bool {
954 self.asks.top().is_some_and(|top| !top.orders.is_empty())
955 }
956
957 #[must_use]
959 pub fn best_bid_price(&self) -> Option<Price> {
960 self.bids.top().map(|top| top.price.value)
961 }
962
963 #[must_use]
965 pub fn best_ask_price(&self) -> Option<Price> {
966 self.asks.top().map(|top| top.price.value)
967 }
968
969 #[must_use]
971 pub fn best_bid_size(&self) -> Option<Quantity> {
972 self.bids
973 .top()
974 .and_then(|top| top.first().map(|order| order.size))
975 }
976
977 #[must_use]
979 pub fn best_ask_size(&self) -> Option<Quantity> {
980 self.asks
981 .top()
982 .and_then(|top| top.first().map(|order| order.size))
983 }
984
985 #[must_use]
987 pub fn spread(&self) -> Option<f64> {
988 match (self.best_ask_price(), self.best_bid_price()) {
989 (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
990 _ => None,
991 }
992 }
993
994 #[must_use]
996 pub fn midpoint(&self) -> Option<f64> {
997 match (self.best_ask_price(), self.best_bid_price()) {
998 (Some(ask), Some(bid)) => Some(f64::midpoint(ask.as_f64(), bid.as_f64())),
999 _ => None,
1000 }
1001 }
1002
1003 #[must_use]
1005 pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
1006 let levels = match order_side {
1007 OrderSide::Buy => &self.asks.levels,
1008 OrderSide::Sell => &self.bids.levels,
1009 };
1010
1011 analysis::get_avg_px_for_quantity(qty, levels)
1012 }
1013
1014 #[must_use]
1016 pub fn get_worst_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> Option<Price> {
1017 let levels = match order_side {
1018 OrderSide::Buy => &self.asks.levels,
1019 OrderSide::Sell => &self.bids.levels,
1020 };
1021
1022 analysis::get_worst_px_for_quantity(qty, levels)
1023 }
1024
1025 #[must_use]
1029 pub fn get_avg_px_qty_for_exposure(
1030 &self,
1031 target_exposure: Quantity,
1032 order_side: OrderSide,
1033 ) -> (f64, f64, f64) {
1034 let levels = match order_side {
1035 OrderSide::Buy => &self.asks.levels,
1036 OrderSide::Sell => &self.bids.levels,
1037 };
1038
1039 analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
1040 }
1041
1042 #[must_use]
1047 pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
1048 let levels = match order_side {
1049 OrderSide::Buy => &self.asks.levels,
1050 OrderSide::Sell => &self.bids.levels,
1051 };
1052
1053 analysis::get_quantity_for_price(price, order_side, levels)
1054 }
1055
1056 #[must_use]
1070 pub fn get_quantity_at_level(
1071 &self,
1072 price: Price,
1073 order_side: OrderSide,
1074 size_precision: u8,
1075 ) -> Quantity {
1076 self.get_quantity_at_level_checked(price, order_side, size_precision)
1077 .expect_display("Failed to get order book level quantity")
1078 }
1079
1080 pub(crate) fn get_quantity_at_level_checked(
1088 &self,
1089 price: Price,
1090 order_side: OrderSide,
1091 size_precision: u8,
1092 ) -> CorrectnessResult<Quantity> {
1093 check_fixed_precision(size_precision)?;
1094
1095 let (levels, book_side) = match order_side {
1098 OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1099 OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1100 };
1101
1102 let book_price = BookPrice::new(price, book_side);
1103
1104 match levels.get(&book_price) {
1105 Some(level) => Quantity::from_raw_checked(level.size_raw_checked()?, size_precision),
1106 None => Quantity::from_raw_checked(0, size_precision),
1107 }
1108 }
1109
1110 #[must_use]
1115 pub fn get_orders_at_level(&self, price: Price, order_side: OrderSide) -> Vec<BookOrder> {
1116 let (levels, book_side) = match order_side {
1117 OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1118 OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1119 };
1120
1121 let book_price = BookPrice::new(price, book_side);
1122
1123 levels
1124 .get(&book_price)
1125 .map_or_else(Vec::new, BookLevel::get_orders)
1126 }
1127
1128 #[must_use]
1134 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
1135 match order.side.expect("BookOrder side must be Buy or Sell") {
1136 OrderSide::Buy => self.asks.simulate_fills(order),
1137 OrderSide::Sell => self.bids.simulate_fills(order),
1138 }
1139 }
1140
1141 #[must_use]
1156 pub fn get_all_crossed_levels(
1157 &self,
1158 order_side: OrderSide,
1159 price: Price,
1160 size_precision: u8,
1161 ) -> Vec<(Price, Quantity)> {
1162 self.get_all_crossed_levels_checked(order_side, price, size_precision)
1163 .expect_display("Failed to collect crossed order book levels")
1164 }
1165
1166 pub(crate) fn get_all_crossed_levels_checked(
1174 &self,
1175 order_side: OrderSide,
1176 price: Price,
1177 size_precision: u8,
1178 ) -> CorrectnessResult<Vec<(Price, Quantity)>> {
1179 let levels = match order_side {
1180 OrderSide::Buy => &self.asks.levels,
1181 OrderSide::Sell => &self.bids.levels,
1182 };
1183
1184 analysis::get_levels_for_price_checked(price, order_side, levels, size_precision)
1185 }
1186
1187 #[must_use]
1189 pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
1190 pprint_book(self, num_levels, group_size)
1191 }
1192
1193 fn increment(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
1194 let is_snapshot = RecordFlag::F_SNAPSHOT.matches(flags);
1196
1197 if !is_snapshot && sequence > 0 && sequence < self.sequence {
1198 log::warn!(
1199 "Out-of-order update: sequence {} < {} (instrument_id={})",
1200 sequence,
1201 self.sequence,
1202 self.instrument_id
1203 );
1204 }
1205
1206 if !is_snapshot && ts_event < self.ts_last {
1207 log::warn!(
1208 "Out-of-order update: ts_event {} < {} (instrument_id={})",
1209 ts_event,
1210 self.ts_last,
1211 self.instrument_id
1212 );
1213 }
1214
1215 if self.update_count == u64::MAX {
1216 debug_assert!(
1217 self.update_count < u64::MAX,
1218 "Update count at u64::MAX limit (about to overflow): {}",
1219 self.update_count
1220 );
1221 log::warn!(
1222 "Update count at u64::MAX: {} (instrument_id={})",
1223 self.update_count,
1224 self.instrument_id
1225 );
1226 }
1227
1228 self.sequence = sequence.max(self.sequence);
1230 self.ts_last = ts_event.max(self.ts_last);
1231 self.update_count = self.update_count.saturating_add(1);
1232 }
1233
1234 pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
1240 if self.book_type != BookType::L1_MBP {
1241 return Err(InvalidBookOperation::Update(self.book_type));
1242 }
1243
1244 if quote.ts_event < self.ts_last {
1245 log::warn!(
1246 "Skipping stale quote: ts_event {} < ts_last {} (instrument_id={})",
1247 quote.ts_event,
1248 self.ts_last,
1249 self.instrument_id
1250 );
1251 return Ok(());
1252 }
1253
1254 if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
1256 log::warn!(
1257 "Quote has crossed prices: bid={}, ask={} for {}",
1258 quote.bid_price,
1259 quote.ask_price,
1260 self.instrument_id
1261 );
1262 }
1263
1264 let bid = BookOrder::new(
1265 OrderSide::Buy,
1266 quote.bid_price,
1267 quote.bid_size,
1268 OrderSide::Buy as u64,
1269 );
1270
1271 let ask = BookOrder::new(
1272 OrderSide::Sell,
1273 quote.ask_price,
1274 quote.ask_size,
1275 OrderSide::Sell as u64,
1276 );
1277
1278 self.update_book_bid(bid);
1279 self.update_book_ask(ask);
1280
1281 self.increment(self.sequence.saturating_add(1), quote.ts_event, 0);
1282
1283 Ok(())
1284 }
1285
1286 pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
1292 if self.book_type != BookType::L1_MBP {
1293 return Err(InvalidBookOperation::Update(self.book_type));
1294 }
1295
1296 if trade.ts_event < self.ts_last {
1297 log::warn!(
1298 "Skipping stale trade: ts_event {} < ts_last {} (instrument_id={})",
1299 trade.ts_event,
1300 self.ts_last,
1301 self.instrument_id
1302 );
1303 return Ok(());
1304 }
1305
1306 debug_assert!(
1308 !trade.price.is_undefined() && !trade.price.is_error(),
1309 "Trade has invalid/uninitialized price: {}",
1310 trade.price
1311 );
1312
1313 debug_assert!(
1315 trade.size.is_positive(),
1316 "Trade has non-positive size: {}",
1317 trade.size
1318 );
1319
1320 let bid = BookOrder::new(
1321 OrderSide::Buy,
1322 trade.price,
1323 trade.size,
1324 OrderSide::Buy as u64,
1325 );
1326
1327 let ask = BookOrder::new(
1328 OrderSide::Sell,
1329 trade.price,
1330 trade.size,
1331 OrderSide::Sell as u64,
1332 );
1333
1334 self.update_book_bid(bid);
1335 self.update_book_ask(ask);
1336
1337 self.increment(self.sequence.saturating_add(1), trade.ts_event, 0);
1338
1339 Ok(())
1340 }
1341
1342 fn update_book_bid(&mut self, order: BookOrder) {
1343 self.bids.replace_l1(order);
1344 }
1345
1346 fn update_book_ask(&mut self, order: BookOrder) {
1347 self.asks.replace_l1(order);
1348 }
1349
1350 #[must_use]
1357 pub fn deltas_to_quotes(book_type: BookType, deltas: &[OrderBookDelta]) -> Vec<QuoteTick> {
1358 assert!(!deltas.is_empty(), "`deltas` must not be empty");
1359
1360 let instrument_id = deltas[0].instrument_id;
1361 let mut book = Self::new(instrument_id, book_type);
1362 let mut quotes = Vec::new();
1363 let mut last_bbo: Option<(Price, Price)> = None;
1364
1365 for delta in deltas {
1366 book.apply_delta(delta).unwrap();
1367 let Some((bid_px, ask_px)) = book.best_bid_price().zip(book.best_ask_price()) else {
1368 last_bbo = None;
1369 continue;
1370 };
1371
1372 let bbo = (bid_px, ask_px);
1373
1374 if last_bbo == Some(bbo) {
1375 continue;
1376 }
1377
1378 last_bbo = Some(bbo);
1379 let bid_level = book.bids.top().unwrap();
1380 let ask_level = book.asks.top().unwrap();
1381 let precision = bid_level.first().unwrap().size.precision;
1382 let bid_sz = Quantity::from_raw(bid_level.size_raw(), precision);
1383 let ask_sz = Quantity::from_raw(ask_level.size_raw(), precision);
1384 let quote = QuoteTick::new(
1385 instrument_id,
1386 bid_px,
1387 ask_px,
1388 bid_sz,
1389 ask_sz,
1390 delta.ts_event,
1391 delta.ts_init,
1392 );
1393
1394 quotes.push(quote);
1395 }
1396
1397 quotes
1398 }
1399}
1400
1401fn filter_quantities(
1402 public_map: &mut IndexMap<Decimal, Decimal>,
1403 own_map: IndexMap<Decimal, Decimal>,
1404) {
1405 for (price, own_size) in own_map {
1406 if let Some(public_size) = public_map.get_mut(&price) {
1407 *public_size = (*public_size - own_size).max(Decimal::ZERO);
1408
1409 if *public_size == Decimal::ZERO {
1410 public_map.shift_remove(&price);
1411 }
1412 }
1413 }
1414}
1415
1416fn group_levels<'a>(
1417 levels_iter: impl Iterator<Item = &'a BookLevel>,
1418 group_size: Decimal,
1419 depth: Option<usize>,
1420 is_bid: bool,
1421) -> IndexMap<Decimal, Decimal> {
1422 if group_size <= Decimal::ZERO {
1423 log::warn!("Invalid group_size: {group_size}, must be positive; returning empty map");
1424 return IndexMap::new();
1425 }
1426
1427 let mut levels = IndexMap::new();
1428 let depth = depth.unwrap_or(usize::MAX);
1429
1430 for level in levels_iter {
1431 let price = level.price.value.as_decimal();
1432 let grouped_price = if is_bid {
1433 (price / group_size).floor() * group_size
1434 } else {
1435 (price / group_size).ceil() * group_size
1436 };
1437 let size = level.size_decimal();
1438
1439 levels
1440 .entry(grouped_price)
1441 .and_modify(|total| *total += size)
1442 .or_insert(size);
1443
1444 if levels.len() > depth {
1445 levels.pop();
1446 break;
1447 }
1448 }
1449
1450 levels
1451}