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 pub fn reset(&mut self) {
112 self.bids.clear();
113 self.asks.clear();
114 self.sequence = 0;
115 self.ts_last = UnixNanos::default();
116 self.update_count = 0;
117 }
118
119 pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
125 let order = pre_process_order(self.book_type, order, flags);
126 match order.side.expect("BookOrder side must be Buy or Sell") {
127 OrderSide::Buy => self.bids.add(order, flags),
128 OrderSide::Sell => self.asks.add(order, flags),
129 }
130
131 self.increment(sequence, ts_event, flags);
132 }
133
134 pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
140 let order = pre_process_order(self.book_type, order, flags);
141 match order.side.expect("BookOrder side must be Buy or Sell") {
142 OrderSide::Buy => self.bids.update(order, flags),
143 OrderSide::Sell => self.asks.update(order, flags),
144 }
145
146 self.increment(sequence, ts_event, flags);
147 }
148
149 pub fn delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
155 let order = pre_process_order(self.book_type, order, flags);
156 match order.side.expect("BookOrder side must be Buy or Sell") {
157 OrderSide::Buy => self.bids.delete(order, sequence, ts_event),
158 OrderSide::Sell => self.asks.delete(order, sequence, ts_event),
159 }
160
161 self.increment(sequence, ts_event, flags);
162 }
163
164 pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
169 self.clear_with_flags(sequence, ts_event, 0);
170 }
171
172 pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
174 self.bids.clear();
175 self.increment(sequence, ts_event, 0);
176 }
177
178 pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
180 self.asks.clear();
181 self.increment(sequence, ts_event, 0);
182 }
183
184 fn clear_with_flags(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
185 self.bids.clear();
186 self.asks.clear();
187 self.increment(sequence, ts_event, flags);
188
189 if !RecordFlag::F_SNAPSHOT.matches(flags) {
191 self.sequence = sequence;
192 }
193 }
194
195 pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
203 if self.book_type == BookType::L1_MBP {
204 return None;
206 }
207
208 let (Some(best_bid), Some(best_ask)) = (self.best_bid_price(), self.best_ask_price())
209 else {
210 return None;
211 };
212
213 if best_bid <= best_ask {
214 return None;
215 }
216
217 let mut removed_levels = Vec::new();
218 let (clear_bids, clear_asks) = match side {
219 Some(OrderSide::Buy) => (true, false),
220 Some(OrderSide::Sell) => (false, true),
221 None => (true, true),
222 };
223
224 let mut ask_prices_to_remove = Vec::new();
226
227 if clear_asks {
228 for bp in self.asks.levels.keys() {
229 if bp.value <= best_bid {
230 ask_prices_to_remove.push(*bp);
231 } else {
232 break;
233 }
234 }
235 }
236
237 let mut bid_prices_to_remove = Vec::new();
239
240 if clear_bids {
241 for bp in self.bids.levels.keys() {
242 if bp.value >= best_ask {
243 bid_prices_to_remove.push(*bp);
244 } else {
245 break;
246 }
247 }
248 }
249
250 if ask_prices_to_remove.is_empty() && bid_prices_to_remove.is_empty() {
251 return None;
252 }
253
254 let bid_count = bid_prices_to_remove.len();
255 let ask_count = ask_prices_to_remove.len();
256
257 for price in bid_prices_to_remove {
259 if let Some(level) = self.bids.remove_level(price) {
260 removed_levels.push(level);
261 }
262 }
263
264 for price in ask_prices_to_remove {
266 if let Some(level) = self.asks.remove_level(price) {
267 removed_levels.push(level);
268 }
269 }
270
271 self.increment(self.sequence, self.ts_last, 0);
272
273 if removed_levels.is_empty() {
274 None
275 } else {
276 let total_orders: usize = removed_levels.iter().map(|level| level.orders.len()).sum();
277
278 log::warn!(
279 "Removed {} stale/crossed levels (instrument_id={}, bid_levels={}, ask_levels={}, total_orders={}), book was crossed with best_bid={} > best_ask={}",
280 removed_levels.len(),
281 self.instrument_id,
282 bid_count,
283 ask_count,
284 total_orders,
285 best_bid,
286 best_ask
287 );
288
289 Some(removed_levels)
290 }
291 }
292
293 pub fn apply_delta(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
307 if delta.instrument_id != self.instrument_id {
308 return Err(BookIntegrityError::InstrumentMismatch(
309 self.instrument_id,
310 delta.instrument_id,
311 ));
312 }
313 self.apply_delta_unchecked(delta)
314 }
315
316 pub fn apply_delta_unchecked(
334 &mut self,
335 delta: &OrderBookDelta,
336 ) -> Result<(), BookIntegrityError> {
337 self.report_out_of_order_snapshot(delta.flags, delta.sequence, delta.ts_event, 1);
339 self.apply_delta_inner(delta)
340 }
341
342 fn apply_delta_inner(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
343 let mut order = delta.order;
344
345 if order.side.is_none() && order.order_id != 0 {
346 match self.resolve_no_side_order(order) {
347 Ok(resolved) => order = resolved,
348 Err(BookIntegrityError::OrderNotFoundForSideResolution(order_id)) => {
349 match delta.action {
350 BookAction::Add => return Err(BookIntegrityError::NoOrderSide),
351 BookAction::Update | BookAction::Delete => {
352 log::debug!(
354 "Skipping {:?} for unknown order_id={order_id}",
355 delta.action
356 );
357 return Ok(());
358 }
359 BookAction::Clear => {} }
361 }
362 Err(BookIntegrityError::AmbiguousOrderSide(order_id)) => {
363 match delta.action {
364 BookAction::Add => {
365 return Err(BookIntegrityError::AmbiguousOrderSide(order_id));
366 }
367 BookAction::Update | BookAction::Delete => {
368 log::warn!(
369 "Skipping {:?} for order_id={order_id} found on both book sides",
370 delta.action
371 );
372 return Ok(());
373 }
374 BookAction::Clear => {} }
376 }
377 Err(e) => return Err(e),
378 }
379 }
380
381 if order.side.is_none() && delta.action != BookAction::Clear {
382 return Err(BookIntegrityError::NoOrderSide);
383 }
384
385 let flags = delta.flags;
386 let sequence = delta.sequence;
387 let ts_event = delta.ts_event;
388
389 match delta.action {
390 BookAction::Add => self.add(order, flags, sequence, ts_event),
391 BookAction::Update => self.update(order, flags, sequence, ts_event),
392 BookAction::Delete => self.delete(order, flags, sequence, ts_event),
393 BookAction::Clear => self.clear_with_flags(sequence, ts_event, flags),
394 }
395
396 Ok(())
397 }
398
399 pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) -> Result<(), BookIntegrityError> {
407 if deltas.instrument_id != self.instrument_id {
408 return Err(BookIntegrityError::InstrumentMismatch(
409 self.instrument_id,
410 deltas.instrument_id,
411 ));
412 }
413 self.apply_deltas_unchecked(deltas)
414 }
415
416 pub fn apply_deltas_unchecked(
430 &mut self,
431 deltas: &OrderBookDeltas,
432 ) -> Result<(), BookIntegrityError> {
433 self.report_out_of_order_snapshot(
434 deltas.flags,
435 deltas.sequence,
436 deltas.ts_event,
437 deltas.deltas.len(),
438 );
439
440 for delta in &deltas.deltas {
441 self.apply_delta_inner(delta)?;
442 }
443
444 Ok(())
445 }
446
447 fn report_out_of_order_snapshot(
450 &self,
451 flags: u8,
452 sequence: u64,
453 ts_event: UnixNanos,
454 count: usize,
455 ) {
456 if !RecordFlag::F_SNAPSHOT.matches(flags) {
457 return;
458 }
459
460 if sequence > 0 && sequence < self.sequence {
461 log::warn!(
462 "Out-of-order snapshot: sequence {} < {} (deltas={}, instrument_id={})",
463 sequence,
464 self.sequence,
465 count,
466 self.instrument_id
467 );
468 }
469
470 if ts_event < self.ts_last {
471 log::warn!(
472 "Out-of-order snapshot: ts_event {} < {} (deltas={}, instrument_id={})",
473 ts_event,
474 self.ts_last,
475 count,
476 self.instrument_id
477 );
478 }
479 }
480
481 #[must_use]
495 pub fn to_deltas(&self, ts_event: UnixNanos, ts_init: UnixNanos) -> OrderBookDeltas {
496 let mut deltas = Vec::new();
497
498 let total_orders = self.bids(None).map(BookLevel::len).sum::<usize>()
499 + self.asks(None).map(BookLevel::len).sum::<usize>();
500
501 let mut clear = OrderBookDelta::clear(self.instrument_id, self.sequence, ts_event, ts_init);
503
504 if total_orders == 0 {
505 clear.flags |= RecordFlag::F_LAST as u8;
506 }
507 deltas.push(clear);
508
509 let mut order_count = 0;
510
511 for level in self.bids(None).chain(self.asks(None)) {
512 for order in level.iter() {
513 order_count += 1;
514 let flags = if order_count == total_orders {
515 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
516 } else {
517 RecordFlag::F_SNAPSHOT as u8
518 };
519
520 deltas.push(OrderBookDelta::new(
521 self.instrument_id,
522 BookAction::Add,
523 *order,
524 flags,
525 self.sequence,
526 ts_event,
527 ts_init,
528 ));
529 }
530 }
531
532 OrderBookDeltas::new(self.instrument_id, deltas)
533 }
534
535 pub fn apply_depth(&mut self, depth: &OrderBookDepth) -> Result<(), BookIntegrityError> {
541 if depth.instrument_id != self.instrument_id {
542 return Err(BookIntegrityError::InstrumentMismatch(
543 self.instrument_id,
544 depth.instrument_id,
545 ));
546 }
547 self.apply_depth_unchecked(depth)
548 }
549
550 pub fn apply_depth_unchecked(
558 &mut self,
559 depth: &OrderBookDepth,
560 ) -> Result<(), BookIntegrityError> {
561 self.bids.clear();
562 self.asks.clear();
563
564 for &order in &depth.bids {
565 if order.side.is_none() || !order.size.is_positive() {
567 continue;
568 }
569
570 if order.side != Some(OrderSide::Buy) {
571 debug_assert_eq!(
572 order.side,
573 Some(OrderSide::Buy),
574 "Bid order must have Buy side, was {:?}",
575 order.side
576 );
577 log::warn!(
578 "Skipping bid order with wrong side {:?} (instrument_id={})",
579 order.side,
580 self.instrument_id
581 );
582 continue;
583 }
584
585 let order = pre_process_order(self.book_type, order, depth.flags);
586 self.bids.add(order, depth.flags);
587 }
588
589 for &order in &depth.asks {
590 if order.side.is_none() || !order.size.is_positive() {
592 continue;
593 }
594
595 if order.side != Some(OrderSide::Sell) {
596 debug_assert_eq!(
597 order.side,
598 Some(OrderSide::Sell),
599 "Ask order must have Sell side, was {:?}",
600 order.side
601 );
602 log::warn!(
603 "Skipping ask 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.asks.add(order, depth.flags);
612 }
613
614 self.increment(depth.sequence, depth.ts_event, 0);
617
618 Ok(())
619 }
620
621 fn resolve_no_side_order(&self, mut order: BookOrder) -> Result<BookOrder, BookIntegrityError> {
622 let bid_price = self.bids.cache.get(&order.order_id);
623 let ask_price = self.asks.cache.get(&order.order_id);
624
625 let book_price = match (bid_price, ask_price) {
628 (Some(_), Some(_)) => {
629 return Err(BookIntegrityError::AmbiguousOrderSide(order.order_id));
630 }
631 (Some(book_price), None) | (None, Some(book_price)) => book_price,
632 (None, None) => {
633 return Err(BookIntegrityError::OrderNotFoundForSideResolution(
634 order.order_id,
635 ));
636 }
637 };
638
639 order.side = book_price.side.into();
640
641 Ok(order)
642 }
643
644 pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
646 self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
647 }
648
649 pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
651 self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
652 }
653
654 #[must_use]
656 pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
657 self.bids(depth)
658 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
659 .collect()
660 }
661
662 #[must_use]
664 pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
665 self.asks(depth)
666 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
667 .collect()
668 }
669
670 #[must_use]
672 pub fn group_bids(
673 &self,
674 group_size: Decimal,
675 depth: Option<usize>,
676 ) -> IndexMap<Decimal, Decimal> {
677 group_levels(self.bids(None), group_size, depth, true)
678 }
679
680 #[must_use]
682 pub fn group_asks(
683 &self,
684 group_size: Decimal,
685 depth: Option<usize>,
686 ) -> IndexMap<Decimal, Decimal> {
687 group_levels(self.asks(None), group_size, depth, false)
688 }
689
690 #[must_use]
701 pub fn bids_filtered_as_map(
702 &self,
703 depth: Option<usize>,
704 own_book: Option<&OwnOrderBook>,
705 status: Option<&AHashSet<OrderStatus>>,
706 accepted_buffer_ns: Option<u64>,
707 now: Option<u64>,
708 ) -> IndexMap<Decimal, Decimal> {
709 let mut public_map = self
710 .bids(depth)
711 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
712 .collect::<IndexMap<Decimal, Decimal>>();
713
714 if let Some(own_book) = own_book {
715 filter_quantities(
716 &mut public_map,
717 own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
718 );
719 }
720
721 public_map
722 }
723
724 #[must_use]
735 pub fn asks_filtered_as_map(
736 &self,
737 depth: Option<usize>,
738 own_book: Option<&OwnOrderBook>,
739 status: Option<&AHashSet<OrderStatus>>,
740 accepted_buffer_ns: Option<u64>,
741 now: Option<u64>,
742 ) -> IndexMap<Decimal, Decimal> {
743 let mut public_map = self
744 .asks(depth)
745 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
746 .collect::<IndexMap<Decimal, Decimal>>();
747
748 if let Some(own_book) = own_book {
749 filter_quantities(
750 &mut public_map,
751 own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
752 );
753 }
754
755 public_map
756 }
757
758 #[must_use]
767 pub fn filtered_view(
768 &self,
769 own_book: Option<&OwnOrderBook>,
770 depth: Option<usize>,
771 status: Option<&AHashSet<OrderStatus>>,
772 accepted_buffer_ns: Option<u64>,
773 now: Option<u64>,
774 ) -> Self {
775 self.filtered_view_checked(own_book, depth, status, accepted_buffer_ns, now)
776 .expect(FAILED)
777 }
778
779 pub fn filtered_view_checked(
792 &self,
793 own_book: Option<&OwnOrderBook>,
794 depth: Option<usize>,
795 status: Option<&AHashSet<OrderStatus>>,
796 accepted_buffer_ns: Option<u64>,
797 now: Option<u64>,
798 ) -> Result<Self, BookViewError> {
799 if let Some(own_book) = own_book
800 && self.instrument_id != own_book.instrument_id
801 {
802 return Err(BookViewError::InstrumentMismatch(
803 self.instrument_id,
804 own_book.instrument_id,
805 ));
806 }
807
808 let bids_map = self.bids_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
809 let asks_map = self.asks_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
810
811 let mut filtered_book = Self::new(self.instrument_id, self.book_type);
812 filtered_book.sequence = self.sequence;
813 filtered_book.ts_last = self.ts_last;
814
815 let sequence = self.sequence;
816 let ts_event = self.ts_last;
817
818 let mut order_id = 1_u64;
819
820 for (price, quantity) in bids_map {
821 if quantity <= Decimal::ZERO {
822 continue;
823 }
824
825 let order = BookOrder::new(
826 OrderSide::Buy,
827 Price::from_decimal(price).expect("Invalid bid price for OrderBook::filtered_view"),
828 Quantity::from_decimal(quantity)
829 .expect("Invalid bid quantity for OrderBook::filtered_view"),
830 order_id,
831 );
832 order_id += 1;
833 filtered_book.add(order, 0, sequence, ts_event);
834 }
835
836 for (price, quantity) in asks_map {
837 if quantity <= Decimal::ZERO {
838 continue;
839 }
840
841 let order = BookOrder::new(
842 OrderSide::Sell,
843 Price::from_decimal(price).expect("Invalid ask price for OrderBook::filtered_view"),
844 Quantity::from_decimal(quantity)
845 .expect("Invalid ask quantity for OrderBook::filtered_view"),
846 order_id,
847 );
848 order_id += 1;
849 filtered_book.add(order, 0, sequence, ts_event);
850 }
851
852 Ok(filtered_book)
853 }
854
855 #[must_use]
866 pub fn group_bids_filtered(
867 &self,
868 group_size: Decimal,
869 depth: Option<usize>,
870 own_book: Option<&OwnOrderBook>,
871 status: Option<&AHashSet<OrderStatus>>,
872 accepted_buffer_ns: Option<u64>,
873 now: Option<u64>,
874 ) -> IndexMap<Decimal, Decimal> {
875 let mut public_map = group_levels(self.bids(None), group_size, depth, true);
876
877 if let Some(own_book) = own_book {
878 filter_quantities(
879 &mut public_map,
880 own_book.bid_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
881 );
882 }
883
884 public_map
885 }
886
887 #[must_use]
898 pub fn group_asks_filtered(
899 &self,
900 group_size: Decimal,
901 depth: Option<usize>,
902 own_book: Option<&OwnOrderBook>,
903 status: Option<&AHashSet<OrderStatus>>,
904 accepted_buffer_ns: Option<u64>,
905 now: Option<u64>,
906 ) -> IndexMap<Decimal, Decimal> {
907 let mut public_map = group_levels(self.asks(None), group_size, depth, false);
908
909 if let Some(own_book) = own_book {
910 filter_quantities(
911 &mut public_map,
912 own_book.ask_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
913 );
914 }
915
916 public_map
917 }
918
919 #[must_use]
921 pub fn has_bid(&self) -> bool {
922 self.bids.top().is_some_and(|top| !top.orders.is_empty())
923 }
924
925 #[must_use]
927 pub fn has_ask(&self) -> bool {
928 self.asks.top().is_some_and(|top| !top.orders.is_empty())
929 }
930
931 #[must_use]
933 pub fn best_bid_price(&self) -> Option<Price> {
934 self.bids.top().map(|top| top.price.value)
935 }
936
937 #[must_use]
939 pub fn best_ask_price(&self) -> Option<Price> {
940 self.asks.top().map(|top| top.price.value)
941 }
942
943 #[must_use]
945 pub fn best_bid_size(&self) -> Option<Quantity> {
946 self.bids
947 .top()
948 .and_then(|top| top.first().map(|order| order.size))
949 }
950
951 #[must_use]
953 pub fn best_ask_size(&self) -> Option<Quantity> {
954 self.asks
955 .top()
956 .and_then(|top| top.first().map(|order| order.size))
957 }
958
959 #[must_use]
961 pub fn spread(&self) -> Option<f64> {
962 match (self.best_ask_price(), self.best_bid_price()) {
963 (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
964 _ => None,
965 }
966 }
967
968 #[must_use]
970 pub fn midpoint(&self) -> Option<f64> {
971 match (self.best_ask_price(), self.best_bid_price()) {
972 (Some(ask), Some(bid)) => Some(f64::midpoint(ask.as_f64(), bid.as_f64())),
973 _ => None,
974 }
975 }
976
977 #[must_use]
979 pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
980 let levels = match order_side {
981 OrderSide::Buy => &self.asks.levels,
982 OrderSide::Sell => &self.bids.levels,
983 };
984
985 analysis::get_avg_px_for_quantity(qty, levels)
986 }
987
988 #[must_use]
990 pub fn get_worst_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> Option<Price> {
991 let levels = match order_side {
992 OrderSide::Buy => &self.asks.levels,
993 OrderSide::Sell => &self.bids.levels,
994 };
995
996 analysis::get_worst_px_for_quantity(qty, levels)
997 }
998
999 #[must_use]
1001 pub fn get_avg_px_qty_for_exposure(
1002 &self,
1003 target_exposure: Quantity,
1004 order_side: OrderSide,
1005 ) -> (f64, f64, 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_qty_for_exposure(target_exposure, levels)
1012 }
1013
1014 #[must_use]
1019 pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
1020 let levels = match order_side {
1021 OrderSide::Buy => &self.asks.levels,
1022 OrderSide::Sell => &self.bids.levels,
1023 };
1024
1025 analysis::get_quantity_for_price(price, order_side, levels)
1026 }
1027
1028 #[must_use]
1042 pub fn get_quantity_at_level(
1043 &self,
1044 price: Price,
1045 order_side: OrderSide,
1046 size_precision: u8,
1047 ) -> Quantity {
1048 self.get_quantity_at_level_checked(price, order_side, size_precision)
1049 .expect_display("Failed to get order book level quantity")
1050 }
1051
1052 pub(crate) fn get_quantity_at_level_checked(
1060 &self,
1061 price: Price,
1062 order_side: OrderSide,
1063 size_precision: u8,
1064 ) -> CorrectnessResult<Quantity> {
1065 check_fixed_precision(size_precision)?;
1066
1067 let (levels, book_side) = match order_side {
1070 OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1071 OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1072 };
1073
1074 let book_price = BookPrice::new(price, book_side);
1075
1076 match levels.get(&book_price) {
1077 Some(level) => Quantity::from_raw_checked(level.size_raw_checked()?, size_precision),
1078 None => Quantity::from_raw_checked(0, size_precision),
1079 }
1080 }
1081
1082 #[must_use]
1087 pub fn get_orders_at_level(&self, price: Price, order_side: OrderSide) -> Vec<BookOrder> {
1088 let (levels, book_side) = match order_side {
1089 OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1090 OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1091 };
1092
1093 let book_price = BookPrice::new(price, book_side);
1094
1095 levels
1096 .get(&book_price)
1097 .map_or_else(Vec::new, BookLevel::get_orders)
1098 }
1099
1100 #[must_use]
1106 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
1107 match order.side.expect("BookOrder side must be Buy or Sell") {
1108 OrderSide::Buy => self.asks.simulate_fills(order),
1109 OrderSide::Sell => self.bids.simulate_fills(order),
1110 }
1111 }
1112
1113 #[must_use]
1128 pub fn get_all_crossed_levels(
1129 &self,
1130 order_side: OrderSide,
1131 price: Price,
1132 size_precision: u8,
1133 ) -> Vec<(Price, Quantity)> {
1134 self.get_all_crossed_levels_checked(order_side, price, size_precision)
1135 .expect_display("Failed to collect crossed order book levels")
1136 }
1137
1138 pub(crate) fn get_all_crossed_levels_checked(
1146 &self,
1147 order_side: OrderSide,
1148 price: Price,
1149 size_precision: u8,
1150 ) -> CorrectnessResult<Vec<(Price, Quantity)>> {
1151 let levels = match order_side {
1152 OrderSide::Buy => &self.asks.levels,
1153 OrderSide::Sell => &self.bids.levels,
1154 };
1155
1156 analysis::get_levels_for_price_checked(price, order_side, levels, size_precision)
1157 }
1158
1159 #[must_use]
1161 pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
1162 pprint_book(self, num_levels, group_size)
1163 }
1164
1165 fn increment(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
1166 let is_snapshot = RecordFlag::F_SNAPSHOT.matches(flags);
1168
1169 if !is_snapshot && sequence > 0 && sequence < self.sequence {
1170 log::warn!(
1171 "Out-of-order update: sequence {} < {} (instrument_id={})",
1172 sequence,
1173 self.sequence,
1174 self.instrument_id
1175 );
1176 }
1177
1178 if !is_snapshot && ts_event < self.ts_last {
1179 log::warn!(
1180 "Out-of-order update: ts_event {} < {} (instrument_id={})",
1181 ts_event,
1182 self.ts_last,
1183 self.instrument_id
1184 );
1185 }
1186
1187 if self.update_count == u64::MAX {
1188 debug_assert!(
1189 self.update_count < u64::MAX,
1190 "Update count at u64::MAX limit (about to overflow): {}",
1191 self.update_count
1192 );
1193 log::warn!(
1194 "Update count at u64::MAX: {} (instrument_id={})",
1195 self.update_count,
1196 self.instrument_id
1197 );
1198 }
1199
1200 self.sequence = sequence.max(self.sequence);
1202 self.ts_last = ts_event.max(self.ts_last);
1203 self.update_count = self.update_count.saturating_add(1);
1204 }
1205
1206 pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
1212 if self.book_type != BookType::L1_MBP {
1213 return Err(InvalidBookOperation::Update(self.book_type));
1214 }
1215
1216 if quote.ts_event < self.ts_last {
1217 log::warn!(
1218 "Skipping stale quote: ts_event {} < ts_last {} (instrument_id={})",
1219 quote.ts_event,
1220 self.ts_last,
1221 self.instrument_id
1222 );
1223 return Ok(());
1224 }
1225
1226 if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
1228 log::warn!(
1229 "Quote has crossed prices: bid={}, ask={} for {}",
1230 quote.bid_price,
1231 quote.ask_price,
1232 self.instrument_id
1233 );
1234 }
1235
1236 let bid = BookOrder::new(
1237 OrderSide::Buy,
1238 quote.bid_price,
1239 quote.bid_size,
1240 OrderSide::Buy as u64,
1241 );
1242
1243 let ask = BookOrder::new(
1244 OrderSide::Sell,
1245 quote.ask_price,
1246 quote.ask_size,
1247 OrderSide::Sell as u64,
1248 );
1249
1250 self.update_book_bid(bid);
1251 self.update_book_ask(ask);
1252
1253 self.increment(self.sequence.saturating_add(1), quote.ts_event, 0);
1254
1255 Ok(())
1256 }
1257
1258 pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
1264 if self.book_type != BookType::L1_MBP {
1265 return Err(InvalidBookOperation::Update(self.book_type));
1266 }
1267
1268 if trade.ts_event < self.ts_last {
1269 log::warn!(
1270 "Skipping stale trade: ts_event {} < ts_last {} (instrument_id={})",
1271 trade.ts_event,
1272 self.ts_last,
1273 self.instrument_id
1274 );
1275 return Ok(());
1276 }
1277
1278 debug_assert!(
1280 !trade.price.is_undefined() && !trade.price.is_error(),
1281 "Trade has invalid/uninitialized price: {}",
1282 trade.price
1283 );
1284
1285 debug_assert!(
1287 trade.size.is_positive(),
1288 "Trade has non-positive size: {}",
1289 trade.size
1290 );
1291
1292 let bid = BookOrder::new(
1293 OrderSide::Buy,
1294 trade.price,
1295 trade.size,
1296 OrderSide::Buy as u64,
1297 );
1298
1299 let ask = BookOrder::new(
1300 OrderSide::Sell,
1301 trade.price,
1302 trade.size,
1303 OrderSide::Sell as u64,
1304 );
1305
1306 self.update_book_bid(bid);
1307 self.update_book_ask(ask);
1308
1309 self.increment(self.sequence.saturating_add(1), trade.ts_event, 0);
1310
1311 Ok(())
1312 }
1313
1314 fn update_book_bid(&mut self, order: BookOrder) {
1315 self.bids.replace_l1(order);
1316 }
1317
1318 fn update_book_ask(&mut self, order: BookOrder) {
1319 self.asks.replace_l1(order);
1320 }
1321
1322 #[must_use]
1329 pub fn deltas_to_quotes(book_type: BookType, deltas: &[OrderBookDelta]) -> Vec<QuoteTick> {
1330 assert!(!deltas.is_empty(), "`deltas` must not be empty");
1331
1332 let instrument_id = deltas[0].instrument_id;
1333 let mut book = Self::new(instrument_id, book_type);
1334 let mut quotes = Vec::new();
1335 let mut last_bbo: Option<(Price, Price)> = None;
1336
1337 for delta in deltas {
1338 book.apply_delta(delta).unwrap();
1339 let Some((bid_px, ask_px)) = book.best_bid_price().zip(book.best_ask_price()) else {
1340 last_bbo = None;
1341 continue;
1342 };
1343
1344 let bbo = (bid_px, ask_px);
1345
1346 if last_bbo == Some(bbo) {
1347 continue;
1348 }
1349
1350 last_bbo = Some(bbo);
1351 let bid_level = book.bids.top().unwrap();
1352 let ask_level = book.asks.top().unwrap();
1353 let precision = bid_level.first().unwrap().size.precision;
1354 let bid_sz = Quantity::from_raw(bid_level.size_raw(), precision);
1355 let ask_sz = Quantity::from_raw(ask_level.size_raw(), precision);
1356 let quote = QuoteTick::new(
1357 instrument_id,
1358 bid_px,
1359 ask_px,
1360 bid_sz,
1361 ask_sz,
1362 delta.ts_event,
1363 delta.ts_init,
1364 );
1365
1366 quotes.push(quote);
1367 }
1368
1369 quotes
1370 }
1371}
1372
1373fn filter_quantities(
1374 public_map: &mut IndexMap<Decimal, Decimal>,
1375 own_map: IndexMap<Decimal, Decimal>,
1376) {
1377 for (price, own_size) in own_map {
1378 if let Some(public_size) = public_map.get_mut(&price) {
1379 *public_size = (*public_size - own_size).max(Decimal::ZERO);
1380
1381 if *public_size == Decimal::ZERO {
1382 public_map.shift_remove(&price);
1383 }
1384 }
1385 }
1386}
1387
1388fn group_levels<'a>(
1389 levels_iter: impl Iterator<Item = &'a BookLevel>,
1390 group_size: Decimal,
1391 depth: Option<usize>,
1392 is_bid: bool,
1393) -> IndexMap<Decimal, Decimal> {
1394 if group_size <= Decimal::ZERO {
1395 log::warn!("Invalid group_size: {group_size}, must be positive; returning empty map");
1396 return IndexMap::new();
1397 }
1398
1399 let mut levels = IndexMap::new();
1400 let depth = depth.unwrap_or(usize::MAX);
1401
1402 for level in levels_iter {
1403 let price = level.price.value.as_decimal();
1404 let grouped_price = if is_bid {
1405 (price / group_size).floor() * group_size
1406 } else {
1407 (price / group_size).ceil() * group_size
1408 };
1409 let size = level.size_decimal();
1410
1411 levels
1412 .entry(grouped_price)
1413 .and_modify(|total| *total += size)
1414 .or_insert(size);
1415
1416 if levels.len() > depth {
1417 levels.pop();
1418 break;
1419 }
1420 }
1421
1422 levels
1423}