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 = "posei_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 apply_delta(&mut self, delta: &OrderBookDelta) {
165 let order = delta.order;
166 let flags = delta.flags;
167 let sequence = delta.sequence;
168 let ts_event = delta.ts_event;
169 match delta.action {
170 BookAction::Add => self.add(order, flags, sequence, ts_event),
171 BookAction::Update => self.update(order, flags, sequence, ts_event),
172 BookAction::Delete => self.delete(order, flags, sequence, ts_event),
173 BookAction::Clear => self.clear(sequence, ts_event),
174 }
175 }
176
177 pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) {
179 for delta in &deltas.deltas {
180 self.apply_delta(delta);
181 }
182 }
183
184 pub fn apply_depth(&mut self, depth: &OrderBookDepth10) {
186 self.bids.clear();
187 self.asks.clear();
188
189 for order in depth.bids {
190 self.add(order, depth.flags, depth.sequence, depth.ts_event);
191 }
192
193 for order in depth.asks {
194 self.add(order, depth.flags, depth.sequence, depth.ts_event);
195 }
196 }
197
198 pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
200 self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
201 }
202
203 pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
205 self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
206 }
207
208 pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
210 self.bids(depth)
211 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
212 .collect()
213 }
214
215 pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
217 self.asks(depth)
218 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
219 .collect()
220 }
221
222 pub fn group_bids(
224 &self,
225 group_size: Decimal,
226 depth: Option<usize>,
227 ) -> IndexMap<Decimal, Decimal> {
228 group_levels(self.bids(None), group_size, depth, true)
229 }
230
231 pub fn group_asks(
233 &self,
234 group_size: Decimal,
235 depth: Option<usize>,
236 ) -> IndexMap<Decimal, Decimal> {
237 group_levels(self.asks(None), group_size, depth, false)
238 }
239
240 pub fn bids_filtered_as_map(
246 &self,
247 depth: Option<usize>,
248 own_book: Option<&OwnOrderBook>,
249 status: Option<HashSet<OrderStatus>>,
250 accepted_buffer_ns: Option<u64>,
251 now: Option<u64>,
252 ) -> IndexMap<Decimal, Decimal> {
253 let mut public_map = self
254 .bids(depth)
255 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
256 .collect::<IndexMap<Decimal, Decimal>>();
257
258 if let Some(own_book) = own_book {
259 filter_quantities(
260 &mut public_map,
261 own_book.bid_quantity(status, accepted_buffer_ns, now),
262 );
263 }
264
265 public_map
266 }
267
268 pub fn asks_filtered_as_map(
274 &self,
275 depth: Option<usize>,
276 own_book: Option<&OwnOrderBook>,
277 status: Option<HashSet<OrderStatus>>,
278 accepted_buffer_ns: Option<u64>,
279 now: Option<u64>,
280 ) -> IndexMap<Decimal, Decimal> {
281 let mut public_map = self
282 .asks(depth)
283 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
284 .collect::<IndexMap<Decimal, Decimal>>();
285
286 if let Some(own_book) = own_book {
287 filter_quantities(
288 &mut public_map,
289 own_book.ask_quantity(status, accepted_buffer_ns, now),
290 );
291 }
292
293 public_map
294 }
295
296 pub fn group_bids_filtered(
302 &self,
303 group_size: Decimal,
304 depth: Option<usize>,
305 own_book: Option<&OwnOrderBook>,
306 status: Option<HashSet<OrderStatus>>,
307 accepted_buffer_ns: Option<u64>,
308 now: Option<u64>,
309 ) -> IndexMap<Decimal, Decimal> {
310 let mut public_map = group_levels(self.bids(None), group_size, depth, true);
311
312 if let Some(own_book) = own_book {
313 filter_quantities(
314 &mut public_map,
315 own_book.group_bids(group_size, depth, status, accepted_buffer_ns, now),
316 );
317 }
318
319 public_map
320 }
321
322 pub fn group_asks_filtered(
328 &self,
329 group_size: Decimal,
330 depth: Option<usize>,
331 own_book: Option<&OwnOrderBook>,
332 status: Option<HashSet<OrderStatus>>,
333 accepted_buffer_ns: Option<u64>,
334 now: Option<u64>,
335 ) -> IndexMap<Decimal, Decimal> {
336 let mut public_map = group_levels(self.asks(None), group_size, depth, false);
337
338 if let Some(own_book) = own_book {
339 filter_quantities(
340 &mut public_map,
341 own_book.group_asks(group_size, depth, status, accepted_buffer_ns, now),
342 );
343 }
344
345 public_map
346 }
347
348 #[must_use]
350 pub fn has_bid(&self) -> bool {
351 self.bids.top().is_some_and(|top| !top.orders.is_empty())
352 }
353
354 #[must_use]
356 pub fn has_ask(&self) -> bool {
357 self.asks.top().is_some_and(|top| !top.orders.is_empty())
358 }
359
360 #[must_use]
362 pub fn best_bid_price(&self) -> Option<Price> {
363 self.bids.top().map(|top| top.price.value)
364 }
365
366 #[must_use]
368 pub fn best_ask_price(&self) -> Option<Price> {
369 self.asks.top().map(|top| top.price.value)
370 }
371
372 #[must_use]
374 pub fn best_bid_size(&self) -> Option<Quantity> {
375 self.bids
376 .top()
377 .and_then(|top| top.first().map(|order| order.size))
378 }
379
380 #[must_use]
382 pub fn best_ask_size(&self) -> Option<Quantity> {
383 self.asks
384 .top()
385 .and_then(|top| top.first().map(|order| order.size))
386 }
387
388 #[must_use]
390 pub fn spread(&self) -> Option<f64> {
391 match (self.best_ask_price(), self.best_bid_price()) {
392 (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
393 _ => None,
394 }
395 }
396
397 #[must_use]
399 pub fn midpoint(&self) -> Option<f64> {
400 match (self.best_ask_price(), self.best_bid_price()) {
401 (Some(ask), Some(bid)) => Some((ask.as_f64() + bid.as_f64()) / 2.0),
402 _ => None,
403 }
404 }
405
406 #[must_use]
408 pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
409 let levels = match order_side.as_specified() {
410 OrderSideSpecified::Buy => &self.asks.levels,
411 OrderSideSpecified::Sell => &self.bids.levels,
412 };
413
414 analysis::get_avg_px_for_quantity(qty, levels)
415 }
416
417 #[must_use]
419 pub fn get_avg_px_qty_for_exposure(
420 &self,
421 target_exposure: Quantity,
422 order_side: OrderSide,
423 ) -> (f64, f64, f64) {
424 let levels = match order_side.as_specified() {
425 OrderSideSpecified::Buy => &self.asks.levels,
426 OrderSideSpecified::Sell => &self.bids.levels,
427 };
428
429 analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
430 }
431
432 #[must_use]
434 pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
435 let levels = match order_side.as_specified() {
436 OrderSideSpecified::Buy => &self.asks.levels,
437 OrderSideSpecified::Sell => &self.bids.levels,
438 };
439
440 analysis::get_quantity_for_price(price, order_side, levels)
441 }
442
443 #[must_use]
445 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
446 match order.side.as_specified() {
447 OrderSideSpecified::Buy => self.asks.simulate_fills(order),
448 OrderSideSpecified::Sell => self.bids.simulate_fills(order),
449 }
450 }
451
452 #[must_use]
454 pub fn pprint(&self, num_levels: usize) -> String {
455 pprint_book(&self.bids, &self.asks, num_levels)
456 }
457
458 fn increment(&mut self, sequence: u64, ts_event: UnixNanos) {
459 debug_assert!(
460 sequence >= self.sequence,
461 "Sequence number should not go backwards: old={}, new={}",
462 self.sequence,
463 sequence
464 );
465 debug_assert!(
466 ts_event >= self.ts_last,
467 "Timestamp should not go backwards: old={}, new={}",
468 self.ts_last,
469 ts_event
470 );
471 debug_assert!(
472 self.update_count < u64::MAX,
473 "Update count approaching overflow: {}",
474 self.update_count
475 );
476
477 self.sequence = sequence;
478 self.ts_last = ts_event;
479 self.update_count += 1;
480 }
481
482 pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
488 if self.book_type != BookType::L1_MBP {
489 return Err(InvalidBookOperation::Update(self.book_type));
490 }
491
492 if cfg!(debug_assertions) && quote.bid_price > quote.ask_price {
495 log::warn!(
496 "Quote has crossed prices: bid={}, ask={} for {}",
497 quote.bid_price,
498 quote.ask_price,
499 self.instrument_id
500 );
501 }
502 debug_assert!(
503 quote.bid_size.is_positive() && quote.ask_size.is_positive(),
504 "Quote has non-positive sizes: bid_size={}, ask_size={}",
505 quote.bid_size,
506 quote.ask_size
507 );
508
509 let bid = BookOrder::new(
510 OrderSide::Buy,
511 quote.bid_price,
512 quote.bid_size,
513 OrderSide::Buy as u64,
514 );
515
516 let ask = BookOrder::new(
517 OrderSide::Sell,
518 quote.ask_price,
519 quote.ask_size,
520 OrderSide::Sell as u64,
521 );
522
523 self.update_book_bid(bid, quote.ts_event);
524 self.update_book_ask(ask, quote.ts_event);
525
526 Ok(())
527 }
528
529 pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
535 if self.book_type != BookType::L1_MBP {
536 return Err(InvalidBookOperation::Update(self.book_type));
537 }
538
539 debug_assert!(
541 trade.price.raw != PRICE_UNDEF && trade.price.raw != PRICE_ERROR,
542 "Trade has invalid/uninitialized price: {}",
543 trade.price
544 );
545 debug_assert!(
546 trade.size.is_positive(),
547 "Trade has non-positive size: {}",
548 trade.size
549 );
550
551 let bid = BookOrder::new(
552 OrderSide::Buy,
553 trade.price,
554 trade.size,
555 OrderSide::Buy as u64,
556 );
557
558 let ask = BookOrder::new(
559 OrderSide::Sell,
560 trade.price,
561 trade.size,
562 OrderSide::Sell as u64,
563 );
564
565 self.update_book_bid(bid, trade.ts_event);
566 self.update_book_ask(ask, trade.ts_event);
567
568 Ok(())
569 }
570
571 fn update_book_bid(&mut self, order: BookOrder, ts_event: UnixNanos) {
572 if let Some(top_bids) = self.bids.top() {
573 if let Some(top_bid) = top_bids.first() {
574 self.bids.remove(top_bid.order_id, 0, ts_event);
575 }
576 }
577 self.bids.add(order);
578 }
579
580 fn update_book_ask(&mut self, order: BookOrder, ts_event: UnixNanos) {
581 if let Some(top_asks) = self.asks.top() {
582 if let Some(top_ask) = top_asks.first() {
583 self.asks.remove(top_ask.order_id, 0, ts_event);
584 }
585 }
586 self.asks.add(order);
587 }
588}
589
590fn filter_quantities(
591 public_map: &mut IndexMap<Decimal, Decimal>,
592 own_map: IndexMap<Decimal, Decimal>,
593) {
594 for (price, own_size) in own_map {
595 if let Some(public_size) = public_map.get_mut(&price) {
596 *public_size = (*public_size - own_size).max(Decimal::ZERO);
597
598 if *public_size == Decimal::ZERO {
599 public_map.shift_remove(&price);
600 }
601 }
602 }
603}
604
605fn group_levels<'a>(
606 levels_iter: impl Iterator<Item = &'a BookLevel>,
607 group_size: Decimal,
608 depth: Option<usize>,
609 is_bid: bool,
610) -> IndexMap<Decimal, Decimal> {
611 let mut levels = IndexMap::new();
612 let depth = depth.unwrap_or(usize::MAX);
613
614 for level in levels_iter {
615 let price = level.price.value.as_decimal();
616 let grouped_price = if is_bid {
617 (price / group_size).floor() * group_size
618 } else {
619 (price / group_size).ceil() * group_size
620 };
621 let size = level.size_decimal();
622
623 levels
624 .entry(grouped_price)
625 .and_modify(|total| *total += size)
626 .or_insert(size);
627
628 if levels.len() > depth {
629 levels.pop();
630 break;
631 }
632 }
633
634 levels
635}