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