nautilus_model/orderbook/
book.rs

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