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