1use std::{collections::HashSet, fmt::Display};
19
20use indexmap::IndexMap;
21use nautilus_core::UnixNanos;
22use rust_decimal::Decimal;
23
24use super::{
25 aggregation::pre_process_order, analysis, display::pprint_book, level::BookLevel,
26 own::OwnOrderBook,
27};
28use crate::{
29 data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
30 enums::{BookAction, BookType, OrderSide, OrderSideSpecified, OrderStatus},
31 identifiers::InstrumentId,
32 orderbook::{InvalidBookOperation, ladder::BookLadder},
33 types::{
34 Price, Quantity,
35 price::{PRICE_ERROR, PRICE_UNDEF},
36 },
37};
38
39#[derive(Clone, Debug)]
47#[cfg_attr(
48 feature = "python",
49 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
50)]
51pub struct OrderBook {
52 pub instrument_id: InstrumentId,
54 pub book_type: BookType,
56 pub sequence: u64,
58 pub ts_last: UnixNanos,
60 pub update_count: u64,
62 pub(crate) bids: BookLadder,
63 pub(crate) asks: BookLadder,
64}
65
66impl PartialEq for OrderBook {
67 fn eq(&self, other: &Self) -> bool {
68 self.instrument_id == other.instrument_id && self.book_type == other.book_type
69 }
70}
71
72impl Eq for OrderBook {}
73
74impl Display for OrderBook {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(
77 f,
78 "{}(instrument_id={}, book_type={}, update_count={})",
79 stringify!(OrderBook),
80 self.instrument_id,
81 self.book_type,
82 self.update_count,
83 )
84 }
85}
86
87impl OrderBook {
88 #[must_use]
90 pub fn new(instrument_id: InstrumentId, book_type: BookType) -> Self {
91 Self {
92 instrument_id,
93 book_type,
94 sequence: 0,
95 ts_last: UnixNanos::default(),
96 update_count: 0,
97 bids: BookLadder::new(OrderSideSpecified::Buy),
98 asks: BookLadder::new(OrderSideSpecified::Sell),
99 }
100 }
101
102 pub fn reset(&mut self) {
104 self.bids.clear();
105 self.asks.clear();
106 self.sequence = 0;
107 self.ts_last = UnixNanos::default();
108 self.update_count = 0;
109 }
110
111 pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
113 let order = pre_process_order(self.book_type, order, flags);
114 match order.side.as_specified() {
115 OrderSideSpecified::Buy => self.bids.add(order),
116 OrderSideSpecified::Sell => self.asks.add(order),
117 }
118
119 self.increment(sequence, ts_event);
120 }
121
122 pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
124 let order = pre_process_order(self.book_type, order, flags);
125 match order.side.as_specified() {
126 OrderSideSpecified::Buy => self.bids.update(order),
127 OrderSideSpecified::Sell => self.asks.update(order),
128 }
129
130 self.increment(sequence, ts_event);
131 }
132
133 pub fn delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
135 let order = pre_process_order(self.book_type, order, flags);
136 match order.side.as_specified() {
137 OrderSideSpecified::Buy => self.bids.delete(order, sequence, ts_event),
138 OrderSideSpecified::Sell => self.asks.delete(order, sequence, ts_event),
139 }
140
141 self.increment(sequence, ts_event);
142 }
143
144 pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
146 self.bids.clear();
147 self.asks.clear();
148 self.increment(sequence, ts_event);
149 }
150
151 pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
153 self.bids.clear();
154 self.increment(sequence, ts_event);
155 }
156
157 pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
159 self.asks.clear();
160 self.increment(sequence, ts_event);
161 }
162
163 pub fn clear_stale_levels(&mut self, side: Option<OrderSide>) -> Option<Vec<BookLevel>> {
171 if self.book_type == BookType::L1_MBP {
172 return None;
174 }
175
176 let (Some(best_bid), Some(best_ask)) = (self.best_bid_price(), self.best_ask_price())
177 else {
178 return None;
179 };
180
181 if best_bid <= best_ask {
182 return None;
183 }
184
185 let mut removed_levels = Vec::new();
186 let mut clear_bids = false;
187 let mut clear_asks = false;
188
189 match side {
190 Some(OrderSide::Buy) => clear_bids = true,
191 Some(OrderSide::Sell) => clear_asks = true,
192 _ => {
193 clear_bids = true;
194 clear_asks = true;
195 }
196 }
197
198 let mut ask_prices_to_remove = Vec::new();
200 if clear_asks {
201 for (bp, _level) in self.asks.levels.iter() {
202 if bp.value <= best_bid {
203 ask_prices_to_remove.push(*bp);
204 } else {
205 break;
206 }
207 }
208 }
209
210 let mut bid_prices_to_remove = Vec::new();
212 if clear_bids {
213 for (bp, _level) in self.bids.levels.iter() {
214 if bp.value >= best_ask {
215 bid_prices_to_remove.push(*bp);
216 } else {
217 break;
218 }
219 }
220 }
221
222 if ask_prices_to_remove.is_empty() && bid_prices_to_remove.is_empty() {
223 return None;
224 }
225
226 let bid_count = bid_prices_to_remove.len();
227 let ask_count = ask_prices_to_remove.len();
228
229 for price in bid_prices_to_remove {
231 if let Some(level) = self.bids.remove_level(price) {
232 removed_levels.push(level);
233 }
234 }
235
236 for price in ask_prices_to_remove {
238 if let Some(level) = self.asks.remove_level(price) {
239 removed_levels.push(level);
240 }
241 }
242
243 self.increment(self.sequence, self.ts_last);
244
245 if removed_levels.is_empty() {
246 None
247 } else {
248 let total_orders: usize = removed_levels.iter().map(|level| level.orders.len()).sum();
249
250 log::warn!(
251 "Removed {} stale/crossed levels (instrument_id={}, bid_levels={}, ask_levels={}, total_orders={}), book was crossed with best_bid={} > best_ask={}",
252 removed_levels.len(),
253 self.instrument_id,
254 bid_count,
255 ask_count,
256 total_orders,
257 best_bid,
258 best_ask
259 );
260
261 Some(removed_levels)
262 }
263 }
264
265 pub fn apply_delta(&mut self, delta: &OrderBookDelta) {
267 let order = delta.order;
268 let flags = delta.flags;
269 let sequence = delta.sequence;
270 let ts_event = delta.ts_event;
271 match delta.action {
272 BookAction::Add => self.add(order, flags, sequence, ts_event),
273 BookAction::Update => self.update(order, flags, sequence, ts_event),
274 BookAction::Delete => self.delete(order, flags, sequence, ts_event),
275 BookAction::Clear => self.clear(sequence, ts_event),
276 }
277 }
278
279 pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) {
281 for delta in &deltas.deltas {
282 self.apply_delta(delta);
283 }
284 }
285
286 pub fn apply_depth(&mut self, depth: &OrderBookDepth10) {
288 self.bids.clear();
289 self.asks.clear();
290
291 for order in depth.bids {
292 self.add(order, depth.flags, depth.sequence, depth.ts_event);
293 }
294
295 for order in depth.asks {
296 self.add(order, depth.flags, depth.sequence, depth.ts_event);
297 }
298 }
299
300 pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
302 self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
303 }
304
305 pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
307 self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
308 }
309
310 pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
312 self.bids(depth)
313 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
314 .collect()
315 }
316
317 pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
319 self.asks(depth)
320 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
321 .collect()
322 }
323
324 pub fn group_bids(
326 &self,
327 group_size: Decimal,
328 depth: Option<usize>,
329 ) -> IndexMap<Decimal, Decimal> {
330 group_levels(self.bids(None), group_size, depth, true)
331 }
332
333 pub fn group_asks(
335 &self,
336 group_size: Decimal,
337 depth: Option<usize>,
338 ) -> IndexMap<Decimal, Decimal> {
339 group_levels(self.asks(None), group_size, depth, false)
340 }
341
342 pub fn bids_filtered_as_map(
348 &self,
349 depth: Option<usize>,
350 own_book: Option<&OwnOrderBook>,
351 status: Option<HashSet<OrderStatus>>,
352 accepted_buffer_ns: Option<u64>,
353 now: Option<u64>,
354 ) -> IndexMap<Decimal, Decimal> {
355 let mut public_map = self
356 .bids(depth)
357 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
358 .collect::<IndexMap<Decimal, Decimal>>();
359
360 if let Some(own_book) = own_book {
361 filter_quantities(
362 &mut public_map,
363 own_book.bid_quantity(status, None, None, accepted_buffer_ns, now),
364 );
365 }
366
367 public_map
368 }
369
370 pub fn asks_filtered_as_map(
376 &self,
377 depth: Option<usize>,
378 own_book: Option<&OwnOrderBook>,
379 status: Option<HashSet<OrderStatus>>,
380 accepted_buffer_ns: Option<u64>,
381 now: Option<u64>,
382 ) -> IndexMap<Decimal, Decimal> {
383 let mut public_map = self
384 .asks(depth)
385 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
386 .collect::<IndexMap<Decimal, Decimal>>();
387
388 if let Some(own_book) = own_book {
389 filter_quantities(
390 &mut public_map,
391 own_book.ask_quantity(status, None, None, accepted_buffer_ns, now),
392 );
393 }
394
395 public_map
396 }
397
398 pub fn group_bids_filtered(
404 &self,
405 group_size: Decimal,
406 depth: Option<usize>,
407 own_book: Option<&OwnOrderBook>,
408 status: Option<HashSet<OrderStatus>>,
409 accepted_buffer_ns: Option<u64>,
410 now: Option<u64>,
411 ) -> IndexMap<Decimal, Decimal> {
412 let mut public_map = group_levels(self.bids(None), group_size, depth, true);
413
414 if let Some(own_book) = own_book {
415 filter_quantities(
416 &mut public_map,
417 own_book.bid_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
418 );
419 }
420
421 public_map
422 }
423
424 pub fn group_asks_filtered(
430 &self,
431 group_size: Decimal,
432 depth: Option<usize>,
433 own_book: Option<&OwnOrderBook>,
434 status: Option<HashSet<OrderStatus>>,
435 accepted_buffer_ns: Option<u64>,
436 now: Option<u64>,
437 ) -> IndexMap<Decimal, Decimal> {
438 let mut public_map = group_levels(self.asks(None), group_size, depth, false);
439
440 if let Some(own_book) = own_book {
441 filter_quantities(
442 &mut public_map,
443 own_book.ask_quantity(status, depth, Some(group_size), accepted_buffer_ns, now),
444 );
445 }
446
447 public_map
448 }
449
450 #[must_use]
452 pub fn has_bid(&self) -> bool {
453 self.bids.top().is_some_and(|top| !top.orders.is_empty())
454 }
455
456 #[must_use]
458 pub fn has_ask(&self) -> bool {
459 self.asks.top().is_some_and(|top| !top.orders.is_empty())
460 }
461
462 #[must_use]
464 pub fn best_bid_price(&self) -> Option<Price> {
465 self.bids.top().map(|top| top.price.value)
466 }
467
468 #[must_use]
470 pub fn best_ask_price(&self) -> Option<Price> {
471 self.asks.top().map(|top| top.price.value)
472 }
473
474 #[must_use]
476 pub fn best_bid_size(&self) -> Option<Quantity> {
477 self.bids
478 .top()
479 .and_then(|top| top.first().map(|order| order.size))
480 }
481
482 #[must_use]
484 pub fn best_ask_size(&self) -> Option<Quantity> {
485 self.asks
486 .top()
487 .and_then(|top| top.first().map(|order| order.size))
488 }
489
490 #[must_use]
492 pub fn spread(&self) -> Option<f64> {
493 match (self.best_ask_price(), self.best_bid_price()) {
494 (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
495 _ => None,
496 }
497 }
498
499 #[must_use]
501 pub fn midpoint(&self) -> Option<f64> {
502 match (self.best_ask_price(), self.best_bid_price()) {
503 (Some(ask), Some(bid)) => Some((ask.as_f64() + bid.as_f64()) / 2.0),
504 _ => None,
505 }
506 }
507
508 #[must_use]
510 pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
511 let levels = match order_side.as_specified() {
512 OrderSideSpecified::Buy => &self.asks.levels,
513 OrderSideSpecified::Sell => &self.bids.levels,
514 };
515
516 analysis::get_avg_px_for_quantity(qty, levels)
517 }
518
519 #[must_use]
521 pub fn get_avg_px_qty_for_exposure(
522 &self,
523 target_exposure: Quantity,
524 order_side: OrderSide,
525 ) -> (f64, f64, f64) {
526 let levels = match order_side.as_specified() {
527 OrderSideSpecified::Buy => &self.asks.levels,
528 OrderSideSpecified::Sell => &self.bids.levels,
529 };
530
531 analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
532 }
533
534 #[must_use]
536 pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
537 let levels = match order_side.as_specified() {
538 OrderSideSpecified::Buy => &self.asks.levels,
539 OrderSideSpecified::Sell => &self.bids.levels,
540 };
541
542 analysis::get_quantity_for_price(price, order_side, levels)
543 }
544
545 #[must_use]
547 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
548 match order.side.as_specified() {
549 OrderSideSpecified::Buy => self.asks.simulate_fills(order),
550 OrderSideSpecified::Sell => self.bids.simulate_fills(order),
551 }
552 }
553
554 #[must_use]
556 pub fn pprint(&self, num_levels: usize, group_size: Option<Decimal>) -> String {
557 pprint_book(self, num_levels, group_size)
558 }
559
560 fn increment(&mut self, sequence: u64, ts_event: UnixNanos) {
561 debug_assert!(
562 sequence >= self.sequence,
563 "Sequence number should not go backwards: old={}, new={}",
564 self.sequence,
565 sequence
566 );
567 debug_assert!(
568 ts_event >= self.ts_last,
569 "Timestamp should not go backwards: old={}, new={}",
570 self.ts_last,
571 ts_event
572 );
573 debug_assert!(
574 self.update_count < u64::MAX,
575 "Update count approaching overflow: {}",
576 self.update_count
577 );
578
579 self.sequence = sequence;
580 self.ts_last = ts_event;
581 self.update_count += 1;
582 }
583
584 pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
590 if self.book_type != BookType::L1_MBP {
591 return Err(InvalidBookOperation::Update(self.book_type));
592 }
593
594 if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
597 log::warn!(
598 "Quote has crossed prices: bid={}, ask={} for {}",
599 quote.bid_price,
600 quote.ask_price,
601 self.instrument_id
602 );
603 }
604 debug_assert!(
605 quote.bid_size.is_positive() && quote.ask_size.is_positive(),
606 "Quote has non-positive sizes: bid_size={}, ask_size={}",
607 quote.bid_size,
608 quote.ask_size
609 );
610
611 let bid = BookOrder::new(
612 OrderSide::Buy,
613 quote.bid_price,
614 quote.bid_size,
615 OrderSide::Buy as u64,
616 );
617
618 let ask = BookOrder::new(
619 OrderSide::Sell,
620 quote.ask_price,
621 quote.ask_size,
622 OrderSide::Sell as u64,
623 );
624
625 self.update_book_bid(bid, quote.ts_event);
626 self.update_book_ask(ask, quote.ts_event);
627
628 Ok(())
629 }
630
631 pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
637 if self.book_type != BookType::L1_MBP {
638 return Err(InvalidBookOperation::Update(self.book_type));
639 }
640
641 debug_assert!(
643 trade.price.raw != PRICE_UNDEF && trade.price.raw != PRICE_ERROR,
644 "Trade has invalid/uninitialized price: {}",
645 trade.price
646 );
647 debug_assert!(
648 trade.size.is_positive(),
649 "Trade has non-positive size: {}",
650 trade.size
651 );
652
653 let bid = BookOrder::new(
654 OrderSide::Buy,
655 trade.price,
656 trade.size,
657 OrderSide::Buy as u64,
658 );
659
660 let ask = BookOrder::new(
661 OrderSide::Sell,
662 trade.price,
663 trade.size,
664 OrderSide::Sell as u64,
665 );
666
667 self.update_book_bid(bid, trade.ts_event);
668 self.update_book_ask(ask, trade.ts_event);
669
670 Ok(())
671 }
672
673 fn update_book_bid(&mut self, order: BookOrder, ts_event: UnixNanos) {
674 if let Some(top_bids) = self.bids.top()
675 && let Some(top_bid) = top_bids.first()
676 {
677 self.bids.remove_order(top_bid.order_id, 0, ts_event);
678 }
679 self.bids.add(order);
680 }
681
682 fn update_book_ask(&mut self, order: BookOrder, ts_event: UnixNanos) {
683 if let Some(top_asks) = self.asks.top()
684 && let Some(top_ask) = top_asks.first()
685 {
686 self.asks.remove_order(top_ask.order_id, 0, ts_event);
687 }
688 self.asks.add(order);
689 }
690}
691
692fn filter_quantities(
693 public_map: &mut IndexMap<Decimal, Decimal>,
694 own_map: IndexMap<Decimal, Decimal>,
695) {
696 for (price, own_size) in own_map {
697 if let Some(public_size) = public_map.get_mut(&price) {
698 *public_size = (*public_size - own_size).max(Decimal::ZERO);
699
700 if *public_size == Decimal::ZERO {
701 public_map.shift_remove(&price);
702 }
703 }
704 }
705}
706
707fn group_levels<'a>(
708 levels_iter: impl Iterator<Item = &'a BookLevel>,
709 group_size: Decimal,
710 depth: Option<usize>,
711 is_bid: bool,
712) -> IndexMap<Decimal, Decimal> {
713 let mut levels = IndexMap::new();
714 let depth = depth.unwrap_or(usize::MAX);
715
716 for level in levels_iter {
717 let price = level.price.value.as_decimal();
718 let grouped_price = if is_bid {
719 (price / group_size).floor() * group_size
720 } else {
721 (price / group_size).ceil() * group_size
722 };
723 let size = level.size_decimal();
724
725 levels
726 .entry(grouped_price)
727 .and_modify(|total| *total += size)
728 .or_insert(size);
729
730 if levels.len() > depth {
731 levels.pop();
732 break;
733 }
734 }
735
736 levels
737}