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 #[allow(
556 clippy::missing_errors_doc,
557 reason = "unchecked depth application returns no integrity errors"
558 )]
559 pub fn apply_depth_unchecked(
560 &mut self,
561 depth: &OrderBookDepth,
562 ) -> Result<(), BookIntegrityError> {
563 self.bids.clear();
564 self.asks.clear();
565
566 for &order in &depth.bids {
567 if order.side.is_none() || !order.size.is_positive() {
569 continue;
570 }
571
572 if order.side != Some(OrderSide::Buy) {
573 debug_assert_eq!(
574 order.side,
575 Some(OrderSide::Buy),
576 "Bid order must have Buy side, was {:?}",
577 order.side
578 );
579 log::warn!(
580 "Skipping bid order with wrong side {:?} (instrument_id={})",
581 order.side,
582 self.instrument_id
583 );
584 continue;
585 }
586
587 let order = pre_process_order(self.book_type, order, depth.flags);
588 self.bids.add(order, depth.flags);
589 }
590
591 for &order in &depth.asks {
592 if order.side.is_none() || !order.size.is_positive() {
594 continue;
595 }
596
597 if order.side != Some(OrderSide::Sell) {
598 debug_assert_eq!(
599 order.side,
600 Some(OrderSide::Sell),
601 "Ask order must have Sell side, was {:?}",
602 order.side
603 );
604 log::warn!(
605 "Skipping ask order with wrong side {:?} (instrument_id={})",
606 order.side,
607 self.instrument_id
608 );
609 continue;
610 }
611
612 let order = pre_process_order(self.book_type, order, depth.flags);
613 self.asks.add(order, depth.flags);
614 }
615
616 self.increment(depth.sequence, depth.ts_event, 0);
619
620 Ok(())
621 }
622
623 fn resolve_no_side_order(&self, mut order: BookOrder) -> Result<BookOrder, BookIntegrityError> {
624 let bid_price = self.bids.cache.get(&order.order_id);
625 let ask_price = self.asks.cache.get(&order.order_id);
626
627 let book_price = match (bid_price, ask_price) {
630 (Some(_), Some(_)) => {
631 return Err(BookIntegrityError::AmbiguousOrderSide(order.order_id));
632 }
633 (Some(book_price), None) | (None, Some(book_price)) => book_price,
634 (None, None) => {
635 return Err(BookIntegrityError::OrderNotFoundForSideResolution(
636 order.order_id,
637 ));
638 }
639 };
640
641 order.side = book_price.side.into();
642
643 Ok(order)
644 }
645
646 pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
648 self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
649 }
650
651 pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
653 self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
654 }
655
656 #[must_use]
658 pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
659 self.bids(depth)
660 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
661 .collect()
662 }
663
664 #[must_use]
666 pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
667 self.asks(depth)
668 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
669 .collect()
670 }
671
672 #[must_use]
674 pub fn group_bids(
675 &self,
676 group_size: Decimal,
677 depth: Option<usize>,
678 ) -> IndexMap<Decimal, Decimal> {
679 group_levels(self.bids(None), group_size, depth, true)
680 }
681
682 #[must_use]
684 pub fn group_asks(
685 &self,
686 group_size: Decimal,
687 depth: Option<usize>,
688 ) -> IndexMap<Decimal, Decimal> {
689 group_levels(self.asks(None), group_size, depth, false)
690 }
691
692 #[must_use]
703 pub fn bids_filtered_as_map(
704 &self,
705 depth: Option<usize>,
706 own_book: Option<&OwnOrderBook>,
707 status: Option<&AHashSet<OrderStatus>>,
708 accepted_buffer_ns: Option<u64>,
709 now: Option<u64>,
710 ) -> IndexMap<Decimal, Decimal> {
711 let mut public_map = self
712 .bids(depth)
713 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
714 .collect::<IndexMap<Decimal, Decimal>>();
715
716 if let Some(own_book) = own_book {
717 filter_quantities(
718 &mut public_map,
719 own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
720 );
721 }
722
723 public_map
724 }
725
726 #[must_use]
737 pub fn asks_filtered_as_map(
738 &self,
739 depth: Option<usize>,
740 own_book: Option<&OwnOrderBook>,
741 status: Option<&AHashSet<OrderStatus>>,
742 accepted_buffer_ns: Option<u64>,
743 now: Option<u64>,
744 ) -> IndexMap<Decimal, Decimal> {
745 let mut public_map = self
746 .asks(depth)
747 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
748 .collect::<IndexMap<Decimal, Decimal>>();
749
750 if let Some(own_book) = own_book {
751 filter_quantities(
752 &mut public_map,
753 own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
754 );
755 }
756
757 public_map
758 }
759
760 #[must_use]
769 pub fn filtered_view(
770 &self,
771 own_book: Option<&OwnOrderBook>,
772 depth: Option<usize>,
773 status: Option<&AHashSet<OrderStatus>>,
774 accepted_buffer_ns: Option<u64>,
775 now: Option<u64>,
776 ) -> Self {
777 self.filtered_view_checked(own_book, depth, status, accepted_buffer_ns, now)
778 .expect(FAILED)
779 }
780
781 pub fn filtered_view_checked(
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 ) -> Result<Self, BookViewError> {
800 if let Some(own_book) = own_book
801 && self.instrument_id != own_book.instrument_id
802 {
803 return Err(BookViewError::InstrumentMismatch(
804 self.instrument_id,
805 own_book.instrument_id,
806 ));
807 }
808
809 let bids_map = self.bids_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
810 let asks_map = self.asks_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
811
812 let mut filtered_book = Self::new(self.instrument_id, self.book_type);
813 filtered_book.sequence = self.sequence;
814 filtered_book.ts_last = self.ts_last;
815
816 let sequence = self.sequence;
817 let ts_event = self.ts_last;
818
819 let mut order_id = 1_u64;
820
821 for (price, quantity) in bids_map {
822 if quantity <= Decimal::ZERO {
823 continue;
824 }
825
826 let order = BookOrder::new(
827 OrderSide::Buy,
828 Price::from_decimal(price).expect("Invalid bid price for OrderBook::filtered_view"),
829 Quantity::from_decimal(quantity)
830 .expect("Invalid bid quantity for OrderBook::filtered_view"),
831 order_id,
832 );
833 order_id += 1;
834 filtered_book.add(order, 0, sequence, ts_event);
835 }
836
837 for (price, quantity) in asks_map {
838 if quantity <= Decimal::ZERO {
839 continue;
840 }
841
842 let order = BookOrder::new(
843 OrderSide::Sell,
844 Price::from_decimal(price).expect("Invalid ask price for OrderBook::filtered_view"),
845 Quantity::from_decimal(quantity)
846 .expect("Invalid ask quantity for OrderBook::filtered_view"),
847 order_id,
848 );
849 order_id += 1;
850 filtered_book.add(order, 0, sequence, ts_event);
851 }
852
853 Ok(filtered_book)
854 }
855
856 #[must_use]
867 pub fn group_bids_filtered(
868 &self,
869 group_size: Decimal,
870 depth: Option<usize>,
871 own_book: Option<&OwnOrderBook>,
872 status: Option<&AHashSet<OrderStatus>>,
873 accepted_buffer_ns: Option<u64>,
874 now: Option<u64>,
875 ) -> IndexMap<Decimal, Decimal> {
876 let mut public_map = group_levels(self.bids(None), group_size, depth, true);
877
878 if let Some(own_book) = own_book {
879 filter_quantities(
881 &mut public_map,
882 own_book.bid_quantity(status, None, Some(group_size), accepted_buffer_ns, now),
883 );
884 }
885
886 public_map
887 }
888
889 #[must_use]
900 pub fn group_asks_filtered(
901 &self,
902 group_size: Decimal,
903 depth: Option<usize>,
904 own_book: Option<&OwnOrderBook>,
905 status: Option<&AHashSet<OrderStatus>>,
906 accepted_buffer_ns: Option<u64>,
907 now: Option<u64>,
908 ) -> IndexMap<Decimal, Decimal> {
909 let mut public_map = group_levels(self.asks(None), group_size, depth, false);
910
911 if let Some(own_book) = own_book {
912 filter_quantities(
914 &mut public_map,
915 own_book.ask_quantity(status, None, Some(group_size), accepted_buffer_ns, now),
916 );
917 }
918
919 public_map
920 }
921
922 #[must_use]
924 pub fn has_bid(&self) -> bool {
925 self.bids.top().is_some_and(|top| !top.orders.is_empty())
926 }
927
928 #[must_use]
930 pub fn has_ask(&self) -> bool {
931 self.asks.top().is_some_and(|top| !top.orders.is_empty())
932 }
933
934 #[must_use]
936 pub fn best_bid_price(&self) -> Option<Price> {
937 self.bids.top().map(|top| top.price.value)
938 }
939
940 #[must_use]
942 pub fn best_ask_price(&self) -> Option<Price> {
943 self.asks.top().map(|top| top.price.value)
944 }
945
946 #[must_use]
948 pub fn best_bid_size(&self) -> Option<Quantity> {
949 self.bids
950 .top()
951 .and_then(|top| top.first().map(|order| order.size))
952 }
953
954 #[must_use]
956 pub fn best_ask_size(&self) -> Option<Quantity> {
957 self.asks
958 .top()
959 .and_then(|top| top.first().map(|order| order.size))
960 }
961
962 #[must_use]
964 pub fn spread(&self) -> Option<f64> {
965 match (self.best_ask_price(), self.best_bid_price()) {
966 (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
967 _ => None,
968 }
969 }
970
971 #[must_use]
973 pub fn midpoint(&self) -> Option<f64> {
974 match (self.best_ask_price(), self.best_bid_price()) {
975 (Some(ask), Some(bid)) => Some(f64::midpoint(ask.as_f64(), bid.as_f64())),
976 _ => None,
977 }
978 }
979
980 #[must_use]
982 pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
983 let levels = match order_side {
984 OrderSide::Buy => &self.asks.levels,
985 OrderSide::Sell => &self.bids.levels,
986 };
987
988 analysis::get_avg_px_for_quantity(qty, levels)
989 }
990
991 #[must_use]
993 pub fn get_worst_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> Option<Price> {
994 let levels = match order_side {
995 OrderSide::Buy => &self.asks.levels,
996 OrderSide::Sell => &self.bids.levels,
997 };
998
999 analysis::get_worst_px_for_quantity(qty, levels)
1000 }
1001
1002 #[must_use]
1006 pub fn get_avg_px_qty_for_exposure(
1007 &self,
1008 target_exposure: Quantity,
1009 order_side: OrderSide,
1010 ) -> (f64, f64, f64) {
1011 let levels = match order_side {
1012 OrderSide::Buy => &self.asks.levels,
1013 OrderSide::Sell => &self.bids.levels,
1014 };
1015
1016 analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
1017 }
1018
1019 #[must_use]
1024 pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
1025 let levels = match order_side {
1026 OrderSide::Buy => &self.asks.levels,
1027 OrderSide::Sell => &self.bids.levels,
1028 };
1029
1030 analysis::get_quantity_for_price(price, order_side, levels)
1031 }
1032
1033 #[must_use]
1047 pub fn get_quantity_at_level(
1048 &self,
1049 price: Price,
1050 order_side: OrderSide,
1051 size_precision: u8,
1052 ) -> Quantity {
1053 self.get_quantity_at_level_checked(price, order_side, size_precision)
1054 .expect_display("Failed to get order book level quantity")
1055 }
1056
1057 pub(crate) fn get_quantity_at_level_checked(
1065 &self,
1066 price: Price,
1067 order_side: OrderSide,
1068 size_precision: u8,
1069 ) -> CorrectnessResult<Quantity> {
1070 check_fixed_precision(size_precision)?;
1071
1072 let (levels, book_side) = match order_side {
1075 OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1076 OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1077 };
1078
1079 let book_price = BookPrice::new(price, book_side);
1080
1081 match levels.get(&book_price) {
1082 Some(level) => Quantity::from_raw_checked(level.size_raw_checked()?, size_precision),
1083 None => Quantity::from_raw_checked(0, size_precision),
1084 }
1085 }
1086
1087 #[must_use]
1092 pub fn get_orders_at_level(&self, price: Price, order_side: OrderSide) -> Vec<BookOrder> {
1093 let (levels, book_side) = match order_side {
1094 OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1095 OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1096 };
1097
1098 let book_price = BookPrice::new(price, book_side);
1099
1100 levels
1101 .get(&book_price)
1102 .map_or_else(Vec::new, BookLevel::get_orders)
1103 }
1104
1105 #[must_use]
1111 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
1112 match order.side.expect("BookOrder side must be Buy or Sell") {
1113 OrderSide::Buy => self.asks.simulate_fills(order),
1114 OrderSide::Sell => self.bids.simulate_fills(order),
1115 }
1116 }
1117
1118 #[must_use]
1133 pub fn get_all_crossed_levels(
1134 &self,
1135 order_side: OrderSide,
1136 price: Price,
1137 size_precision: u8,
1138 ) -> Vec<(Price, Quantity)> {
1139 self.get_all_crossed_levels_checked(order_side, price, size_precision)
1140 .expect_display("Failed to collect crossed order book levels")
1141 }
1142
1143 pub(crate) fn get_all_crossed_levels_checked(
1151 &self,
1152 order_side: OrderSide,
1153 price: Price,
1154 size_precision: u8,
1155 ) -> CorrectnessResult<Vec<(Price, Quantity)>> {
1156 let levels = match order_side {
1157 OrderSide::Buy => &self.asks.levels,
1158 OrderSide::Sell => &self.bids.levels,
1159 };
1160
1161 analysis::get_levels_for_price_checked(price, order_side, levels, size_precision)
1162 }
1163
1164 #[must_use]
1166 pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
1167 pprint_book(self, num_levels, group_size)
1168 }
1169
1170 fn increment(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
1171 let is_snapshot = RecordFlag::F_SNAPSHOT.matches(flags);
1173
1174 if !is_snapshot && sequence > 0 && sequence < self.sequence {
1175 log::warn!(
1176 "Out-of-order update: sequence {} < {} (instrument_id={})",
1177 sequence,
1178 self.sequence,
1179 self.instrument_id
1180 );
1181 }
1182
1183 if !is_snapshot && ts_event < self.ts_last {
1184 log::warn!(
1185 "Out-of-order update: ts_event {} < {} (instrument_id={})",
1186 ts_event,
1187 self.ts_last,
1188 self.instrument_id
1189 );
1190 }
1191
1192 if self.update_count == u64::MAX {
1193 debug_assert!(
1194 self.update_count < u64::MAX,
1195 "Update count at u64::MAX limit (about to overflow): {}",
1196 self.update_count
1197 );
1198 log::warn!(
1199 "Update count at u64::MAX: {} (instrument_id={})",
1200 self.update_count,
1201 self.instrument_id
1202 );
1203 }
1204
1205 self.sequence = sequence.max(self.sequence);
1207 self.ts_last = ts_event.max(self.ts_last);
1208 self.update_count = self.update_count.saturating_add(1);
1209 }
1210
1211 pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
1217 if self.book_type != BookType::L1_MBP {
1218 return Err(InvalidBookOperation::Update(self.book_type));
1219 }
1220
1221 if quote.ts_event < self.ts_last {
1222 log::warn!(
1223 "Skipping stale quote: ts_event {} < ts_last {} (instrument_id={})",
1224 quote.ts_event,
1225 self.ts_last,
1226 self.instrument_id
1227 );
1228 return Ok(());
1229 }
1230
1231 if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
1233 log::warn!(
1234 "Quote has crossed prices: bid={}, ask={} for {}",
1235 quote.bid_price,
1236 quote.ask_price,
1237 self.instrument_id
1238 );
1239 }
1240
1241 let bid = BookOrder::new(
1242 OrderSide::Buy,
1243 quote.bid_price,
1244 quote.bid_size,
1245 OrderSide::Buy as u64,
1246 );
1247
1248 let ask = BookOrder::new(
1249 OrderSide::Sell,
1250 quote.ask_price,
1251 quote.ask_size,
1252 OrderSide::Sell as u64,
1253 );
1254
1255 self.update_book_bid(bid);
1256 self.update_book_ask(ask);
1257
1258 self.increment(self.sequence.saturating_add(1), quote.ts_event, 0);
1259
1260 Ok(())
1261 }
1262
1263 pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
1269 if self.book_type != BookType::L1_MBP {
1270 return Err(InvalidBookOperation::Update(self.book_type));
1271 }
1272
1273 if trade.ts_event < self.ts_last {
1274 log::warn!(
1275 "Skipping stale trade: ts_event {} < ts_last {} (instrument_id={})",
1276 trade.ts_event,
1277 self.ts_last,
1278 self.instrument_id
1279 );
1280 return Ok(());
1281 }
1282
1283 debug_assert!(
1285 !trade.price.is_undefined() && !trade.price.is_error(),
1286 "Trade has invalid/uninitialized price: {}",
1287 trade.price
1288 );
1289
1290 debug_assert!(
1292 trade.size.is_positive(),
1293 "Trade has non-positive size: {}",
1294 trade.size
1295 );
1296
1297 let bid = BookOrder::new(
1298 OrderSide::Buy,
1299 trade.price,
1300 trade.size,
1301 OrderSide::Buy as u64,
1302 );
1303
1304 let ask = BookOrder::new(
1305 OrderSide::Sell,
1306 trade.price,
1307 trade.size,
1308 OrderSide::Sell as u64,
1309 );
1310
1311 self.update_book_bid(bid);
1312 self.update_book_ask(ask);
1313
1314 self.increment(self.sequence.saturating_add(1), trade.ts_event, 0);
1315
1316 Ok(())
1317 }
1318
1319 fn update_book_bid(&mut self, order: BookOrder) {
1320 self.bids.replace_l1(order);
1321 }
1322
1323 fn update_book_ask(&mut self, order: BookOrder) {
1324 self.asks.replace_l1(order);
1325 }
1326
1327 #[must_use]
1334 pub fn deltas_to_quotes(book_type: BookType, deltas: &[OrderBookDelta]) -> Vec<QuoteTick> {
1335 assert!(!deltas.is_empty(), "`deltas` must not be empty");
1336
1337 let instrument_id = deltas[0].instrument_id;
1338 let mut book = Self::new(instrument_id, book_type);
1339 let mut quotes = Vec::new();
1340 let mut last_bbo: Option<(Price, Price)> = None;
1341
1342 for delta in deltas {
1343 book.apply_delta(delta).unwrap();
1344 let Some((bid_px, ask_px)) = book.best_bid_price().zip(book.best_ask_price()) else {
1345 last_bbo = None;
1346 continue;
1347 };
1348
1349 let bbo = (bid_px, ask_px);
1350
1351 if last_bbo == Some(bbo) {
1352 continue;
1353 }
1354
1355 last_bbo = Some(bbo);
1356 let bid_level = book.bids.top().unwrap();
1357 let ask_level = book.asks.top().unwrap();
1358 let precision = bid_level.first().unwrap().size.precision;
1359 let bid_sz = Quantity::from_raw(bid_level.size_raw(), precision);
1360 let ask_sz = Quantity::from_raw(ask_level.size_raw(), precision);
1361 let quote = QuoteTick::new(
1362 instrument_id,
1363 bid_px,
1364 ask_px,
1365 bid_sz,
1366 ask_sz,
1367 delta.ts_event,
1368 delta.ts_init,
1369 );
1370
1371 quotes.push(quote);
1372 }
1373
1374 quotes
1375 }
1376}
1377
1378fn filter_quantities(
1379 public_map: &mut IndexMap<Decimal, Decimal>,
1380 own_map: IndexMap<Decimal, Decimal>,
1381) {
1382 for (price, own_size) in own_map {
1383 if let Some(public_size) = public_map.get_mut(&price) {
1384 *public_size = (*public_size - own_size).max(Decimal::ZERO);
1385
1386 if *public_size == Decimal::ZERO {
1387 public_map.shift_remove(&price);
1388 }
1389 }
1390 }
1391}
1392
1393fn group_levels<'a>(
1394 levels_iter: impl Iterator<Item = &'a BookLevel>,
1395 group_size: Decimal,
1396 depth: Option<usize>,
1397 is_bid: bool,
1398) -> IndexMap<Decimal, Decimal> {
1399 if group_size <= Decimal::ZERO {
1400 log::warn!("Invalid group_size: {group_size}, must be positive; returning empty map");
1401 return IndexMap::new();
1402 }
1403
1404 let mut levels = IndexMap::new();
1405 let depth = depth.unwrap_or(usize::MAX);
1406
1407 for level in levels_iter {
1408 let price = level.price.value.as_decimal();
1409 let grouped_price = if is_bid {
1410 (price / group_size).floor() * group_size
1411 } else {
1412 (price / group_size).ceil() * group_size
1413 };
1414 let size = level.size_decimal();
1415
1416 levels
1417 .entry(grouped_price)
1418 .and_modify(|total| *total += size)
1419 .or_insert(size);
1420
1421 if levels.len() > depth {
1422 levels.pop();
1423 break;
1424 }
1425 }
1426
1427 levels
1428}