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::UnixNanos;
23use rust_decimal::Decimal;
24
25use super::{
26    aggregation::pre_process_order, analysis, display::pprint_book, level::BookLevel,
27    own::OwnOrderBook,
28};
29use crate::{
30    data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
31    enums::{BookAction, BookType, OrderSide, OrderSideSpecified, OrderStatus, RecordFlag},
32    identifiers::InstrumentId,
33    orderbook::{
34        BookIntegrityError, InvalidBookOperation,
35        ladder::{BookLadder, BookPrice},
36    },
37    types::{
38        Price, Quantity,
39        price::{PRICE_ERROR, PRICE_UNDEF},
40    },
41};
42
43/// 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.core.nautilus_pyo3.model", from_py_object)
54)]
55pub struct OrderBook {
56    /// The instrument ID for the order book.
57    pub instrument_id: InstrumentId,
58    /// The order book type (MBP types will aggregate orders).
59    pub book_type: BookType,
60    /// The last event sequence number for the order book.
61    pub sequence: u64,
62    /// The timestamp of the last event applied to the order book.
63    pub ts_last: UnixNanos,
64    /// The current count of updates applied to the order book.
65    pub update_count: u64,
66    pub(crate) bids: BookLadder,
67    pub(crate) asks: BookLadder,
68}
69
70impl PartialEq for OrderBook {
71    fn eq(&self, other: &Self) -> bool {
72        self.instrument_id == other.instrument_id && self.book_type == other.book_type
73    }
74}
75
76impl Eq for OrderBook {}
77
78impl Display for OrderBook {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(
81            f,
82            "{}(instrument_id={}, book_type={}, update_count={})",
83            stringify!(OrderBook),
84            self.instrument_id,
85            self.book_type,
86            self.update_count,
87        )
88    }
89}
90
91impl OrderBook {
92    /// Creates a new [`OrderBook`] instance.
93    #[must_use]
94    pub fn new(instrument_id: InstrumentId, book_type: BookType) -> Self {
95        Self {
96            instrument_id,
97            book_type,
98            sequence: 0,
99            ts_last: UnixNanos::default(),
100            update_count: 0,
101            bids: BookLadder::new(OrderSideSpecified::Buy, book_type),
102            asks: BookLadder::new(OrderSideSpecified::Sell, book_type),
103        }
104    }
105
106    /// Resets the order book to its initial empty state.
107    pub fn reset(&mut self) {
108        self.bids.clear();
109        self.asks.clear();
110        self.sequence = 0;
111        self.ts_last = UnixNanos::default();
112        self.update_count = 0;
113    }
114
115    /// Adds an order to the book after preprocessing based on book type.
116    pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
117        let order = pre_process_order(self.book_type, order, flags);
118        match order.side.as_specified() {
119            OrderSideSpecified::Buy => self.bids.add(order, flags),
120            OrderSideSpecified::Sell => self.asks.add(order, flags),
121        }
122
123        self.increment(sequence, ts_event);
124    }
125
126    /// Updates an existing order in the book after preprocessing based on book type.
127    pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
128        let order = pre_process_order(self.book_type, order, flags);
129        match order.side.as_specified() {
130            OrderSideSpecified::Buy => self.bids.update(order, flags),
131            OrderSideSpecified::Sell => self.asks.update(order, flags),
132        }
133
134        self.increment(sequence, ts_event);
135    }
136
137    /// Deletes an order from the book after preprocessing based on book type.
138    pub fn delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
139        let order = pre_process_order(self.book_type, order, flags);
140        match order.side.as_specified() {
141            OrderSideSpecified::Buy => self.bids.delete(order, sequence, ts_event),
142            OrderSideSpecified::Sell => self.asks.delete(order, sequence, ts_event),
143        }
144
145        self.increment(sequence, ts_event);
146    }
147
148    /// Clears all orders from both sides of the book.
149    pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
150        self.bids.clear();
151        self.asks.clear();
152        self.increment(sequence, ts_event);
153    }
154
155    /// Clears all bid orders from the book.
156    pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
157        self.bids.clear();
158        self.increment(sequence, ts_event);
159    }
160
161    /// Clears all ask orders from the book.
162    pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
163        self.asks.clear();
164        self.increment(sequence, ts_event);
165    }
166
167    /// Removes overlapped bid/ask levels when the book is strictly crossed (best bid > best ask)
168    ///
169    /// - Acts only when both sides exist and the book is crossed.
170    /// - Deletes by removing whole price levels via the ladder API to preserve invariants.
171    /// - `side=None` or `NoOrderSide` clears both overlapped ranges (conservative, may widen spread).
172    /// - `side=Buy` clears crossed bids only; side=Sell clears crossed asks only.
173    /// - Returns removed price levels (crossed bids first, then crossed asks), or None if nothing removed.
174    pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
175        if self.book_type == BookType::L1_MBP {
176            // L1_MBP maintains a single top-of-book price per side; nothing to do
177            return None;
178        }
179
180        let (Some(best_bid), Some(best_ask)) = (self.best_bid_price(), self.best_ask_price())
181        else {
182            return None;
183        };
184
185        if best_bid <= best_ask {
186            return None;
187        }
188
189        let mut removed_levels = Vec::new();
190        let mut clear_bids = false;
191        let mut clear_asks = false;
192
193        match side {
194            Some(OrderSide::Buy) => clear_bids = true,
195            Some(OrderSide::Sell) => clear_asks = true,
196            _ => {
197                clear_bids = true;
198                clear_asks = true;
199            }
200        }
201
202        // Collect prices to remove for asks (prices <= best_bid)
203        let mut ask_prices_to_remove = Vec::new();
204        if clear_asks {
205            for bp in self.asks.levels.keys() {
206                if bp.value <= best_bid {
207                    ask_prices_to_remove.push(*bp);
208                } else {
209                    break;
210                }
211            }
212        }
213
214        // Collect prices to remove for bids (prices >= best_ask)
215        let mut bid_prices_to_remove = Vec::new();
216        if clear_bids {
217            for bp in self.bids.levels.keys() {
218                if bp.value >= best_ask {
219                    bid_prices_to_remove.push(*bp);
220                } else {
221                    break;
222                }
223            }
224        }
225
226        if ask_prices_to_remove.is_empty() && bid_prices_to_remove.is_empty() {
227            return None;
228        }
229
230        let bid_count = bid_prices_to_remove.len();
231        let ask_count = ask_prices_to_remove.len();
232
233        // Remove and collect bid levels
234        for price in bid_prices_to_remove {
235            if let Some(level) = self.bids.remove_level(price) {
236                removed_levels.push(level);
237            }
238        }
239
240        // Remove and collect ask levels
241        for price in ask_prices_to_remove {
242            if let Some(level) = self.asks.remove_level(price) {
243                removed_levels.push(level);
244            }
245        }
246
247        self.increment(self.sequence, self.ts_last);
248
249        if removed_levels.is_empty() {
250            None
251        } else {
252            let total_orders: usize = removed_levels.iter().map(|level| level.orders.len()).sum();
253
254            log::warn!(
255                "Removed {} stale/crossed levels (instrument_id={}, bid_levels={}, ask_levels={}, total_orders={}), book was crossed with best_bid={} > best_ask={}",
256                removed_levels.len(),
257                self.instrument_id,
258                bid_count,
259                ask_count,
260                total_orders,
261                best_bid,
262                best_ask
263            );
264
265            Some(removed_levels)
266        }
267    }
268
269    /// Applies a single order book delta operation.
270    ///
271    /// # Errors
272    ///
273    /// Returns an error if:
274    /// - The delta's instrument ID does not match this book's instrument ID.
275    /// - An `Add` is given with `NoOrderSide` (either explicitly or because the cache lookup failed).
276    /// - After resolution the delta still has `NoOrderSide` but its action is not `Clear`.
277    pub fn apply_delta(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
278        if delta.instrument_id != self.instrument_id {
279            return Err(BookIntegrityError::InstrumentMismatch(
280                self.instrument_id,
281                delta.instrument_id,
282            ));
283        }
284        self.apply_delta_unchecked(delta)
285    }
286
287    /// Applies a single order book delta operation without instrument ID validation.
288    ///
289    /// "Unchecked" refers only to skipping the instrument ID match - other validations
290    /// still apply and errors are still returned. This exists because `Ustr` interning
291    /// is not shared across FFI boundaries, causing pointer-based equality to fail even
292    /// when string values match. This limitation may be resolved in a future version.
293    ///
294    /// # Errors
295    ///
296    /// Returns an error if:
297    /// - An `Add` is given with `NoOrderSide` (either explicitly or because the cache lookup failed).
298    /// - After resolution the delta still has `NoOrderSide` but its action is not `Clear`.
299    pub fn apply_delta_unchecked(
300        &mut self,
301        delta: &OrderBookDelta,
302    ) -> Result<(), BookIntegrityError> {
303        let mut order = delta.order;
304
305        if order.side == OrderSide::NoOrderSide && order.order_id != 0 {
306            match self.resolve_no_side_order(order) {
307                Ok(resolved) => order = resolved,
308                Err(BookIntegrityError::OrderNotFoundForSideResolution(order_id)) => {
309                    match delta.action {
310                        BookAction::Add => return Err(BookIntegrityError::NoOrderSide),
311                        BookAction::Update | BookAction::Delete => {
312                            // Already consistent
313                            log::debug!(
314                                "Skipping {:?} for unknown order_id={order_id}",
315                                delta.action
316                            );
317                            return Ok(());
318                        }
319                        BookAction::Clear => {} // Won't hit this (order_id != 0)
320                    }
321                }
322                Err(e) => return Err(e),
323            }
324        }
325
326        if order.side == OrderSide::NoOrderSide && delta.action != BookAction::Clear {
327            return Err(BookIntegrityError::NoOrderSide);
328        }
329
330        let flags = delta.flags;
331        let sequence = delta.sequence;
332        let ts_event = delta.ts_event;
333
334        match delta.action {
335            BookAction::Add => self.add(order, flags, sequence, ts_event),
336            BookAction::Update => self.update(order, flags, sequence, ts_event),
337            BookAction::Delete => self.delete(order, flags, sequence, ts_event),
338            BookAction::Clear => self.clear(sequence, ts_event),
339        }
340
341        Ok(())
342    }
343
344    /// Applies multiple order book delta operations.
345    ///
346    /// # Errors
347    ///
348    /// Returns an error if:
349    /// - The deltas' instrument ID does not match this book's instrument ID.
350    /// - Any individual delta application fails (see [`Self::apply_delta`]).
351    pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) -> Result<(), BookIntegrityError> {
352        if deltas.instrument_id != self.instrument_id {
353            return Err(BookIntegrityError::InstrumentMismatch(
354                self.instrument_id,
355                deltas.instrument_id,
356            ));
357        }
358        self.apply_deltas_unchecked(deltas)
359    }
360
361    /// Applies multiple order book delta operations without instrument ID validation.
362    ///
363    /// See [`Self::apply_delta_unchecked`] for details on why this function exists.
364    ///
365    /// # Errors
366    ///
367    /// Returns an error if any individual delta application fails.
368    pub fn apply_deltas_unchecked(
369        &mut self,
370        deltas: &OrderBookDeltas,
371    ) -> Result<(), BookIntegrityError> {
372        for delta in &deltas.deltas {
373            self.apply_delta_unchecked(delta)?;
374        }
375        Ok(())
376    }
377
378    /// Creates an `OrderBookDeltas` snapshot from the current order book state.
379    ///
380    /// This is the reverse operation of `apply_deltas`: it converts the current book state
381    /// back into a snapshot format with a `Clear` delta followed by `Add` deltas for all orders.
382    ///
383    /// # Parameters
384    ///
385    /// * `ts_event` - UNIX timestamp (nanoseconds) when the book event occurred.
386    /// * `ts_init` - UNIX timestamp (nanoseconds) when the instance was created.
387    ///
388    /// # Returns
389    ///
390    /// An `OrderBookDeltas` containing a snapshot of the current order book state.
391    #[must_use]
392    pub fn to_deltas(&self, ts_event: UnixNanos, ts_init: UnixNanos) -> OrderBookDeltas {
393        let mut deltas = Vec::new();
394
395        let total_orders = self.bids(None).map(|level| level.len()).sum::<usize>()
396            + self.asks(None).map(|level| level.len()).sum::<usize>();
397
398        // Set F_LAST on clear when book is empty so buffered consumers flush
399        let mut clear = OrderBookDelta::clear(self.instrument_id, self.sequence, ts_event, ts_init);
400        if total_orders == 0 {
401            clear.flags |= RecordFlag::F_LAST as u8;
402        }
403        deltas.push(clear);
404
405        let mut order_count = 0;
406
407        // Add bid orders
408        for level in self.bids(None) {
409            for order in level.iter() {
410                order_count += 1;
411                let flags = if order_count == total_orders {
412                    RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
413                } else {
414                    RecordFlag::F_SNAPSHOT as u8
415                };
416
417                deltas.push(OrderBookDelta::new(
418                    self.instrument_id,
419                    BookAction::Add,
420                    *order,
421                    flags,
422                    self.sequence,
423                    ts_event,
424                    ts_init,
425                ));
426            }
427        }
428
429        // Add ask orders
430        for level in self.asks(None) {
431            for order in level.iter() {
432                order_count += 1;
433                let flags = if order_count == total_orders {
434                    RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
435                } else {
436                    RecordFlag::F_SNAPSHOT as u8
437                };
438
439                deltas.push(OrderBookDelta::new(
440                    self.instrument_id,
441                    BookAction::Add,
442                    *order,
443                    flags,
444                    self.sequence,
445                    ts_event,
446                    ts_init,
447                ));
448            }
449        }
450
451        OrderBookDeltas::new(self.instrument_id, deltas)
452    }
453
454    /// Replaces current book state with a depth snapshot.
455    ///
456    /// # Errors
457    ///
458    /// Returns an error if the depth's instrument ID does not match this book's instrument ID.
459    pub fn apply_depth(&mut self, depth: &OrderBookDepth10) -> Result<(), BookIntegrityError> {
460        if depth.instrument_id != self.instrument_id {
461            return Err(BookIntegrityError::InstrumentMismatch(
462                self.instrument_id,
463                depth.instrument_id,
464            ));
465        }
466        self.apply_depth_unchecked(depth)
467    }
468
469    /// Replaces current book state with a depth snapshot without instrument ID validation.
470    ///
471    /// See [`Self::apply_delta_unchecked`] for details on why this function exists.
472    ///
473    /// # Errors
474    ///
475    /// This function currently does not return errors, but returns `Result` for API consistency.
476    pub fn apply_depth_unchecked(
477        &mut self,
478        depth: &OrderBookDepth10,
479    ) -> Result<(), BookIntegrityError> {
480        self.bids.clear();
481        self.asks.clear();
482
483        for order in depth.bids {
484            // Skip padding entries
485            if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
486                continue;
487            }
488
489            if order.side != OrderSide::Buy {
490                debug_assert_eq!(
491                    order.side,
492                    OrderSide::Buy,
493                    "Bid order must have Buy side, was {:?}",
494                    order.side
495                );
496                log::warn!(
497                    "Skipping bid order with wrong side {:?} (instrument_id={})",
498                    order.side,
499                    self.instrument_id
500                );
501                continue;
502            }
503
504            let order = pre_process_order(self.book_type, order, depth.flags);
505            self.bids.add(order, depth.flags);
506        }
507
508        for order in depth.asks {
509            // Skip padding entries
510            if order.side == OrderSide::NoOrderSide || !order.size.is_positive() {
511                continue;
512            }
513
514            if order.side != OrderSide::Sell {
515                debug_assert_eq!(
516                    order.side,
517                    OrderSide::Sell,
518                    "Ask order must have Sell side, was {:?}",
519                    order.side
520                );
521                log::warn!(
522                    "Skipping ask order with wrong side {:?} (instrument_id={})",
523                    order.side,
524                    self.instrument_id
525                );
526                continue;
527            }
528
529            let order = pre_process_order(self.book_type, order, depth.flags);
530            self.asks.add(order, depth.flags);
531        }
532
533        self.increment(depth.sequence, depth.ts_event);
534
535        Ok(())
536    }
537
538    fn resolve_no_side_order(&self, mut order: BookOrder) -> Result<BookOrder, BookIntegrityError> {
539        let resolved_side = self
540            .bids
541            .cache
542            .get(&order.order_id)
543            .or_else(|| self.asks.cache.get(&order.order_id))
544            .map(|book_price| match book_price.side {
545                OrderSideSpecified::Buy => OrderSide::Buy,
546                OrderSideSpecified::Sell => OrderSide::Sell,
547            })
548            .ok_or(BookIntegrityError::OrderNotFoundForSideResolution(
549                order.order_id,
550            ))?;
551
552        order.side = resolved_side;
553
554        Ok(order)
555    }
556
557    /// Returns an iterator over bid price levels.
558    pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
559        self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
560    }
561
562    /// Returns an iterator over ask price levels.
563    pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
564        self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
565    }
566
567    /// Returns bid price levels as a map of price to size.
568    pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
569        self.bids(depth)
570            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
571            .collect()
572    }
573
574    /// Returns ask price levels as a map of price to size.
575    pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
576        self.asks(depth)
577            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
578            .collect()
579    }
580
581    /// Groups bid quantities by price into buckets, limited by depth.
582    pub fn group_bids(
583        &self,
584        group_size: Decimal,
585        depth: Option<usize>,
586    ) -> IndexMap<Decimal, Decimal> {
587        group_levels(self.bids(None), group_size, depth, true)
588    }
589
590    /// Groups ask quantities by price into buckets, limited by depth.
591    pub fn group_asks(
592        &self,
593        group_size: Decimal,
594        depth: Option<usize>,
595    ) -> IndexMap<Decimal, Decimal> {
596        group_levels(self.asks(None), group_size, depth, false)
597    }
598
599    /// Maps bid prices to total public size per level, excluding own orders up to a depth limit.
600    ///
601    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
602    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
603    /// nanoseconds before `now` (defaults to now).
604    pub fn bids_filtered_as_map(
605        &self,
606        depth: Option<usize>,
607        own_book: Option<&OwnOrderBook>,
608        status: Option<AHashSet<OrderStatus>>,
609        accepted_buffer_ns: Option<u64>,
610        now: Option<u64>,
611    ) -> IndexMap<Decimal, Decimal> {
612        let mut public_map = self
613            .bids(depth)
614            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
615            .collect::<IndexMap<Decimal, Decimal>>();
616
617        if let Some(own_book) = own_book {
618            filter_quantities(
619                &mut public_map,
620                own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
621            );
622        }
623
624        public_map
625    }
626
627    /// Maps ask prices to total public size per level, excluding own orders up to a depth limit.
628    ///
629    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
630    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
631    /// nanoseconds before `now` (defaults to now).
632    pub fn asks_filtered_as_map(
633        &self,
634        depth: Option<usize>,
635        own_book: Option<&OwnOrderBook>,
636        status: Option<AHashSet<OrderStatus>>,
637        accepted_buffer_ns: Option<u64>,
638        now: Option<u64>,
639    ) -> IndexMap<Decimal, Decimal> {
640        let mut public_map = self
641            .asks(depth)
642            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
643            .collect::<IndexMap<Decimal, Decimal>>();
644
645        if let Some(own_book) = own_book {
646            filter_quantities(
647                &mut public_map,
648                own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
649            );
650        }
651
652        public_map
653    }
654
655    /// Groups bid quantities into price buckets, truncating to a maximum depth, excluding own orders.
656    ///
657    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
658    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
659    /// nanoseconds before `now` (defaults to now).
660    pub fn group_bids_filtered(
661        &self,
662        group_size: Decimal,
663        depth: Option<usize>,
664        own_book: Option<&OwnOrderBook>,
665        status: Option<AHashSet<OrderStatus>>,
666        accepted_buffer_ns: Option<u64>,
667        now: Option<u64>,
668    ) -> IndexMap<Decimal, Decimal> {
669        let mut public_map = group_levels(self.bids(None), group_size, depth, true);
670
671        if let Some(own_book) = own_book {
672            filter_quantities(
673                &mut public_map,
674                own_book.bid_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
675            );
676        }
677
678        public_map
679    }
680
681    /// Groups ask quantities into price buckets, truncating to a maximum depth, excluding own orders.
682    ///
683    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
684    /// Uses `accepted_buffer_ns` to include only orders accepted at least that many
685    /// nanoseconds before `now` (defaults to now).
686    pub fn group_asks_filtered(
687        &self,
688        group_size: Decimal,
689        depth: Option<usize>,
690        own_book: Option<&OwnOrderBook>,
691        status: Option<AHashSet<OrderStatus>>,
692        accepted_buffer_ns: Option<u64>,
693        now: Option<u64>,
694    ) -> IndexMap<Decimal, Decimal> {
695        let mut public_map = group_levels(self.asks(None), group_size, depth, false);
696
697        if let Some(own_book) = own_book {
698            filter_quantities(
699                &mut public_map,
700                own_book.ask_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
701            );
702        }
703
704        public_map
705    }
706
707    /// Returns true if the book has any bid orders.
708    #[must_use]
709    pub fn has_bid(&self) -> bool {
710        self.bids.top().is_some_and(|top| !top.orders.is_empty())
711    }
712
713    /// Returns true if the book has any ask orders.
714    #[must_use]
715    pub fn has_ask(&self) -> bool {
716        self.asks.top().is_some_and(|top| !top.orders.is_empty())
717    }
718
719    /// Returns the best bid price if available.
720    #[must_use]
721    pub fn best_bid_price(&self) -> Option<Price> {
722        self.bids.top().map(|top| top.price.value)
723    }
724
725    /// Returns the best ask price if available.
726    #[must_use]
727    pub fn best_ask_price(&self) -> Option<Price> {
728        self.asks.top().map(|top| top.price.value)
729    }
730
731    /// Returns the size at the best bid price if available.
732    #[must_use]
733    pub fn best_bid_size(&self) -> Option<Quantity> {
734        self.bids
735            .top()
736            .and_then(|top| top.first().map(|order| order.size))
737    }
738
739    /// Returns the size at the best ask price if available.
740    #[must_use]
741    pub fn best_ask_size(&self) -> Option<Quantity> {
742        self.asks
743            .top()
744            .and_then(|top| top.first().map(|order| order.size))
745    }
746
747    /// Returns the spread between best ask and bid prices if both exist.
748    #[must_use]
749    pub fn spread(&self) -> Option<f64> {
750        match (self.best_ask_price(), self.best_bid_price()) {
751            (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
752            _ => None,
753        }
754    }
755
756    /// Returns the midpoint between best ask and bid prices if both exist.
757    #[must_use]
758    pub fn midpoint(&self) -> Option<f64> {
759        match (self.best_ask_price(), self.best_bid_price()) {
760            (Some(ask), Some(bid)) => Some((ask.as_f64() + bid.as_f64()) / 2.0),
761            _ => None,
762        }
763    }
764
765    /// Calculates the average price to fill the specified quantity.
766    #[must_use]
767    pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
768        let levels = match order_side.as_specified() {
769            OrderSideSpecified::Buy => &self.asks.levels,
770            OrderSideSpecified::Sell => &self.bids.levels,
771        };
772
773        analysis::get_avg_px_for_quantity(qty, levels)
774    }
775
776    /// Calculates average price and quantity for target exposure. Returns (price, quantity, executed_exposure).
777    #[must_use]
778    pub fn get_avg_px_qty_for_exposure(
779        &self,
780        target_exposure: Quantity,
781        order_side: OrderSide,
782    ) -> (f64, f64, f64) {
783        let levels = match order_side.as_specified() {
784            OrderSideSpecified::Buy => &self.asks.levels,
785            OrderSideSpecified::Sell => &self.bids.levels,
786        };
787
788        analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
789    }
790
791    /// Returns the cumulative quantity available at or better than the specified price.
792    ///
793    /// For a BUY order, sums ask levels at or below the price.
794    /// For a SELL order, sums bid levels at or above the price.
795    #[must_use]
796    pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
797        let side = order_side.as_specified();
798        let levels = match side {
799            OrderSideSpecified::Buy => &self.asks.levels,
800            OrderSideSpecified::Sell => &self.bids.levels,
801        };
802
803        analysis::get_quantity_for_price(price, side, levels)
804    }
805
806    /// Returns the quantity at a specific price level only, or 0 if no level exists.
807    ///
808    /// Unlike `get_quantity_for_price` which returns cumulative quantity across
809    /// multiple levels, this returns only the quantity at the exact price level.
810    #[must_use]
811    pub fn get_quantity_at_level(
812        &self,
813        price: Price,
814        order_side: OrderSide,
815        size_precision: u8,
816    ) -> Quantity {
817        let side = order_side.as_specified();
818
819        // For a BUY order, we look in asks (sell side); for SELL order, we look in bids (buy side)
820        // BookPrice keys use the side of orders IN the book, not the incoming order side
821        let (levels, book_side) = match side {
822            OrderSideSpecified::Buy => (&self.asks.levels, OrderSideSpecified::Sell),
823            OrderSideSpecified::Sell => (&self.bids.levels, OrderSideSpecified::Buy),
824        };
825
826        let book_price = BookPrice::new(price, book_side);
827
828        levels
829            .get(&book_price)
830            .map_or(Quantity::zero(size_precision), |level| {
831                Quantity::from_raw(level.size_raw(), size_precision)
832            })
833    }
834
835    /// Simulates fills for an order, returning list of (price, quantity) tuples.
836    #[must_use]
837    pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
838        match order.side.as_specified() {
839            OrderSideSpecified::Buy => self.asks.simulate_fills(order),
840            OrderSideSpecified::Sell => self.bids.simulate_fills(order),
841        }
842    }
843
844    /// Returns all price levels crossed by an order at the given price and side.
845    ///
846    /// Unlike `simulate_fills`, this returns ALL crossed levels regardless of
847    /// order quantity. Used when liquidity consumption tracking needs visibility
848    /// into all available levels.
849    #[must_use]
850    pub fn get_all_crossed_levels(
851        &self,
852        order_side: OrderSide,
853        price: Price,
854        size_precision: u8,
855    ) -> Vec<(Price, Quantity)> {
856        let side = order_side.as_specified();
857        let levels = match side {
858            OrderSideSpecified::Buy => &self.asks.levels,
859            OrderSideSpecified::Sell => &self.bids.levels,
860        };
861
862        analysis::get_levels_for_price(price, side, levels, size_precision)
863    }
864
865    /// Return a formatted string representation of the order book.
866    #[must_use]
867    pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
868        pprint_book(self, num_levels, group_size)
869    }
870
871    fn increment(&mut self, sequence: u64, ts_event: UnixNanos) {
872        if sequence > 0 && sequence < self.sequence {
873            log::warn!(
874                "Out-of-order update: sequence {} < {} (instrument_id={})",
875                sequence,
876                self.sequence,
877                self.instrument_id
878            );
879        }
880        if ts_event < self.ts_last {
881            log::warn!(
882                "Out-of-order update: ts_event {} < {} (instrument_id={})",
883                ts_event,
884                self.ts_last,
885                self.instrument_id
886            );
887        }
888
889        if self.update_count == u64::MAX {
890            debug_assert!(
891                self.update_count < u64::MAX,
892                "Update count at u64::MAX limit (about to overflow): {}",
893                self.update_count
894            );
895            log::warn!(
896                "Update count at u64::MAX: {} (instrument_id={})",
897                self.update_count,
898                self.instrument_id
899            );
900        }
901
902        // High-water mark prevents metadata regression from out-of-order updates
903        self.sequence = sequence.max(self.sequence);
904        self.ts_last = ts_event.max(self.ts_last);
905        self.update_count = self.update_count.saturating_add(1);
906    }
907
908    /// Updates L1 book state from a quote tick. Only valid for L1_MBP book type.
909    ///
910    /// # Errors
911    ///
912    /// Returns an error if the book type is not `L1_MBP`.
913    pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
914        if self.book_type != BookType::L1_MBP {
915            return Err(InvalidBookOperation::Update(self.book_type));
916        }
917
918        // Crossed quotes (bid > ask) can occur temporarily in volatile markets
919        if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
920            log::warn!(
921                "Quote has crossed prices: bid={}, ask={} for {}",
922                quote.bid_price,
923                quote.ask_price,
924                self.instrument_id
925            );
926        }
927
928        let bid = BookOrder::new(
929            OrderSide::Buy,
930            quote.bid_price,
931            quote.bid_size,
932            OrderSide::Buy as u64,
933        );
934
935        let ask = BookOrder::new(
936            OrderSide::Sell,
937            quote.ask_price,
938            quote.ask_size,
939            OrderSide::Sell as u64,
940        );
941
942        self.update_book_bid(bid, quote.ts_event);
943        self.update_book_ask(ask, quote.ts_event);
944
945        self.increment(self.sequence.saturating_add(1), quote.ts_event);
946
947        Ok(())
948    }
949
950    /// Updates L1 book state from a trade tick. Only valid for L1_MBP book type.
951    ///
952    /// # Errors
953    ///
954    /// Returns an error if the book type is not `L1_MBP`.
955    pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
956        if self.book_type != BookType::L1_MBP {
957            return Err(InvalidBookOperation::Update(self.book_type));
958        }
959
960        // Prices can be zero or negative for certain instruments (options, spreads)
961        debug_assert!(
962            trade.price.raw != PRICE_UNDEF && trade.price.raw != PRICE_ERROR,
963            "Trade has invalid/uninitialized price: {}",
964            trade.price
965        );
966
967        // TradeTick enforces positive size at construction, but assert as sanity check
968        debug_assert!(
969            trade.size.is_positive(),
970            "Trade has non-positive size: {}",
971            trade.size
972        );
973
974        let bid = BookOrder::new(
975            OrderSide::Buy,
976            trade.price,
977            trade.size,
978            OrderSide::Buy as u64,
979        );
980
981        let ask = BookOrder::new(
982            OrderSide::Sell,
983            trade.price,
984            trade.size,
985            OrderSide::Sell as u64,
986        );
987
988        self.update_book_bid(bid, trade.ts_event);
989        self.update_book_ask(ask, trade.ts_event);
990
991        self.increment(self.sequence.saturating_add(1), trade.ts_event);
992
993        Ok(())
994    }
995
996    fn update_book_bid(&mut self, order: BookOrder, ts_event: UnixNanos) {
997        if let Some(top_bids) = self.bids.top()
998            && let Some(top_bid) = top_bids.first()
999        {
1000            self.bids.remove_order(top_bid.order_id, 0, ts_event);
1001        }
1002        self.bids.add(order, 0); // Internal replacement, no F_MBP flags
1003    }
1004
1005    fn update_book_ask(&mut self, order: BookOrder, ts_event: UnixNanos) {
1006        if let Some(top_asks) = self.asks.top()
1007            && let Some(top_ask) = top_asks.first()
1008        {
1009            self.asks.remove_order(top_ask.order_id, 0, ts_event);
1010        }
1011        self.asks.add(order, 0); // Internal replacement, no F_MBP flags
1012    }
1013}
1014
1015fn filter_quantities(
1016    public_map: &mut IndexMap<Decimal, Decimal>,
1017    own_map: IndexMap<Decimal, Decimal>,
1018) {
1019    for (price, own_size) in own_map {
1020        if let Some(public_size) = public_map.get_mut(&price) {
1021            *public_size = (*public_size - own_size).max(Decimal::ZERO);
1022
1023            if *public_size == Decimal::ZERO {
1024                public_map.shift_remove(&price);
1025            }
1026        }
1027    }
1028}
1029
1030fn group_levels<'a>(
1031    levels_iter: impl Iterator<Item = &'a BookLevel>,
1032    group_size: Decimal,
1033    depth: Option<usize>,
1034    is_bid: bool,
1035) -> IndexMap<Decimal, Decimal> {
1036    if group_size <= Decimal::ZERO {
1037        log::error!("Invalid group_size: {group_size}, must be positive; returning empty map");
1038        return IndexMap::new();
1039    }
1040
1041    let mut levels = IndexMap::new();
1042    let depth = depth.unwrap_or(usize::MAX);
1043
1044    for level in levels_iter {
1045        let price = level.price.value.as_decimal();
1046        let grouped_price = if is_bid {
1047            (price / group_size).floor() * group_size
1048        } else {
1049            (price / group_size).ceil() * group_size
1050        };
1051        let size = level.size_decimal();
1052
1053        levels
1054            .entry(grouped_price)
1055            .and_modify(|total| *total += size)
1056            .or_insert(size);
1057
1058        if levels.len() > depth {
1059            levels.pop();
1060            break;
1061        }
1062    }
1063
1064    levels
1065}