nautilus_model/orderbook/
book.rs

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