nautilus_model/orderbook/
book.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! A performant, generic, multi-purpose order book.
17
18use 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/// Provides a high-performance, versatile order book.
34///
35/// Maintains buy (bid) and sell (ask) orders in price-time priority, supporting multiple
36/// market data formats:
37/// - L3 (MBO): Market By Order - tracks individual orders with unique IDs.
38/// - L2 (MBP): Market By Price - aggregates orders at each price level.
39/// - L1 (MBP): Top-of-Book - maintains only the best bid and ask prices.
40#[derive(Clone, Debug)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
44)]
45pub struct OrderBook {
46    /// The instrument ID for the order book.
47    pub instrument_id: InstrumentId,
48    /// The order book type (MBP types will aggregate orders).
49    pub book_type: BookType,
50    /// The last event sequence number for the order book.
51    pub sequence: u64,
52    /// The timestamp of the last event applied to the order book.
53    pub ts_last: UnixNanos,
54    /// The current count of events applied to the order book.
55    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    /// Creates a new [`OrderBook`] instance.
82    #[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    /// Resets the order book to its initial empty state.
96    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    /// Adds an order to the book after preprocessing based on book type.
105    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    /// Updates an existing order in the book after preprocessing based on book type.
116    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    /// Deletes an order from the book after preprocessing based on book type.
127    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    /// Clears all orders from both sides of the book.
138    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    /// Clears all bid orders from the book.
145    pub fn clear_bids(&mut self, sequence: u64, ts_event: UnixNanos) {
146        self.bids.clear();
147        self.increment(sequence, ts_event);
148    }
149
150    /// Clears all ask orders from the book.
151    pub fn clear_asks(&mut self, sequence: u64, ts_event: UnixNanos) {
152        self.asks.clear();
153        self.increment(sequence, ts_event);
154    }
155
156    /// Applies a single order book delta operation.
157    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    /// Applies multiple order book delta operations.
171    pub fn apply_deltas(&mut self, deltas: &OrderBookDeltas) {
172        for delta in &deltas.deltas {
173            self.apply_delta(delta);
174        }
175    }
176
177    /// Replaces current book state with a depth snapshot.
178    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    /// Returns an iterator over bid price levels.
192    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    /// Returns an iterator over ask price levels.
197    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    /// Returns bid price levels as a map of price to size.
202    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    /// Returns ask price levels as a map of price to size.
209    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    /// Groups bid levels by price, up to specified depth.
216    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    /// Groups ask levels by price, up to specified depth.
225    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    /// Returns true if the book has any bid orders.
267    #[must_use]
268    pub fn has_bid(&self) -> bool {
269        self.bids.top().is_some_and(|top| !top.orders.is_empty())
270    }
271
272    /// Returns true if the book has any ask orders.
273    #[must_use]
274    pub fn has_ask(&self) -> bool {
275        self.asks.top().is_some_and(|top| !top.orders.is_empty())
276    }
277
278    /// Returns the best bid price if available.
279    #[must_use]
280    pub fn best_bid_price(&self) -> Option<Price> {
281        self.bids.top().map(|top| top.price.value)
282    }
283
284    /// Returns the best ask price if available.
285    #[must_use]
286    pub fn best_ask_price(&self) -> Option<Price> {
287        self.asks.top().map(|top| top.price.value)
288    }
289
290    /// Returns the size at the best bid price if available.
291    #[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    /// Returns the size at the best ask price if available.
299    #[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    /// Returns the spread between best ask and bid prices if both exist.
307    #[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    /// Returns the midpoint between best ask and bid prices if both exist.
316    #[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    /// Calculates the average price to fill the specified quantity.
325    #[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    /// Calculates average price and quantity for target exposure. Returns (price, quantity, executed_exposure).
336    #[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    /// Returns the total quantity available at specified price level.
351    #[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    /// Simulates fills for an order, returning list of (price, quantity) tuples.
362    #[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    /// Return a formatted string representation of the order book.
371    #[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    /// Updates L1 book state from a quote tick. Only valid for L1_MBP book type.
383    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    /// Updates L1 book state from a trade tick. Only valid for L1_MBP book type.
409    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////////////////////////////////////////////////////////////////////////////////
454// Tests
455////////////////////////////////////////////////////////////////////////////////
456#[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, // order_id not applicable
694        );
695        let ask1 = BookOrder::new(
696            OrderSide::Sell,
697            Price::from("2.000"),
698            Quantity::from("1.0"),
699            0, // order_id not applicable
700        );
701        let bid1 = BookOrder::new(
702            OrderSide::Buy,
703            Price::from("1.000"),
704            Quantity::from("1.0"),
705            0, // order_id not applicable
706        );
707        let bid2 = BookOrder::new(
708            OrderSide::Buy,
709            Price::from("0.990"),
710            Quantity::from("2.0"),
711            0, // order_id not applicable
712        );
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, // order_id not applicable
740        );
741        let ask2 = BookOrder::new(
742            OrderSide::Sell,
743            Price::from("2.010"),
744            Quantity::from("2.0"),
745            0, // order_id not applicable
746        );
747        let ask1 = BookOrder::new(
748            OrderSide::Sell,
749            Price::from("2.000"),
750            Quantity::from("1.0"),
751            0, // order_id not applicable
752        );
753        let bid1 = BookOrder::new(
754            OrderSide::Buy,
755            Price::from("1.000"),
756            Quantity::from("1.0"),
757            0, // order_id not applicable
758        );
759        let bid2 = BookOrder::new(
760            OrderSide::Buy,
761            Price::from("0.990"),
762            Quantity::from("2.0"),
763            0, // order_id not applicable
764        );
765        let bid3 = BookOrder::new(
766            OrderSide::Buy,
767            Price::from("0.989"),
768            Quantity::from("3.0"),
769            0, // order_id not applicable
770        );
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        // TODO: Revisit calculations
818        // assert_eq!(
819        //     book.get_avg_px_qty_for_exposure(qty, OrderSide::Sell),
820        //     (99.0, 0.01010101, 99.0)
821        // );
822    }
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(&quote).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))); // 1.1, 1.2 group to 1.0
1013        assert_eq!(grouped_bids.get(&dec!(1.5)), Some(&dec!(3))); // 1.8 groups to 1.5
1014        assert_eq!(grouped_asks.get(&dec!(2.5)), Some(&dec!(3))); // 2.1, 2.2 group to 2.5
1015        assert_eq!(grouped_asks.get(&dec!(3.0)), Some(&dec!(3))); // 2.8 groups to 3.0
1016    }
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); // Should only have levels at 2.0 and 3.0
1040        assert_eq!(grouped_asks.len(), 2); // Should only have levels at 5.0 and 6.0
1041        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))); // 2000 + 3000 grouped
1100        assert_eq!(grouped_asks.get(&dec!(102.0)), Some(&dec!(3000))); // 1000 + 2000 grouped
1101        assert_eq!(grouped_asks.get(&dec!(104.0)), Some(&dec!(3000)));
1102    }
1103}