nautilus_model/python/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
16use indexmap::IndexMap;
17use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
18use pyo3::prelude::*;
19use rust_decimal::Decimal;
20
21use crate::{
22    data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
23    enums::{BookType, OrderSide, OrderStatus},
24    identifiers::InstrumentId,
25    orderbook::{BookLevel, OrderBook, analysis::book_check_integrity, own::OwnOrderBook},
26    types::{Price, Quantity},
27};
28
29#[pymethods]
30impl OrderBook {
31    #[new]
32    fn py_new(instrument_id: InstrumentId, book_type: BookType) -> Self {
33        Self::new(instrument_id, book_type)
34    }
35
36    fn __repr__(&self) -> String {
37        format!("{self:?}")
38    }
39
40    fn __str__(&self) -> String {
41        self.to_string()
42    }
43
44    #[getter]
45    #[pyo3(name = "instrument_id")]
46    fn py_instrument_id(&self) -> InstrumentId {
47        self.instrument_id
48    }
49
50    #[getter]
51    #[pyo3(name = "book_type")]
52    fn py_book_type(&self) -> BookType {
53        self.book_type
54    }
55
56    #[getter]
57    #[pyo3(name = "sequence")]
58    fn py_sequence(&self) -> u64 {
59        self.sequence
60    }
61
62    #[getter]
63    #[pyo3(name = "ts_event")]
64    fn py_ts_event(&self) -> u64 {
65        self.ts_last.as_u64()
66    }
67
68    #[getter]
69    #[pyo3(name = "ts_init")]
70    fn py_ts_init(&self) -> u64 {
71        self.ts_last.as_u64()
72    }
73
74    #[getter]
75    #[pyo3(name = "ts_last")]
76    fn py_ts_last(&self) -> u64 {
77        self.ts_last.as_u64()
78    }
79
80    #[getter]
81    #[pyo3(name = "update_count")]
82    fn py_update_count(&self) -> u64 {
83        self.update_count
84    }
85
86    #[pyo3(name = "reset")]
87    fn py_reset(&mut self) {
88        self.reset();
89    }
90
91    #[pyo3(name = "add")]
92    #[pyo3(signature = (order, flags, sequence, ts_event))]
93    fn py_add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
94        self.add(order, flags, sequence, ts_event.into());
95    }
96
97    #[pyo3(name = "update")]
98    #[pyo3(signature = (order, flags, sequence, ts_event))]
99    fn py_update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
100        self.update(order, flags, sequence, ts_event.into());
101    }
102
103    #[pyo3(name = "delete")]
104    #[pyo3(signature = (order, flags, sequence, ts_event))]
105    fn py_delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: u64) {
106        self.delete(order, flags, sequence, ts_event.into());
107    }
108
109    #[pyo3(name = "clear")]
110    #[pyo3(signature = (sequence, ts_event))]
111    fn py_clear(&mut self, sequence: u64, ts_event: u64) {
112        self.clear(sequence, ts_event.into());
113    }
114
115    #[pyo3(name = "clear_bids")]
116    #[pyo3(signature = (sequence, ts_event))]
117    fn py_clear_bids(&mut self, sequence: u64, ts_event: u64) {
118        self.clear_bids(sequence, ts_event.into());
119    }
120
121    #[pyo3(name = "clear_asks")]
122    #[pyo3(signature = (sequence, ts_event))]
123    fn py_clear_asks(&mut self, sequence: u64, ts_event: u64) {
124        self.clear_asks(sequence, ts_event.into());
125    }
126
127    #[pyo3(name = "clear_stale_levels")]
128    #[pyo3(signature = (side=None))]
129    fn py_clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
130        self.clear_stale_levels(side)
131    }
132
133    #[pyo3(name = "apply_delta")]
134    fn py_apply_delta(&mut self, delta: &OrderBookDelta) -> PyResult<()> {
135        self.apply_delta_unchecked(delta).map_err(to_pyruntime_err)
136    }
137
138    #[pyo3(name = "apply_deltas")]
139    fn py_apply_deltas(&mut self, deltas: &OrderBookDeltas) -> PyResult<()> {
140        self.apply_deltas_unchecked(deltas)
141            .map_err(to_pyruntime_err)
142    }
143
144    #[pyo3(name = "apply_depth")]
145    fn py_apply_depth(&mut self, depth: &OrderBookDepth10) -> PyResult<()> {
146        self.apply_depth_unchecked(depth).map_err(to_pyruntime_err)
147    }
148
149    #[pyo3(name = "check_integrity")]
150    fn py_check_integrity(&mut self) -> PyResult<()> {
151        book_check_integrity(self).map_err(to_pyruntime_err)
152    }
153
154    #[pyo3(name = "bids")]
155    #[pyo3(signature = (depth=None))]
156    fn py_bids(&self, depth: Option<usize>) -> Vec<BookLevel> {
157        self.bids(depth)
158            .map(|level_ref| (*level_ref).clone())
159            .collect()
160    }
161
162    #[pyo3(name = "asks")]
163    #[pyo3(signature = (depth=None))]
164    fn py_asks(&self, depth: Option<usize>) -> Vec<BookLevel> {
165        self.asks(depth)
166            .map(|level_ref| (*level_ref).clone())
167            .collect()
168    }
169
170    #[pyo3(name = "bids_to_dict")]
171    #[pyo3(signature = (depth=None))]
172    fn py_bids_to_dict(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
173        self.bids_as_map(depth)
174    }
175
176    #[pyo3(name = "asks_to_dict")]
177    #[pyo3(signature = (depth=None))]
178    fn py_asks_to_dict(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
179        self.asks_as_map(depth)
180    }
181
182    #[pyo3(name = "group_bids")]
183    #[pyo3(signature = (group_size, depth=None))]
184    pub fn py_group_bids(
185        &self,
186        group_size: Decimal,
187        depth: Option<usize>,
188    ) -> IndexMap<Decimal, Decimal> {
189        self.group_bids(group_size, depth)
190    }
191
192    #[pyo3(name = "group_asks")]
193    #[pyo3(signature = (group_size, depth=None))]
194    pub fn py_group_asks(
195        &self,
196        group_size: Decimal,
197        depth: Option<usize>,
198    ) -> IndexMap<Decimal, Decimal> {
199        self.group_asks(group_size, depth)
200    }
201
202    #[pyo3(name = "bids_filtered_to_dict")]
203    #[pyo3(signature = (depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
204    fn py_bids_filtered_to_dict(
205        &self,
206        depth: Option<usize>,
207        own_book: Option<&OwnOrderBook>,
208        status: Option<std::collections::HashSet<OrderStatus>>,
209        accepted_buffer_ns: Option<u64>,
210        ts_now: Option<u64>,
211    ) -> IndexMap<Decimal, Decimal> {
212        self.bids_filtered_as_map(
213            depth,
214            own_book,
215            status.map(|s| s.into_iter().collect()),
216            accepted_buffer_ns,
217            ts_now,
218        )
219    }
220
221    #[pyo3(name = "asks_filtered_to_dict")]
222    #[pyo3(signature = (depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
223    fn py_asks_filtered_to_dict(
224        &self,
225        depth: Option<usize>,
226        own_book: Option<&OwnOrderBook>,
227        status: Option<std::collections::HashSet<OrderStatus>>,
228        accepted_buffer_ns: Option<u64>,
229        ts_now: Option<u64>,
230    ) -> IndexMap<Decimal, Decimal> {
231        self.asks_filtered_as_map(
232            depth,
233            own_book,
234            status.map(|s| s.into_iter().collect()),
235            accepted_buffer_ns,
236            ts_now,
237        )
238    }
239
240    #[pyo3(name = "group_bids_filtered")]
241    #[pyo3(signature = (group_size, depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
242    fn py_group_bids_filered(
243        &self,
244        group_size: Decimal,
245        depth: Option<usize>,
246        own_book: Option<&OwnOrderBook>,
247        status: Option<std::collections::HashSet<OrderStatus>>,
248        accepted_buffer_ns: Option<u64>,
249        ts_now: Option<u64>,
250    ) -> IndexMap<Decimal, Decimal> {
251        self.group_bids_filtered(
252            group_size,
253            depth,
254            own_book,
255            status.map(|s| s.into_iter().collect()),
256            accepted_buffer_ns,
257            ts_now,
258        )
259    }
260
261    #[pyo3(name = "group_asks_filtered")]
262    #[pyo3(signature = (group_size, depth=None, own_book=None, status=None, accepted_buffer_ns=None, ts_now=None))]
263    fn py_group_asks_filtered(
264        &self,
265        group_size: Decimal,
266        depth: Option<usize>,
267        own_book: Option<&OwnOrderBook>,
268        status: Option<std::collections::HashSet<OrderStatus>>,
269        accepted_buffer_ns: Option<u64>,
270        ts_now: Option<u64>,
271    ) -> IndexMap<Decimal, Decimal> {
272        self.group_asks_filtered(
273            group_size,
274            depth,
275            own_book,
276            status.map(|s| s.into_iter().collect()),
277            accepted_buffer_ns,
278            ts_now,
279        )
280    }
281
282    #[pyo3(name = "best_bid_price")]
283    fn py_best_bid_price(&self) -> Option<Price> {
284        self.best_bid_price()
285    }
286
287    #[pyo3(name = "best_ask_price")]
288    fn py_best_ask_price(&self) -> Option<Price> {
289        self.best_ask_price()
290    }
291
292    #[pyo3(name = "best_bid_size")]
293    fn py_best_bid_size(&self) -> Option<Quantity> {
294        self.best_bid_size()
295    }
296
297    #[pyo3(name = "best_ask_size")]
298    fn py_best_ask_size(&self) -> Option<Quantity> {
299        self.best_ask_size()
300    }
301
302    #[pyo3(name = "spread")]
303    fn py_spread(&self) -> Option<f64> {
304        self.spread()
305    }
306
307    #[pyo3(name = "midpoint")]
308    fn py_midpoint(&self) -> Option<f64> {
309        self.midpoint()
310    }
311
312    #[pyo3(name = "get_avg_px_for_quantity")]
313    fn py_get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
314        self.get_avg_px_for_quantity(qty, order_side)
315    }
316
317    #[pyo3(name = "get_avg_px_qty_for_exposure")]
318    fn py_get_avg_px_qty_for_exposure(
319        &self,
320        qty: Quantity,
321        order_side: OrderSide,
322    ) -> (f64, f64, f64) {
323        self.get_avg_px_qty_for_exposure(qty, order_side)
324    }
325
326    #[pyo3(name = "get_quantity_for_price")]
327    fn py_get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
328        self.get_quantity_for_price(price, order_side)
329    }
330
331    #[pyo3(name = "simulate_fills")]
332    fn py_simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
333        self.simulate_fills(order)
334    }
335
336    #[pyo3(name = "pprint")]
337    #[pyo3(signature = (num_levels=3, group_size=None))]
338    fn py_pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
339        self.pprint(num_levels, group_size)
340    }
341}
342
343/// Updates the `OrderBook` with a [`QuoteTick`].
344///
345/// # Errors
346///
347/// Returns a `PyErr` if the update operation fails.
348#[pyfunction()]
349#[pyo3(name = "update_book_with_quote_tick")]
350pub fn py_update_book_with_quote_tick(book: &mut OrderBook, quote: &QuoteTick) -> PyResult<()> {
351    book.update_quote_tick(quote).map_err(to_pyvalue_err)
352}
353
354/// Updates the `OrderBook` with a [`TradeTick`].
355///
356/// # Errors
357///
358/// Returns a `PyErr` if the update operation fails.
359#[pyfunction()]
360#[pyo3(name = "update_book_with_trade_tick")]
361pub fn py_update_book_with_trade_tick(book: &mut OrderBook, trade: &TradeTick) -> PyResult<()> {
362    book.update_trade_tick(trade).map_err(to_pyvalue_err)
363}