1use std::fmt::Display;
19
20use ahash::AHashSet;
21use indexmap::IndexMap;
22use nautilus_core::{UnixNanos, correctness::FAILED};
23use rust_decimal::Decimal;
24
25use super::{
26 BookViewError, aggregation::pre_process_order, analysis, display::pprint_book,
27 level::BookLevel, own::OwnOrderBook,
28};
29use crate::{
30 data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
31 enums::{BookAction, BookType, OrderSide, OrderStatus, RecordFlag},
32 identifiers::InstrumentId,
33 orderbook::{
34 BookIntegrityError, InvalidBookOperation,
35 ladder::{BookLadder, BookPrice},
36 },
37 types::{
38 Price, Quantity,
39 price::{PRICE_ERROR, PRICE_UNDEF},
40 },
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) {
166 self.clear_with_flags(sequence, ts_event, 0);
167 }
168
169 pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
171 self.bids.clear();
172 self.increment(sequence, ts_event, 0);
173 }
174
175 pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
177 self.asks.clear();
178 self.increment(sequence, ts_event, 0);
179 }
180
181 fn clear_with_flags(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
182 self.bids.clear();
183 self.asks.clear();
184 self.increment(sequence, ts_event, flags);
185 }
186
187 pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
195 if self.book_type == BookType::L1_MBP {
196 return None;
198 }
199
200 let (Some(best_bid), Some(best_ask)) = (self.best_bid_price(), self.best_ask_price())
201 else {
202 return None;
203 };
204
205 if best_bid <= best_ask {
206 return None;
207 }
208
209 let mut removed_levels = Vec::new();
210 let (clear_bids, clear_asks) = match side {
211 Some(OrderSide::Buy) => (true, false),
212 Some(OrderSide::Sell) => (false, true),
213 None => (true, true),
214 };
215
216 let mut ask_prices_to_remove = Vec::new();
218
219 if clear_asks {
220 for bp in self.asks.levels.keys() {
221 if bp.value <= best_bid {
222 ask_prices_to_remove.push(*bp);
223 } else {
224 break;
225 }
226 }
227 }
228
229 let mut bid_prices_to_remove = Vec::new();
231
232 if clear_bids {
233 for bp in self.bids.levels.keys() {
234 if bp.value >= best_ask {
235 bid_prices_to_remove.push(*bp);
236 } else {
237 break;
238 }
239 }
240 }
241
242 if ask_prices_to_remove.is_empty() && bid_prices_to_remove.is_empty() {
243 return None;
244 }
245
246 let bid_count = bid_prices_to_remove.len();
247 let ask_count = ask_prices_to_remove.len();
248
249 for price in bid_prices_to_remove {
251 if let Some(level) = self.bids.remove_level(price) {
252 removed_levels.push(level);
253 }
254 }
255
256 for price in ask_prices_to_remove {
258 if let Some(level) = self.asks.remove_level(price) {
259 removed_levels.push(level);
260 }
261 }
262
263 self.increment(self.sequence, self.ts_last, 0);
264
265 if removed_levels.is_empty() {
266 None
267 } else {
268 let total_orders: usize = removed_levels.iter().map(|level| level.orders.len()).sum();
269
270 log::warn!(
271 "Removed {} stale/crossed levels (instrument_id={}, bid_levels={}, ask_levels={}, total_orders={}), book was crossed with best_bid={} > best_ask={}",
272 removed_levels.len(),
273 self.instrument_id,
274 bid_count,
275 ask_count,
276 total_orders,
277 best_bid,
278 best_ask
279 );
280
281 Some(removed_levels)
282 }
283 }
284
285 pub fn apply_delta(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
299 if delta.instrument_id != self.instrument_id {
300 return Err(BookIntegrityError::InstrumentMismatch(
301 self.instrument_id,
302 delta.instrument_id,
303 ));
304 }
305 self.apply_delta_unchecked(delta)
306 }
307
308 pub fn apply_delta_unchecked(
326 &mut self,
327 delta: &OrderBookDelta,
328 ) -> Result<(), BookIntegrityError> {
329 self.report_out_of_order_snapshot(delta.flags, delta.sequence, delta.ts_event, 1);
331 self.apply_delta_inner(delta)
332 }
333
334 fn apply_delta_inner(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
335 let mut order = delta.order;
336
337 if order.side.is_none() && order.order_id != 0 {
338 match self.resolve_no_side_order(order) {
339 Ok(resolved) => order = resolved,
340 Err(BookIntegrityError::OrderNotFoundForSideResolution(order_id)) => {
341 match delta.action {
342 BookAction::Add => return Err(BookIntegrityError::NoOrderSide),
343 BookAction::Update | BookAction::Delete => {
344 log::debug!(
346 "Skipping {:?} for unknown order_id={order_id}",
347 delta.action
348 );
349 return Ok(());
350 }
351 BookAction::Clear => {} }
353 }
354 Err(BookIntegrityError::AmbiguousOrderSide(order_id)) => {
355 match delta.action {
356 BookAction::Add => {
357 return Err(BookIntegrityError::AmbiguousOrderSide(order_id));
358 }
359 BookAction::Update | BookAction::Delete => {
360 log::warn!(
361 "Skipping {:?} for order_id={order_id} found on both book sides",
362 delta.action
363 );
364 return Ok(());
365 }
366 BookAction::Clear => {} }
368 }
369 Err(e) => return Err(e),
370 }
371 }
372
373 if order.side.is_none() && delta.action != BookAction::Clear {
374 return Err(BookIntegrityError::NoOrderSide);
375 }
376
377 let flags = delta.flags;
378 let sequence = delta.sequence;
379 let ts_event = delta.ts_event;
380
381 match delta.action {
382 BookAction::Add => self.add(order, flags, sequence, ts_event),
383 BookAction::Update => self.update(order, flags, sequence, ts_event),
384 BookAction::Delete => self.delete(order, flags, sequence, ts_event),
385 BookAction::Clear => self.clear_with_flags(sequence, ts_event, flags),
386 }
387
388 Ok(())
389 }
390
391 pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) -> Result<(), BookIntegrityError> {
399 if deltas.instrument_id != self.instrument_id {
400 return Err(BookIntegrityError::InstrumentMismatch(
401 self.instrument_id,
402 deltas.instrument_id,
403 ));
404 }
405 self.apply_deltas_unchecked(deltas)
406 }
407
408 pub fn apply_deltas_unchecked(
422 &mut self,
423 deltas: &OrderBookDeltas,
424 ) -> Result<(), BookIntegrityError> {
425 self.report_out_of_order_snapshot(
426 deltas.flags,
427 deltas.sequence,
428 deltas.ts_event,
429 deltas.deltas.len(),
430 );
431
432 for delta in &deltas.deltas {
433 self.apply_delta_inner(delta)?;
434 }
435
436 Ok(())
437 }
438
439 fn report_out_of_order_snapshot(
442 &self,
443 flags: u8,
444 sequence: u64,
445 ts_event: UnixNanos,
446 count: usize,
447 ) {
448 if !RecordFlag::F_SNAPSHOT.matches(flags) {
449 return;
450 }
451
452 if sequence > 0 && sequence < self.sequence {
453 log::warn!(
454 "Out-of-order snapshot: sequence {} < {} (deltas={}, instrument_id={})",
455 sequence,
456 self.sequence,
457 count,
458 self.instrument_id
459 );
460 }
461
462 if ts_event < self.ts_last {
463 log::warn!(
464 "Out-of-order snapshot: ts_event {} < {} (deltas={}, instrument_id={})",
465 ts_event,
466 self.ts_last,
467 count,
468 self.instrument_id
469 );
470 }
471 }
472
473 #[must_use]
487 pub fn to_deltas(&self, ts_event: UnixNanos, ts_init: UnixNanos) -> OrderBookDeltas {
488 let mut deltas = Vec::new();
489
490 let total_orders = self.bids(None).map(BookLevel::len).sum::<usize>()
491 + self.asks(None).map(BookLevel::len).sum::<usize>();
492
493 let mut clear = OrderBookDelta::clear(self.instrument_id, self.sequence, ts_event, ts_init);
495
496 if total_orders == 0 {
497 clear.flags |= RecordFlag::F_LAST as u8;
498 }
499 deltas.push(clear);
500
501 let mut order_count = 0;
502
503 for level in self.bids(None).chain(self.asks(None)) {
504 for order in level.iter() {
505 order_count += 1;
506 let flags = if order_count == total_orders {
507 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
508 } else {
509 RecordFlag::F_SNAPSHOT as u8
510 };
511
512 deltas.push(OrderBookDelta::new(
513 self.instrument_id,
514 BookAction::Add,
515 *order,
516 flags,
517 self.sequence,
518 ts_event,
519 ts_init,
520 ));
521 }
522 }
523
524 OrderBookDeltas::new(self.instrument_id, deltas)
525 }
526
527 pub fn apply_depth(&mut self, depth: &OrderBookDepth10) -> Result<(), BookIntegrityError> {
533 if depth.instrument_id != self.instrument_id {
534 return Err(BookIntegrityError::InstrumentMismatch(
535 self.instrument_id,
536 depth.instrument_id,
537 ));
538 }
539 self.apply_depth_unchecked(depth)
540 }
541
542 pub fn apply_depth_unchecked(
550 &mut self,
551 depth: &OrderBookDepth10,
552 ) -> Result<(), BookIntegrityError> {
553 self.bids.clear();
554 self.asks.clear();
555
556 for order in depth.bids {
557 if order.side.is_none() || !order.size.is_positive() {
559 continue;
560 }
561
562 if order.side != Some(OrderSide::Buy) {
563 debug_assert_eq!(
564 order.side,
565 Some(OrderSide::Buy),
566 "Bid order must have Buy side, was {:?}",
567 order.side
568 );
569 log::warn!(
570 "Skipping bid order with wrong side {:?} (instrument_id={})",
571 order.side,
572 self.instrument_id
573 );
574 continue;
575 }
576
577 let order = pre_process_order(self.book_type, order, depth.flags);
578 self.bids.add(order, depth.flags);
579 }
580
581 for order in depth.asks {
582 if order.side.is_none() || !order.size.is_positive() {
584 continue;
585 }
586
587 if order.side != Some(OrderSide::Sell) {
588 debug_assert_eq!(
589 order.side,
590 Some(OrderSide::Sell),
591 "Ask order must have Sell side, was {:?}",
592 order.side
593 );
594 log::warn!(
595 "Skipping ask order with wrong side {:?} (instrument_id={})",
596 order.side,
597 self.instrument_id
598 );
599 continue;
600 }
601
602 let order = pre_process_order(self.book_type, order, depth.flags);
603 self.asks.add(order, depth.flags);
604 }
605
606 self.increment(depth.sequence, depth.ts_event, 0);
609
610 Ok(())
611 }
612
613 fn resolve_no_side_order(&self, mut order: BookOrder) -> Result<BookOrder, BookIntegrityError> {
614 let bid_price = self.bids.cache.get(&order.order_id);
615 let ask_price = self.asks.cache.get(&order.order_id);
616
617 let book_price = match (bid_price, ask_price) {
620 (Some(_), Some(_)) => {
621 return Err(BookIntegrityError::AmbiguousOrderSide(order.order_id));
622 }
623 (Some(book_price), None) | (None, Some(book_price)) => book_price,
624 (None, None) => {
625 return Err(BookIntegrityError::OrderNotFoundForSideResolution(
626 order.order_id,
627 ));
628 }
629 };
630
631 order.side = book_price.side.into();
632
633 Ok(order)
634 }
635
636 pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
638 self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
639 }
640
641 pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
643 self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
644 }
645
646 #[must_use]
648 pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
649 self.bids(depth)
650 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
651 .collect()
652 }
653
654 #[must_use]
656 pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
657 self.asks(depth)
658 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
659 .collect()
660 }
661
662 #[must_use]
664 pub fn group_bids(
665 &self,
666 group_size: Decimal,
667 depth: Option<usize>,
668 ) -> IndexMap<Decimal, Decimal> {
669 group_levels(self.bids(None), group_size, depth, true)
670 }
671
672 #[must_use]
674 pub fn group_asks(
675 &self,
676 group_size: Decimal,
677 depth: Option<usize>,
678 ) -> IndexMap<Decimal, Decimal> {
679 group_levels(self.asks(None), group_size, depth, false)
680 }
681
682 #[must_use]
693 pub fn bids_filtered_as_map(
694 &self,
695 depth: Option<usize>,
696 own_book: Option<&OwnOrderBook>,
697 status: Option<&AHashSet<OrderStatus>>,
698 accepted_buffer_ns: Option<u64>,
699 now: Option<u64>,
700 ) -> IndexMap<Decimal, Decimal> {
701 let mut public_map = self
702 .bids(depth)
703 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
704 .collect::<IndexMap<Decimal, Decimal>>();
705
706 if let Some(own_book) = own_book {
707 filter_quantities(
708 &mut public_map,
709 own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
710 );
711 }
712
713 public_map
714 }
715
716 #[must_use]
727 pub fn asks_filtered_as_map(
728 &self,
729 depth: Option<usize>,
730 own_book: Option<&OwnOrderBook>,
731 status: Option<&AHashSet<OrderStatus>>,
732 accepted_buffer_ns: Option<u64>,
733 now: Option<u64>,
734 ) -> IndexMap<Decimal, Decimal> {
735 let mut public_map = self
736 .asks(depth)
737 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
738 .collect::<IndexMap<Decimal, Decimal>>();
739
740 if let Some(own_book) = own_book {
741 filter_quantities(
742 &mut public_map,
743 own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
744 );
745 }
746
747 public_map
748 }
749
750 #[must_use]
759 pub fn filtered_view(
760 &self,
761 own_book: Option<&OwnOrderBook>,
762 depth: Option<usize>,
763 status: Option<&AHashSet<OrderStatus>>,
764 accepted_buffer_ns: Option<u64>,
765 now: Option<u64>,
766 ) -> Self {
767 self.filtered_view_checked(own_book, depth, status, accepted_buffer_ns, now)
768 .expect(FAILED)
769 }
770
771 pub fn filtered_view_checked(
784 &self,
785 own_book: Option<&OwnOrderBook>,
786 depth: Option<usize>,
787 status: Option<&AHashSet<OrderStatus>>,
788 accepted_buffer_ns: Option<u64>,
789 now: Option<u64>,
790 ) -> Result<Self, BookViewError> {
791 if let Some(own_book) = own_book
792 && self.instrument_id != own_book.instrument_id
793 {
794 return Err(BookViewError::InstrumentMismatch(
795 self.instrument_id,
796 own_book.instrument_id,
797 ));
798 }
799
800 let bids_map = self.bids_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
801 let asks_map = self.asks_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
802
803 let mut filtered_book = Self::new(self.instrument_id, self.book_type);
804 filtered_book.sequence = self.sequence;
805 filtered_book.ts_last = self.ts_last;
806
807 let sequence = self.sequence;
808 let ts_event = self.ts_last;
809
810 let mut order_id = 1_u64;
811
812 for (price, quantity) in bids_map {
813 if quantity <= Decimal::ZERO {
814 continue;
815 }
816
817 let order = BookOrder::new(
818 OrderSide::Buy,
819 Price::from_decimal(price).expect("Invalid bid price for OrderBook::filtered_view"),
820 Quantity::from_decimal(quantity)
821 .expect("Invalid bid quantity for OrderBook::filtered_view"),
822 order_id,
823 );
824 order_id += 1;
825 filtered_book.add(order, 0, sequence, ts_event);
826 }
827
828 for (price, quantity) in asks_map {
829 if quantity <= Decimal::ZERO {
830 continue;
831 }
832
833 let order = BookOrder::new(
834 OrderSide::Sell,
835 Price::from_decimal(price).expect("Invalid ask price for OrderBook::filtered_view"),
836 Quantity::from_decimal(quantity)
837 .expect("Invalid ask quantity for OrderBook::filtered_view"),
838 order_id,
839 );
840 order_id += 1;
841 filtered_book.add(order, 0, sequence, ts_event);
842 }
843
844 Ok(filtered_book)
845 }
846
847 #[must_use]
858 pub fn group_bids_filtered(
859 &self,
860 group_size: Decimal,
861 depth: Option<usize>,
862 own_book: Option<&OwnOrderBook>,
863 status: Option<&AHashSet<OrderStatus>>,
864 accepted_buffer_ns: Option<u64>,
865 now: Option<u64>,
866 ) -> IndexMap<Decimal, Decimal> {
867 let mut public_map = group_levels(self.bids(None), group_size, depth, true);
868
869 if let Some(own_book) = own_book {
870 filter_quantities(
871 &mut public_map,
872 own_book.bid_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
873 );
874 }
875
876 public_map
877 }
878
879 #[must_use]
890 pub fn group_asks_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.asks(None), group_size, depth, false);
900
901 if let Some(own_book) = own_book {
902 filter_quantities(
903 &mut public_map,
904 own_book.ask_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
905 );
906 }
907
908 public_map
909 }
910
911 #[must_use]
913 pub fn has_bid(&self) -> bool {
914 self.bids.top().is_some_and(|top| !top.orders.is_empty())
915 }
916
917 #[must_use]
919 pub fn has_ask(&self) -> bool {
920 self.asks.top().is_some_and(|top| !top.orders.is_empty())
921 }
922
923 #[must_use]
925 pub fn best_bid_price(&self) -> Option<Price> {
926 self.bids.top().map(|top| top.price.value)
927 }
928
929 #[must_use]
931 pub fn best_ask_price(&self) -> Option<Price> {
932 self.asks.top().map(|top| top.price.value)
933 }
934
935 #[must_use]
937 pub fn best_bid_size(&self) -> Option<Quantity> {
938 self.bids
939 .top()
940 .and_then(|top| top.first().map(|order| order.size))
941 }
942
943 #[must_use]
945 pub fn best_ask_size(&self) -> Option<Quantity> {
946 self.asks
947 .top()
948 .and_then(|top| top.first().map(|order| order.size))
949 }
950
951 #[must_use]
953 pub fn spread(&self) -> Option<f64> {
954 match (self.best_ask_price(), self.best_bid_price()) {
955 (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
956 _ => None,
957 }
958 }
959
960 #[must_use]
962 pub fn midpoint(&self) -> Option<f64> {
963 match (self.best_ask_price(), self.best_bid_price()) {
964 (Some(ask), Some(bid)) => Some(f64::midpoint(ask.as_f64(), bid.as_f64())),
965 _ => None,
966 }
967 }
968
969 #[must_use]
971 pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
972 let levels = match order_side {
973 OrderSide::Buy => &self.asks.levels,
974 OrderSide::Sell => &self.bids.levels,
975 };
976
977 analysis::get_avg_px_for_quantity(qty, levels)
978 }
979
980 #[must_use]
982 pub fn get_worst_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> Option<Price> {
983 let levels = match order_side {
984 OrderSide::Buy => &self.asks.levels,
985 OrderSide::Sell => &self.bids.levels,
986 };
987
988 analysis::get_worst_px_for_quantity(qty, levels)
989 }
990
991 #[must_use]
993 pub fn get_avg_px_qty_for_exposure(
994 &self,
995 target_exposure: Quantity,
996 order_side: OrderSide,
997 ) -> (f64, f64, f64) {
998 let levels = match order_side {
999 OrderSide::Buy => &self.asks.levels,
1000 OrderSide::Sell => &self.bids.levels,
1001 };
1002
1003 analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
1004 }
1005
1006 #[must_use]
1011 pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
1012 let levels = match order_side {
1013 OrderSide::Buy => &self.asks.levels,
1014 OrderSide::Sell => &self.bids.levels,
1015 };
1016
1017 analysis::get_quantity_for_price(price, order_side, levels)
1018 }
1019
1020 #[must_use]
1025 pub fn get_quantity_at_level(
1026 &self,
1027 price: Price,
1028 order_side: OrderSide,
1029 size_precision: u8,
1030 ) -> Quantity {
1031 let (levels, book_side) = match order_side {
1034 OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1035 OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1036 };
1037
1038 let book_price = BookPrice::new(price, book_side);
1039
1040 levels
1041 .get(&book_price)
1042 .map_or(Quantity::zero(size_precision), |level| {
1043 Quantity::from_raw(level.size_raw(), size_precision)
1044 })
1045 }
1046
1047 #[must_use]
1052 pub fn get_orders_at_level(&self, price: Price, order_side: OrderSide) -> Vec<BookOrder> {
1053 let (levels, book_side) = match order_side {
1054 OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1055 OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1056 };
1057
1058 let book_price = BookPrice::new(price, book_side);
1059
1060 levels
1061 .get(&book_price)
1062 .map_or_else(Vec::new, BookLevel::get_orders)
1063 }
1064
1065 #[must_use]
1071 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
1072 match order.side.expect("BookOrder side must be Buy or Sell") {
1073 OrderSide::Buy => self.asks.simulate_fills(order),
1074 OrderSide::Sell => self.bids.simulate_fills(order),
1075 }
1076 }
1077
1078 #[must_use]
1084 pub fn get_all_crossed_levels(
1085 &self,
1086 order_side: OrderSide,
1087 price: Price,
1088 size_precision: u8,
1089 ) -> Vec<(Price, Quantity)> {
1090 let levels = match order_side {
1091 OrderSide::Buy => &self.asks.levels,
1092 OrderSide::Sell => &self.bids.levels,
1093 };
1094
1095 analysis::get_levels_for_price(price, order_side, levels, size_precision)
1096 }
1097
1098 #[must_use]
1100 pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
1101 pprint_book(self, num_levels, group_size)
1102 }
1103
1104 fn increment(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
1105 let is_snapshot = RecordFlag::F_SNAPSHOT.matches(flags);
1107
1108 if !is_snapshot && sequence > 0 && sequence < self.sequence {
1109 log::warn!(
1110 "Out-of-order update: sequence {} < {} (instrument_id={})",
1111 sequence,
1112 self.sequence,
1113 self.instrument_id
1114 );
1115 }
1116
1117 if !is_snapshot && ts_event < self.ts_last {
1118 log::warn!(
1119 "Out-of-order update: ts_event {} < {} (instrument_id={})",
1120 ts_event,
1121 self.ts_last,
1122 self.instrument_id
1123 );
1124 }
1125
1126 if self.update_count == u64::MAX {
1127 debug_assert!(
1128 self.update_count < u64::MAX,
1129 "Update count at u64::MAX limit (about to overflow): {}",
1130 self.update_count
1131 );
1132 log::warn!(
1133 "Update count at u64::MAX: {} (instrument_id={})",
1134 self.update_count,
1135 self.instrument_id
1136 );
1137 }
1138
1139 self.sequence = sequence.max(self.sequence);
1141 self.ts_last = ts_event.max(self.ts_last);
1142 self.update_count = self.update_count.saturating_add(1);
1143 }
1144
1145 pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
1151 if self.book_type != BookType::L1_MBP {
1152 return Err(InvalidBookOperation::Update(self.book_type));
1153 }
1154
1155 if quote.ts_event < self.ts_last {
1156 log::warn!(
1157 "Skipping stale quote: ts_event {} < ts_last {} (instrument_id={})",
1158 quote.ts_event,
1159 self.ts_last,
1160 self.instrument_id
1161 );
1162 return Ok(());
1163 }
1164
1165 if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
1167 log::warn!(
1168 "Quote has crossed prices: bid={}, ask={} for {}",
1169 quote.bid_price,
1170 quote.ask_price,
1171 self.instrument_id
1172 );
1173 }
1174
1175 let bid = BookOrder::new(
1176 OrderSide::Buy,
1177 quote.bid_price,
1178 quote.bid_size,
1179 OrderSide::Buy as u64,
1180 );
1181
1182 let ask = BookOrder::new(
1183 OrderSide::Sell,
1184 quote.ask_price,
1185 quote.ask_size,
1186 OrderSide::Sell as u64,
1187 );
1188
1189 self.update_book_bid(bid);
1190 self.update_book_ask(ask);
1191
1192 self.increment(self.sequence.saturating_add(1), quote.ts_event, 0);
1193
1194 Ok(())
1195 }
1196
1197 pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
1203 if self.book_type != BookType::L1_MBP {
1204 return Err(InvalidBookOperation::Update(self.book_type));
1205 }
1206
1207 if trade.ts_event < self.ts_last {
1208 log::warn!(
1209 "Skipping stale trade: ts_event {} < ts_last {} (instrument_id={})",
1210 trade.ts_event,
1211 self.ts_last,
1212 self.instrument_id
1213 );
1214 return Ok(());
1215 }
1216
1217 debug_assert!(
1219 trade.price.raw != PRICE_UNDEF && trade.price.raw != PRICE_ERROR,
1220 "Trade has invalid/uninitialized price: {}",
1221 trade.price
1222 );
1223
1224 debug_assert!(
1226 trade.size.is_positive(),
1227 "Trade has non-positive size: {}",
1228 trade.size
1229 );
1230
1231 let bid = BookOrder::new(
1232 OrderSide::Buy,
1233 trade.price,
1234 trade.size,
1235 OrderSide::Buy as u64,
1236 );
1237
1238 let ask = BookOrder::new(
1239 OrderSide::Sell,
1240 trade.price,
1241 trade.size,
1242 OrderSide::Sell as u64,
1243 );
1244
1245 self.update_book_bid(bid);
1246 self.update_book_ask(ask);
1247
1248 self.increment(self.sequence.saturating_add(1), trade.ts_event, 0);
1249
1250 Ok(())
1251 }
1252
1253 fn update_book_bid(&mut self, order: BookOrder) {
1254 self.bids.replace_l1(order);
1255 }
1256
1257 fn update_book_ask(&mut self, order: BookOrder) {
1258 self.asks.replace_l1(order);
1259 }
1260
1261 #[must_use]
1268 pub fn deltas_to_quotes(book_type: BookType, deltas: &[OrderBookDelta]) -> Vec<QuoteTick> {
1269 assert!(!deltas.is_empty(), "`deltas` must not be empty");
1270
1271 let instrument_id = deltas[0].instrument_id;
1272 let mut book = Self::new(instrument_id, book_type);
1273 let mut quotes = Vec::new();
1274 let mut last_bbo: Option<(Price, Price)> = None;
1275
1276 for delta in deltas {
1277 book.apply_delta(delta).unwrap();
1278 let Some((bid_px, ask_px)) = book.best_bid_price().zip(book.best_ask_price()) else {
1279 last_bbo = None;
1280 continue;
1281 };
1282
1283 let bbo = (bid_px, ask_px);
1284
1285 if last_bbo == Some(bbo) {
1286 continue;
1287 }
1288
1289 last_bbo = Some(bbo);
1290 let bid_level = book.bids.top().unwrap();
1291 let ask_level = book.asks.top().unwrap();
1292 let precision = bid_level.first().unwrap().size.precision;
1293 let bid_sz = Quantity::from_raw(bid_level.size_raw(), precision);
1294 let ask_sz = Quantity::from_raw(ask_level.size_raw(), precision);
1295 let quote = QuoteTick::new(
1296 instrument_id,
1297 bid_px,
1298 ask_px,
1299 bid_sz,
1300 ask_sz,
1301 delta.ts_event,
1302 delta.ts_init,
1303 );
1304
1305 quotes.push(quote);
1306 }
1307
1308 quotes
1309 }
1310}
1311
1312fn filter_quantities(
1313 public_map: &mut IndexMap<Decimal, Decimal>,
1314 own_map: IndexMap<Decimal, Decimal>,
1315) {
1316 for (price, own_size) in own_map {
1317 if let Some(public_size) = public_map.get_mut(&price) {
1318 *public_size = (*public_size - own_size).max(Decimal::ZERO);
1319
1320 if *public_size == Decimal::ZERO {
1321 public_map.shift_remove(&price);
1322 }
1323 }
1324 }
1325}
1326
1327fn group_levels<'a>(
1328 levels_iter: impl Iterator<Item = &'a BookLevel>,
1329 group_size: Decimal,
1330 depth: Option<usize>,
1331 is_bid: bool,
1332) -> IndexMap<Decimal, Decimal> {
1333 if group_size <= Decimal::ZERO {
1334 log::warn!("Invalid group_size: {group_size}, must be positive; returning empty map");
1335 return IndexMap::new();
1336 }
1337
1338 let mut levels = IndexMap::new();
1339 let depth = depth.unwrap_or(usize::MAX);
1340
1341 for level in levels_iter {
1342 let price = level.price.value.as_decimal();
1343 let grouped_price = if is_bid {
1344 (price / group_size).floor() * group_size
1345 } else {
1346 (price / group_size).ceil() * group_size
1347 };
1348 let size = level.size_decimal();
1349
1350 levels
1351 .entry(grouped_price)
1352 .and_modify(|total| *total += size)
1353 .or_insert(size);
1354
1355 if levels.len() > depth {
1356 levels.pop();
1357 break;
1358 }
1359 }
1360
1361 levels
1362}