nautilus_model/orderbook/
book.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::{collections::HashSet, fmt::Display};
19
20use indexmap::IndexMap;
21use nautilus_core::UnixNanos;
22use rust_decimal::Decimal;
23
24use super::{
25    aggregation::pre_process_order, analysis, display::pprint_book, level::BookLevel,
26    own::OwnOrderBook,
27};
28use crate::{
29    data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
30    enums::{BookAction, BookType, OrderSide, OrderSideSpecified, OrderStatus},
31    identifiers::InstrumentId,
32    orderbook::{InvalidBookOperation, ladder::BookLadder},
33    types::{
34        Price, Quantity,
35        price::{PRICE_ERROR, PRICE_UNDEF},
36    },
37};
38
39/// Provides a high-performance, versatile order book.
40///
41/// Maintains buy (bid) and sell (ask) orders in price-time priority, supporting multiple
42/// market data formats:
43/// - L3 (MBO): Market By Order - tracks individual orders with unique IDs.
44/// - L2 (MBP): Market By Price - aggregates orders at each price level.
45/// - L1 (MBP): Top-of-Book - maintains only the best bid and ask prices.
46#[derive(Clone, Debug)]
47#[cfg_attr(
48    feature = "python",
49    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
50)]
51pub struct OrderBook {
52    /// The instrument ID for the order book.
53    pub instrument_id: InstrumentId,
54    /// The order book type (MBP types will aggregate orders).
55    pub book_type: BookType,
56    /// The last event sequence number for the order book.
57    pub sequence: u64,
58    /// The timestamp of the last event applied to the order book.
59    pub ts_last: UnixNanos,
60    /// The current count of updates applied to the order book.
61    pub update_count: u64,
62    pub(crate) bids: BookLadder,
63    pub(crate) asks: BookLadder,
64}
65
66impl PartialEq for OrderBook {
67    fn eq(&self, other: &Self) -> bool {
68        self.instrument_id == other.instrument_id && self.book_type == other.book_type
69    }
70}
71
72impl Eq for OrderBook {}
73
74impl Display for OrderBook {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(
77            f,
78            "{}(instrument_id={}, book_type={}, update_count={})",
79            stringify!(OrderBook),
80            self.instrument_id,
81            self.book_type,
82            self.update_count,
83        )
84    }
85}
86
87impl OrderBook {
88    /// Creates a new [`OrderBook`] instance.
89    #[must_use]
90    pub fn new(instrument_id: InstrumentId, book_type: BookType) -> Self {
91        Self {
92            instrument_id,
93            book_type,
94            sequence: 0,
95            ts_last: UnixNanos::default(),
96            update_count: 0,
97            bids: BookLadder::new(OrderSideSpecified::Buy),
98            asks: BookLadder::new(OrderSideSpecified::Sell),
99        }
100    }
101
102    /// Resets the order book to its initial empty state.
103    pub fn reset(&mut self) {
104        self.bids.clear();
105        self.asks.clear();
106        self.sequence = 0;
107        self.ts_last = UnixNanos::default();
108        self.update_count = 0;
109    }
110
111    /// Adds an order to the book after preprocessing based on book type.
112    pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
113        let order = pre_process_order(self.book_type, order, flags);
114        match order.side.as_specified() {
115            OrderSideSpecified::Buy => self.bids.add(order),
116            OrderSideSpecified::Sell => self.asks.add(order),
117        }
118
119        self.increment(sequence, ts_event);
120    }
121
122    /// Updates an existing order in the book after preprocessing based on book type.
123    pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
124        let order = pre_process_order(self.book_type, order, flags);
125        match order.side.as_specified() {
126            OrderSideSpecified::Buy => self.bids.update(order),
127            OrderSideSpecified::Sell => self.asks.update(order),
128        }
129
130        self.increment(sequence, ts_event);
131    }
132
133    /// Deletes an order from the book after preprocessing based on book type.
134    pub fn delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
135        let order = pre_process_order(self.book_type, order, flags);
136        match order.side.as_specified() {
137            OrderSideSpecified::Buy => self.bids.delete(order, sequence, ts_event),
138            OrderSideSpecified::Sell => self.asks.delete(order, sequence, ts_event),
139        }
140
141        self.increment(sequence, ts_event);
142    }
143
144    /// Clears all orders from both sides of the book.
145    pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
146        self.bids.clear();
147        self.asks.clear();
148        self.increment(sequence, ts_event);
149    }
150
151    /// Clears all bid orders from the book.
152    pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
153        self.bids.clear();
154        self.increment(sequence, ts_event);
155    }
156
157    /// Clears all ask orders from the book.
158    pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
159        self.asks.clear();
160        self.increment(sequence, ts_event);
161    }
162
163    /// Removes overlapped bid/ask levels when the book is strictly crossed (best bid > best ask)
164    ///
165    /// - Acts only when both sides exist and the book is crossed.
166    /// - Deletes by removing whole price levels via the ladder API to preserve invariants.
167    /// - `side=None` or `NoOrderSide` clears both overlapped ranges (conservative, may widen spread).
168    /// - `side=Buy` clears crossed bids only; side=Sell clears crossed asks only.
169    /// - Returns removed price levels (crossed bids first, then crossed asks), or None if nothing removed.
170    pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
171        if self.book_type == BookType::L1_MBP {
172            // L1_MBP maintains a single top-of-book price per side; nothing to do
173            return None;
174        }
175
176        let (Some(best_bid), Some(best_ask)) = (self.best_bid_price(), self.best_ask_price())
177        else {
178            return None;
179        };
180
181        if best_bid <= best_ask {
182            return None;
183        }
184
185        let mut removed_levels = Vec::new();
186        let mut clear_bids = false;
187        let mut clear_asks = false;
188
189        match side {
190            Some(OrderSide::Buy) => clear_bids = true,
191            Some(OrderSide::Sell) => clear_asks = true,
192            _ => {
193                clear_bids = true;
194                clear_asks = true;
195            }
196        }
197
198        // Collect prices to remove for asks (prices <= best_bid)
199        let mut ask_prices_to_remove = Vec::new();
200        if clear_asks {
201            for (bp, _level) in self.asks.levels.iter() {
202                if bp.value <= best_bid {
203                    ask_prices_to_remove.push(*bp);
204                } else {
205                    break;
206                }
207            }
208        }
209
210        // Collect prices to remove for bids (prices >= best_ask)
211        let mut bid_prices_to_remove = Vec::new();
212        if clear_bids {
213            for (bp, _level) in self.bids.levels.iter() {
214                if bp.value >= best_ask {
215                    bid_prices_to_remove.push(*bp);
216                } else {
217                    break;
218                }
219            }
220        }
221
222        if ask_prices_to_remove.is_empty() && bid_prices_to_remove.is_empty() {
223            return None;
224        }
225
226        let bid_count = bid_prices_to_remove.len();
227        let ask_count = ask_prices_to_remove.len();
228
229        // Remove and collect bid levels
230        for price in bid_prices_to_remove {
231            if let Some(level) = self.bids.remove_level(price) {
232                removed_levels.push(level);
233            }
234        }
235
236        // Remove and collect ask levels
237        for price in ask_prices_to_remove {
238            if let Some(level) = self.asks.remove_level(price) {
239                removed_levels.push(level);
240            }
241        }
242
243        self.increment(self.sequence, self.ts_last);
244
245        if removed_levels.is_empty() {
246            None
247        } else {
248            let total_orders: usize = removed_levels.iter().map(|level| level.orders.len()).sum();
249
250            log::warn!(
251                "Removed {} stale/crossed levels (instrument_id={}, bid_levels={}, ask_levels={}, total_orders={}), book was crossed with best_bid={} > best_ask={}",
252                removed_levels.len(),
253                self.instrument_id,
254                bid_count,
255                ask_count,
256                total_orders,
257                best_bid,
258                best_ask
259            );
260
261            Some(removed_levels)
262        }
263    }
264
265    /// Applies a single order book delta operation.
266    pub fn apply_delta(&mut self, delta: &OrderBookDelta) {
267        let order = delta.order;
268        let flags = delta.flags;
269        let sequence = delta.sequence;
270        let ts_event = delta.ts_event;
271        match delta.action {
272            BookAction::Add => self.add(order, flags, sequence, ts_event),
273            BookAction::Update => self.update(order, flags, sequence, ts_event),
274            BookAction::Delete => self.delete(order, flags, sequence, ts_event),
275            BookAction::Clear => self.clear(sequence, ts_event),
276        }
277    }
278
279    /// Applies multiple order book delta operations.
280    pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) {
281        for delta in &deltas.deltas {
282            self.apply_delta(delta);
283        }
284    }
285
286    /// Replaces current book state with a depth snapshot.
287    pub fn apply_depth(&mut self, depth: &OrderBookDepth10) {
288        self.bids.clear();
289        self.asks.clear();
290
291        for order in depth.bids {
292            self.add(order, depth.flags, depth.sequence, depth.ts_event);
293        }
294
295        for order in depth.asks {
296            self.add(order, depth.flags, depth.sequence, depth.ts_event);
297        }
298    }
299
300    /// Returns an iterator over bid price levels.
301    pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
302        self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
303    }
304
305    /// Returns an iterator over ask price levels.
306    pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
307        self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
308    }
309
310    /// Returns bid price levels as a map of price to size.
311    pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
312        self.bids(depth)
313            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
314            .collect()
315    }
316
317    /// Returns ask price levels as a map of price to size.
318    pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
319        self.asks(depth)
320            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
321            .collect()
322    }
323
324    /// Groups bid quantities by price into buckets, limited by depth.
325    pub fn group_bids(
326        &self,
327        group_size: Decimal,
328        depth: Option<usize>,
329    ) -> IndexMap<Decimal, Decimal> {
330        group_levels(self.bids(None), group_size, depth, true)
331    }
332
333    /// Groups ask quantities by price into buckets, limited by depth.
334    pub fn group_asks(
335        &self,
336        group_size: Decimal,
337        depth: Option<usize>,
338    ) -> IndexMap<Decimal, Decimal> {
339        group_levels(self.asks(None), group_size, depth, false)
340    }
341
342    /// Maps bid prices to total public size per level, excluding own orders up to a depth limit.
343    ///
344    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
345    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
346    /// nanoseconds before `now` (defaults to now).
347    pub fn bids_filtered_as_map(
348        &self,
349        depth: Option<usize>,
350        own_book: Option<&OwnOrderBook>,
351        status: Option<HashSet<OrderStatus>>,
352        accepted_buffer_ns: Option<u64>,
353        now: Option<u64>,
354    ) -> IndexMap<Decimal, Decimal> {
355        let mut public_map = self
356            .bids(depth)
357            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
358            .collect::<IndexMap<Decimal, Decimal>>();
359
360        if let Some(own_book) = own_book {
361            filter_quantities(
362                &mut public_map,
363                own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
364            );
365        }
366
367        public_map
368    }
369
370    /// Maps ask prices to total public size per level, excluding own orders up to a depth limit.
371    ///
372    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
373    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
374    /// nanoseconds before `now` (defaults to now).
375    pub fn asks_filtered_as_map(
376        &self,
377        depth: Option<usize>,
378        own_book: Option<&OwnOrderBook>,
379        status: Option<HashSet<OrderStatus>>,
380        accepted_buffer_ns: Option<u64>,
381        now: Option<u64>,
382    ) -> IndexMap<Decimal, Decimal> {
383        let mut public_map = self
384            .asks(depth)
385            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
386            .collect::<IndexMap<Decimal, Decimal>>();
387
388        if let Some(own_book) = own_book {
389            filter_quantities(
390                &mut public_map,
391                own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
392            );
393        }
394
395        public_map
396    }
397
398    /// Groups bid quantities into price buckets, truncating to a maximum depth, excluding own orders.
399    ///
400    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
401    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
402    /// nanoseconds before `now` (defaults to now).
403    pub fn group_bids_filtered(
404        &self,
405        group_size: Decimal,
406        depth: Option<usize>,
407        own_book: Option<&OwnOrderBook>,
408        status: Option<HashSet<OrderStatus>>,
409        accepted_buffer_ns: Option<u64>,
410        now: Option<u64>,
411    ) -> IndexMap<Decimal, Decimal> {
412        let mut public_map = group_levels(self.bids(None), group_size, depth, true);
413
414        if let Some(own_book) = own_book {
415            filter_quantities(
416                &mut public_map,
417                own_book.bid_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
418            );
419        }
420
421        public_map
422    }
423
424    /// Groups ask quantities into price buckets, truncating to a maximum depth, excluding own orders.
425    ///
426    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
427    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
428    /// nanoseconds before `now` (defaults to now).
429    pub fn group_asks_filtered(
430        &self,
431        group_size: Decimal,
432        depth: Option<usize>,
433        own_book: Option<&OwnOrderBook>,
434        status: Option<HashSet<OrderStatus>>,
435        accepted_buffer_ns: Option<u64>,
436        now: Option<u64>,
437    ) -> IndexMap<Decimal, Decimal> {
438        let mut public_map = group_levels(self.asks(None), group_size, depth, false);
439
440        if let Some(own_book) = own_book {
441            filter_quantities(
442                &mut public_map,
443                own_book.ask_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
444            );
445        }
446
447        public_map
448    }
449
450    /// Returns true if the book has any bid orders.
451    #[must_use]
452    pub fn has_bid(&self) -> bool {
453        self.bids.top().is_some_and(|top| !top.orders.is_empty())
454    }
455
456    /// Returns true if the book has any ask orders.
457    #[must_use]
458    pub fn has_ask(&self) -> bool {
459        self.asks.top().is_some_and(|top| !top.orders.is_empty())
460    }
461
462    /// Returns the best bid price if available.
463    #[must_use]
464    pub fn best_bid_price(&self) -> Option<Price> {
465        self.bids.top().map(|top| top.price.value)
466    }
467
468    /// Returns the best ask price if available.
469    #[must_use]
470    pub fn best_ask_price(&self) -> Option<Price> {
471        self.asks.top().map(|top| top.price.value)
472    }
473
474    /// Returns the size at the best bid price if available.
475    #[must_use]
476    pub fn best_bid_size(&self) -> Option<Quantity> {
477        self.bids
478            .top()
479            .and_then(|top| top.first().map(|order| order.size))
480    }
481
482    /// Returns the size at the best ask price if available.
483    #[must_use]
484    pub fn best_ask_size(&self) -> Option<Quantity> {
485        self.asks
486            .top()
487            .and_then(|top| top.first().map(|order| order.size))
488    }
489
490    /// Returns the spread between best ask and bid prices if both exist.
491    #[must_use]
492    pub fn spread(&self) -> Option<f64> {
493        match (self.best_ask_price(), self.best_bid_price()) {
494            (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
495            _ => None,
496        }
497    }
498
499    /// Returns the midpoint between best ask and bid prices if both exist.
500    #[must_use]
501    pub fn midpoint(&self) -> Option<f64> {
502        match (self.best_ask_price(), self.best_bid_price()) {
503            (Some(ask), Some(bid)) => Some((ask.as_f64() + bid.as_f64()) / 2.0),
504            _ => None,
505        }
506    }
507
508    /// Calculates the average price to fill the specified quantity.
509    #[must_use]
510    pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
511        let levels = match order_side.as_specified() {
512            OrderSideSpecified::Buy => &self.asks.levels,
513            OrderSideSpecified::Sell => &self.bids.levels,
514        };
515
516        analysis::get_avg_px_for_quantity(qty, levels)
517    }
518
519    /// Calculates average price and quantity for target exposure. Returns (price, quantity, executed_exposure).
520    #[must_use]
521    pub fn get_avg_px_qty_for_exposure(
522        &self,
523        target_exposure: Quantity,
524        order_side: OrderSide,
525    ) -> (f64, f64, f64) {
526        let levels = match order_side.as_specified() {
527            OrderSideSpecified::Buy => &self.asks.levels,
528            OrderSideSpecified::Sell => &self.bids.levels,
529        };
530
531        analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
532    }
533
534    /// Returns the total quantity available at specified price level.
535    #[must_use]
536    pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
537        let levels = match order_side.as_specified() {
538            OrderSideSpecified::Buy => &self.asks.levels,
539            OrderSideSpecified::Sell => &self.bids.levels,
540        };
541
542        analysis::get_quantity_for_price(price, order_side, levels)
543    }
544
545    /// Simulates fills for an order, returning list of (price, quantity) tuples.
546    #[must_use]
547    pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
548        match order.side.as_specified() {
549            OrderSideSpecified::Buy => self.asks.simulate_fills(order),
550            OrderSideSpecified::Sell => self.bids.simulate_fills(order),
551        }
552    }
553
554    /// Return a formatted string representation of the order book.
555    #[must_use]
556    pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
557        pprint_book(self, num_levels, group_size)
558    }
559
560    fn increment(&mut self, sequence: u64, ts_event: UnixNanos) {
561        debug_assert!(
562            sequence >= self.sequence,
563            "Sequence number should not go backwards: old={}, new={}",
564            self.sequence,
565            sequence
566        );
567        debug_assert!(
568            ts_event >= self.ts_last,
569            "Timestamp should not go backwards: old={}, new={}",
570            self.ts_last,
571            ts_event
572        );
573        debug_assert!(
574            self.update_count < u64::MAX,
575            "Update count approaching overflow: {}",
576            self.update_count
577        );
578
579        self.sequence = sequence;
580        self.ts_last = ts_event;
581        self.update_count += 1;
582    }
583
584    /// Updates L1 book state from a quote tick. Only valid for L1_MBP book type.
585    ///
586    /// # Errors
587    ///
588    /// Returns an error if the book type is not `L1_MBP` (operation is invalid).
589    pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
590        if self.book_type != BookType::L1_MBP {
591            return Err(InvalidBookOperation::Update(self.book_type));
592        }
593
594        // Note: Crossed quotes (bid > ask) can occur temporarily in volatile markets or during updates
595        // This is more of a data quality warning than a hard invariant
596        if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
597            log::warn!(
598                "Quote has crossed prices: bid={}, ask={} for {}",
599                quote.bid_price,
600                quote.ask_price,
601                self.instrument_id
602            );
603        }
604        debug_assert!(
605            quote.bid_size.is_positive() && quote.ask_size.is_positive(),
606            "Quote has non-positive sizes: bid_size={}, ask_size={}",
607            quote.bid_size,
608            quote.ask_size
609        );
610
611        let bid = BookOrder::new(
612            OrderSide::Buy,
613            quote.bid_price,
614            quote.bid_size,
615            OrderSide::Buy as u64,
616        );
617
618        let ask = BookOrder::new(
619            OrderSide::Sell,
620            quote.ask_price,
621            quote.ask_size,
622            OrderSide::Sell as u64,
623        );
624
625        self.update_book_bid(bid, quote.ts_event);
626        self.update_book_ask(ask, quote.ts_event);
627
628        Ok(())
629    }
630
631    /// Updates L1 book state from a trade tick. Only valid for L1_MBP book type.
632    ///
633    /// # Errors
634    ///
635    /// Returns an error if the book type is not `L1_MBP` (operation is invalid).
636    pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
637        if self.book_type != BookType::L1_MBP {
638            return Err(InvalidBookOperation::Update(self.book_type));
639        }
640
641        // Note: Prices can be zero or negative for certain instruments (options, commodities, spreads)
642        debug_assert!(
643            trade.price.raw != PRICE_UNDEF && trade.price.raw != PRICE_ERROR,
644            "Trade has invalid/uninitialized price: {}",
645            trade.price
646        );
647        debug_assert!(
648            trade.size.is_positive(),
649            "Trade has non-positive size: {}",
650            trade.size
651        );
652
653        let bid = BookOrder::new(
654            OrderSide::Buy,
655            trade.price,
656            trade.size,
657            OrderSide::Buy as u64,
658        );
659
660        let ask = BookOrder::new(
661            OrderSide::Sell,
662            trade.price,
663            trade.size,
664            OrderSide::Sell as u64,
665        );
666
667        self.update_book_bid(bid, trade.ts_event);
668        self.update_book_ask(ask, trade.ts_event);
669
670        Ok(())
671    }
672
673    fn update_book_bid(&mut self, order: BookOrder, ts_event: UnixNanos) {
674        if let Some(top_bids) = self.bids.top()
675            && let Some(top_bid) = top_bids.first()
676        {
677            self.bids.remove_order(top_bid.order_id, 0, ts_event);
678        }
679        self.bids.add(order);
680    }
681
682    fn update_book_ask(&mut self, order: BookOrder, ts_event: UnixNanos) {
683        if let Some(top_asks) = self.asks.top()
684            && let Some(top_ask) = top_asks.first()
685        {
686            self.asks.remove_order(top_ask.order_id, 0, ts_event);
687        }
688        self.asks.add(order);
689    }
690}
691
692fn filter_quantities(
693    public_map: &mut IndexMap<Decimal, Decimal>,
694    own_map: IndexMap<Decimal, Decimal>,
695) {
696    for (price, own_size) in own_map {
697        if let Some(public_size) = public_map.get_mut(&price) {
698            *public_size = (*public_size - own_size).max(Decimal::ZERO);
699
700            if *public_size == Decimal::ZERO {
701                public_map.shift_remove(&price);
702            }
703        }
704    }
705}
706
707fn group_levels<'a>(
708    levels_iter: impl Iterator<Item = &'a BookLevel>,
709    group_size: Decimal,
710    depth: Option<usize>,
711    is_bid: bool,
712) -> IndexMap<Decimal, Decimal> {
713    let mut levels = IndexMap::new();
714    let depth = depth.unwrap_or(usize::MAX);
715
716    for level in levels_iter {
717        let price = level.price.value.as_decimal();
718        let grouped_price = if is_bid {
719            (price / group_size).floor() * group_size
720        } else {
721            (price / group_size).ceil() * group_size
722        };
723        let size = level.size_decimal();
724
725        levels
726            .entry(grouped_price)
727            .and_modify(|total| *total += size)
728            .or_insert(size);
729
730        if levels.len() > depth {
731            levels.pop();
732            break;
733        }
734    }
735
736    levels
737}