Skip to main content

nautilus_model/python/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
16use ahash::AHashSet;
17use indexmap::IndexMap;
18use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
19use pyo3::prelude::*;
20use rust_decimal::Decimal;
21
22use crate::{
23    data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
24    enums::{BookType, OrderSide, OrderStatus},
25    identifiers::InstrumentId,
26    orderbook::{
27        BookLevel, OrderBook,
28        analysis::book_check_integrity,
29        own::{OwnOrderBook, validate_accepted_buffer},
30    },
31    types::{Price, Quantity},
32};
33
34#[pymethods]
35#[pyo3_stub_gen::derive::gen_stub_pymethods]
36impl OrderBook {
37    /// Provides a high-performance, versatile order book.
38    ///
39    /// Maintains buy (bid) and sell (ask) orders in price-time priority, supporting multiple
40    /// market data formats:
41    /// - L3 (MBO): Market By Order - tracks individual orders with unique IDs.
42    /// - L2 (MBP): Market By Price - aggregates orders at each price level.
43    /// - L1 (MBP): Top-of-Book - maintains only the best bid and ask prices.
44    #[new]
45    fn py_new(instrument_id: InstrumentId, book_type: BookType) -> Self {
46        Self::new(instrument_id, book_type)
47    }
48
49    fn __repr__(&self) -> String {
50        format!("{self:?}")
51    }
52
53    fn __str__(&self) -> String {
54        self.to_string()
55    }
56
57    #[getter]
58    #[pyo3(name = "instrument_id")]
59    fn py_instrument_id(&self) -> InstrumentId {
60        self.instrument_id
61    }
62
63    #[getter]
64    #[pyo3(name = "book_type")]
65    fn py_book_type(&self) -> BookType {
66        self.book_type
67    }
68
69    #[getter]
70    #[pyo3(name = "sequence")]
71    fn py_sequence(&self) -> u64 {
72        self.sequence
73    }
74
75    #[getter]
76    #[pyo3(name = "ts_event")]
77    fn py_ts_event(&self) -> u64 {
78        self.ts_last.as_u64()
79    }
80
81    #[getter]
82    #[pyo3(name = "ts_init")]
83    fn py_ts_init(&self) -> u64 {
84        self.ts_last.as_u64()
85    }
86
87    #[getter]
88    #[pyo3(name = "ts_last")]
89    fn py_ts_last(&self) -> u64 {
90        self.ts_last.as_u64()
91    }
92
93    #[getter]
94    #[pyo3(name = "update_count")]
95    fn py_update_count(&self) -> u64 {
96        self.update_count
97    }
98
99    /// Resets the order book to its initial empty state.
100    #[pyo3(name = "reset")]
101    fn py_reset(&mut self) {
102        self.reset();
103    }
104
105    /// Adds an order to the book after preprocessing based on book type.
106    #[pyo3(name = "add")]
107    #[pyo3(signature = (order, flags, sequence, ts_event))]
108    fn py_add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
109        self.add(order, flags, sequence, ts_event.into());
110    }
111
112    /// Updates an existing order in the book after preprocessing based on book type.
113    #[pyo3(name = "update")]
114    #[pyo3(signature = (order, flags, sequence, ts_event))]
115    fn py_update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
116        self.update(order, flags, sequence, ts_event.into());
117    }
118
119    /// Deletes an order from the book after preprocessing based on book type.
120    #[pyo3(name = "delete")]
121    #[pyo3(signature = (order, flags, sequence, ts_event))]
122    fn py_delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
123        self.delete(order, flags, sequence, ts_event.into());
124    }
125
126    /// Clears all orders from both sides of the book.
127    #[pyo3(name = "clear")]
128    #[pyo3(signature = (sequence, ts_event))]
129    fn py_clear(&mut self, sequence: u64, ts_event: u64) {
130        self.clear(sequence, ts_event.into());
131    }
132
133    /// Clears all bid orders from the book.
134    #[pyo3(name = "clear_bids")]
135    #[pyo3(signature = (sequence, ts_event))]
136    fn py_clear_bids(&mut self, sequence: u64, ts_event: u64) {
137        self.clear_bids(sequence, ts_event.into());
138    }
139
140    /// Clears all ask orders from the book.
141    #[pyo3(name = "clear_asks")]
142    #[pyo3(signature = (sequence, ts_event))]
143    fn py_clear_asks(&mut self, sequence: u64, ts_event: u64) {
144        self.clear_asks(sequence, ts_event.into());
145    }
146
147    /// Removes overlapped bid/ask levels when the book is strictly crossed (best bid > best ask)
148    ///
149    /// - Acts only when both sides exist and the book is crossed.
150    /// - Deletes by removing whole price levels via the ladder API to preserve invariants.
151    /// - `side=None` clears both overlapped ranges (conservative, may widen spread).
152    /// - `side=Buy` clears crossed bids only; side=Sell clears crossed asks only.
153    /// - Returns removed price levels (crossed bids first, then crossed asks), or None if nothing removed.
154    #[pyo3(name = "clear_stale_levels")]
155    #[pyo3(signature = (side=None))]
156    fn py_clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
157        self.clear_stale_levels(side)
158    }
159
160    /// Applies a single order book delta operation.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if:
165    /// - The delta's instrument ID does not match this book's instrument ID.
166    /// - An `Add` is given with no side, either explicitly or because the cache lookup failed.
167    /// - An `Add` with no side matches an order ID on both sides of the book.
168    /// - After resolution the delta still has no side but its action is not `Clear`.
169    ///
170    /// # Notes
171    ///
172    /// An ambiguous no-side `Update` or `Delete` is skipped with a warning.
173    #[pyo3(name = "apply_delta")]
174    fn py_apply_delta(&mut self, delta: &OrderBookDelta) -> PyResult<()> {
175        self.apply_delta_unchecked(delta).map_err(to_pyruntime_err)
176    }
177
178    /// Applies multiple order book delta operations.
179    ///
180    /// # Errors
181    ///
182    /// Returns an error if:
183    /// - The deltas' instrument ID does not match this book's instrument ID.
184    /// - Any individual delta application fails (see `Self.apply_delta`).
185    #[pyo3(name = "apply_deltas")]
186    fn py_apply_deltas(&mut self, deltas: &OrderBookDeltas) -> PyResult<()> {
187        self.apply_deltas_unchecked(deltas)
188            .map_err(to_pyruntime_err)
189    }
190
191    /// Replaces current book state with a depth snapshot.
192    ///
193    /// # Errors
194    ///
195    /// Returns an error if the depth's instrument ID does not match this book's instrument ID.
196    #[pyo3(name = "apply_depth")]
197    fn py_apply_depth(&mut self, depth: &OrderBookDepth10) -> PyResult<()> {
198        self.apply_depth_unchecked(depth).map_err(to_pyruntime_err)
199    }
200
201    #[pyo3(name = "check_integrity")]
202    fn py_check_integrity(&mut self) -> PyResult<()> {
203        book_check_integrity(self).map_err(to_pyruntime_err)
204    }
205
206    /// Returns an iterator over bid price levels.
207    #[pyo3(name = "bids")]
208    #[pyo3(signature = (depth=None))]
209    fn py_bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
210        self.bids(depth)
211            .map(|level_ref| (*level_ref).clone())
212            .collect()
213    }
214
215    /// Returns an iterator over ask price levels.
216    #[pyo3(name = "asks")]
217    #[pyo3(signature = (depth=None))]
218    fn py_asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
219        self.asks(depth)
220            .map(|level_ref| (*level_ref).clone())
221            .collect()
222    }
223
224    #[pyo3(name = "bids_to_dict")]
225    #[pyo3(signature = (depth=None))]
226    fn py_bids_to_dict(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
227        self.bids_as_map(depth)
228    }
229
230    #[pyo3(name = "asks_to_dict")]
231    #[pyo3(signature = (depth=None))]
232    fn py_asks_to_dict(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
233        self.asks_as_map(depth)
234    }
235
236    /// Groups bid quantities by price into buckets, limited by depth.
237    #[pyo3(name = "group_bids")]
238    #[pyo3(signature = (group_size, depth=None))]
239    #[must_use]
240    pub fn py_group_bids(
241        &self,
242        group_size: Decimal,
243        depth: Option<usize>,
244    ) -> IndexMap<Decimal, Decimal> {
245        self.group_bids(group_size, depth)
246    }
247
248    /// Groups ask quantities by price into buckets, limited by depth.
249    #[pyo3(name = "group_asks")]
250    #[pyo3(signature = (group_size, depth=None))]
251    #[must_use]
252    pub fn py_group_asks(
253        &self,
254        group_size: Decimal,
255        depth: Option<usize>,
256    ) -> IndexMap<Decimal, Decimal> {
257        self.group_asks(group_size, depth)
258    }
259
260    #[pyo3(name = "bids_filtered_to_dict")]
261    #[pyo3(signature = (depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
262    fn py_bids_filtered_to_dict(
263        &self,
264        depth: Option<usize>,
265        own_book: Option<&OwnOrderBook>,
266        status: Option<std::collections::HashSet<OrderStatus>>,
267        accepted_buffer_ns: Option<u64>,
268        ts_now: Option<u64>,
269    ) -> PyResult<IndexMap<Decimal, Decimal>> {
270        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
271        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
272        Ok(self.bids_filtered_as_map(
273            depth,
274            own_book,
275            status_set.as_ref(),
276            accepted_buffer_ns,
277            ts_now,
278        ))
279    }
280
281    #[pyo3(name = "asks_filtered_to_dict")]
282    #[pyo3(signature = (depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
283    fn py_asks_filtered_to_dict(
284        &self,
285        depth: Option<usize>,
286        own_book: Option<&OwnOrderBook>,
287        status: Option<std::collections::HashSet<OrderStatus>>,
288        accepted_buffer_ns: Option<u64>,
289        ts_now: Option<u64>,
290    ) -> PyResult<IndexMap<Decimal, Decimal>> {
291        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
292        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
293        Ok(self.asks_filtered_as_map(
294            depth,
295            own_book,
296            status_set.as_ref(),
297            accepted_buffer_ns,
298            ts_now,
299        ))
300    }
301
302    #[pyo3(name = "group_bids_filtered")]
303    #[pyo3(signature = (group_size, depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
304    fn py_group_bids_filered(
305        &self,
306        group_size: Decimal,
307        depth: Option<usize>,
308        own_book: Option<&OwnOrderBook>,
309        status: Option<std::collections::HashSet<OrderStatus>>,
310        accepted_buffer_ns: Option<u64>,
311        ts_now: Option<u64>,
312    ) -> PyResult<IndexMap<Decimal, Decimal>> {
313        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
314        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
315        Ok(self.group_bids_filtered(
316            group_size,
317            depth,
318            own_book,
319            status_set.as_ref(),
320            accepted_buffer_ns,
321            ts_now,
322        ))
323    }
324
325    /// Groups ask quantities into price buckets, truncating to a maximum depth, excluding own orders.
326    ///
327    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
328    /// When `now` is provided, only subtracts orders whose acceptance time plus
329    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
330    /// filtering is disabled.
331    #[pyo3(name = "group_asks_filtered")]
332    #[pyo3(signature = (group_size, depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
333    fn py_group_asks_filtered(
334        &self,
335        group_size: Decimal,
336        depth: Option<usize>,
337        own_book: Option<&OwnOrderBook>,
338        status: Option<std::collections::HashSet<OrderStatus>>,
339        accepted_buffer_ns: Option<u64>,
340        ts_now: Option<u64>,
341    ) -> PyResult<IndexMap<Decimal, Decimal>> {
342        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
343        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
344        Ok(self.group_asks_filtered(
345            group_size,
346            depth,
347            own_book,
348            status_set.as_ref(),
349            accepted_buffer_ns,
350            ts_now,
351        ))
352    }
353
354    /// Returns a filtered `OrderBook` view with own sizes subtracted from public levels.
355    #[pyo3(name = "filtered_view")]
356    #[pyo3(signature = (own_book=None, depth=None, status=None, accepted_buffer_ns=None, ts_now=None))]
357    fn py_filtered_view(
358        &self,
359        own_book: Option<&OwnOrderBook>,
360        depth: Option<usize>,
361        status: Option<std::collections::HashSet<OrderStatus>>,
362        accepted_buffer_ns: Option<u64>,
363        ts_now: Option<u64>,
364    ) -> PyResult<Self> {
365        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
366        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
367        self.filtered_view_checked(
368            own_book,
369            depth,
370            status_set.as_ref(),
371            accepted_buffer_ns,
372            ts_now,
373        )
374        .map_err(to_pyvalue_err)
375    }
376
377    /// Returns the best bid price if available.
378    #[pyo3(name = "best_bid_price")]
379    fn py_best_bid_price(&self) -> Option<Price> {
380        self.best_bid_price()
381    }
382
383    /// Returns the best ask price if available.
384    #[pyo3(name = "best_ask_price")]
385    fn py_best_ask_price(&self) -> Option<Price> {
386        self.best_ask_price()
387    }
388
389    /// Returns the size at the best bid price if available.
390    #[pyo3(name = "best_bid_size")]
391    fn py_best_bid_size(&self) -> Option<Quantity> {
392        self.best_bid_size()
393    }
394
395    /// Returns the size at the best ask price if available.
396    #[pyo3(name = "best_ask_size")]
397    fn py_best_ask_size(&self) -> Option<Quantity> {
398        self.best_ask_size()
399    }
400
401    /// Returns the spread between best ask and bid prices if both exist.
402    #[pyo3(name = "spread")]
403    fn py_spread(&self) -> Option<f64> {
404        self.spread()
405    }
406
407    /// Returns the midpoint between best ask and bid prices if both exist.
408    #[pyo3(name = "midpoint")]
409    fn py_midpoint(&self) -> Option<f64> {
410        self.midpoint()
411    }
412
413    /// Calculates the average price to fill the specified quantity.
414    #[pyo3(name = "get_avg_px_for_quantity")]
415    fn py_get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
416        self.get_avg_px_for_quantity(qty, order_side)
417    }
418
419    /// Calculates the worst (last-touched) price to fill the specified quantity.
420    #[pyo3(name = "get_worst_px_for_quantity")]
421    fn py_get_worst_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> Option<Price> {
422        self.get_worst_px_for_quantity(qty, order_side)
423    }
424
425    /// Calculates average price and quantity for target exposure. Returns (price, quantity, `executed_exposure`).
426    #[pyo3(name = "get_avg_px_qty_for_exposure")]
427    fn py_get_avg_px_qty_for_exposure(
428        &self,
429        qty: Quantity,
430        order_side: OrderSide,
431    ) -> (f64, f64, f64) {
432        self.get_avg_px_qty_for_exposure(qty, order_side)
433    }
434
435    /// Returns the cumulative quantity available at or better than the specified price.
436    ///
437    /// For a BUY order, sums ask levels at or below the price.
438    /// For a SELL order, sums bid levels at or above the price.
439    #[pyo3(name = "get_quantity_for_price")]
440    fn py_get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
441        self.get_quantity_for_price(price, order_side)
442    }
443
444    /// Returns the quantity at a specific price level only, or 0 if no level exists.
445    ///
446    /// Unlike `get_quantity_for_price` which returns cumulative quantity across
447    /// multiple levels, this returns only the quantity at the exact price level.
448    #[pyo3(name = "get_quantity_at_level")]
449    fn py_get_quantity_at_level(
450        &self,
451        price: Price,
452        order_side: OrderSide,
453        size_precision: u8,
454    ) -> Quantity {
455        self.get_quantity_at_level(price, order_side, size_precision)
456    }
457
458    /// Simulates fills for an order, returning list of (price, quantity) tuples.
459    #[pyo3(name = "simulate_fills")]
460    fn py_simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
461        self.simulate_fills(order)
462    }
463
464    /// Return a formatted string representation of the order book.
465    #[pyo3(name = "pprint")]
466    #[pyo3(signature = (num_levels=3, group_size=None))]
467    fn py_pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
468        self.pprint(num_levels, group_size)
469    }
470}
471
472/// Updates the `OrderBook` with a [`QuoteTick`].
473///
474/// # Errors
475///
476/// Returns a `PyErr` if the update operation fails.
477#[pyfunction()]
478#[pyo3(name = "update_book_with_quote_tick")]
479pub fn py_update_book_with_quote_tick(book: &mut OrderBook, quote: &QuoteTick) -> PyResult<()> {
480    book.update_quote_tick(quote).map_err(to_pyvalue_err)
481}
482
483/// Updates the `OrderBook` with a [`TradeTick`].
484///
485/// # Errors
486///
487/// Returns a `PyErr` if the update operation fails.
488#[pyfunction()]
489#[pyo3(name = "update_book_with_trade_tick")]
490pub fn py_update_book_with_trade_tick(book: &mut OrderBook, trade: &TradeTick) -> PyResult<()> {
491    book.update_trade_tick(trade).map_err(to_pyvalue_err)
492}