Skip to main content

nautilus_model/orderbook/
book.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A performant, generic, multi-purpose order book.
17
18use 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/// Provides a high-performance, versatile order book.
44///
45/// Maintains buy (bid) and sell (ask) orders in price-time priority, supporting multiple
46/// market data formats:
47/// - L3 (MBO): Market By Order - tracks individual orders with unique IDs.
48/// - L2 (MBP): Market By Price - aggregates orders at each price level.
49/// - L1 (MBP): Top-of-Book - maintains only the best bid and ask prices.
50#[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    /// The instrument ID for the order book.
61    pub instrument_id: InstrumentId,
62    /// The order book type (MBP types will aggregate orders).
63    pub book_type: BookType,
64    /// The last event sequence number for the order book.
65    pub sequence: u64,
66    /// The timestamp of the last event applied to the order book.
67    pub ts_last: UnixNanos,
68    /// The current count of updates applied to the order book.
69    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    /// Creates a new [`OrderBook`] instance.
97    #[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    /// Resets the order book to its initial empty state.
111    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    /// Adds an order to the book after preprocessing based on book type.
120    ///
121    /// # Panics
122    ///
123    /// Panics if `order.side` is `None`.
124    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    /// Updates an existing order in the book after preprocessing based on book type.
135    ///
136    /// # Panics
137    ///
138    /// Panics if `order.side` is `None`.
139    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    /// Deletes an order from the book after preprocessing based on book type.
150    ///
151    /// # Panics
152    ///
153    /// Panics if `order.side` is `None`.
154    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    /// Clears all orders from both sides of the book.
165    ///
166    /// A full clear uses its `sequence` as the new sequence high-water.
167    /// `clear_bids` and `clear_asks` preserve the current high-water.
168    pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
169        self.clear_with_flags(sequence, ts_event, 0);
170    }
171
172    /// Clears all bid orders from the book.
173    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    /// Clears all ask orders from the book.
179    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        // Check the clear against the old high-water before using its sequence as the new one
190        if !RecordFlag::F_SNAPSHOT.matches(flags) {
191            self.sequence = sequence;
192        }
193    }
194
195    /// Removes overlapped bid/ask levels when the book is strictly crossed (best bid > best ask)
196    ///
197    /// - Acts only when both sides exist and the book is crossed.
198    /// - Deletes by removing whole price levels via the ladder API to preserve invariants.
199    /// - `side=None` clears both overlapped ranges (conservative, may widen spread).
200    /// - `side=Buy` clears crossed bids only; side=Sell clears crossed asks only.
201    /// - Returns removed price levels (crossed bids first, then crossed asks), or None if nothing removed.
202    pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
203        if self.book_type == BookType::L1_MBP {
204            // L1_MBP maintains a single top-of-book price per side; nothing to do
205            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        // Collect prices to remove for asks (prices <= best_bid)
225        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        // Collect prices to remove for bids (prices >= best_ask)
238        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        // Remove and collect bid levels
258        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        // Remove and collect ask levels
265        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    /// Applies a single order book delta operation.
294    ///
295    /// # Errors
296    ///
297    /// Returns an error if:
298    /// - The delta's instrument ID does not match this book's instrument ID.
299    /// - An `Add` is given with no side, either explicitly or because the cache lookup failed.
300    /// - An `Add` with no side matches an order ID on both sides of the book.
301    /// - After resolution the delta still has no side but its action is not `Clear`.
302    ///
303    /// # Notes
304    ///
305    /// An ambiguous no-side `Update` or `Delete` is skipped with a warning.
306    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    /// Applies a single order book delta operation without instrument ID validation.
317    ///
318    /// "Unchecked" refers only to skipping the instrument ID match - other validations
319    /// still apply and errors are still returned. This exists because `Ustr` interning
320    /// is not shared across FFI boundaries, causing pointer-based equality to fail even
321    /// when string values match. This limitation may be resolved in a future version.
322    ///
323    /// # Errors
324    ///
325    /// Returns an error if:
326    /// - An `Add` is given with no side, either explicitly or because the cache lookup failed.
327    /// - An `Add` with no side matches an order ID on both sides of the book.
328    /// - After resolution the delta still has no side but its action is not `Clear`.
329    ///
330    /// # Notes
331    ///
332    /// An ambiguous no-side `Update` or `Delete` is skipped with a warning.
333    pub fn apply_delta_unchecked(
334        &mut self,
335        delta: &OrderBookDelta,
336    ) -> Result<(), BookIntegrityError> {
337        // No batch wraps a standalone delta, so it reports its own stale metadata
338        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                            // Already consistent
353                            log::debug!(
354                                "Skipping {:?} for unknown order_id={order_id}",
355                                delta.action
356                            );
357                            return Ok(());
358                        }
359                        BookAction::Clear => {} // Won't hit this (order_id != 0)
360                    }
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 => {} // Won't hit this (order_id != 0)
375                    }
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    /// Applies multiple order book delta operations.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error if:
404    /// - The deltas' instrument ID does not match this book's instrument ID.
405    /// - Any individual delta application fails (see [`Self::apply_delta`]).
406    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    /// Applies multiple order book delta operations without instrument ID validation.
417    ///
418    /// See [`Self::apply_delta_unchecked`] for details on why this function exists.
419    ///
420    /// # Errors
421    ///
422    /// Returns an error if any individual delta application fails.
423    ///
424    /// # Notes
425    ///
426    /// A snapshot batch carrying metadata earlier than the last applied update is reported once
427    /// here rather than per delta, since every delta in the batch shares the snapshot sequence and
428    /// timestamp.
429    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    // Reports the incoming snapshot, so the result does not depend on whether every delta in it
448    // reaches the book
449    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    /// Creates an `OrderBookDeltas` snapshot from the current order book state.
482    ///
483    /// This is the reverse operation of `apply_deltas`: it converts the current book state
484    /// back into a snapshot format with a `Clear` delta followed by `Add` deltas for all orders.
485    ///
486    /// # Parameters
487    ///
488    /// * `ts_event` - UNIX timestamp (nanoseconds) when the book event occurred.
489    /// * `ts_init` - UNIX timestamp (nanoseconds) when the instance was created.
490    ///
491    /// # Returns
492    ///
493    /// An `OrderBookDeltas` containing a snapshot of the current order book state.
494    #[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        // Set F_LAST on clear when book is empty so buffered consumers flush
502        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    /// Replaces current book state with a depth snapshot.
536    ///
537    /// # Errors
538    ///
539    /// Returns an error if the depth's instrument ID does not match this book's instrument ID.
540    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    /// Replaces current book state with a depth snapshot without instrument ID validation.
551    ///
552    /// See [`Self::apply_delta_unchecked`] for details on why this function exists.
553    ///
554    /// This function does not return errors, but returns `Result` for API consistency.
555    #[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            // Skip padding entries
568            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            // Skip padding entries
593            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        // Depth increments once per snapshot, so there is no per-level flood to suppress and
617        // the existing single warning is already the one-per-rebuild signal
618        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        // For L2 books the order ID is a pure price hash, so in a locked market the
628        // same ID exists on both sides and the side cannot be resolved safely.
629        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    /// Returns an iterator over bid price levels.
647    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    /// Returns an iterator over ask price levels.
652    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    /// Returns bid price levels as a map of price to size.
657    #[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    /// Returns ask price levels as a map of price to size.
665    #[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    /// Groups bid quantities by price into buckets, limited by depth.
673    #[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    /// Groups ask quantities by price into buckets, limited by depth.
683    #[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    /// Maps bid prices to total public size per level, excluding own orders up to a depth limit.
693    ///
694    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
695    /// When `now` is provided, only subtracts orders whose acceptance time plus
696    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
697    /// filtering is disabled.
698    ///
699    /// # Panics
700    ///
701    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
702    #[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    /// Maps ask prices to total public size per level, excluding own orders up to a depth limit.
727    ///
728    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
729    /// When `now` is provided, only subtracts orders whose acceptance time plus
730    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
731    /// filtering is disabled.
732    ///
733    /// # Panics
734    ///
735    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
736    #[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    /// Returns a filtered [`OrderBook`] view with own sizes subtracted from public levels.
761    ///
762    /// # Panics
763    ///
764    /// Panics if `self` and `own_book` have different instrument IDs.
765    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
766    ///
767    /// [`Self::filtered_view_checked`] for fallible construction.
768    #[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    /// Fallible version of [`Self::filtered_view`].
782    ///
783    /// # Errors
784    ///
785    /// Returns an error if `self` and `own_book` have different instrument IDs.
786    ///
787    /// # Panics
788    ///
789    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
790    /// Panics if `Price::from_decimal` or `Quantity::from_decimal` fails when
791    /// reconstructing filtered levels.
792    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    /// Groups bid quantities into price buckets, truncating to a maximum depth, excluding own orders.
857    ///
858    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
859    /// When `now` is provided, only subtracts orders whose acceptance time plus
860    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
861    /// filtering is disabled.
862    ///
863    /// # Panics
864    ///
865    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
866    #[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            // Leave own buckets untruncated so better-priced ones cannot evict those within depth
880            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    /// Groups ask quantities into price buckets, truncating to a maximum depth, excluding own orders.
890    ///
891    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
892    /// When `now` is provided, only subtracts orders whose acceptance time plus
893    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
894    /// filtering is disabled.
895    ///
896    /// # Panics
897    ///
898    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
899    #[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            // Leave own buckets untruncated so better-priced ones cannot evict those within depth
913            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    /// Returns true if the book has any bid orders.
923    #[must_use]
924    pub fn has_bid(&self) -> bool {
925        self.bids.top().is_some_and(|top| !top.orders.is_empty())
926    }
927
928    /// Returns true if the book has any ask orders.
929    #[must_use]
930    pub fn has_ask(&self) -> bool {
931        self.asks.top().is_some_and(|top| !top.orders.is_empty())
932    }
933
934    /// Returns the best bid price if available.
935    #[must_use]
936    pub fn best_bid_price(&self) -> Option<Price> {
937        self.bids.top().map(|top| top.price.value)
938    }
939
940    /// Returns the best ask price if available.
941    #[must_use]
942    pub fn best_ask_price(&self) -> Option<Price> {
943        self.asks.top().map(|top| top.price.value)
944    }
945
946    /// Returns the size at the best bid price if available.
947    #[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    /// Returns the size at the best ask price if available.
955    #[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    /// Returns the spread between best ask and bid prices if both exist.
963    #[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    /// Returns the midpoint between best ask and bid prices if both exist.
972    #[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    /// Calculates the average price to fill the specified quantity.
981    #[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    /// Calculates the worst (last-touched) price to fill the specified quantity.
992    #[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    /// Calculates average price and quantity for target exposure.
1003    ///
1004    /// Returns (average price, quantity, last-touched price).
1005    #[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    /// Returns the cumulative quantity available at or better than the specified price.
1020    ///
1021    /// For a BUY order, sums ask levels at or below the price.
1022    /// For a SELL order, sums bid levels at or above the price.
1023    #[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    /// Returns the quantity at a specific price level only, or 0 if no level exists.
1034    ///
1035    /// Unlike `get_quantity_for_price` which returns cumulative quantity across
1036    /// multiple levels, this returns only the quantity at the exact price level.
1037    ///
1038    /// The Python binding raises `ValueError` for an unsupported `size_precision` or an
1039    /// aggregated level size that cannot be represented as a [`Quantity`].
1040    ///
1041    /// # Panics
1042    ///
1043    /// Panics if:
1044    /// - `size_precision` exceeds the supported fixed precision.
1045    /// - The aggregated level size cannot be represented as a [`Quantity`].
1046    #[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    /// Returns the quantity at a specific price level only, or zero if no level exists.
1058    ///
1059    /// # Errors
1060    ///
1061    /// Returns an error if:
1062    /// - `size_precision` exceeds the supported fixed precision.
1063    /// - The aggregated level size cannot be represented as a [`Quantity`].
1064    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        // For a BUY order, we look in asks (sell side); for SELL order, we look in bids (buy side)
1073        // BookPrice keys use the side of orders IN the book, not the incoming order side
1074        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    /// Returns the orders at a specific price level in FIFO order, or an empty vec if no level exists.
1088    ///
1089    /// Follows the same side convention as `get_quantity_at_level`: for a BUY
1090    /// order this reads the asks (sell side), for a SELL order the bids.
1091    #[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    /// Simulates fills for an order, returning list of (price, quantity) tuples.
1106    ///
1107    /// # Panics
1108    ///
1109    /// Panics if `order.side` is `None`.
1110    #[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    /// Returns all price levels crossed by an order at the given price and side.
1119    ///
1120    /// Unlike `simulate_fills`, this returns ALL crossed levels regardless of
1121    /// order quantity. Used when liquidity consumption tracking needs visibility
1122    /// into all available levels.
1123    ///
1124    /// The Python binding raises `ValueError` for an unsupported `size_precision` or an
1125    /// aggregated level size that cannot be represented as a [`Quantity`].
1126    ///
1127    /// # Panics
1128    ///
1129    /// Panics if:
1130    /// - `size_precision` exceeds the supported fixed precision.
1131    /// - An aggregated level size cannot be represented as a [`Quantity`].
1132    #[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    /// Returns all price levels crossed by an order at the given price and side.
1144    ///
1145    /// # Errors
1146    ///
1147    /// Returns an error if:
1148    /// - `size_precision` exceeds the supported fixed precision.
1149    /// - An aggregated level size cannot be represented as a [`Quantity`].
1150    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    /// Return a formatted string representation of the order book.
1165    #[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        // A snapshot rebuild legitimately carries metadata behind the last applied update
1172        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        // High-water mark prevents metadata regression from out-of-order updates
1206        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    /// Updates L1 book state from a quote tick. Only valid for `L1_MBP` book type.
1212    ///
1213    /// # Errors
1214    ///
1215    /// Returns an error if the book type is not `L1_MBP`.
1216    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        // Crossed quotes (bid > ask) can occur temporarily in volatile markets
1232        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    /// Updates L1 book state from a trade tick. Only valid for `L1_MBP` book type.
1264    ///
1265    /// # Errors
1266    ///
1267    /// Returns an error if the book type is not `L1_MBP`.
1268    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        // Prices can be zero or negative for certain instruments (options, spreads)
1284        debug_assert!(
1285            !trade.price.is_undefined() && !trade.price.is_error(),
1286            "Trade has invalid/uninitialized price: {}",
1287            trade.price
1288        );
1289
1290        // TradeTick enforces positive size at construction, but assert as sanity check
1291        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    /// Replays `deltas` through a fresh book of the given type and returns
1328    /// a [`QuoteTick`] for every best-bid/ask price change.
1329    ///
1330    /// # Panics
1331    ///
1332    /// Panics if `deltas` is empty.
1333    #[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}