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 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 #[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 #[pyo3(name = "reset")]
192 fn py_reset(&mut self) {
193 self.reset();
194 }
195
196 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[pyo3(name = "best_bid_price")]
473 fn py_best_bid_price(&self) -> Option<Price> {
474 self.best_bid_price()
475 }
476
477 #[pyo3(name = "best_ask_price")]
479 fn py_best_ask_price(&self) -> Option<Price> {
480 self.best_ask_price()
481 }
482
483 #[pyo3(name = "best_bid_size")]
485 fn py_best_bid_size(&self) -> Option<Quantity> {
486 self.best_bid_size()
487 }
488
489 #[pyo3(name = "best_ask_size")]
491 fn py_best_ask_size(&self) -> Option<Quantity> {
492 self.best_ask_size()
493 }
494
495 #[pyo3(name = "spread")]
497 fn py_spread(&self) -> PyResult<Option<f64>> {
498 check_top_float_precision(self)?;
499 Ok(self.spread())
500 }
501
502 #[pyo3(name = "midpoint")]
504 fn py_midpoint(&self) -> PyResult<Option<f64>> {
505 check_top_float_precision(self)?;
506 Ok(self.midpoint())
507 }
508
509 #[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 #[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 #[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 #[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 #[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 #[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 #[pyo3(name = "simulate_fills")]
592 fn py_simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
593 self.simulate_fills(order)
594 }
595
596 #[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 #[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#[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#[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}