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, correctness::FAILED};
23use rust_decimal::Decimal;
24
25use super::{
26    BookViewError, aggregation::pre_process_order, analysis, display::pprint_book,
27    level::BookLevel, own::OwnOrderBook,
28};
29use crate::{
30    data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
31    enums::{BookAction, BookType, OrderSide, 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.model", from_py_object)
54)]
55#[cfg_attr(
56    feature = "python",
57    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
58)]
59pub struct OrderBook {
60    /// The instrument ID for the order book.
61    pub instrument_id: InstrumentId,
62    /// The order book type (MBP types will aggregate orders).
63    pub book_type: BookType,
64    /// The last event sequence number for the order book.
65    pub sequence: u64,
66    /// The timestamp of the last event applied to the order book.
67    pub ts_last: UnixNanos,
68    /// The current count of updates applied to the order book.
69    pub update_count: u64,
70    pub(crate) bids: BookLadder,
71    pub(crate) asks: BookLadder,
72}
73
74impl PartialEq for OrderBook {
75    fn eq(&self, other: &Self) -> bool {
76        self.instrument_id == other.instrument_id && self.book_type == other.book_type
77    }
78}
79
80impl Eq for OrderBook {}
81
82impl Display for OrderBook {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(
85            f,
86            "{}(instrument_id={}, book_type={}, update_count={})",
87            stringify!(OrderBook),
88            self.instrument_id,
89            self.book_type,
90            self.update_count,
91        )
92    }
93}
94
95impl OrderBook {
96    /// Creates a new [`OrderBook`] instance.
97    #[must_use]
98    pub fn new(instrument_id: InstrumentId, book_type: BookType) -> Self {
99        Self {
100            instrument_id,
101            book_type,
102            sequence: 0,
103            ts_last: UnixNanos::default(),
104            update_count: 0,
105            bids: BookLadder::new(OrderSide::Buy, book_type),
106            asks: BookLadder::new(OrderSide::Sell, book_type),
107        }
108    }
109
110    /// Resets the order book to its initial empty state.
111    pub fn reset(&mut self) {
112        self.bids.clear();
113        self.asks.clear();
114        self.sequence = 0;
115        self.ts_last = UnixNanos::default();
116        self.update_count = 0;
117    }
118
119    /// Adds an order to the book after preprocessing based on book type.
120    ///
121    /// # Panics
122    ///
123    /// Panics if `order.side` is `None`.
124    pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
125        let order = pre_process_order(self.book_type, order, flags);
126        match order.side.expect("BookOrder side must be Buy or Sell") {
127            OrderSide::Buy => self.bids.add(order, flags),
128            OrderSide::Sell => self.asks.add(order, flags),
129        }
130
131        self.increment(sequence, ts_event, flags);
132    }
133
134    /// Updates an existing order in the book after preprocessing based on book type.
135    ///
136    /// # Panics
137    ///
138    /// Panics if `order.side` is `None`.
139    pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
140        let order = pre_process_order(self.book_type, order, flags);
141        match order.side.expect("BookOrder side must be Buy or Sell") {
142            OrderSide::Buy => self.bids.update(order, flags),
143            OrderSide::Sell => self.asks.update(order, flags),
144        }
145
146        self.increment(sequence, ts_event, flags);
147    }
148
149    /// Deletes an order from the book after preprocessing based on book type.
150    ///
151    /// # Panics
152    ///
153    /// Panics if `order.side` is `None`.
154    pub fn delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
155        let order = pre_process_order(self.book_type, order, flags);
156        match order.side.expect("BookOrder side must be Buy or Sell") {
157            OrderSide::Buy => self.bids.delete(order, sequence, ts_event),
158            OrderSide::Sell => self.asks.delete(order, sequence, ts_event),
159        }
160
161        self.increment(sequence, ts_event, flags);
162    }
163
164    /// Clears all orders from both sides of the book.
165    pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
166        self.clear_with_flags(sequence, ts_event, 0);
167    }
168
169    /// Clears all bid orders from the book.
170    pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
171        self.bids.clear();
172        self.increment(sequence, ts_event, 0);
173    }
174
175    /// Clears all ask orders from the book.
176    pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
177        self.asks.clear();
178        self.increment(sequence, ts_event, 0);
179    }
180
181    fn clear_with_flags(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
182        self.bids.clear();
183        self.asks.clear();
184        self.increment(sequence, ts_event, flags);
185    }
186
187    /// Removes overlapped bid/ask levels when the book is strictly crossed (best bid > best ask)
188    ///
189    /// - Acts only when both sides exist and the book is crossed.
190    /// - Deletes by removing whole price levels via the ladder API to preserve invariants.
191    /// - `side=None` clears both overlapped ranges (conservative, may widen spread).
192    /// - `side=Buy` clears crossed bids only; side=Sell clears crossed asks only.
193    /// - Returns removed price levels (crossed bids first, then crossed asks), or None if nothing removed.
194    pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
195        if self.book_type == BookType::L1_MBP {
196            // L1_MBP maintains a single top-of-book price per side; nothing to do
197            return None;
198        }
199
200        let (Some(best_bid), Some(best_ask)) = (self.best_bid_price(), self.best_ask_price())
201        else {
202            return None;
203        };
204
205        if best_bid <= best_ask {
206            return None;
207        }
208
209        let mut removed_levels = Vec::new();
210        let (clear_bids, clear_asks) = match side {
211            Some(OrderSide::Buy) => (true, false),
212            Some(OrderSide::Sell) => (false, true),
213            None => (true, true),
214        };
215
216        // Collect prices to remove for asks (prices <= best_bid)
217        let mut ask_prices_to_remove = Vec::new();
218
219        if clear_asks {
220            for bp in self.asks.levels.keys() {
221                if bp.value <= best_bid {
222                    ask_prices_to_remove.push(*bp);
223                } else {
224                    break;
225                }
226            }
227        }
228
229        // Collect prices to remove for bids (prices >= best_ask)
230        let mut bid_prices_to_remove = Vec::new();
231
232        if clear_bids {
233            for bp in self.bids.levels.keys() {
234                if bp.value >= best_ask {
235                    bid_prices_to_remove.push(*bp);
236                } else {
237                    break;
238                }
239            }
240        }
241
242        if ask_prices_to_remove.is_empty() && bid_prices_to_remove.is_empty() {
243            return None;
244        }
245
246        let bid_count = bid_prices_to_remove.len();
247        let ask_count = ask_prices_to_remove.len();
248
249        // Remove and collect bid levels
250        for price in bid_prices_to_remove {
251            if let Some(level) = self.bids.remove_level(price) {
252                removed_levels.push(level);
253            }
254        }
255
256        // Remove and collect ask levels
257        for price in ask_prices_to_remove {
258            if let Some(level) = self.asks.remove_level(price) {
259                removed_levels.push(level);
260            }
261        }
262
263        self.increment(self.sequence, self.ts_last, 0);
264
265        if removed_levels.is_empty() {
266            None
267        } else {
268            let total_orders: usize = removed_levels.iter().map(|level| level.orders.len()).sum();
269
270            log::warn!(
271                "Removed {} stale/crossed levels (instrument_id={}, bid_levels={}, ask_levels={}, total_orders={}), book was crossed with best_bid={} > best_ask={}",
272                removed_levels.len(),
273                self.instrument_id,
274                bid_count,
275                ask_count,
276                total_orders,
277                best_bid,
278                best_ask
279            );
280
281            Some(removed_levels)
282        }
283    }
284
285    /// Applies a single order book delta operation.
286    ///
287    /// # Errors
288    ///
289    /// Returns an error if:
290    /// - The delta's instrument ID does not match this book's instrument ID.
291    /// - An `Add` is given with no side, either explicitly or because the cache lookup failed.
292    /// - An `Add` with no side matches an order ID on both sides of the book.
293    /// - After resolution the delta still has no side but its action is not `Clear`.
294    ///
295    /// # Notes
296    ///
297    /// An ambiguous no-side `Update` or `Delete` is skipped with a warning.
298    pub fn apply_delta(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
299        if delta.instrument_id != self.instrument_id {
300            return Err(BookIntegrityError::InstrumentMismatch(
301                self.instrument_id,
302                delta.instrument_id,
303            ));
304        }
305        self.apply_delta_unchecked(delta)
306    }
307
308    /// Applies a single order book delta operation without instrument ID validation.
309    ///
310    /// "Unchecked" refers only to skipping the instrument ID match - other validations
311    /// still apply and errors are still returned. This exists because `Ustr` interning
312    /// is not shared across FFI boundaries, causing pointer-based equality to fail even
313    /// when string values match. This limitation may be resolved in a future version.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if:
318    /// - An `Add` is given with no side, either explicitly or because the cache lookup failed.
319    /// - An `Add` with no side matches an order ID on both sides of the book.
320    /// - After resolution the delta still has no side but its action is not `Clear`.
321    ///
322    /// # Notes
323    ///
324    /// An ambiguous no-side `Update` or `Delete` is skipped with a warning.
325    pub fn apply_delta_unchecked(
326        &mut self,
327        delta: &OrderBookDelta,
328    ) -> Result<(), BookIntegrityError> {
329        // No batch wraps a standalone delta, so it reports its own stale metadata
330        self.report_out_of_order_snapshot(delta.flags, delta.sequence, delta.ts_event, 1);
331        self.apply_delta_inner(delta)
332    }
333
334    fn apply_delta_inner(&mut self, delta: &OrderBookDelta) -> Result<(), BookIntegrityError> {
335        let mut order = delta.order;
336
337        if order.side.is_none() && order.order_id != 0 {
338            match self.resolve_no_side_order(order) {
339                Ok(resolved) => order = resolved,
340                Err(BookIntegrityError::OrderNotFoundForSideResolution(order_id)) => {
341                    match delta.action {
342                        BookAction::Add => return Err(BookIntegrityError::NoOrderSide),
343                        BookAction::Update | BookAction::Delete => {
344                            // Already consistent
345                            log::debug!(
346                                "Skipping {:?} for unknown order_id={order_id}",
347                                delta.action
348                            );
349                            return Ok(());
350                        }
351                        BookAction::Clear => {} // Won't hit this (order_id != 0)
352                    }
353                }
354                Err(BookIntegrityError::AmbiguousOrderSide(order_id)) => {
355                    match delta.action {
356                        BookAction::Add => {
357                            return Err(BookIntegrityError::AmbiguousOrderSide(order_id));
358                        }
359                        BookAction::Update | BookAction::Delete => {
360                            log::warn!(
361                                "Skipping {:?} for order_id={order_id} found on both book sides",
362                                delta.action
363                            );
364                            return Ok(());
365                        }
366                        BookAction::Clear => {} // Won't hit this (order_id != 0)
367                    }
368                }
369                Err(e) => return Err(e),
370            }
371        }
372
373        if order.side.is_none() && delta.action != BookAction::Clear {
374            return Err(BookIntegrityError::NoOrderSide);
375        }
376
377        let flags = delta.flags;
378        let sequence = delta.sequence;
379        let ts_event = delta.ts_event;
380
381        match delta.action {
382            BookAction::Add => self.add(order, flags, sequence, ts_event),
383            BookAction::Update => self.update(order, flags, sequence, ts_event),
384            BookAction::Delete => self.delete(order, flags, sequence, ts_event),
385            BookAction::Clear => self.clear_with_flags(sequence, ts_event, flags),
386        }
387
388        Ok(())
389    }
390
391    /// Applies multiple order book delta operations.
392    ///
393    /// # Errors
394    ///
395    /// Returns an error if:
396    /// - The deltas' instrument ID does not match this book's instrument ID.
397    /// - Any individual delta application fails (see [`Self::apply_delta`]).
398    pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) -> Result<(), BookIntegrityError> {
399        if deltas.instrument_id != self.instrument_id {
400            return Err(BookIntegrityError::InstrumentMismatch(
401                self.instrument_id,
402                deltas.instrument_id,
403            ));
404        }
405        self.apply_deltas_unchecked(deltas)
406    }
407
408    /// Applies multiple order book delta operations without instrument ID validation.
409    ///
410    /// See [`Self::apply_delta_unchecked`] for details on why this function exists.
411    ///
412    /// # Errors
413    ///
414    /// Returns an error if any individual delta application fails.
415    ///
416    /// # Notes
417    ///
418    /// A snapshot batch carrying metadata earlier than the last applied update is reported once
419    /// here rather than per delta, since every delta in the batch shares the snapshot sequence and
420    /// timestamp.
421    pub fn apply_deltas_unchecked(
422        &mut self,
423        deltas: &OrderBookDeltas,
424    ) -> Result<(), BookIntegrityError> {
425        self.report_out_of_order_snapshot(
426            deltas.flags,
427            deltas.sequence,
428            deltas.ts_event,
429            deltas.deltas.len(),
430        );
431
432        for delta in &deltas.deltas {
433            self.apply_delta_inner(delta)?;
434        }
435
436        Ok(())
437    }
438
439    // Reports the incoming snapshot, so the result does not depend on whether every delta in it
440    // reaches the book
441    fn report_out_of_order_snapshot(
442        &self,
443        flags: u8,
444        sequence: u64,
445        ts_event: UnixNanos,
446        count: usize,
447    ) {
448        if !RecordFlag::F_SNAPSHOT.matches(flags) {
449            return;
450        }
451
452        if sequence > 0 && sequence < self.sequence {
453            log::warn!(
454                "Out-of-order snapshot: sequence {} < {} (deltas={}, instrument_id={})",
455                sequence,
456                self.sequence,
457                count,
458                self.instrument_id
459            );
460        }
461
462        if ts_event < self.ts_last {
463            log::warn!(
464                "Out-of-order snapshot: ts_event {} < {} (deltas={}, instrument_id={})",
465                ts_event,
466                self.ts_last,
467                count,
468                self.instrument_id
469            );
470        }
471    }
472
473    /// Creates an `OrderBookDeltas` snapshot from the current order book state.
474    ///
475    /// This is the reverse operation of `apply_deltas`: it converts the current book state
476    /// back into a snapshot format with a `Clear` delta followed by `Add` deltas for all orders.
477    ///
478    /// # Parameters
479    ///
480    /// * `ts_event` - UNIX timestamp (nanoseconds) when the book event occurred.
481    /// * `ts_init` - UNIX timestamp (nanoseconds) when the instance was created.
482    ///
483    /// # Returns
484    ///
485    /// An `OrderBookDeltas` containing a snapshot of the current order book state.
486    #[must_use]
487    pub fn to_deltas(&self, ts_event: UnixNanos, ts_init: UnixNanos) -> OrderBookDeltas {
488        let mut deltas = Vec::new();
489
490        let total_orders = self.bids(None).map(BookLevel::len).sum::<usize>()
491            + self.asks(None).map(BookLevel::len).sum::<usize>();
492
493        // Set F_LAST on clear when book is empty so buffered consumers flush
494        let mut clear = OrderBookDelta::clear(self.instrument_id, self.sequence, ts_event, ts_init);
495
496        if total_orders == 0 {
497            clear.flags |= RecordFlag::F_LAST as u8;
498        }
499        deltas.push(clear);
500
501        let mut order_count = 0;
502
503        for level in self.bids(None).chain(self.asks(None)) {
504            for order in level.iter() {
505                order_count += 1;
506                let flags = if order_count == total_orders {
507                    RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
508                } else {
509                    RecordFlag::F_SNAPSHOT as u8
510                };
511
512                deltas.push(OrderBookDelta::new(
513                    self.instrument_id,
514                    BookAction::Add,
515                    *order,
516                    flags,
517                    self.sequence,
518                    ts_event,
519                    ts_init,
520                ));
521            }
522        }
523
524        OrderBookDeltas::new(self.instrument_id, deltas)
525    }
526
527    /// Replaces current book state with a depth snapshot.
528    ///
529    /// # Errors
530    ///
531    /// Returns an error if the depth's instrument ID does not match this book's instrument ID.
532    pub fn apply_depth(&mut self, depth: &OrderBookDepth10) -> Result<(), BookIntegrityError> {
533        if depth.instrument_id != self.instrument_id {
534            return Err(BookIntegrityError::InstrumentMismatch(
535                self.instrument_id,
536                depth.instrument_id,
537            ));
538        }
539        self.apply_depth_unchecked(depth)
540    }
541
542    /// Replaces current book state with a depth snapshot without instrument ID validation.
543    ///
544    /// See [`Self::apply_delta_unchecked`] for details on why this function exists.
545    ///
546    /// # Errors
547    ///
548    /// This function currently does not return errors, but returns `Result` for API consistency.
549    pub fn apply_depth_unchecked(
550        &mut self,
551        depth: &OrderBookDepth10,
552    ) -> Result<(), BookIntegrityError> {
553        self.bids.clear();
554        self.asks.clear();
555
556        for order in depth.bids {
557            // Skip padding entries
558            if order.side.is_none() || !order.size.is_positive() {
559                continue;
560            }
561
562            if order.side != Some(OrderSide::Buy) {
563                debug_assert_eq!(
564                    order.side,
565                    Some(OrderSide::Buy),
566                    "Bid order must have Buy side, was {:?}",
567                    order.side
568                );
569                log::warn!(
570                    "Skipping bid order with wrong side {:?} (instrument_id={})",
571                    order.side,
572                    self.instrument_id
573                );
574                continue;
575            }
576
577            let order = pre_process_order(self.book_type, order, depth.flags);
578            self.bids.add(order, depth.flags);
579        }
580
581        for order in depth.asks {
582            // Skip padding entries
583            if order.side.is_none() || !order.size.is_positive() {
584                continue;
585            }
586
587            if order.side != Some(OrderSide::Sell) {
588                debug_assert_eq!(
589                    order.side,
590                    Some(OrderSide::Sell),
591                    "Ask order must have Sell side, was {:?}",
592                    order.side
593                );
594                log::warn!(
595                    "Skipping ask order with wrong side {:?} (instrument_id={})",
596                    order.side,
597                    self.instrument_id
598                );
599                continue;
600            }
601
602            let order = pre_process_order(self.book_type, order, depth.flags);
603            self.asks.add(order, depth.flags);
604        }
605
606        // Depth increments once per snapshot, so there is no per-level flood to suppress and
607        // the existing single warning is already the one-per-rebuild signal
608        self.increment(depth.sequence, depth.ts_event, 0);
609
610        Ok(())
611    }
612
613    fn resolve_no_side_order(&self, mut order: BookOrder) -> Result<BookOrder, BookIntegrityError> {
614        let bid_price = self.bids.cache.get(&order.order_id);
615        let ask_price = self.asks.cache.get(&order.order_id);
616
617        // For L2 books the order ID is a pure price hash, so in a locked market the
618        // same ID exists on both sides and the side cannot be resolved safely.
619        let book_price = match (bid_price, ask_price) {
620            (Some(_), Some(_)) => {
621                return Err(BookIntegrityError::AmbiguousOrderSide(order.order_id));
622            }
623            (Some(book_price), None) | (None, Some(book_price)) => book_price,
624            (None, None) => {
625                return Err(BookIntegrityError::OrderNotFoundForSideResolution(
626                    order.order_id,
627                ));
628            }
629        };
630
631        order.side = book_price.side.into();
632
633        Ok(order)
634    }
635
636    /// Returns an iterator over bid price levels.
637    pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
638        self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
639    }
640
641    /// Returns an iterator over ask price levels.
642    pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
643        self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
644    }
645
646    /// Returns bid price levels as a map of price to size.
647    #[must_use]
648    pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
649        self.bids(depth)
650            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
651            .collect()
652    }
653
654    /// Returns ask price levels as a map of price to size.
655    #[must_use]
656    pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
657        self.asks(depth)
658            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
659            .collect()
660    }
661
662    /// Groups bid quantities by price into buckets, limited by depth.
663    #[must_use]
664    pub fn group_bids(
665        &self,
666        group_size: Decimal,
667        depth: Option<usize>,
668    ) -> IndexMap<Decimal, Decimal> {
669        group_levels(self.bids(None), group_size, depth, true)
670    }
671
672    /// Groups ask quantities by price into buckets, limited by depth.
673    #[must_use]
674    pub fn group_asks(
675        &self,
676        group_size: Decimal,
677        depth: Option<usize>,
678    ) -> IndexMap<Decimal, Decimal> {
679        group_levels(self.asks(None), group_size, depth, false)
680    }
681
682    /// Maps bid prices to total public size per level, excluding own orders up to a depth limit.
683    ///
684    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
685    /// When `now` is provided, only subtracts orders whose acceptance time plus
686    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
687    /// filtering is disabled.
688    ///
689    /// # Panics
690    ///
691    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
692    #[must_use]
693    pub fn bids_filtered_as_map(
694        &self,
695        depth: Option<usize>,
696        own_book: Option<&OwnOrderBook>,
697        status: Option<&AHashSet<OrderStatus>>,
698        accepted_buffer_ns: Option<u64>,
699        now: Option<u64>,
700    ) -> IndexMap<Decimal, Decimal> {
701        let mut public_map = self
702            .bids(depth)
703            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
704            .collect::<IndexMap<Decimal, Decimal>>();
705
706        if let Some(own_book) = own_book {
707            filter_quantities(
708                &mut public_map,
709                own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
710            );
711        }
712
713        public_map
714    }
715
716    /// Maps ask prices to total public size per level, excluding own orders up to a depth limit.
717    ///
718    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
719    /// When `now` is provided, only subtracts orders whose acceptance time plus
720    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
721    /// filtering is disabled.
722    ///
723    /// # Panics
724    ///
725    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
726    #[must_use]
727    pub fn asks_filtered_as_map(
728        &self,
729        depth: Option<usize>,
730        own_book: Option<&OwnOrderBook>,
731        status: Option<&AHashSet<OrderStatus>>,
732        accepted_buffer_ns: Option<u64>,
733        now: Option<u64>,
734    ) -> IndexMap<Decimal, Decimal> {
735        let mut public_map = self
736            .asks(depth)
737            .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
738            .collect::<IndexMap<Decimal, Decimal>>();
739
740        if let Some(own_book) = own_book {
741            filter_quantities(
742                &mut public_map,
743                own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
744            );
745        }
746
747        public_map
748    }
749
750    /// Returns a filtered [`OrderBook`] view with own sizes subtracted from public levels.
751    ///
752    /// # Panics
753    ///
754    /// Panics if `self` and `own_book` have different instrument IDs.
755    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
756    ///
757    /// [`Self::filtered_view_checked`] for fallible construction.
758    #[must_use]
759    pub fn filtered_view(
760        &self,
761        own_book: Option<&OwnOrderBook>,
762        depth: Option<usize>,
763        status: Option<&AHashSet<OrderStatus>>,
764        accepted_buffer_ns: Option<u64>,
765        now: Option<u64>,
766    ) -> Self {
767        self.filtered_view_checked(own_book, depth, status, accepted_buffer_ns, now)
768            .expect(FAILED)
769    }
770
771    /// Fallible version of [`Self::filtered_view`].
772    ///
773    /// # Errors
774    ///
775    /// Returns [`BookViewError::InstrumentMismatch`] if `self` and `own_book` have different
776    /// instrument IDs.
777    ///
778    /// # Panics
779    ///
780    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
781    /// Panics if `Price::from_decimal` or `Quantity::from_decimal` fails when
782    /// reconstructing filtered levels.
783    pub fn filtered_view_checked(
784        &self,
785        own_book: Option<&OwnOrderBook>,
786        depth: Option<usize>,
787        status: Option<&AHashSet<OrderStatus>>,
788        accepted_buffer_ns: Option<u64>,
789        now: Option<u64>,
790    ) -> Result<Self, BookViewError> {
791        if let Some(own_book) = own_book
792            && self.instrument_id != own_book.instrument_id
793        {
794            return Err(BookViewError::InstrumentMismatch(
795                self.instrument_id,
796                own_book.instrument_id,
797            ));
798        }
799
800        let bids_map = self.bids_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
801        let asks_map = self.asks_filtered_as_map(depth, own_book, status, accepted_buffer_ns, now);
802
803        let mut filtered_book = Self::new(self.instrument_id, self.book_type);
804        filtered_book.sequence = self.sequence;
805        filtered_book.ts_last = self.ts_last;
806
807        let sequence = self.sequence;
808        let ts_event = self.ts_last;
809
810        let mut order_id = 1_u64;
811
812        for (price, quantity) in bids_map {
813            if quantity <= Decimal::ZERO {
814                continue;
815            }
816
817            let order = BookOrder::new(
818                OrderSide::Buy,
819                Price::from_decimal(price).expect("Invalid bid price for OrderBook::filtered_view"),
820                Quantity::from_decimal(quantity)
821                    .expect("Invalid bid quantity for OrderBook::filtered_view"),
822                order_id,
823            );
824            order_id += 1;
825            filtered_book.add(order, 0, sequence, ts_event);
826        }
827
828        for (price, quantity) in asks_map {
829            if quantity <= Decimal::ZERO {
830                continue;
831            }
832
833            let order = BookOrder::new(
834                OrderSide::Sell,
835                Price::from_decimal(price).expect("Invalid ask price for OrderBook::filtered_view"),
836                Quantity::from_decimal(quantity)
837                    .expect("Invalid ask quantity for OrderBook::filtered_view"),
838                order_id,
839            );
840            order_id += 1;
841            filtered_book.add(order, 0, sequence, ts_event);
842        }
843
844        Ok(filtered_book)
845    }
846
847    /// Groups bid quantities into price buckets, truncating to a maximum depth, excluding own orders.
848    ///
849    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
850    /// When `now` is provided, only subtracts orders whose acceptance time plus
851    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
852    /// filtering is disabled.
853    ///
854    /// # Panics
855    ///
856    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
857    #[must_use]
858    pub fn group_bids_filtered(
859        &self,
860        group_size: Decimal,
861        depth: Option<usize>,
862        own_book: Option<&OwnOrderBook>,
863        status: Option<&AHashSet<OrderStatus>>,
864        accepted_buffer_ns: Option<u64>,
865        now: Option<u64>,
866    ) -> IndexMap<Decimal, Decimal> {
867        let mut public_map = group_levels(self.bids(None), group_size, depth, true);
868
869        if let Some(own_book) = own_book {
870            filter_quantities(
871                &mut public_map,
872                own_book.bid_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
873            );
874        }
875
876        public_map
877    }
878
879    /// Groups ask quantities into price buckets, truncating to a maximum depth, excluding own orders.
880    ///
881    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
882    /// When `now` is provided, only subtracts orders whose acceptance time plus
883    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
884    /// filtering is disabled.
885    ///
886    /// # Panics
887    ///
888    /// Panics if `own_book` is `Some`, `accepted_buffer_ns` is positive, and `now` is `None`.
889    #[must_use]
890    pub fn group_asks_filtered(
891        &self,
892        group_size: Decimal,
893        depth: Option<usize>,
894        own_book: Option<&OwnOrderBook>,
895        status: Option<&AHashSet<OrderStatus>>,
896        accepted_buffer_ns: Option<u64>,
897        now: Option<u64>,
898    ) -> IndexMap<Decimal, Decimal> {
899        let mut public_map = group_levels(self.asks(None), group_size, depth, false);
900
901        if let Some(own_book) = own_book {
902            filter_quantities(
903                &mut public_map,
904                own_book.ask_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
905            );
906        }
907
908        public_map
909    }
910
911    /// Returns true if the book has any bid orders.
912    #[must_use]
913    pub fn has_bid(&self) -> bool {
914        self.bids.top().is_some_and(|top| !top.orders.is_empty())
915    }
916
917    /// Returns true if the book has any ask orders.
918    #[must_use]
919    pub fn has_ask(&self) -> bool {
920        self.asks.top().is_some_and(|top| !top.orders.is_empty())
921    }
922
923    /// Returns the best bid price if available.
924    #[must_use]
925    pub fn best_bid_price(&self) -> Option<Price> {
926        self.bids.top().map(|top| top.price.value)
927    }
928
929    /// Returns the best ask price if available.
930    #[must_use]
931    pub fn best_ask_price(&self) -> Option<Price> {
932        self.asks.top().map(|top| top.price.value)
933    }
934
935    /// Returns the size at the best bid price if available.
936    #[must_use]
937    pub fn best_bid_size(&self) -> Option<Quantity> {
938        self.bids
939            .top()
940            .and_then(|top| top.first().map(|order| order.size))
941    }
942
943    /// Returns the size at the best ask price if available.
944    #[must_use]
945    pub fn best_ask_size(&self) -> Option<Quantity> {
946        self.asks
947            .top()
948            .and_then(|top| top.first().map(|order| order.size))
949    }
950
951    /// Returns the spread between best ask and bid prices if both exist.
952    #[must_use]
953    pub fn spread(&self) -> Option<f64> {
954        match (self.best_ask_price(), self.best_bid_price()) {
955            (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
956            _ => None,
957        }
958    }
959
960    /// Returns the midpoint between best ask and bid prices if both exist.
961    #[must_use]
962    pub fn midpoint(&self) -> Option<f64> {
963        match (self.best_ask_price(), self.best_bid_price()) {
964            (Some(ask), Some(bid)) => Some(f64::midpoint(ask.as_f64(), bid.as_f64())),
965            _ => None,
966        }
967    }
968
969    /// Calculates the average price to fill the specified quantity.
970    #[must_use]
971    pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
972        let levels = match order_side {
973            OrderSide::Buy => &self.asks.levels,
974            OrderSide::Sell => &self.bids.levels,
975        };
976
977        analysis::get_avg_px_for_quantity(qty, levels)
978    }
979
980    /// Calculates the worst (last-touched) price to fill the specified quantity.
981    #[must_use]
982    pub fn get_worst_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> Option<Price> {
983        let levels = match order_side {
984            OrderSide::Buy => &self.asks.levels,
985            OrderSide::Sell => &self.bids.levels,
986        };
987
988        analysis::get_worst_px_for_quantity(qty, levels)
989    }
990
991    /// Calculates average price and quantity for target exposure. Returns (price, quantity, `executed_exposure`).
992    #[must_use]
993    pub fn get_avg_px_qty_for_exposure(
994        &self,
995        target_exposure: Quantity,
996        order_side: OrderSide,
997    ) -> (f64, f64, f64) {
998        let levels = match order_side {
999            OrderSide::Buy => &self.asks.levels,
1000            OrderSide::Sell => &self.bids.levels,
1001        };
1002
1003        analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
1004    }
1005
1006    /// Returns the cumulative quantity available at or better than the specified price.
1007    ///
1008    /// For a BUY order, sums ask levels at or below the price.
1009    /// For a SELL order, sums bid levels at or above the price.
1010    #[must_use]
1011    pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
1012        let levels = match order_side {
1013            OrderSide::Buy => &self.asks.levels,
1014            OrderSide::Sell => &self.bids.levels,
1015        };
1016
1017        analysis::get_quantity_for_price(price, order_side, levels)
1018    }
1019
1020    /// Returns the quantity at a specific price level only, or 0 if no level exists.
1021    ///
1022    /// Unlike `get_quantity_for_price` which returns cumulative quantity across
1023    /// multiple levels, this returns only the quantity at the exact price level.
1024    #[must_use]
1025    pub fn get_quantity_at_level(
1026        &self,
1027        price: Price,
1028        order_side: OrderSide,
1029        size_precision: u8,
1030    ) -> Quantity {
1031        // For a BUY order, we look in asks (sell side); for SELL order, we look in bids (buy side)
1032        // BookPrice keys use the side of orders IN the book, not the incoming order side
1033        let (levels, book_side) = match order_side {
1034            OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1035            OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1036        };
1037
1038        let book_price = BookPrice::new(price, book_side);
1039
1040        levels
1041            .get(&book_price)
1042            .map_or(Quantity::zero(size_precision), |level| {
1043                Quantity::from_raw(level.size_raw(), size_precision)
1044            })
1045    }
1046
1047    /// Returns the orders at a specific price level in FIFO order, or an empty vec if no level exists.
1048    ///
1049    /// Follows the same side convention as `get_quantity_at_level`: for a BUY
1050    /// order this reads the asks (sell side), for a SELL order the bids.
1051    #[must_use]
1052    pub fn get_orders_at_level(&self, price: Price, order_side: OrderSide) -> Vec<BookOrder> {
1053        let (levels, book_side) = match order_side {
1054            OrderSide::Buy => (&self.asks.levels, OrderSide::Sell),
1055            OrderSide::Sell => (&self.bids.levels, OrderSide::Buy),
1056        };
1057
1058        let book_price = BookPrice::new(price, book_side);
1059
1060        levels
1061            .get(&book_price)
1062            .map_or_else(Vec::new, BookLevel::get_orders)
1063    }
1064
1065    /// Simulates fills for an order, returning list of (price, quantity) tuples.
1066    ///
1067    /// # Panics
1068    ///
1069    /// Panics if `order.side` is `None`.
1070    #[must_use]
1071    pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
1072        match order.side.expect("BookOrder side must be Buy or Sell") {
1073            OrderSide::Buy => self.asks.simulate_fills(order),
1074            OrderSide::Sell => self.bids.simulate_fills(order),
1075        }
1076    }
1077
1078    /// Returns all price levels crossed by an order at the given price and side.
1079    ///
1080    /// Unlike `simulate_fills`, this returns ALL crossed levels regardless of
1081    /// order quantity. Used when liquidity consumption tracking needs visibility
1082    /// into all available levels.
1083    #[must_use]
1084    pub fn get_all_crossed_levels(
1085        &self,
1086        order_side: OrderSide,
1087        price: Price,
1088        size_precision: u8,
1089    ) -> Vec<(Price, Quantity)> {
1090        let levels = match order_side {
1091            OrderSide::Buy => &self.asks.levels,
1092            OrderSide::Sell => &self.bids.levels,
1093        };
1094
1095        analysis::get_levels_for_price(price, order_side, levels, size_precision)
1096    }
1097
1098    /// Return a formatted string representation of the order book.
1099    #[must_use]
1100    pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
1101        pprint_book(self, num_levels, group_size)
1102    }
1103
1104    fn increment(&mut self, sequence: u64, ts_event: UnixNanos, flags: u8) {
1105        // A snapshot rebuild legitimately carries metadata behind the last applied update
1106        let is_snapshot = RecordFlag::F_SNAPSHOT.matches(flags);
1107
1108        if !is_snapshot && sequence > 0 && sequence < self.sequence {
1109            log::warn!(
1110                "Out-of-order update: sequence {} < {} (instrument_id={})",
1111                sequence,
1112                self.sequence,
1113                self.instrument_id
1114            );
1115        }
1116
1117        if !is_snapshot && ts_event < self.ts_last {
1118            log::warn!(
1119                "Out-of-order update: ts_event {} < {} (instrument_id={})",
1120                ts_event,
1121                self.ts_last,
1122                self.instrument_id
1123            );
1124        }
1125
1126        if self.update_count == u64::MAX {
1127            debug_assert!(
1128                self.update_count < u64::MAX,
1129                "Update count at u64::MAX limit (about to overflow): {}",
1130                self.update_count
1131            );
1132            log::warn!(
1133                "Update count at u64::MAX: {} (instrument_id={})",
1134                self.update_count,
1135                self.instrument_id
1136            );
1137        }
1138
1139        // High-water mark prevents metadata regression from out-of-order updates
1140        self.sequence = sequence.max(self.sequence);
1141        self.ts_last = ts_event.max(self.ts_last);
1142        self.update_count = self.update_count.saturating_add(1);
1143    }
1144
1145    /// Updates L1 book state from a quote tick. Only valid for `L1_MBP` book type.
1146    ///
1147    /// # Errors
1148    ///
1149    /// Returns an error if the book type is not `L1_MBP`.
1150    pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
1151        if self.book_type != BookType::L1_MBP {
1152            return Err(InvalidBookOperation::Update(self.book_type));
1153        }
1154
1155        if quote.ts_event < self.ts_last {
1156            log::warn!(
1157                "Skipping stale quote: ts_event {} < ts_last {} (instrument_id={})",
1158                quote.ts_event,
1159                self.ts_last,
1160                self.instrument_id
1161            );
1162            return Ok(());
1163        }
1164
1165        // Crossed quotes (bid > ask) can occur temporarily in volatile markets
1166        if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
1167            log::warn!(
1168                "Quote has crossed prices: bid={}, ask={} for {}",
1169                quote.bid_price,
1170                quote.ask_price,
1171                self.instrument_id
1172            );
1173        }
1174
1175        let bid = BookOrder::new(
1176            OrderSide::Buy,
1177            quote.bid_price,
1178            quote.bid_size,
1179            OrderSide::Buy as u64,
1180        );
1181
1182        let ask = BookOrder::new(
1183            OrderSide::Sell,
1184            quote.ask_price,
1185            quote.ask_size,
1186            OrderSide::Sell as u64,
1187        );
1188
1189        self.update_book_bid(bid);
1190        self.update_book_ask(ask);
1191
1192        self.increment(self.sequence.saturating_add(1), quote.ts_event, 0);
1193
1194        Ok(())
1195    }
1196
1197    /// Updates L1 book state from a trade tick. Only valid for `L1_MBP` book type.
1198    ///
1199    /// # Errors
1200    ///
1201    /// Returns an error if the book type is not `L1_MBP`.
1202    pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
1203        if self.book_type != BookType::L1_MBP {
1204            return Err(InvalidBookOperation::Update(self.book_type));
1205        }
1206
1207        if trade.ts_event < self.ts_last {
1208            log::warn!(
1209                "Skipping stale trade: ts_event {} < ts_last {} (instrument_id={})",
1210                trade.ts_event,
1211                self.ts_last,
1212                self.instrument_id
1213            );
1214            return Ok(());
1215        }
1216
1217        // Prices can be zero or negative for certain instruments (options, spreads)
1218        debug_assert!(
1219            trade.price.raw != PRICE_UNDEF && trade.price.raw != PRICE_ERROR,
1220            "Trade has invalid/uninitialized price: {}",
1221            trade.price
1222        );
1223
1224        // TradeTick enforces positive size at construction, but assert as sanity check
1225        debug_assert!(
1226            trade.size.is_positive(),
1227            "Trade has non-positive size: {}",
1228            trade.size
1229        );
1230
1231        let bid = BookOrder::new(
1232            OrderSide::Buy,
1233            trade.price,
1234            trade.size,
1235            OrderSide::Buy as u64,
1236        );
1237
1238        let ask = BookOrder::new(
1239            OrderSide::Sell,
1240            trade.price,
1241            trade.size,
1242            OrderSide::Sell as u64,
1243        );
1244
1245        self.update_book_bid(bid);
1246        self.update_book_ask(ask);
1247
1248        self.increment(self.sequence.saturating_add(1), trade.ts_event, 0);
1249
1250        Ok(())
1251    }
1252
1253    fn update_book_bid(&mut self, order: BookOrder) {
1254        self.bids.replace_l1(order);
1255    }
1256
1257    fn update_book_ask(&mut self, order: BookOrder) {
1258        self.asks.replace_l1(order);
1259    }
1260
1261    /// Replays `deltas` through a fresh book of the given type and returns
1262    /// a [`QuoteTick`] for every best-bid/ask price change.
1263    ///
1264    /// # Panics
1265    ///
1266    /// Panics if `deltas` is empty.
1267    #[must_use]
1268    pub fn deltas_to_quotes(book_type: BookType, deltas: &[OrderBookDelta]) -> Vec<QuoteTick> {
1269        assert!(!deltas.is_empty(), "`deltas` must not be empty");
1270
1271        let instrument_id = deltas[0].instrument_id;
1272        let mut book = Self::new(instrument_id, book_type);
1273        let mut quotes = Vec::new();
1274        let mut last_bbo: Option<(Price, Price)> = None;
1275
1276        for delta in deltas {
1277            book.apply_delta(delta).unwrap();
1278            let Some((bid_px, ask_px)) = book.best_bid_price().zip(book.best_ask_price()) else {
1279                last_bbo = None;
1280                continue;
1281            };
1282
1283            let bbo = (bid_px, ask_px);
1284
1285            if last_bbo == Some(bbo) {
1286                continue;
1287            }
1288
1289            last_bbo = Some(bbo);
1290            let bid_level = book.bids.top().unwrap();
1291            let ask_level = book.asks.top().unwrap();
1292            let precision = bid_level.first().unwrap().size.precision;
1293            let bid_sz = Quantity::from_raw(bid_level.size_raw(), precision);
1294            let ask_sz = Quantity::from_raw(ask_level.size_raw(), precision);
1295            let quote = QuoteTick::new(
1296                instrument_id,
1297                bid_px,
1298                ask_px,
1299                bid_sz,
1300                ask_sz,
1301                delta.ts_event,
1302                delta.ts_init,
1303            );
1304
1305            quotes.push(quote);
1306        }
1307
1308        quotes
1309    }
1310}
1311
1312fn filter_quantities(
1313    public_map: &mut IndexMap<Decimal, Decimal>,
1314    own_map: IndexMap<Decimal, Decimal>,
1315) {
1316    for (price, own_size) in own_map {
1317        if let Some(public_size) = public_map.get_mut(&price) {
1318            *public_size = (*public_size - own_size).max(Decimal::ZERO);
1319
1320            if *public_size == Decimal::ZERO {
1321                public_map.shift_remove(&price);
1322            }
1323        }
1324    }
1325}
1326
1327fn group_levels<'a>(
1328    levels_iter: impl Iterator<Item = &'a BookLevel>,
1329    group_size: Decimal,
1330    depth: Option<usize>,
1331    is_bid: bool,
1332) -> IndexMap<Decimal, Decimal> {
1333    if group_size <= Decimal::ZERO {
1334        log::warn!("Invalid group_size: {group_size}, must be positive; returning empty map");
1335        return IndexMap::new();
1336    }
1337
1338    let mut levels = IndexMap::new();
1339    let depth = depth.unwrap_or(usize::MAX);
1340
1341    for level in levels_iter {
1342        let price = level.price.value.as_decimal();
1343        let grouped_price = if is_bid {
1344            (price / group_size).floor() * group_size
1345        } else {
1346            (price / group_size).ceil() * group_size
1347        };
1348        let size = level.size_decimal();
1349
1350        levels
1351            .entry(grouped_price)
1352            .and_modify(|total| *total += size)
1353            .or_insert(size);
1354
1355        if levels.len() > depth {
1356            levels.pop();
1357            break;
1358        }
1359    }
1360
1361    levels
1362}