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 std::str::FromStr;
17
18use ahash::AHashSet;
19use indexmap::IndexMap;
20use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
21use pyo3::{IntoPyObjectExt, prelude::*};
22use rust_decimal::Decimal;
23
24use crate::{
25    data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick},
26    enums::{BookType, OrderSide, OrderStatus},
27    identifiers::InstrumentId,
28    orderbook::{
29        BookLevel, OrderBook,
30        analysis::book_check_integrity,
31        own::{OwnOrderBook, validate_accepted_buffer},
32    },
33    python::{orderbook::level::check_sizes_float_precision, types::fixed::FloatArithmetic},
34    types::{Price, Quantity},
35};
36
37#[pymethods]
38#[pyo3_stub_gen::derive::gen_stub_pymethods]
39impl OrderBook {
40    /// Provides a high-performance, versatile order book.
41    ///
42    /// Maintains buy (bid) and sell (ask) orders in price-time priority, supporting multiple
43    /// market data formats:
44    /// - L3 (MBO): Market By Order - tracks individual orders with unique IDs.
45    /// - L2 (MBP): Market By Price - aggregates orders at each price level.
46    /// - L1 (MBP): Top-of-Book - maintains only the best bid and ask prices.
47    #[new]
48    fn py_new(instrument_id: InstrumentId, book_type: BookType) -> Self {
49        Self::new(instrument_id, book_type)
50    }
51
52    fn __repr__(&self) -> String {
53        format!("{self:?}")
54    }
55
56    fn __str__(&self) -> String {
57        self.to_string()
58    }
59
60    fn __getstate__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
61        let orders = self
62            .bids(None)
63            .chain(self.asks(None))
64            .flat_map(|level| level.iter().copied())
65            .collect::<Vec<_>>();
66        (
67            self.instrument_id,
68            self.book_type.to_string(),
69            self.sequence,
70            self.ts_last.as_u64(),
71            self.update_count,
72            orders,
73            self.bids.batch_state_code(),
74            self.asks.batch_state_code(),
75        )
76            .into_py_any(py)
77    }
78
79    fn __setstate__(&mut self, state: &Bound<'_, PyAny>) -> PyResult<()> {
80        let (
81            instrument_id,
82            book_type,
83            sequence,
84            ts_last,
85            update_count,
86            orders,
87            bid_batch_state,
88            ask_batch_state,
89        ): (InstrumentId, String, u64, u64, u64, Vec<BookOrder>, u8, u8) = state.extract()?;
90        let book_type = BookType::from_str(&book_type).map_err(to_pyvalue_err)?;
91
92        if instrument_id != self.instrument_id {
93            return Err(to_pyvalue_err(format!(
94                "OrderBook state instrument ID {instrument_id} does not match instance instrument ID {}",
95                self.instrument_id
96            )));
97        }
98
99        if book_type != self.book_type {
100            return Err(to_pyvalue_err(format!(
101                "OrderBook state book type {book_type:?} does not match instance book type {:?}",
102                self.book_type
103            )));
104        }
105
106        if orders.iter().any(|order| order.side.is_none()) {
107            return Err(to_pyvalue_err(
108                "OrderBook state contains an order with no side",
109            ));
110        }
111
112        let mut restored = Self::new(instrument_id, book_type);
113        for order in orders {
114            restored.add(order, 0, 0, 0.into());
115        }
116        restored
117            .bids
118            .set_batch_state_code(bid_batch_state)
119            .map_err(to_pyvalue_err)?;
120        restored
121            .asks
122            .set_batch_state_code(ask_batch_state)
123            .map_err(to_pyvalue_err)?;
124        restored.sequence = sequence;
125        restored.ts_last = ts_last.into();
126        restored.update_count = update_count;
127        *self = restored;
128        Ok(())
129    }
130
131    fn __reduce__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
132        let constructor = py.get_type::<Self>().getattr("_safe_constructor")?;
133        let args = (self.instrument_id, self.book_type.to_string());
134        let state = self.__getstate__(py)?;
135        (constructor, args, state).into_py_any(py)
136    }
137
138    #[staticmethod]
139    fn _safe_constructor(instrument_id: InstrumentId, book_type: &str) -> PyResult<Self> {
140        let book_type = BookType::from_str(book_type).map_err(to_pyvalue_err)?;
141        Ok(Self::new(instrument_id, book_type))
142    }
143
144    fn __deepcopy__(&self, _memo: &Bound<'_, PyAny>) -> Self {
145        self.clone()
146    }
147
148    #[getter]
149    #[pyo3(name = "instrument_id")]
150    fn py_instrument_id(&self) -> InstrumentId {
151        self.instrument_id
152    }
153
154    #[getter]
155    #[pyo3(name = "book_type")]
156    fn py_book_type(&self) -> BookType {
157        self.book_type
158    }
159
160    #[getter]
161    #[pyo3(name = "sequence")]
162    fn py_sequence(&self) -> u64 {
163        self.sequence
164    }
165
166    #[getter]
167    #[pyo3(name = "ts_event")]
168    fn py_ts_event(&self) -> u64 {
169        self.ts_last.as_u64()
170    }
171
172    #[getter]
173    #[pyo3(name = "ts_init")]
174    fn py_ts_init(&self) -> u64 {
175        self.ts_last.as_u64()
176    }
177
178    #[getter]
179    #[pyo3(name = "ts_last")]
180    fn py_ts_last(&self) -> u64 {
181        self.ts_last.as_u64()
182    }
183
184    #[getter]
185    #[pyo3(name = "update_count")]
186    fn py_update_count(&self) -> u64 {
187        self.update_count
188    }
189
190    /// Resets the order book to its initial empty state.
191    #[pyo3(name = "reset")]
192    fn py_reset(&mut self) {
193        self.reset();
194    }
195
196    /// Adds an order to the book after preprocessing based on book type.
197    #[pyo3(name = "add")]
198    #[pyo3(signature = (order, flags, sequence, ts_event))]
199    fn py_add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
200        self.add(order, flags, sequence, ts_event.into());
201    }
202
203    /// Updates an existing order in the book after preprocessing based on book type.
204    #[pyo3(name = "update")]
205    #[pyo3(signature = (order, flags, sequence, ts_event))]
206    fn py_update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
207        self.update(order, flags, sequence, ts_event.into());
208    }
209
210    /// Deletes an order from the book after preprocessing based on book type.
211    #[pyo3(name = "delete")]
212    #[pyo3(signature = (order, flags, sequence, ts_event))]
213    fn py_delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
214        self.delete(order, flags, sequence, ts_event.into());
215    }
216
217    /// Clears all orders from both sides of the book.
218    ///
219    /// A full clear uses its `sequence` as the new sequence high-water.
220    /// `clear_bids` and `clear_asks` preserve the current high-water.
221    #[pyo3(name = "clear")]
222    #[pyo3(signature = (sequence, ts_event))]
223    fn py_clear(&mut self, sequence: u64, ts_event: u64) {
224        self.clear(sequence, ts_event.into());
225    }
226
227    /// Clears all bid orders from the book.
228    #[pyo3(name = "clear_bids")]
229    #[pyo3(signature = (sequence, ts_event))]
230    fn py_clear_bids(&mut self, sequence: u64, ts_event: u64) {
231        self.clear_bids(sequence, ts_event.into());
232    }
233
234    /// Clears all ask orders from the book.
235    #[pyo3(name = "clear_asks")]
236    #[pyo3(signature = (sequence, ts_event))]
237    fn py_clear_asks(&mut self, sequence: u64, ts_event: u64) {
238        self.clear_asks(sequence, ts_event.into());
239    }
240
241    /// Removes overlapped bid/ask levels when the book is strictly crossed (best bid > best ask)
242    ///
243    /// - Acts only when both sides exist and the book is crossed.
244    /// - Deletes by removing whole price levels via the ladder API to preserve invariants.
245    /// - `side=None` clears both overlapped ranges (conservative, may widen spread).
246    /// - `side=Buy` clears crossed bids only; side=Sell clears crossed asks only.
247    /// - Returns removed price levels (crossed bids first, then crossed asks), or None if nothing removed.
248    #[pyo3(name = "clear_stale_levels")]
249    #[pyo3(signature = (side=None))]
250    fn py_clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
251        self.clear_stale_levels(side)
252    }
253
254    /// Applies a single order book delta operation.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if:
259    /// - The delta's instrument ID does not match this book's instrument ID.
260    /// - An `Add` is given with no side, either explicitly or because the cache lookup failed.
261    /// - An `Add` with no side matches an order ID on both sides of the book.
262    /// - After resolution the delta still has no side but its action is not `Clear`.
263    ///
264    /// # Notes
265    ///
266    /// An ambiguous no-side `Update` or `Delete` is skipped with a warning.
267    #[pyo3(name = "apply_delta")]
268    fn py_apply_delta(&mut self, delta: &OrderBookDelta) -> PyResult<()> {
269        self.apply_delta_unchecked(delta).map_err(to_pyruntime_err)
270    }
271
272    /// Applies multiple order book delta operations.
273    ///
274    /// # Errors
275    ///
276    /// Returns an error if:
277    /// - The deltas' instrument ID does not match this book's instrument ID.
278    /// - Any individual delta application fails (see `Self.apply_delta`).
279    #[pyo3(name = "apply_deltas")]
280    fn py_apply_deltas(&mut self, deltas: &OrderBookDeltas) -> PyResult<()> {
281        self.apply_deltas_unchecked(deltas)
282            .map_err(to_pyruntime_err)
283    }
284
285    /// Replaces current book state with a depth snapshot.
286    ///
287    /// # Errors
288    ///
289    /// Returns an error if the depth's instrument ID does not match this book's instrument ID.
290    #[pyo3(name = "apply_depth")]
291    fn py_apply_depth(&mut self, depth: &OrderBookDepth) -> PyResult<()> {
292        self.apply_depth_unchecked(depth).map_err(to_pyruntime_err)
293    }
294
295    #[pyo3(name = "check_integrity")]
296    fn py_check_integrity(&mut self) -> PyResult<()> {
297        book_check_integrity(self).map_err(to_pyruntime_err)
298    }
299
300    /// Returns an iterator over bid price levels.
301    #[pyo3(name = "bids")]
302    #[pyo3(signature = (depth=None))]
303    fn py_bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
304        self.bids(depth)
305            .map(|level_ref| (*level_ref).clone())
306            .collect()
307    }
308
309    /// Returns an iterator over ask price levels.
310    #[pyo3(name = "asks")]
311    #[pyo3(signature = (depth=None))]
312    fn py_asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
313        self.asks(depth)
314            .map(|level_ref| (*level_ref).clone())
315            .collect()
316    }
317
318    #[pyo3(name = "bids_to_dict")]
319    #[pyo3(signature = (depth=None))]
320    fn py_bids_to_dict(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
321        self.bids_as_map(depth)
322    }
323
324    #[pyo3(name = "asks_to_dict")]
325    #[pyo3(signature = (depth=None))]
326    fn py_asks_to_dict(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
327        self.asks_as_map(depth)
328    }
329
330    /// Groups bid quantities by price into buckets, limited by depth.
331    #[pyo3(name = "group_bids")]
332    #[pyo3(signature = (group_size, depth=None))]
333    #[must_use]
334    pub fn py_group_bids(
335        &self,
336        group_size: Decimal,
337        depth: Option<usize>,
338    ) -> IndexMap<Decimal, Decimal> {
339        self.group_bids(group_size, depth)
340    }
341
342    /// Groups ask quantities by price into buckets, limited by depth.
343    #[pyo3(name = "group_asks")]
344    #[pyo3(signature = (group_size, depth=None))]
345    #[must_use]
346    pub fn py_group_asks(
347        &self,
348        group_size: Decimal,
349        depth: Option<usize>,
350    ) -> IndexMap<Decimal, Decimal> {
351        self.group_asks(group_size, depth)
352    }
353
354    #[pyo3(name = "bids_filtered_to_dict")]
355    #[pyo3(signature = (depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
356    fn py_bids_filtered_to_dict(
357        &self,
358        depth: Option<usize>,
359        own_book: Option<&OwnOrderBook>,
360        status: Option<std::collections::HashSet<OrderStatus>>,
361        accepted_buffer_ns: Option<u64>,
362        ts_now: Option<u64>,
363    ) -> PyResult<IndexMap<Decimal, Decimal>> {
364        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
365        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
366        Ok(self.bids_filtered_as_map(
367            depth,
368            own_book,
369            status_set.as_ref(),
370            accepted_buffer_ns,
371            ts_now,
372        ))
373    }
374
375    #[pyo3(name = "asks_filtered_to_dict")]
376    #[pyo3(signature = (depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
377    fn py_asks_filtered_to_dict(
378        &self,
379        depth: Option<usize>,
380        own_book: Option<&OwnOrderBook>,
381        status: Option<std::collections::HashSet<OrderStatus>>,
382        accepted_buffer_ns: Option<u64>,
383        ts_now: Option<u64>,
384    ) -> PyResult<IndexMap<Decimal, Decimal>> {
385        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
386        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
387        Ok(self.asks_filtered_as_map(
388            depth,
389            own_book,
390            status_set.as_ref(),
391            accepted_buffer_ns,
392            ts_now,
393        ))
394    }
395
396    #[pyo3(name = "group_bids_filtered")]
397    #[pyo3(signature = (group_size, depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
398    fn py_group_bids_filered(
399        &self,
400        group_size: Decimal,
401        depth: Option<usize>,
402        own_book: Option<&OwnOrderBook>,
403        status: Option<std::collections::HashSet<OrderStatus>>,
404        accepted_buffer_ns: Option<u64>,
405        ts_now: Option<u64>,
406    ) -> PyResult<IndexMap<Decimal, Decimal>> {
407        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
408        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
409        Ok(self.group_bids_filtered(
410            group_size,
411            depth,
412            own_book,
413            status_set.as_ref(),
414            accepted_buffer_ns,
415            ts_now,
416        ))
417    }
418
419    /// Groups ask quantities into price buckets, truncating to a maximum depth, excluding own orders.
420    ///
421    /// With `own_book`, subtracts own order sizes, filtered by `status` if provided.
422    /// When `now` is provided, only subtracts orders whose acceptance time plus
423    /// `accepted_buffer_ns` is at or before `now`. When `now` is `None`, acceptance-time
424    /// filtering is disabled.
425    #[pyo3(name = "group_asks_filtered")]
426    #[pyo3(signature = (group_size, depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
427    fn py_group_asks_filtered(
428        &self,
429        group_size: Decimal,
430        depth: Option<usize>,
431        own_book: Option<&OwnOrderBook>,
432        status: Option<std::collections::HashSet<OrderStatus>>,
433        accepted_buffer_ns: Option<u64>,
434        ts_now: Option<u64>,
435    ) -> PyResult<IndexMap<Decimal, Decimal>> {
436        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
437        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
438        Ok(self.group_asks_filtered(
439            group_size,
440            depth,
441            own_book,
442            status_set.as_ref(),
443            accepted_buffer_ns,
444            ts_now,
445        ))
446    }
447
448    /// Returns a filtered `OrderBook` view with own sizes subtracted from public levels.
449    #[pyo3(name = "filtered_view")]
450    #[pyo3(signature = (own_book=None, depth=None, status=None, accepted_buffer_ns=None, ts_now=None))]
451    fn py_filtered_view(
452        &self,
453        own_book: Option<&OwnOrderBook>,
454        depth: Option<usize>,
455        status: Option<std::collections::HashSet<OrderStatus>>,
456        accepted_buffer_ns: Option<u64>,
457        ts_now: Option<u64>,
458    ) -> PyResult<Self> {
459        validate_accepted_buffer(accepted_buffer_ns, ts_now).map_err(to_pyvalue_err)?;
460        let status_set: Option<AHashSet<OrderStatus>> = status.map(|s| s.into_iter().collect());
461        self.filtered_view_checked(
462            own_book,
463            depth,
464            status_set.as_ref(),
465            accepted_buffer_ns,
466            ts_now,
467        )
468        .map_err(to_pyvalue_err)
469    }
470
471    /// Returns the best bid price if available.
472    #[pyo3(name = "best_bid_price")]
473    fn py_best_bid_price(&self) -> Option<Price> {
474        self.best_bid_price()
475    }
476
477    /// Returns the best ask price if available.
478    #[pyo3(name = "best_ask_price")]
479    fn py_best_ask_price(&self) -> Option<Price> {
480        self.best_ask_price()
481    }
482
483    /// Returns the size at the best bid price if available.
484    #[pyo3(name = "best_bid_size")]
485    fn py_best_bid_size(&self) -> Option<Quantity> {
486        self.best_bid_size()
487    }
488
489    /// Returns the size at the best ask price if available.
490    #[pyo3(name = "best_ask_size")]
491    fn py_best_ask_size(&self) -> Option<Quantity> {
492        self.best_ask_size()
493    }
494
495    /// Returns the spread between best ask and bid prices if both exist.
496    #[pyo3(name = "spread")]
497    fn py_spread(&self) -> PyResult<Option<f64>> {
498        check_top_float_precision(self)?;
499        Ok(self.spread())
500    }
501
502    /// Returns the midpoint between best ask and bid prices if both exist.
503    #[pyo3(name = "midpoint")]
504    fn py_midpoint(&self) -> PyResult<Option<f64>> {
505        check_top_float_precision(self)?;
506        Ok(self.midpoint())
507    }
508
509    /// Calculates the average price to fill the specified quantity.
510    #[pyo3(name = "get_avg_px_for_quantity")]
511    fn py_get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
512        self.get_avg_px_for_quantity(qty, order_side)
513    }
514
515    /// Calculates the worst (last-touched) price to fill the specified quantity.
516    #[pyo3(name = "get_worst_px_for_quantity")]
517    fn py_get_worst_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> Option<Price> {
518        self.get_worst_px_for_quantity(qty, order_side)
519    }
520
521    /// Calculates average price and quantity for target exposure.
522    ///
523    /// Returns (average price, quantity, last-touched price).
524    #[pyo3(name = "get_avg_px_qty_for_exposure")]
525    fn py_get_avg_px_qty_for_exposure(
526        &self,
527        qty: Quantity,
528        order_side: OrderSide,
529    ) -> PyResult<(f64, f64, f64)> {
530        qty.check_float_precision()?;
531
532        for level in side_levels(self, order_side) {
533            level.price.value.check_float_precision()?;
534            check_sizes_float_precision(level)?;
535        }
536
537        Ok(self.get_avg_px_qty_for_exposure(qty, order_side))
538    }
539
540    /// Returns the cumulative quantity available at or better than the specified price.
541    ///
542    /// For a BUY order, sums ask levels at or below the price.
543    /// For a SELL order, sums bid levels at or above the price.
544    #[pyo3(name = "get_quantity_for_price")]
545    fn py_get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> PyResult<f64> {
546        for level in side_levels(self, order_side) {
547            check_sizes_float_precision(level)?;
548        }
549
550        Ok(self.get_quantity_for_price(price, order_side))
551    }
552
553    /// Returns the quantity at a specific price level only, or 0 if no level exists.
554    ///
555    /// Unlike `get_quantity_for_price` which returns cumulative quantity across
556    /// multiple levels, this returns only the quantity at the exact price level.
557    ///
558    /// The Python binding raises `ValueError` for an unsupported `size_precision` or an
559    /// aggregated level size that cannot be represented as a `Quantity`.
560    #[pyo3(name = "get_quantity_at_level")]
561    fn py_get_quantity_at_level(
562        &self,
563        price: Price,
564        order_side: OrderSide,
565        size_precision: u8,
566    ) -> PyResult<Quantity> {
567        self.get_quantity_at_level_checked(price, order_side, size_precision)
568            .map_err(to_pyvalue_err)
569    }
570
571    /// Returns all price levels crossed by an order at the given price and side.
572    ///
573    /// Unlike `simulate_fills`, this returns ALL crossed levels regardless of
574    /// order quantity. Used when liquidity consumption tracking needs visibility
575    /// into all available levels.
576    ///
577    /// The Python binding raises `ValueError` for an unsupported `size_precision` or an
578    /// aggregated level size that cannot be represented as a `Quantity`.
579    #[pyo3(name = "get_all_crossed_levels")]
580    fn py_get_all_crossed_levels(
581        &self,
582        order_side: OrderSide,
583        price: Price,
584        size_precision: u8,
585    ) -> PyResult<Vec<(Price, Quantity)>> {
586        self.get_all_crossed_levels_checked(order_side, price, size_precision)
587            .map_err(to_pyvalue_err)
588    }
589
590    /// Simulates fills for an order, returning list of (price, quantity) tuples.
591    #[pyo3(name = "simulate_fills")]
592    fn py_simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
593        self.simulate_fills(order)
594    }
595
596    /// Creates an `OrderBookDeltas` snapshot from the current order book state.
597    ///
598    /// This is the reverse operation of `apply_deltas`: it converts the current book state
599    /// back into a snapshot format with a `Clear` delta followed by `Add` deltas for all orders.
600    ///
601    /// # Parameters
602    ///
603    /// * `ts_event` - UNIX timestamp (nanoseconds) when the book event occurred.
604    /// * `ts_init` - UNIX timestamp (nanoseconds) when the instance was created.
605    ///
606    /// # Returns
607    ///
608    /// An `OrderBookDeltas` containing a snapshot of the current order book state.
609    #[pyo3(name = "to_deltas")]
610    fn py_to_deltas(&self, ts_event: u64, ts_init: u64) -> OrderBookDeltas {
611        self.to_deltas(ts_event.into(), ts_init.into())
612    }
613
614    /// Return a formatted string representation of the order book.
615    #[pyo3(name = "pprint")]
616    #[pyo3(signature = (num_levels=3, group_size=None))]
617    fn py_pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
618        self.pprint(num_levels, group_size)
619    }
620}
621
622/// Updates the `OrderBook` with a [`QuoteTick`].
623///
624/// # Errors
625///
626/// Returns an error if the update operation fails.
627#[pyfunction()]
628#[pyo3(name = "update_book_with_quote_tick")]
629pub fn py_update_book_with_quote_tick(book: &mut OrderBook, quote: &QuoteTick) -> PyResult<()> {
630    book.update_quote_tick(quote).map_err(to_pyvalue_err)
631}
632
633/// Updates the `OrderBook` with a [`TradeTick`].
634///
635/// # Errors
636///
637/// Returns an error if the update operation fails.
638#[pyfunction()]
639#[pyo3(name = "update_book_with_trade_tick")]
640pub fn py_update_book_with_trade_tick(book: &mut OrderBook, trade: &TradeTick) -> PyResult<()> {
641    book.update_trade_tick(trade).map_err(to_pyvalue_err)
642}
643
644fn check_top_float_precision(book: &OrderBook) -> PyResult<()> {
645    if let (Some(bid), Some(ask)) = (book.best_bid_price(), book.best_ask_price()) {
646        bid.check_float_precision()?;
647        ask.check_float_precision()?;
648    }
649
650    Ok(())
651}
652
653fn side_levels(book: &OrderBook, order_side: OrderSide) -> impl Iterator<Item = &BookLevel> {
654    match order_side {
655        OrderSide::Buy => book.asks.levels.values(),
656        OrderSide::Sell => book.bids.levels.values(),
657    }
658}