1use 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 #[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 #[pyo3(name = "reset")]
191 fn py_reset(&mut self) {
192 self.reset();
193 }
194
195 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[pyo3(name = "best_bid_price")]
472 fn py_best_bid_price(&self) -> Option<Price> {
473 self.best_bid_price()
474 }
475
476 #[pyo3(name = "best_ask_price")]
478 fn py_best_ask_price(&self) -> Option<Price> {
479 self.best_ask_price()
480 }
481
482 #[pyo3(name = "best_bid_size")]
484 fn py_best_bid_size(&self) -> Option<Quantity> {
485 self.best_bid_size()
486 }
487
488 #[pyo3(name = "best_ask_size")]
490 fn py_best_ask_size(&self) -> Option<Quantity> {
491 self.best_ask_size()
492 }
493
494 #[pyo3(name = "spread")]
496 fn py_spread(&self) -> Option<f64> {
497 self.spread()
498 }
499
500 #[pyo3(name = "midpoint")]
502 fn py_midpoint(&self) -> Option<f64> {
503 self.midpoint()
504 }
505
506 #[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 #[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 #[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 #[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 #[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 #[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 #[pyo3(name = "simulate_fills")]
576 fn py_simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
577 self.simulate_fills(order)
578 }
579
580 #[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 #[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#[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#[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}