1use std::fmt::Display;
19
20use indexmap::IndexMap;
21use nautilus_core::UnixNanos;
22use rust_decimal::Decimal;
23
24use super::{aggregation::pre_process_order, analysis, display::pprint_book, level::BookLevel};
25use crate::{
26 data::{BookOrder, OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick},
27 enums::{BookAction, BookType, OrderSide, OrderSideSpecified},
28 identifiers::InstrumentId,
29 orderbook::{ladder::BookLadder, InvalidBookOperation},
30 types::{Price, Quantity},
31};
32
33#[derive(Clone, Debug)]
41#[cfg_attr(
42 feature = "python",
43 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
44)]
45pub struct OrderBook {
46 pub instrument_id: InstrumentId,
48 pub book_type: BookType,
50 pub sequence: u64,
52 pub ts_last: UnixNanos,
54 pub count: u64,
56 pub(crate) bids: BookLadder,
57 pub(crate) asks: BookLadder,
58}
59
60impl PartialEq for OrderBook {
61 fn eq(&self, other: &Self) -> bool {
62 self.instrument_id == other.instrument_id && self.book_type == other.book_type
63 }
64}
65
66impl Eq for OrderBook {}
67
68impl Display for OrderBook {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 write!(
71 f,
72 "{}(instrument_id={}, book_type={})",
73 stringify!(OrderBook),
74 self.instrument_id,
75 self.book_type,
76 )
77 }
78}
79
80impl OrderBook {
81 #[must_use]
83 pub fn new(instrument_id: InstrumentId, book_type: BookType) -> Self {
84 Self {
85 instrument_id,
86 book_type,
87 sequence: 0,
88 ts_last: UnixNanos::default(),
89 count: 0,
90 bids: BookLadder::new(OrderSide::Buy),
91 asks: BookLadder::new(OrderSide::Sell),
92 }
93 }
94
95 pub fn reset(&mut self) {
97 self.bids.clear();
98 self.asks.clear();
99 self.sequence = 0;
100 self.ts_last = UnixNanos::default();
101 self.count = 0;
102 }
103
104 pub fn add(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
106 let order = pre_process_order(self.book_type, order, flags);
107 match order.side.as_specified() {
108 OrderSideSpecified::Buy => self.bids.add(order),
109 OrderSideSpecified::Sell => self.asks.add(order),
110 }
111
112 self.increment(sequence, ts_event);
113 }
114
115 pub fn update(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
117 let order = pre_process_order(self.book_type, order, flags);
118 match order.side.as_specified() {
119 OrderSideSpecified::Buy => self.bids.update(order),
120 OrderSideSpecified::Sell => self.asks.update(order),
121 }
122
123 self.increment(sequence, ts_event);
124 }
125
126 pub fn delete(&mut self, order: BookOrder, flags: u8, sequence: u64, ts_event: UnixNanos) {
128 let order = pre_process_order(self.book_type, order, flags);
129 match order.side.as_specified() {
130 OrderSideSpecified::Buy => self.bids.delete(order, sequence, ts_event),
131 OrderSideSpecified::Sell => self.asks.delete(order, sequence, ts_event),
132 }
133
134 self.increment(sequence, ts_event);
135 }
136
137 pub fn clear(&mut self, sequence: u64, ts_event: UnixNanos) {
139 self.bids.clear();
140 self.asks.clear();
141 self.increment(sequence, ts_event);
142 }
143
144 pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
146 self.bids.clear();
147 self.increment(sequence, ts_event);
148 }
149
150 pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
152 self.asks.clear();
153 self.increment(sequence, ts_event);
154 }
155
156 pub fn apply_delta(&mut self, delta: &OrderBookDelta) {
158 let order = delta.order;
159 let flags = delta.flags;
160 let sequence = delta.sequence;
161 let ts_event = delta.ts_event;
162 match delta.action {
163 BookAction::Add => self.add(order, flags, sequence, ts_event),
164 BookAction::Update => self.update(order, flags, sequence, ts_event),
165 BookAction::Delete => self.delete(order, flags, sequence, ts_event),
166 BookAction::Clear => self.clear(sequence, ts_event),
167 }
168 }
169
170 pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) {
172 for delta in &deltas.deltas {
173 self.apply_delta(delta);
174 }
175 }
176
177 pub fn apply_depth(&mut self, depth: &OrderBookDepth10) {
179 self.bids.clear();
180 self.asks.clear();
181
182 for order in depth.bids {
183 self.add(order, depth.flags, depth.sequence, depth.ts_event);
184 }
185
186 for order in depth.asks {
187 self.add(order, depth.flags, depth.sequence, depth.ts_event);
188 }
189 }
190
191 pub fn bids(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
193 self.bids.levels.values().take(depth.unwrap_or(usize::MAX))
194 }
195
196 pub fn asks(&self, depth: Option<usize>) -> impl Iterator<Item = &BookLevel> {
198 self.asks.levels.values().take(depth.unwrap_or(usize::MAX))
199 }
200
201 pub fn bids_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
203 self.bids(depth)
204 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
205 .collect()
206 }
207
208 pub fn asks_as_map(&self, depth: Option<usize>) -> IndexMap<Decimal, Decimal> {
210 self.asks(depth)
211 .map(|level| (level.price.value.as_decimal(), level.size_decimal()))
212 .collect()
213 }
214
215 pub fn group_bids(
217 &self,
218 group_size: Decimal,
219 depth: Option<usize>,
220 ) -> IndexMap<Decimal, Decimal> {
221 self.group_levels(self.bids(None), group_size, true, depth)
222 }
223
224 pub fn group_asks(
226 &self,
227 group_size: Decimal,
228 depth: Option<usize>,
229 ) -> IndexMap<Decimal, Decimal> {
230 self.group_levels(self.asks(None), group_size, false, depth)
231 }
232
233 fn group_levels<'a>(
234 &self,
235 levels_iter: impl Iterator<Item = &'a BookLevel>,
236 group_size: Decimal,
237 is_bid: bool,
238 depth: Option<usize>,
239 ) -> IndexMap<Decimal, Decimal> {
240 let mut levels = IndexMap::new();
241 let depth = depth.unwrap_or(usize::MAX);
242
243 for level in levels_iter {
244 let price = level.price.value.as_decimal();
245 let grouped_price = if is_bid {
246 (price / group_size).floor() * group_size
247 } else {
248 (price / group_size).ceil() * group_size
249 };
250 let size = level.size_decimal();
251
252 levels
253 .entry(grouped_price)
254 .and_modify(|total| *total += size)
255 .or_insert(size);
256
257 if levels.len() > depth {
258 levels.pop();
259 break;
260 }
261 }
262
263 levels
264 }
265
266 #[must_use]
268 pub fn has_bid(&self) -> bool {
269 self.bids.top().is_some_and(|top| !top.orders.is_empty())
270 }
271
272 #[must_use]
274 pub fn has_ask(&self) -> bool {
275 self.asks.top().is_some_and(|top| !top.orders.is_empty())
276 }
277
278 #[must_use]
280 pub fn best_bid_price(&self) -> Option<Price> {
281 self.bids.top().map(|top| top.price.value)
282 }
283
284 #[must_use]
286 pub fn best_ask_price(&self) -> Option<Price> {
287 self.asks.top().map(|top| top.price.value)
288 }
289
290 #[must_use]
292 pub fn best_bid_size(&self) -> Option<Quantity> {
293 self.bids
294 .top()
295 .and_then(|top| top.first().map(|order| order.size))
296 }
297
298 #[must_use]
300 pub fn best_ask_size(&self) -> Option<Quantity> {
301 self.asks
302 .top()
303 .and_then(|top| top.first().map(|order| order.size))
304 }
305
306 #[must_use]
308 pub fn spread(&self) -> Option<f64> {
309 match (self.best_ask_price(), self.best_bid_price()) {
310 (Some(ask), Some(bid)) => Some(ask.as_f64() - bid.as_f64()),
311 _ => None,
312 }
313 }
314
315 #[must_use]
317 pub fn midpoint(&self) -> Option<f64> {
318 match (self.best_ask_price(), self.best_bid_price()) {
319 (Some(ask), Some(bid)) => Some((ask.as_f64() + bid.as_f64()) / 2.0),
320 _ => None,
321 }
322 }
323
324 #[must_use]
326 pub fn get_avg_px_for_quantity(&self, qty: Quantity, order_side: OrderSide) -> f64 {
327 let levels = match order_side.as_specified() {
328 OrderSideSpecified::Buy => &self.asks.levels,
329 OrderSideSpecified::Sell => &self.bids.levels,
330 };
331
332 analysis::get_avg_px_for_quantity(qty, levels)
333 }
334
335 #[must_use]
337 pub fn get_avg_px_qty_for_exposure(
338 &self,
339 target_exposure: Quantity,
340 order_side: OrderSide,
341 ) -> (f64, f64, f64) {
342 let levels = match order_side.as_specified() {
343 OrderSideSpecified::Buy => &self.asks.levels,
344 OrderSideSpecified::Sell => &self.bids.levels,
345 };
346
347 analysis::get_avg_px_qty_for_exposure(target_exposure, levels)
348 }
349
350 #[must_use]
352 pub fn get_quantity_for_price(&self, price: Price, order_side: OrderSide) -> f64 {
353 let levels = match order_side.as_specified() {
354 OrderSideSpecified::Buy => &self.asks.levels,
355 OrderSideSpecified::Sell => &self.bids.levels,
356 };
357
358 analysis::get_quantity_for_price(price, order_side, levels)
359 }
360
361 #[must_use]
363 pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
364 match order.side.as_specified() {
365 OrderSideSpecified::Buy => self.asks.simulate_fills(order),
366 OrderSideSpecified::Sell => self.bids.simulate_fills(order),
367 }
368 }
369
370 #[must_use]
372 pub fn pprint(&self, num_levels: usize) -> String {
373 pprint_book(&self.bids, &self.asks, num_levels)
374 }
375
376 fn increment(&mut self, sequence: u64, ts_event: UnixNanos) {
377 self.sequence = sequence;
378 self.ts_last = ts_event;
379 self.count += 1;
380 }
381
382 pub fn update_quote_tick(&mut self, quote: &QuoteTick) -> Result<(), InvalidBookOperation> {
384 if self.book_type != BookType::L1_MBP {
385 return Err(InvalidBookOperation::Update(self.book_type));
386 };
387
388 let bid = BookOrder::new(
389 OrderSide::Buy,
390 quote.bid_price,
391 quote.bid_size,
392 OrderSide::Buy as u64,
393 );
394
395 let ask = BookOrder::new(
396 OrderSide::Sell,
397 quote.ask_price,
398 quote.ask_size,
399 OrderSide::Sell as u64,
400 );
401
402 self.update_book_bid(bid, quote.ts_event);
403 self.update_book_ask(ask, quote.ts_event);
404
405 Ok(())
406 }
407
408 pub fn update_trade_tick(&mut self, trade: &TradeTick) -> Result<(), InvalidBookOperation> {
410 if self.book_type != BookType::L1_MBP {
411 return Err(InvalidBookOperation::Update(self.book_type));
412 };
413
414 let bid = BookOrder::new(
415 OrderSide::Buy,
416 trade.price,
417 trade.size,
418 OrderSide::Buy as u64,
419 );
420
421 let ask = BookOrder::new(
422 OrderSide::Sell,
423 trade.price,
424 trade.size,
425 OrderSide::Sell as u64,
426 );
427
428 self.update_book_bid(bid, trade.ts_event);
429 self.update_book_ask(ask, trade.ts_event);
430
431 Ok(())
432 }
433
434 fn update_book_bid(&mut self, order: BookOrder, ts_event: UnixNanos) {
435 if let Some(top_bids) = self.bids.top() {
436 if let Some(top_bid) = top_bids.first() {
437 self.bids.remove(top_bid.order_id, 0, ts_event);
438 }
439 }
440 self.bids.add(order);
441 }
442
443 fn update_book_ask(&mut self, order: BookOrder, ts_event: UnixNanos) {
444 if let Some(top_asks) = self.asks.top() {
445 if let Some(top_ask) = top_asks.first() {
446 self.asks.remove(top_ask.order_id, 0, ts_event);
447 }
448 }
449 self.asks.add(order);
450 }
451}
452
453#[cfg(test)]
457mod tests {
458 use rstest::rstest;
459 use rust_decimal_macros::dec;
460
461 use crate::{
462 data::{depth::OrderBookDepth10, order::BookOrder, stubs::*, QuoteTick, TradeTick},
463 enums::{AggressorSide, BookType, OrderSide},
464 identifiers::{InstrumentId, TradeId},
465 orderbook::{analysis::book_check_integrity, BookIntegrityError, BookPrice, OrderBook},
466 types::{Price, Quantity},
467 };
468
469 #[rstest]
470 #[case::valid_book(
471 BookType::L2_MBP,
472 vec![
473 (OrderSide::Buy, "99.00", 100, 1001),
474 (OrderSide::Sell, "101.00", 100, 2001),
475 ],
476 Ok(())
477)]
478 #[case::crossed_book(
479 BookType::L2_MBP,
480 vec![
481 (OrderSide::Buy, "101.00", 100, 1001),
482 (OrderSide::Sell, "99.00", 100, 2001),
483 ],
484 Err(BookIntegrityError::OrdersCrossed(
485 BookPrice::new(Price::from("101.00"), OrderSide::Buy),
486 BookPrice::new(Price::from("99.00"), OrderSide::Sell),
487 ))
488)]
489 #[case::too_many_levels_l1(
490 BookType::L1_MBP,
491 vec![
492 (OrderSide::Buy, "99.00", 100, 1001),
493 (OrderSide::Buy, "98.00", 100, 1002),
494 ],
495 Err(BookIntegrityError::TooManyLevels(OrderSide::Buy, 2))
496)]
497 fn test_book_integrity_cases(
498 #[case] book_type: BookType,
499 #[case] orders: Vec<(OrderSide, &str, i64, u64)>,
500 #[case] expected: Result<(), BookIntegrityError>,
501 ) {
502 let instrument_id = InstrumentId::from("AAPL.XNAS");
503 let mut book = OrderBook::new(instrument_id, book_type);
504
505 for (side, price, size, id) in orders {
506 let order = BookOrder::new(side, Price::from(price), Quantity::from(size), id);
507 book.add(order, 0, id, id.into());
508 }
509
510 assert_eq!(book_check_integrity(&book), expected);
511 }
512
513 #[rstest]
514 fn test_book_integrity_price_boundaries() {
515 let instrument_id = InstrumentId::from("AAPL.XNAS");
516 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
517 let min_bid = BookOrder::new(OrderSide::Buy, Price::min(2), Quantity::from(100), 1);
518 let max_ask = BookOrder::new(OrderSide::Sell, Price::max(2), Quantity::from(100), 2);
519
520 book.add(min_bid, 0, 1, 1.into());
521 book.add(max_ask, 0, 2, 2.into());
522
523 assert!(book_check_integrity(&book).is_ok());
524 }
525
526 #[rstest]
527 #[case::small_quantity(100)]
528 #[case::medium_quantity(1000)]
529 #[case::large_quantity(1000000)]
530 fn test_book_integrity_quantity_sizes(#[case] quantity: i64) {
531 let instrument_id = InstrumentId::from("AAPL.XNAS");
532 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
533
534 let bid = BookOrder::new(
535 OrderSide::Buy,
536 Price::from("100.00"),
537 Quantity::from(quantity),
538 1,
539 );
540 book.add(bid, 0, 1, 1.into());
541
542 assert!(book_check_integrity(&book).is_ok());
543 assert_eq!(book.best_bid_size().unwrap().as_f64() as i64, quantity);
544 }
545
546 #[rstest]
547 fn test_display() {
548 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
549 let book = OrderBook::new(instrument_id, BookType::L2_MBP);
550 assert_eq!(
551 book.to_string(),
552 "OrderBook(instrument_id=ETHUSDT-PERP.BINANCE, book_type=L2_MBP)"
553 );
554 }
555
556 #[rstest]
557 fn test_empty_book_state() {
558 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
559 let book = OrderBook::new(instrument_id, BookType::L2_MBP);
560
561 assert_eq!(book.best_bid_price(), None);
562 assert_eq!(book.best_ask_price(), None);
563 assert_eq!(book.best_bid_size(), None);
564 assert_eq!(book.best_ask_size(), None);
565 assert!(!book.has_bid());
566 assert!(!book.has_ask());
567 }
568
569 #[rstest]
570 fn test_single_bid_state() {
571 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
572 let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
573 let order1 = BookOrder::new(
574 OrderSide::Buy,
575 Price::from("1.000"),
576 Quantity::from("1.0"),
577 1,
578 );
579 book.add(order1, 0, 1, 100.into());
580
581 assert_eq!(book.best_bid_price(), Some(Price::from("1.000")));
582 assert_eq!(book.best_bid_size(), Some(Quantity::from("1.0")));
583 assert!(book.has_bid());
584 }
585
586 #[rstest]
587 fn test_single_ask_state() {
588 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
589 let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
590 let order = BookOrder::new(
591 OrderSide::Sell,
592 Price::from("2.000"),
593 Quantity::from("2.0"),
594 2,
595 );
596 book.add(order, 0, 2, 200.into());
597
598 assert_eq!(book.best_ask_price(), Some(Price::from("2.000")));
599 assert_eq!(book.best_ask_size(), Some(Quantity::from("2.0")));
600 assert!(book.has_ask());
601 }
602
603 #[rstest]
604 fn test_empty_book_spread() {
605 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
606 let book = OrderBook::new(instrument_id, BookType::L3_MBO);
607 assert_eq!(book.spread(), None);
608 }
609
610 #[rstest]
611 fn test_spread_with_orders() {
612 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
613 let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
614 let bid1 = BookOrder::new(
615 OrderSide::Buy,
616 Price::from("1.000"),
617 Quantity::from("1.0"),
618 1,
619 );
620 let ask1 = BookOrder::new(
621 OrderSide::Sell,
622 Price::from("2.000"),
623 Quantity::from("2.0"),
624 2,
625 );
626 book.add(bid1, 0, 1, 100.into());
627 book.add(ask1, 0, 2, 200.into());
628
629 assert_eq!(book.spread(), Some(1.0));
630 }
631
632 #[rstest]
633 fn test_empty_book_midpoint() {
634 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
635 let book = OrderBook::new(instrument_id, BookType::L2_MBP);
636 assert_eq!(book.midpoint(), None);
637 }
638
639 #[rstest]
640 fn test_midpoint_with_orders() {
641 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
642 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
643
644 let bid1 = BookOrder::new(
645 OrderSide::Buy,
646 Price::from("1.000"),
647 Quantity::from("1.0"),
648 1,
649 );
650 let ask1 = BookOrder::new(
651 OrderSide::Sell,
652 Price::from("2.000"),
653 Quantity::from("2.0"),
654 2,
655 );
656 book.add(bid1, 0, 1, 100.into());
657 book.add(ask1, 0, 2, 200.into());
658
659 assert_eq!(book.midpoint(), Some(1.5));
660 }
661
662 #[rstest]
663 fn test_get_price_for_quantity_no_market() {
664 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
665 let book = OrderBook::new(instrument_id, BookType::L2_MBP);
666
667 let qty = Quantity::from(1);
668
669 assert_eq!(book.get_avg_px_for_quantity(qty, OrderSide::Buy), 0.0);
670 assert_eq!(book.get_avg_px_for_quantity(qty, OrderSide::Sell), 0.0);
671 }
672
673 #[rstest]
674 fn test_get_quantity_for_price_no_market() {
675 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
676 let book = OrderBook::new(instrument_id, BookType::L2_MBP);
677
678 let price = Price::from("1.0");
679
680 assert_eq!(book.get_quantity_for_price(price, OrderSide::Buy), 0.0);
681 assert_eq!(book.get_quantity_for_price(price, OrderSide::Sell), 0.0);
682 }
683
684 #[rstest]
685 fn test_get_price_for_quantity() {
686 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
687 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
688
689 let ask2 = BookOrder::new(
690 OrderSide::Sell,
691 Price::from("2.010"),
692 Quantity::from("2.0"),
693 0, );
695 let ask1 = BookOrder::new(
696 OrderSide::Sell,
697 Price::from("2.000"),
698 Quantity::from("1.0"),
699 0, );
701 let bid1 = BookOrder::new(
702 OrderSide::Buy,
703 Price::from("1.000"),
704 Quantity::from("1.0"),
705 0, );
707 let bid2 = BookOrder::new(
708 OrderSide::Buy,
709 Price::from("0.990"),
710 Quantity::from("2.0"),
711 0, );
713 book.add(bid1, 0, 1, 2.into());
714 book.add(bid2, 0, 1, 2.into());
715 book.add(ask1, 0, 1, 2.into());
716 book.add(ask2, 0, 1, 2.into());
717
718 let qty = Quantity::from("1.5");
719
720 assert_eq!(
721 book.get_avg_px_for_quantity(qty, OrderSide::Buy),
722 2.003_333_333_333_333_4
723 );
724 assert_eq!(
725 book.get_avg_px_for_quantity(qty, OrderSide::Sell),
726 0.996_666_666_666_666_7
727 );
728 }
729
730 #[rstest]
731 fn test_get_quantity_for_price() {
732 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
733 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
734
735 let ask3 = BookOrder::new(
736 OrderSide::Sell,
737 Price::from("2.011"),
738 Quantity::from("3.0"),
739 0, );
741 let ask2 = BookOrder::new(
742 OrderSide::Sell,
743 Price::from("2.010"),
744 Quantity::from("2.0"),
745 0, );
747 let ask1 = BookOrder::new(
748 OrderSide::Sell,
749 Price::from("2.000"),
750 Quantity::from("1.0"),
751 0, );
753 let bid1 = BookOrder::new(
754 OrderSide::Buy,
755 Price::from("1.000"),
756 Quantity::from("1.0"),
757 0, );
759 let bid2 = BookOrder::new(
760 OrderSide::Buy,
761 Price::from("0.990"),
762 Quantity::from("2.0"),
763 0, );
765 let bid3 = BookOrder::new(
766 OrderSide::Buy,
767 Price::from("0.989"),
768 Quantity::from("3.0"),
769 0, );
771 book.add(bid1, 0, 0, 1.into());
772 book.add(bid2, 0, 0, 1.into());
773 book.add(bid3, 0, 0, 1.into());
774 book.add(ask1, 0, 0, 1.into());
775 book.add(ask2, 0, 0, 1.into());
776 book.add(ask3, 0, 0, 1.into());
777
778 assert_eq!(
779 book.get_quantity_for_price(Price::from("2.010"), OrderSide::Buy),
780 3.0
781 );
782 assert_eq!(
783 book.get_quantity_for_price(Price::from("0.990"), OrderSide::Sell),
784 3.0
785 );
786 }
787
788 #[rstest]
789 fn test_get_price_for_exposure_no_market() {
790 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
791 let book = OrderBook::new(instrument_id, BookType::L2_MBP);
792 let qty = Quantity::from(1);
793
794 assert_eq!(
795 book.get_avg_px_qty_for_exposure(qty, OrderSide::Buy),
796 (0.0, 0.0, 0.0)
797 );
798 assert_eq!(
799 book.get_avg_px_qty_for_exposure(qty, OrderSide::Sell),
800 (0.0, 0.0, 0.0)
801 );
802 }
803
804 #[rstest]
805 fn test_get_price_for_exposure(stub_depth10: OrderBookDepth10) {
806 let depth = stub_depth10;
807 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
808 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
809 book.apply_depth(&depth);
810
811 let qty = Quantity::from(1);
812
813 assert_eq!(
814 book.get_avg_px_qty_for_exposure(qty, OrderSide::Buy),
815 (100.0, 0.01, 100.0)
816 );
817 }
823
824 #[rstest]
825 fn test_apply_depth(stub_depth10: OrderBookDepth10) {
826 let depth = stub_depth10;
827 let instrument_id = InstrumentId::from("AAPL.XNAS");
828 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
829
830 book.apply_depth(&depth);
831
832 assert_eq!(book.best_bid_price().unwrap().as_f64(), 99.00);
833 assert_eq!(book.best_ask_price().unwrap().as_f64(), 100.00);
834 assert_eq!(book.best_bid_size().unwrap().as_f64(), 100.0);
835 assert_eq!(book.best_ask_size().unwrap().as_f64(), 100.0);
836 }
837
838 #[rstest]
839 fn test_orderbook_creation() {
840 let instrument_id = InstrumentId::from("AAPL.XNAS");
841 let book = OrderBook::new(instrument_id, BookType::L2_MBP);
842
843 assert_eq!(book.instrument_id, instrument_id);
844 assert_eq!(book.book_type, BookType::L2_MBP);
845 assert_eq!(book.sequence, 0);
846 assert_eq!(book.ts_last, 0);
847 assert_eq!(book.count, 0);
848 }
849
850 #[rstest]
851 fn test_orderbook_reset() {
852 let instrument_id = InstrumentId::from("AAPL.XNAS");
853 let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
854 book.sequence = 10;
855 book.ts_last = 100.into();
856 book.count = 3;
857
858 book.reset();
859
860 assert_eq!(book.book_type, BookType::L1_MBP);
861 assert_eq!(book.sequence, 0);
862 assert_eq!(book.ts_last, 0);
863 assert_eq!(book.count, 0);
864 }
865
866 #[rstest]
867 fn test_update_quote_tick_l1() {
868 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
869 let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
870 let quote = QuoteTick::new(
871 InstrumentId::from("ETHUSDT-PERP.BINANCE"),
872 Price::from("5000.000"),
873 Price::from("5100.000"),
874 Quantity::from("100.00000000"),
875 Quantity::from("99.00000000"),
876 0.into(),
877 0.into(),
878 );
879
880 book.update_quote_tick("e).unwrap();
881
882 assert_eq!(book.best_bid_price().unwrap(), quote.bid_price);
883 assert_eq!(book.best_ask_price().unwrap(), quote.ask_price);
884 assert_eq!(book.best_bid_size().unwrap(), quote.bid_size);
885 assert_eq!(book.best_ask_size().unwrap(), quote.ask_size);
886 }
887
888 #[rstest]
889 fn test_update_trade_tick_l1() {
890 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
891 let mut book = OrderBook::new(instrument_id, BookType::L1_MBP);
892
893 let price = Price::from("15000.000");
894 let size = Quantity::from("10.00000000");
895 let trade = TradeTick::new(
896 instrument_id,
897 price,
898 size,
899 AggressorSide::Buyer,
900 TradeId::new("123456789"),
901 0.into(),
902 0.into(),
903 );
904
905 book.update_trade_tick(&trade).unwrap();
906
907 assert_eq!(book.best_bid_price().unwrap(), price);
908 assert_eq!(book.best_ask_price().unwrap(), price);
909 assert_eq!(book.best_bid_size().unwrap(), size);
910 assert_eq!(book.best_ask_size().unwrap(), size);
911 }
912
913 #[rstest]
914 fn test_pprint() {
915 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
916 let mut book = OrderBook::new(instrument_id, BookType::L3_MBO);
917
918 let order1 = BookOrder::new(
919 OrderSide::Buy,
920 Price::from("1.000"),
921 Quantity::from("1.0"),
922 1,
923 );
924 let order2 = BookOrder::new(
925 OrderSide::Buy,
926 Price::from("1.500"),
927 Quantity::from("2.0"),
928 2,
929 );
930 let order3 = BookOrder::new(
931 OrderSide::Buy,
932 Price::from("2.000"),
933 Quantity::from("3.0"),
934 3,
935 );
936 let order4 = BookOrder::new(
937 OrderSide::Sell,
938 Price::from("3.000"),
939 Quantity::from("3.0"),
940 4,
941 );
942 let order5 = BookOrder::new(
943 OrderSide::Sell,
944 Price::from("4.000"),
945 Quantity::from("4.0"),
946 5,
947 );
948 let order6 = BookOrder::new(
949 OrderSide::Sell,
950 Price::from("5.000"),
951 Quantity::from("8.0"),
952 6,
953 );
954
955 book.add(order1, 0, 1, 100.into());
956 book.add(order2, 0, 2, 200.into());
957 book.add(order3, 0, 3, 300.into());
958 book.add(order4, 0, 4, 400.into());
959 book.add(order5, 0, 5, 500.into());
960 book.add(order6, 0, 6, 600.into());
961
962 let pprint_output = book.pprint(3);
963
964 let expected_output = "╭───────┬───────┬───────╮\n\
965 │ bids │ price │ asks │\n\
966 ├───────┼───────┼───────┤\n\
967 │ │ 5.000 │ [8.0] │\n\
968 │ │ 4.000 │ [4.0] │\n\
969 │ │ 3.000 │ [3.0] │\n\
970 │ [3.0] │ 2.000 │ │\n\
971 │ [2.0] │ 1.500 │ │\n\
972 │ [1.0] │ 1.000 │ │\n\
973 ╰───────┴───────┴───────╯";
974
975 println!("{pprint_output}");
976 assert_eq!(pprint_output, expected_output);
977 }
978
979 #[rstest]
980 fn test_group_empty_book() {
981 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
982 let book = OrderBook::new(instrument_id, BookType::L2_MBP);
983
984 let grouped_bids = book.group_bids(dec!(1), None);
985 let grouped_asks = book.group_asks(dec!(1), None);
986
987 assert!(grouped_bids.is_empty());
988 assert!(grouped_asks.is_empty());
989 }
990
991 #[rstest]
992 fn test_group_price_levels() {
993 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
994 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
995 let orders = vec![
996 BookOrder::new(OrderSide::Buy, Price::from("1.1"), Quantity::from(1), 1),
997 BookOrder::new(OrderSide::Buy, Price::from("1.2"), Quantity::from(2), 2),
998 BookOrder::new(OrderSide::Buy, Price::from("1.8"), Quantity::from(3), 3),
999 BookOrder::new(OrderSide::Sell, Price::from("2.1"), Quantity::from(1), 4),
1000 BookOrder::new(OrderSide::Sell, Price::from("2.2"), Quantity::from(2), 5),
1001 BookOrder::new(OrderSide::Sell, Price::from("2.8"), Quantity::from(3), 6),
1002 ];
1003 for (i, order) in orders.into_iter().enumerate() {
1004 book.add(order, 0, i as u64, 100.into());
1005 }
1006
1007 let grouped_bids = book.group_bids(dec!(0.5), Some(10));
1008 let grouped_asks = book.group_asks(dec!(0.5), Some(10));
1009
1010 assert_eq!(grouped_bids.len(), 2);
1011 assert_eq!(grouped_asks.len(), 2);
1012 assert_eq!(grouped_bids.get(&dec!(1.0)), Some(&dec!(3))); assert_eq!(grouped_bids.get(&dec!(1.5)), Some(&dec!(3))); assert_eq!(grouped_asks.get(&dec!(2.5)), Some(&dec!(3))); assert_eq!(grouped_asks.get(&dec!(3.0)), Some(&dec!(3))); }
1017
1018 #[rstest]
1019 fn test_group_with_depth_limit() {
1020 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1021 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
1022
1023 let orders = vec![
1024 BookOrder::new(OrderSide::Buy, Price::from("1.0"), Quantity::from(1), 1),
1025 BookOrder::new(OrderSide::Buy, Price::from("2.0"), Quantity::from(2), 2),
1026 BookOrder::new(OrderSide::Buy, Price::from("3.0"), Quantity::from(3), 3),
1027 BookOrder::new(OrderSide::Sell, Price::from("4.0"), Quantity::from(1), 4),
1028 BookOrder::new(OrderSide::Sell, Price::from("5.0"), Quantity::from(2), 5),
1029 BookOrder::new(OrderSide::Sell, Price::from("6.0"), Quantity::from(3), 6),
1030 ];
1031
1032 for (i, order) in orders.into_iter().enumerate() {
1033 book.add(order, 0, i as u64, 100.into());
1034 }
1035
1036 let grouped_bids = book.group_bids(dec!(1), Some(2));
1037 let grouped_asks = book.group_asks(dec!(1), Some(2));
1038
1039 assert_eq!(grouped_bids.len(), 2); assert_eq!(grouped_asks.len(), 2); assert_eq!(grouped_bids.get(&dec!(3)), Some(&dec!(3)));
1042 assert_eq!(grouped_bids.get(&dec!(2)), Some(&dec!(2)));
1043 assert_eq!(grouped_asks.get(&dec!(4)), Some(&dec!(1)));
1044 assert_eq!(grouped_asks.get(&dec!(5)), Some(&dec!(2)));
1045 }
1046
1047 #[rstest]
1048 fn test_group_price_realistic() {
1049 let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE");
1050 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
1051 let orders = vec![
1052 BookOrder::new(
1053 OrderSide::Buy,
1054 Price::from("100.00000"),
1055 Quantity::from(1000),
1056 1,
1057 ),
1058 BookOrder::new(
1059 OrderSide::Buy,
1060 Price::from("99.00000"),
1061 Quantity::from(2000),
1062 2,
1063 ),
1064 BookOrder::new(
1065 OrderSide::Buy,
1066 Price::from("98.00000"),
1067 Quantity::from(3000),
1068 3,
1069 ),
1070 BookOrder::new(
1071 OrderSide::Sell,
1072 Price::from("101.00000"),
1073 Quantity::from(1000),
1074 4,
1075 ),
1076 BookOrder::new(
1077 OrderSide::Sell,
1078 Price::from("102.00000"),
1079 Quantity::from(2000),
1080 5,
1081 ),
1082 BookOrder::new(
1083 OrderSide::Sell,
1084 Price::from("103.00000"),
1085 Quantity::from(3000),
1086 6,
1087 ),
1088 ];
1089 for (i, order) in orders.into_iter().enumerate() {
1090 book.add(order, 0, i as u64, 100.into());
1091 }
1092
1093 let grouped_bids = book.group_bids(dec!(2), Some(10));
1094 let grouped_asks = book.group_asks(dec!(2), Some(10));
1095
1096 assert_eq!(grouped_bids.len(), 2);
1097 assert_eq!(grouped_asks.len(), 2);
1098 assert_eq!(grouped_bids.get(&dec!(100.0)), Some(&dec!(1000)));
1099 assert_eq!(grouped_bids.get(&dec!(98.0)), Some(&dec!(5000))); assert_eq!(grouped_asks.get(&dec!(102.0)), Some(&dec!(3000))); assert_eq!(grouped_asks.get(&dec!(104.0)), Some(&dec!(3000)));
1102 }
1103}