1use std::fmt::Display;
19
20use ahash::AHashSet;
21use indexmap::IndexMap;
22use nautilus_core::UnixNanos;
23use rust_decimal::Decimal;
24
25use super::{
26 aggregation::pre_process_order, analysis, display::pprint_book, level::BookLevel,
27 own::OwnOrderBook,
28};
29use crate::{
30 data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
31 enums::{BookAction, BookType, OrderSide, OrderSideSpecified, 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.core.nautilus_pyo3.model", from_py_object)
54)]
55pub struct OrderBook {
56 pub instrument_id: InstrumentId,
58 pub book_type: BookType,
60 pub sequence: u64,
62 pub ts_last: UnixNanos,
64 pub update_count: u64,
66 pub(crate) bids: BookLadder,
67 pub(crate) asks: BookLadder,
68}
69
70impl PartialEq for OrderBook {
71 fn eq(&self, other: &Self) -> bool {
72 self.instrument_id == other.instrument_id && self.book_type == other.book_type
73 }
74}
75
76impl Eq for OrderBook {}
77
78impl Display for OrderBook {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 write!(
81 f,
82 "{}(instrument_id={}, book_type={}, update_count={})",
83 stringify!(OrderBook),
84 self.instrument_id,
85 self.book_type,
86 self.update_count,
87 )
88 }
89}
90
91impl OrderBook {
92 #[must_use]
94 pub fn new(instrument_id: InstrumentId, book_type: BookType) -> Self {
95 Self {
96 instrument_id,
97 book_type,
98 sequence: 0,
99 ts_last: UnixNanos::default(),
100 update_count: 0,
101 bids: BookLadder::new(OrderSideSpecified::Buy, book_type),
102 asks: BookLadder::new(OrderSideSpecified::Sell, book_type),
103 }
104 }
105
106 pub fn reset(&mut self) {
108 self.bids.clear();
109 self.asks.clear();
110 self.sequence = 0;
111 self.ts_last = UnixNanos::default();
112 self.update_count = 0;
113 }
114
115 pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
117 let order = pre_process_order(self.book_type, order, flags);
118 match order.side.as_specified() {
119 OrderSideSpecified::Buy => self.bids.add(order, flags),
120 OrderSideSpecified::Sell => self.asks.add(order, flags),
121 }
122
123 self.increment(sequence, ts_event);
124 }
125
126 pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
128 let order = pre_process_order(self.book_type, order, flags);
129 match order.side.as_specified() {
130 OrderSideSpecified::Buy => self.bids.update(order, flags),
131 OrderSideSpecified::Sell => self.asks.update(order, flags),
132 }
133
134 self.increment(sequence, ts_event);
135 }
136
137 pub fn delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
139 let order = pre_process_order(self.book_type, order, flags);
140 match order.side.as_specified() {
141 OrderSideSpecified::Buy => self.bids.delete(order, sequence, ts_event),
142 OrderSideSpecified::Sell => self.asks.delete(order, sequence, ts_event),
143 }
144
145 self.increment(sequence, ts_event);
146 }
147
148 pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
150 self.bids.clear();
151 self.asks.clear();
152 self.increment(sequence, ts_event);
153 }
154
155 pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
157 self.bids.clear();
158 self.increment(sequence, ts_event);
159 }
160
161 pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
163 self.asks.clear();
164 self.increment(sequence, ts_event);
165 }
166
167 pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
175 if self.book_type == BookType::L1_MBP {
176 return None;
178 }
179
180 let (Some(best_bid), Some(best_ask)) = (self.best_bid_price(), self.best_ask_price())
181 else {
182 return None;
183 };
184
185 if best_bid <= best_ask {
186 return None;
187 }
188
189 let mut removed_levels = Vec::new();
190 let mut clear_bids = false;
191 let mut clear_asks = false;
192
193 match side {
194 Some(OrderSide::Buy) => clear_bids = true,
195 Some(OrderSide::Sell) => clear_asks = true,
196 _ => {
197 clear_bids = true;
198 clear_asks = true;
199 }
200 }
201
202 let mut ask_prices_to_remove = Vec::new();
204 if clear_asks {
205 for bp in self.asks.levels.keys() {
206 if bp.value <= best_bid {
207 ask_prices_to_remove.push(*bp);
208 } else {
209 break;
210 }
211 }
212 }
213
214 let mut bid_prices_to_remove = Vec::new();
216 if clear_bids {
217 for bp in self.bids.levels.keys() {
218 if bp.value >= best_ask {
219 bid_prices_to_remove.push(*bp);
220 } else {
221 break;
222 }
223 }
224 }
225
226 if ask_prices_to_remove.is_empty() && bid_prices_to_remove.is_empty() {
227 return None;
228 }
229
230 let bid_count = bid_prices_to_remove.len();
231 let ask_count = ask_prices_to_remove.len();
232
233 for price in bid_prices_to_remove {
235 if let Some(level) = self.bids.remove_level(price) {
236 removed_levels.push(level);
237 }
238 }
239
240 for price in ask_prices_to_remove {
242 if let Some(level) = self.asks.remove_level(price) {
243 removed_levels.push(level);
244 }
245 }
246
247 self.increment(self.sequence, self.ts_last);
248
249 if removed_levels.is_empty() {
250 None
251 } else {
252 let total_orders: usize = removed_levels.iter().map(|level| level.orders.len()).sum();
253
254 log::warn!(
255 "Removed {} stale/crossed levels (instrument_id={}, bid_levels={}, ask_levels={}, total_orders={}), book was crossed with best_bid={} > best_ask={}",
256 removed_levels.len(),
257 self.instrument_id,
258 bid_count,
259 ask_count,
260 total_orders,
261 best_bid,
262 best_ask
263 );
264
265 Some(removed_levels)
266 }
267 }
268
269 pub fn apply_delta(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
278 if delta.instrument_id != self.instrument_id {
279 return Err(BookIntegrityError::InstrumentMismatch(
280 self.instrument_id,
281 delta.instrument_id,
282 ));
283 }
284 self.apply_delta_unchecked(delta)
285 }
286
287 pub fn apply_delta_unchecked(
300 &mut self,
301 delta: &OrderBookDelta,
302 ) -> Result<(), BookIntegrityError> {
303 let mut order = delta.order;
304
305 if order.side == OrderSide::NoOrderSide && order.order_id != 0 {
306 match self.resolve_no_side_order(order) {
307 Ok(resolved) => order = resolved,
308 Err(BookIntegrityError::OrderNotFoundForSideResolution(order_id)) => {
309 match delta.action {
310 BookAction::Add => return Err(BookIntegrityError::NoOrderSide),
311 BookAction::Update | BookAction::Delete => {
312 log::debug!(
314 "Skipping {:?} for unknown order_id={order_id}",
315 delta.action
316 );
317 return Ok(());
318 }
319 BookAction::Clear => {} }
321 }
322 Err(e) => return Err(e),
323 }
324 }
325
326 if order.side == OrderSide::NoOrderSide && delta.action != BookAction::Clear {
327 return Err(BookIntegrityError::NoOrderSide);
328 }
329
330 let flags = delta.flags;
331 let sequence = delta.sequence;
332 let ts_event = delta.ts_event;
333
334 match delta.action {
335 BookAction::Add => self.add(order, flags, sequence, ts_event),
336 BookAction::Update => self.update(order, flags, sequence, ts_event),
337 BookAction::Delete => self.delete(order, flags, sequence, ts_event),
338 BookAction::Clear => self.clear(sequence, ts_event),
339 }
340
341 Ok(())
342 }
343
344 pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) -> Result<(), BookIntegrityError> {
352 if deltas.instrument_id != self.instrument_id {
353 return Err(BookIntegrityError::InstrumentMismatch(
354 self.instrument_id,
355 deltas.instrument_id,
356 ));
357 }
358 self.apply_deltas_unchecked(deltas)
359 }
360
361 pub fn apply_deltas_unchecked(
369 &mut self,
370 deltas: &OrderBookDeltas,
371 ) -> Result<(), BookIntegrityError> {
372 for delta in &deltas.deltas {
373 self.apply_delta_unchecked(delta)?;
374 }
375 Ok(())
376 }
377
378 #[must_use]
392 pub fn to_deltas(&self, ts_event: UnixNanos, ts_init: UnixNanos) -> OrderBookDeltas {
393 let mut deltas = Vec::new();
394
395 let total_orders = self.bids(None).map(|level| level.len()).sum::<usize>()
396 + self.asks(None).map(|level| level.len()).sum::<usize>();
397
398 let mut clear = OrderBookDelta::clear(self.instrument_id, self.sequence, ts_event, ts_init);
400 if total_orders == 0 {
401 clear.flags |= RecordFlag::F_LAST as u8;
402 }
403 deltas.push(clear);
404
405 let mut order_count = 0;
406
407 for level in self.bids(None) {
409 for order in level.iter() {
410 order_count += 1;
411 let flags = if order_count == total_orders {
412 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
413 } else {
414 RecordFlag::F_SNAPSHOT as u8
415 };
416
417 deltas.push(OrderBookDelta::new(
418 self.instrument_id,
419 BookAction::Add,
420 *order,
421 flags,
422 self.sequence,
423 ts_event,
424 ts_init,
425 ));
426 }
427 }
428
429 for level in self.asks(None) {
431 for order in level.iter() {
432 order_count += 1;
433 let flags = if order_count == total_orders {
434 RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
435 } else {
436 RecordFlag::F_SNAPSHOT as u8
437 };
438
439 deltas.push(OrderBookDelta::new(
440 self.instrument_id,
441 BookAction::Add,
442 *order,
443 flags,
444 self.sequence,
445 ts_event,
446 ts_init,
447 ));
448 }
449 }
450
451 OrderBookDeltas::new(self.instrument_id, deltas)
452 }
453
454 pub fn apply_depth(&mut self, depth: &OrderBookDepth10) -> Result<(), BookIntegrityError> {
460 if depth.instrument_id != self.instrument_id {
461 return Err(BookIntegrityError::InstrumentMismatch(
462 self.instrument_id,
463 depth.instrument_id,
464 ));
465 }
466 self.apply_depth_unchecked(depth)
467 }
468
469 pub fn apply_depth_unchecked(
477 &mut self,
478 depth: &OrderBookDepth10,
479 ) -> Result<(), BookIntegrityError> {
480 self.bids.clear();
481 self.asks.clear();
482
483 for order in depth.bids {
484 if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
486 continue;
487 }
488
489 if order.side != OrderSide::Buy {
490 debug_assert_eq!(
491 order.side,
492 OrderSide::Buy,
493 "Bid order must have Buy side, was {:?}",
494 order.side
495 );
496 log::warn!(
497 "Skipping bid order with wrong side {:?} (instrument_id={})",
498 order.side,
499 self.instrument_id
500 );
501 continue;
502 }
503
504 let order = pre_process_order(self.book_type, order, depth.flags);
505 self.bids.add(order, depth.flags);
506 }
507
508 for order in depth.asks {
509 if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
511 continue;
512 }
513
514 if order.side != OrderSide::Sell {
515 debug_assert_eq!(
516 order.side,
517 OrderSide::Sell,
518 "Ask order must have Sell side, was {:?}",
519 order.side
520 );
521 log::warn!(
522 "Skipping ask order with wrong side {:?} (instrument_id={})",
523 order.side,
524 self.instrument_id
525 );
526 continue;
527 }
528
529 let order = pre_process_order(self.book_type, order, depth.flags);
530 self.asks.add(order, depth.flags);
531 }
532
533 self.increment(depth.sequence, depth.ts_event);
534
535 Ok(())
536 }
537
538 fn resolve_no_side_order(&self, mut order: BookOrder) -> Result<BookOrder, BookIntegrityError> {
539 let resolved_side = self
540 .bids
541 .cache
542 .get(&order.order_id)
543 .or_else(|| self.asks.cache.get(&order.order_id))
544 .map(|book_price| match book_price.side {
545 OrderSideSpecified::Buy => OrderSide::Buy,
546 OrderSideSpecified::Sell => OrderSide::Sell,
547 })
548 .ok_or(BookIntegrityError::OrderNotFoundForSideResolution(
549 order.order_id,
550 ))?;
551
552 order.side = resolved_side;
553
554 Ok(order)
555 }
556
557 pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
559 self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
560 }
561
562 pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
564 self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
565 }
566
567 pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
569 self.bids(depth)
570 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
571 .collect()
572 }
573
574 pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
576 self.asks(depth)
577 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
578 .collect()
579 }
580
581 pub fn group_bids(
583 &self,
584 group_size: Decimal,
585 depth: Option<usize>,
586 ) -> IndexMap<Decimal, Decimal> {
587 group_levels(self.bids(None), group_size, depth, true)
588 }
589
590 pub fn group_asks(
592 &self,
593 group_size: Decimal,
594 depth: Option<usize>,
595 ) -> IndexMap<Decimal, Decimal> {
596 group_levels(self.asks(None), group_size, depth, false)
597 }
598
599 pub fn bids_filtered_as_map(
605 &self,
606 depth: Option<usize>,
607 own_book: Option<&OwnOrderBook>,
608 status: Option<AHashSet<OrderStatus>>,
609 accepted_buffer_ns: Option<u64>,
610 now: Option<u64>,
611 ) -> IndexMap<Decimal, Decimal> {
612 let mut public_map = self
613 .bids(depth)
614 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
615 .collect::<IndexMap<Decimal, Decimal>>();
616
617 if let Some(own_book) = own_book {
618 filter_quantities(
619 &mut public_map,
620 own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
621 );
622 }
623
624 public_map
625 }
626
627 pub fn asks_filtered_as_map(
633 &self,
634 depth: Option<usize>,
635 own_book: Option<&OwnOrderBook>,
636 status: Option<AHashSet<OrderStatus>>,
637 accepted_buffer_ns: Option<u64>,
638 now: Option<u64>,
639 ) -> IndexMap<Decimal, Decimal> {
640 let mut public_map = self
641 .asks(depth)
642 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
643 .collect::<IndexMap<Decimal, Decimal>>();
644
645 if let Some(own_book) = own_book {
646 filter_quantities(
647 &mut public_map,
648 own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
649 );
650 }
651
652 public_map
653 }
654
655 pub fn group_bids_filtered(
661 &self,
662 group_size: Decimal,
663 depth: Option<usize>,
664 own_book: Option<&OwnOrderBook>,
665 status: Option<AHashSet<OrderStatus>>,
666 accepted_buffer_ns: Option<u64>,
667 now: Option<u64>,
668 ) -> IndexMap<Decimal, Decimal> {
669 let mut public_map = group_levels(self.bids(None), group_size, depth, true);
670
671 if let Some(own_book) = own_book {
672 filter_quantities(
673 &mut public_map,
674 own_book.bid_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
675 );
676 }
677
678 public_map
679 }
680
681 pub fn group_asks_filtered(
687 &self,
688 group_size: Decimal,
689 depth: Option<usize>,
690 own_book: Option<&OwnOrderBook>,
691 status: Option<AHashSet<OrderStatus>>,
692 accepted_buffer_ns: Option<u64>,
693 now: Option<u64>,
694 ) -> IndexMap<Decimal, Decimal> {
695 let mut public_map = group_levels(self.asks(None), group_size, depth, false);
696
697 if let Some(own_book) = own_book {
698 filter_quantities(
699 &mut public_map,
700 own_book.ask_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
701 );
702 }
703
704 public_map
705 }
706
707 #[must_use]
709 pub fn has_bid(&self) -> bool {
710 self.bids.top().is_some_and(|top| !top.orders.is_empty())
711 }
712
713 #[must_use]
715 pub fn has_ask(&self) -> bool {
716 self.asks.top().is_some_and(|top| !top.orders.is_empty())
717 }
718
719 #[must_use]
721 pub fn best_bid_price(&self) -> Option<Price> {
722 self.bids.top().map(|top| top.price.value)
723 }
724
725 #[must_use]
727 pub fn best_ask_price(&self) -> Option<Price> {
728 self.asks.top().map(|top| top.price.value)
729 }
730
731 #[must_use]
733 pub fn best_bid_size(&self) -> Option<Quantity> {
734 self.bids
735 .top()
736 .and_then(|top| top.first().map(|order| order.size))
737 }
738
739 #[must_use]
741 pub fn best_ask_size(&self) -> Option<Quantity> {
742 self.asks
743 .top()
744 .and_then(|top| top.first().map(|order| order.size))
745 }
746
747 #[must_use]
749 pub fn spread(&self) -> Option<f64> {
750 match (self.best_ask_price(), self.best_bid_price()) {
751 (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
752 _ => None,
753 }
754 }
755
756 #[must_use]
758 pub fn midpoint(&self) -> Option<f64> {
759 match (self.best_ask_price(), self.best_bid_price()) {
760 (Some(ask), Some(bid)) => Some((ask.as_f64() + bid.as_f64()) / 2.0),
761 _ => None,
762 }
763 }
764
765 #[must_use]
767 pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
768 let levels = match order_side.as_specified() {
769 OrderSideSpecified::Buy => &self.asks.levels,
770 OrderSideSpecified::Sell => &self.bids.levels,
771 };
772
773 analysis::get_avg_px_for_quantity(qty, levels)
774 }
775
776 #[must_use]
778 pub fn get_avg_px_qty_for_exposure(
779 &self,
780 target_exposure: Quantity,
781 order_side: OrderSide,
782 ) -> (f64, f64, f64) {
783 let levels = match order_side.as_specified() {
784 OrderSideSpecified::Buy => &self.asks.levels,
785 OrderSideSpecified::Sell => &self.bids.levels,
786 };
787
788 analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
789 }
790
791 #[must_use]
796 pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
797 let side = order_side.as_specified();
798 let levels = match side {
799 OrderSideSpecified::Buy => &self.asks.levels,
800 OrderSideSpecified::Sell => &self.bids.levels,
801 };
802
803 analysis::get_quantity_for_price(price, side, levels)
804 }
805
806 #[must_use]
811 pub fn get_quantity_at_level(
812 &self,
813 price: Price,
814 order_side: OrderSide,
815 size_precision: u8,
816 ) -> Quantity {
817 let side = order_side.as_specified();
818
819 let (levels, book_side) = match side {
822 OrderSideSpecified::Buy => (&self.asks.levels, OrderSideSpecified::Sell),
823 OrderSideSpecified::Sell => (&self.bids.levels, OrderSideSpecified::Buy),
824 };
825
826 let book_price = BookPrice::new(price, book_side);
827
828 levels
829 .get(&book_price)
830 .map_or(Quantity::zero(size_precision), |level| {
831 Quantity::from_raw(level.size_raw(), size_precision)
832 })
833 }
834
835 #[must_use]
837 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
838 match order.side.as_specified() {
839 OrderSideSpecified::Buy => self.asks.simulate_fills(order),
840 OrderSideSpecified::Sell => self.bids.simulate_fills(order),
841 }
842 }
843
844 #[must_use]
850 pub fn get_all_crossed_levels(
851 &self,
852 order_side: OrderSide,
853 price: Price,
854 size_precision: u8,
855 ) -> Vec<(Price, Quantity)> {
856 let side = order_side.as_specified();
857 let levels = match side {
858 OrderSideSpecified::Buy => &self.asks.levels,
859 OrderSideSpecified::Sell => &self.bids.levels,
860 };
861
862 analysis::get_levels_for_price(price, side, levels, size_precision)
863 }
864
865 #[must_use]
867 pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
868 pprint_book(self, num_levels, group_size)
869 }
870
871 fn increment(&mut self, sequence: u64, ts_event: UnixNanos) {
872 if sequence > 0 && sequence < self.sequence {
873 log::warn!(
874 "Out-of-order update: sequence {} < {} (instrument_id={})",
875 sequence,
876 self.sequence,
877 self.instrument_id
878 );
879 }
880 if ts_event < self.ts_last {
881 log::warn!(
882 "Out-of-order update: ts_event {} < {} (instrument_id={})",
883 ts_event,
884 self.ts_last,
885 self.instrument_id
886 );
887 }
888
889 if self.update_count == u64::MAX {
890 debug_assert!(
891 self.update_count < u64::MAX,
892 "Update count at u64::MAX limit (about to overflow): {}",
893 self.update_count
894 );
895 log::warn!(
896 "Update count at u64::MAX: {} (instrument_id={})",
897 self.update_count,
898 self.instrument_id
899 );
900 }
901
902 self.sequence = sequence.max(self.sequence);
904 self.ts_last = ts_event.max(self.ts_last);
905 self.update_count = self.update_count.saturating_add(1);
906 }
907
908 pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
914 if self.book_type != BookType::L1_MBP {
915 return Err(InvalidBookOperation::Update(self.book_type));
916 }
917
918 if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
920 log::warn!(
921 "Quote has crossed prices: bid={}, ask={} for {}",
922 quote.bid_price,
923 quote.ask_price,
924 self.instrument_id
925 );
926 }
927
928 let bid = BookOrder::new(
929 OrderSide::Buy,
930 quote.bid_price,
931 quote.bid_size,
932 OrderSide::Buy as u64,
933 );
934
935 let ask = BookOrder::new(
936 OrderSide::Sell,
937 quote.ask_price,
938 quote.ask_size,
939 OrderSide::Sell as u64,
940 );
941
942 self.update_book_bid(bid, quote.ts_event);
943 self.update_book_ask(ask, quote.ts_event);
944
945 self.increment(self.sequence.saturating_add(1), quote.ts_event);
946
947 Ok(())
948 }
949
950 pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
956 if self.book_type != BookType::L1_MBP {
957 return Err(InvalidBookOperation::Update(self.book_type));
958 }
959
960 debug_assert!(
962 trade.price.raw != PRICE_UNDEF && trade.price.raw != PRICE_ERROR,
963 "Trade has invalid/uninitialized price: {}",
964 trade.price
965 );
966
967 debug_assert!(
969 trade.size.is_positive(),
970 "Trade has non-positive size: {}",
971 trade.size
972 );
973
974 let bid = BookOrder::new(
975 OrderSide::Buy,
976 trade.price,
977 trade.size,
978 OrderSide::Buy as u64,
979 );
980
981 let ask = BookOrder::new(
982 OrderSide::Sell,
983 trade.price,
984 trade.size,
985 OrderSide::Sell as u64,
986 );
987
988 self.update_book_bid(bid, trade.ts_event);
989 self.update_book_ask(ask, trade.ts_event);
990
991 self.increment(self.sequence.saturating_add(1), trade.ts_event);
992
993 Ok(())
994 }
995
996 fn update_book_bid(&mut self, order: BookOrder, ts_event: UnixNanos) {
997 if let Some(top_bids) = self.bids.top()
998 && let Some(top_bid) = top_bids.first()
999 {
1000 self.bids.remove_order(top_bid.order_id, 0, ts_event);
1001 }
1002 self.bids.add(order, 0); }
1004
1005 fn update_book_ask(&mut self, order: BookOrder, ts_event: UnixNanos) {
1006 if let Some(top_asks) = self.asks.top()
1007 && let Some(top_ask) = top_asks.first()
1008 {
1009 self.asks.remove_order(top_ask.order_id, 0, ts_event);
1010 }
1011 self.asks.add(order, 0); }
1013}
1014
1015fn filter_quantities(
1016 public_map: &mut IndexMap<Decimal, Decimal>,
1017 own_map: IndexMap<Decimal, Decimal>,
1018) {
1019 for (price, own_size) in own_map {
1020 if let Some(public_size) = public_map.get_mut(&price) {
1021 *public_size = (*public_size - own_size).max(Decimal::ZERO);
1022
1023 if *public_size == Decimal::ZERO {
1024 public_map.shift_remove(&price);
1025 }
1026 }
1027 }
1028}
1029
1030fn group_levels<'a>(
1031 levels_iter: impl Iterator<Item = &'a BookLevel>,
1032 group_size: Decimal,
1033 depth: Option<usize>,
1034 is_bid: bool,
1035) -> IndexMap<Decimal, Decimal> {
1036 if group_size <= Decimal::ZERO {
1037 log::error!("Invalid group_size: {group_size}, must be positive; returning empty map");
1038 return IndexMap::new();
1039 }
1040
1041 let mut levels = IndexMap::new();
1042 let depth = depth.unwrap_or(usize::MAX);
1043
1044 for level in levels_iter {
1045 let price = level.price.value.as_decimal();
1046 let grouped_price = if is_bid {
1047 (price / group_size).floor() * group_size
1048 } else {
1049 (price / group_size).ceil() * group_size
1050 };
1051 let size = level.size_decimal();
1052
1053 levels
1054 .entry(grouped_price)
1055 .and_modify(|total| *total += size)
1056 .or_insert(size);
1057
1058 if levels.len() > depth {
1059 levels.pop();
1060 break;
1061 }
1062 }
1063
1064 levels
1065}