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