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