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::{BookIntegrityError, 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, book_type),
98            asks: BookLadder::new(OrderSideSpecified::Sell, book_type),
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    ///
267    /// # Errors
268    ///
269    /// Returns an error if:
270    /// - An `Add` is given with `NoOrderSide` (either explicitly or because the cache lookup failed).
271    /// - After resolution the delta still has `NoOrderSide` but its action is not `Clear`.
272    pub fn apply_delta(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
273        let mut order = delta.order;
274
275        if order.side == OrderSide::NoOrderSide && order.order_id != 0 {
276            match self.resolve_no_side_order(order) {
277                Ok(resolved) => order = resolved,
278                Err(BookIntegrityError::OrderNotFoundForSideResolution(order_id)) => {
279                    match delta.action {
280                        BookAction::Add => return Err(BookIntegrityError::NoOrderSide),
281                        BookAction::Update | BookAction::Delete => {
282                            // Already consistent
283                            log::debug!(
284                                "Skipping {:?} for unknown order_id={order_id}",
285                                delta.action
286                            );
287                            return Ok(());
288                        }
289                        BookAction::Clear => {} // Won't hit this (order_id != 0)
290                    }
291                }
292                Err(e) => return Err(e),
293            }
294        }
295
296        if order.side == OrderSide::NoOrderSide && delta.action != BookAction::Clear {
297            return Err(BookIntegrityError::NoOrderSide);
298        }
299
300        let flags = delta.flags;
301        let sequence = delta.sequence;
302        let ts_event = delta.ts_event;
303
304        match delta.action {
305            BookAction::Add => self.add(order, flags, sequence, ts_event),
306            BookAction::Update => self.update(order, flags, sequence, ts_event),
307            BookAction::Delete => self.delete(order, flags, sequence, ts_event),
308            BookAction::Clear => self.clear(sequence, ts_event),
309        }
310
311        Ok(())
312    }
313
314    /// Applies multiple order book delta operations.
315    ///
316    /// # Errors
317    ///
318    /// Returns the first error encountered when applying deltas.
319    pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) -> Result<(), BookIntegrityError> {
320        for delta in &deltas.deltas {
321            self.apply_delta(delta)?;
322        }
323        Ok(())
324    }
325
326    /// Replaces current book state with a depth snapshot.
327    pub fn apply_depth(&mut self, depth: &OrderBookDepth10) {
328        self.bids.clear();
329        self.asks.clear();
330
331        for order in depth.bids {
332            // Skip padding entries
333            if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
334                continue;
335            }
336
337            debug_assert_eq!(
338                order.side,
339                OrderSide::Buy,
340                "Bid order must have Buy side, was {:?}",
341                order.side
342            );
343
344            let order = pre_process_order(self.book_type, order, depth.flags);
345            self.bids.add(order);
346        }
347
348        for order in depth.asks {
349            // Skip padding entries
350            if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
351                continue;
352            }
353
354            debug_assert_eq!(
355                order.side,
356                OrderSide::Sell,
357                "Ask order must have Sell side, was {:?}",
358                order.side
359            );
360
361            let order = pre_process_order(self.book_type, order, depth.flags);
362            self.asks.add(order);
363        }
364
365        self.increment(depth.sequence, depth.ts_event);
366    }
367
368    fn resolve_no_side_order(&self, mut order: BookOrder) -> Result<BookOrder, BookIntegrityError> {
369        let resolved_side = self
370            .bids
371            .cache
372            .get(&order.order_id)
373            .or_else(|| self.asks.cache.get(&order.order_id))
374            .map(|book_price| match book_price.side {
375                OrderSideSpecified::Buy => OrderSide::Buy,
376                OrderSideSpecified::Sell => OrderSide::Sell,
377            })
378            .ok_or(BookIntegrityError::OrderNotFoundForSideResolution(
379                order.order_id,
380            ))?;
381
382        order.side = resolved_side;
383
384        Ok(order)
385    }
386
387    /// Returns an iterator over bid price levels.
388    pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
389        self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
390    }
391
392    /// Returns an iterator over ask price levels.
393    pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
394        self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
395    }
396
397    /// Returns bid price levels as a map of price to size.
398    pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
399        self.bids(depth)
400            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
401            .collect()
402    }
403
404    /// Returns ask price levels as a map of price to size.
405    pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
406        self.asks(depth)
407            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
408            .collect()
409    }
410
411    /// Groups bid quantities by price into buckets, limited by depth.
412    pub fn group_bids(
413        &self,
414        group_size: Decimal,
415        depth: Option<usize>,
416    ) -> IndexMap<Decimal, Decimal> {
417        group_levels(self.bids(None), group_size, depth, true)
418    }
419
420    /// Groups ask quantities by price into buckets, limited by depth.
421    pub fn group_asks(
422        &self,
423        group_size: Decimal,
424        depth: Option<usize>,
425    ) -> IndexMap<Decimal, Decimal> {
426        group_levels(self.asks(None), group_size, depth, false)
427    }
428
429    /// Maps bid prices to total public size per level, excluding own orders up to a depth limit.
430    ///
431    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
432    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
433    /// nanoseconds before `now` (defaults to now).
434    pub fn bids_filtered_as_map(
435        &self,
436        depth: Option<usize>,
437        own_book: Option<&OwnOrderBook>,
438        status: Option<HashSet<OrderStatus>>,
439        accepted_buffer_ns: Option<u64>,
440        now: Option<u64>,
441    ) -> IndexMap<Decimal, Decimal> {
442        let mut public_map = self
443            .bids(depth)
444            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
445            .collect::<IndexMap<Decimal, Decimal>>();
446
447        if let Some(own_book) = own_book {
448            filter_quantities(
449                &mut public_map,
450                own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
451            );
452        }
453
454        public_map
455    }
456
457    /// Maps ask prices to total public size per level, excluding own orders up to a depth limit.
458    ///
459    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
460    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
461    /// nanoseconds before `now` (defaults to now).
462    pub fn asks_filtered_as_map(
463        &self,
464        depth: Option<usize>,
465        own_book: Option<&OwnOrderBook>,
466        status: Option<HashSet<OrderStatus>>,
467        accepted_buffer_ns: Option<u64>,
468        now: Option<u64>,
469    ) -> IndexMap<Decimal, Decimal> {
470        let mut public_map = self
471            .asks(depth)
472            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
473            .collect::<IndexMap<Decimal, Decimal>>();
474
475        if let Some(own_book) = own_book {
476            filter_quantities(
477                &mut public_map,
478                own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
479            );
480        }
481
482        public_map
483    }
484
485    /// Groups bid quantities into price buckets, truncating to a maximum depth, excluding own orders.
486    ///
487    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
488    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
489    /// nanoseconds before `now` (defaults to now).
490    pub fn group_bids_filtered(
491        &self,
492        group_size: Decimal,
493        depth: Option<usize>,
494        own_book: Option<&OwnOrderBook>,
495        status: Option<HashSet<OrderStatus>>,
496        accepted_buffer_ns: Option<u64>,
497        now: Option<u64>,
498    ) -> IndexMap<Decimal, Decimal> {
499        let mut public_map = group_levels(self.bids(None), group_size, depth, true);
500
501        if let Some(own_book) = own_book {
502            filter_quantities(
503                &mut public_map,
504                own_book.bid_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
505            );
506        }
507
508        public_map
509    }
510
511    /// Groups ask quantities into price buckets, truncating to a maximum depth, excluding own orders.
512    ///
513    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
514    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
515    /// nanoseconds before `now` (defaults to now).
516    pub fn group_asks_filtered(
517        &self,
518        group_size: Decimal,
519        depth: Option<usize>,
520        own_book: Option<&OwnOrderBook>,
521        status: Option<HashSet<OrderStatus>>,
522        accepted_buffer_ns: Option<u64>,
523        now: Option<u64>,
524    ) -> IndexMap<Decimal, Decimal> {
525        let mut public_map = group_levels(self.asks(None), group_size, depth, false);
526
527        if let Some(own_book) = own_book {
528            filter_quantities(
529                &mut public_map,
530                own_book.ask_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
531            );
532        }
533
534        public_map
535    }
536
537    /// Returns true if the book has any bid orders.
538    #[must_use]
539    pub fn has_bid(&self) -> bool {
540        self.bids.top().is_some_and(|top| !top.orders.is_empty())
541    }
542
543    /// Returns true if the book has any ask orders.
544    #[must_use]
545    pub fn has_ask(&self) -> bool {
546        self.asks.top().is_some_and(|top| !top.orders.is_empty())
547    }
548
549    /// Returns the best bid price if available.
550    #[must_use]
551    pub fn best_bid_price(&self) -> Option<Price> {
552        self.bids.top().map(|top| top.price.value)
553    }
554
555    /// Returns the best ask price if available.
556    #[must_use]
557    pub fn best_ask_price(&self) -> Option<Price> {
558        self.asks.top().map(|top| top.price.value)
559    }
560
561    /// Returns the size at the best bid price if available.
562    #[must_use]
563    pub fn best_bid_size(&self) -> Option<Quantity> {
564        self.bids
565            .top()
566            .and_then(|top| top.first().map(|order| order.size))
567    }
568
569    /// Returns the size at the best ask price if available.
570    #[must_use]
571    pub fn best_ask_size(&self) -> Option<Quantity> {
572        self.asks
573            .top()
574            .and_then(|top| top.first().map(|order| order.size))
575    }
576
577    /// Returns the spread between best ask and bid prices if both exist.
578    #[must_use]
579    pub fn spread(&self) -> Option<f64> {
580        match (self.best_ask_price(), self.best_bid_price()) {
581            (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
582            _ => None,
583        }
584    }
585
586    /// Returns the midpoint between best ask and bid prices if both exist.
587    #[must_use]
588    pub fn midpoint(&self) -> Option<f64> {
589        match (self.best_ask_price(), self.best_bid_price()) {
590            (Some(ask), Some(bid)) => Some((ask.as_f64() + bid.as_f64()) / 2.0),
591            _ => None,
592        }
593    }
594
595    /// Calculates the average price to fill the specified quantity.
596    #[must_use]
597    pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
598        let levels = match order_side.as_specified() {
599            OrderSideSpecified::Buy => &self.asks.levels,
600            OrderSideSpecified::Sell => &self.bids.levels,
601        };
602
603        analysis::get_avg_px_for_quantity(qty, levels)
604    }
605
606    /// Calculates average price and quantity for target exposure. Returns (price, quantity, executed_exposure).
607    #[must_use]
608    pub fn get_avg_px_qty_for_exposure(
609        &self,
610        target_exposure: Quantity,
611        order_side: OrderSide,
612    ) -> (f64, f64, f64) {
613        let levels = match order_side.as_specified() {
614            OrderSideSpecified::Buy => &self.asks.levels,
615            OrderSideSpecified::Sell => &self.bids.levels,
616        };
617
618        analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
619    }
620
621    /// Returns the total quantity available at specified price level.
622    #[must_use]
623    pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
624        let levels = match order_side.as_specified() {
625            OrderSideSpecified::Buy => &self.asks.levels,
626            OrderSideSpecified::Sell => &self.bids.levels,
627        };
628
629        analysis::get_quantity_for_price(price, order_side, levels)
630    }
631
632    /// Simulates fills for an order, returning list of (price, quantity) tuples.
633    #[must_use]
634    pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
635        match order.side.as_specified() {
636            OrderSideSpecified::Buy => self.asks.simulate_fills(order),
637            OrderSideSpecified::Sell => self.bids.simulate_fills(order),
638        }
639    }
640
641    /// Return a formatted string representation of the order book.
642    #[must_use]
643    pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
644        pprint_book(self, num_levels, group_size)
645    }
646
647    fn increment(&mut self, sequence: u64, ts_event: UnixNanos) {
648        // Critical invariant checks: panic in debug, warn in release
649        if sequence < self.sequence {
650            let msg = format!(
651                "Sequence number should not go backwards: old={}, new={}",
652                self.sequence, sequence
653            );
654            debug_assert!(sequence >= self.sequence, "{}", msg);
655            log::warn!("{}", msg);
656        }
657
658        if ts_event < self.ts_last {
659            let msg = format!(
660                "Timestamp should not go backwards: old={}, new={}",
661                self.ts_last, ts_event
662            );
663            debug_assert!(ts_event >= self.ts_last, "{}", msg);
664            log::warn!("{}", msg);
665        }
666
667        if self.update_count == u64::MAX {
668            // Debug assert to catch in development
669            debug_assert!(
670                self.update_count < u64::MAX,
671                "Update count at u64::MAX limit (about to overflow): {}",
672                self.update_count
673            );
674
675            // Spam warnings in production when at/near u64::MAX
676            log::warn!(
677                "Update count at u64::MAX: {} (instrument_id={})",
678                self.update_count,
679                self.instrument_id
680            );
681        }
682
683        self.sequence = sequence;
684        self.ts_last = ts_event;
685        self.update_count = self.update_count.saturating_add(1);
686    }
687
688    /// Updates L1 book state from a quote tick. Only valid for L1_MBP book type.
689    ///
690    /// # Errors
691    ///
692    /// Returns an error if the book type is not `L1_MBP` (operation is invalid).
693    pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
694        if self.book_type != BookType::L1_MBP {
695            return Err(InvalidBookOperation::Update(self.book_type));
696        }
697
698        // Note: Crossed quotes (bid > ask) can occur temporarily in volatile markets or during updates
699        // This is more of a data quality warning than a hard invariant
700        if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
701            log::warn!(
702                "Quote has crossed prices: bid={}, ask={} for {}",
703                quote.bid_price,
704                quote.ask_price,
705                self.instrument_id
706            );
707        }
708        debug_assert!(
709            quote.bid_size.is_positive() && quote.ask_size.is_positive(),
710            "Quote has non-positive sizes: bid_size={}, ask_size={}",
711            quote.bid_size,
712            quote.ask_size
713        );
714
715        let bid = BookOrder::new(
716            OrderSide::Buy,
717            quote.bid_price,
718            quote.bid_size,
719            OrderSide::Buy as u64,
720        );
721
722        let ask = BookOrder::new(
723            OrderSide::Sell,
724            quote.ask_price,
725            quote.ask_size,
726            OrderSide::Sell as u64,
727        );
728
729        self.update_book_bid(bid, quote.ts_event);
730        self.update_book_ask(ask, quote.ts_event);
731
732        self.increment(self.sequence.saturating_add(1), quote.ts_event);
733
734        Ok(())
735    }
736
737    /// Updates L1 book state from a trade tick. Only valid for L1_MBP book type.
738    ///
739    /// # Errors
740    ///
741    /// Returns an error if the book type is not `L1_MBP` (operation is invalid).
742    pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
743        if self.book_type != BookType::L1_MBP {
744            return Err(InvalidBookOperation::Update(self.book_type));
745        }
746
747        // Note: Prices can be zero or negative for certain instruments (options, commodities, spreads)
748        debug_assert!(
749            trade.price.raw != PRICE_UNDEF && trade.price.raw != PRICE_ERROR,
750            "Trade has invalid/uninitialized price: {}",
751            trade.price
752        );
753        debug_assert!(
754            trade.size.is_positive(),
755            "Trade has non-positive size: {}",
756            trade.size
757        );
758
759        let bid = BookOrder::new(
760            OrderSide::Buy,
761            trade.price,
762            trade.size,
763            OrderSide::Buy as u64,
764        );
765
766        let ask = BookOrder::new(
767            OrderSide::Sell,
768            trade.price,
769            trade.size,
770            OrderSide::Sell as u64,
771        );
772
773        self.update_book_bid(bid, trade.ts_event);
774        self.update_book_ask(ask, trade.ts_event);
775
776        self.increment(self.sequence.saturating_add(1), trade.ts_event);
777
778        Ok(())
779    }
780
781    fn update_book_bid(&mut self, order: BookOrder, ts_event: UnixNanos) {
782        if let Some(top_bids) = self.bids.top()
783            && let Some(top_bid) = top_bids.first()
784        {
785            self.bids.remove_order(top_bid.order_id, 0, ts_event);
786        }
787        self.bids.add(order);
788    }
789
790    fn update_book_ask(&mut self, order: BookOrder, ts_event: UnixNanos) {
791        if let Some(top_asks) = self.asks.top()
792            && let Some(top_ask) = top_asks.first()
793        {
794            self.asks.remove_order(top_ask.order_id, 0, ts_event);
795        }
796        self.asks.add(order);
797    }
798}
799
800fn filter_quantities(
801    public_map: &mut IndexMap<Decimal, Decimal>,
802    own_map: IndexMap<Decimal, Decimal>,
803) {
804    for (price, own_size) in own_map {
805        if let Some(public_size) = public_map.get_mut(&price) {
806            *public_size = (*public_size - own_size).max(Decimal::ZERO);
807
808            if *public_size == Decimal::ZERO {
809                public_map.shift_remove(&price);
810            }
811        }
812    }
813}
814
815fn group_levels<'a>(
816    levels_iter: impl Iterator<Item = &'a BookLevel>,
817    group_size: Decimal,
818    depth: Option<usize>,
819    is_bid: bool,
820) -> IndexMap<Decimal, Decimal> {
821    if group_size <= Decimal::ZERO {
822        log::error!("Invalid group_size: {group_size}, must be positive; returning empty map");
823        return IndexMap::new();
824    }
825
826    let mut levels = IndexMap::new();
827    let depth = depth.unwrap_or(usize::MAX);
828
829    for level in levels_iter {
830        let price = level.price.value.as_decimal();
831        let grouped_price = if is_bid {
832            (price / group_size).floor() * group_size
833        } else {
834            (price / group_size).ceil() * group_size
835        };
836        let size = level.size_decimal();
837
838        levels
839            .entry(grouped_price)
840            .and_modify(|total| *total += size)
841            .or_insert(size);
842
843        if levels.len() > depth {
844            levels.pop();
845            break;
846        }
847    }
848
849    levels
850}