nautilus_model/orderbook/
ladder.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//! Represents a ladder of price levels for one side of an order book.
17
18use std::{
19    cmp::Ordering,
20    collections::{BTreeMap, HashMap},
21    fmt::{Debug, Display, Formatter},
22};
23
24use nautilus_core::UnixNanos;
25
26use crate::{
27    data::order::{BookOrder, OrderId},
28    enums::{BookType, OrderSideSpecified},
29    orderbook::BookLevel,
30    types::{Price, Quantity},
31};
32
33/// Represents a price level with a specified side in an order books ladder.
34///
35/// # Comparison Semantics
36///
37/// `BookPrice` instances are only meaningfully compared within the same side
38/// (i.e., within a single `BookLadder`). Cross-side comparisons are not expected
39/// in normal use, as bid and ask ladders maintain separate `BTreeMap<BookPrice, BookLevel>`
40/// collections.
41///
42/// - Equality requires both `value` and `side` to match.
43/// - Ordering is side-dependent: Buy side sorts descending, Sell side ascending.
44#[derive(Clone, Copy, Debug, Eq)]
45#[cfg_attr(
46    feature = "python",
47    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
48)]
49pub struct BookPrice {
50    pub value: Price,
51    pub side: OrderSideSpecified,
52}
53
54impl BookPrice {
55    /// Creates a new [`BookPrice`] instance.
56    #[must_use]
57    pub fn new(value: Price, side: OrderSideSpecified) -> Self {
58        Self { value, side }
59    }
60}
61
62impl PartialOrd for BookPrice {
63    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
64        Some(self.cmp(other))
65    }
66}
67
68impl PartialEq for BookPrice {
69    fn eq(&self, other: &Self) -> bool {
70        self.side == other.side && self.value == other.value
71    }
72}
73
74impl Ord for BookPrice {
75    fn cmp(&self, other: &Self) -> Ordering {
76        assert_eq!(
77            self.side, other.side,
78            "BookPrice compared across sides: {:?} vs {:?}",
79            self.side, other.side
80        );
81
82        match self.side.cmp(&other.side) {
83            Ordering::Equal => match self.side {
84                OrderSideSpecified::Buy => other.value.cmp(&self.value),
85                OrderSideSpecified::Sell => self.value.cmp(&other.value),
86            },
87            non_equal => non_equal,
88        }
89    }
90}
91
92impl Display for BookPrice {
93    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{}", self.value)
95    }
96}
97
98/// Represents a ladder of price levels for one side of an order book.
99#[derive(Clone, Debug)]
100pub(crate) struct BookLadder {
101    pub side: OrderSideSpecified,
102    pub book_type: BookType,
103    pub levels: BTreeMap<BookPrice, BookLevel>,
104    pub cache: HashMap<u64, BookPrice>,
105}
106
107impl BookLadder {
108    /// Creates a new [`Ladder`] instance.
109    #[must_use]
110    pub fn new(side: OrderSideSpecified, book_type: BookType) -> Self {
111        Self {
112            side,
113            book_type,
114            levels: BTreeMap::new(),
115            cache: HashMap::new(),
116        }
117    }
118
119    /// Returns the number of price levels in the ladder.
120    #[must_use]
121    pub fn len(&self) -> usize {
122        self.levels.len()
123    }
124
125    /// Returns true if the ladder has no price levels.
126    #[must_use]
127    #[allow(dead_code, reason = "Used in tests")]
128    pub fn is_empty(&self) -> bool {
129        self.levels.is_empty()
130    }
131
132    #[allow(dead_code, reason = "Used in tests")]
133    /// Adds multiple orders to the ladder.
134    pub fn add_bulk(&mut self, orders: Vec<BookOrder>) {
135        for order in orders {
136            self.add(order);
137        }
138    }
139
140    /// Removes all orders and price levels from the ladder.
141    pub fn clear(&mut self) {
142        self.levels.clear();
143        self.cache.clear();
144    }
145
146    /// Adds an order to the ladder at its price level.
147    pub fn add(&mut self, order: BookOrder) {
148        if self.book_type == BookType::L1_MBP && !self.handle_l1_add(&order) {
149            return;
150        }
151
152        if self.book_type != BookType::L1_MBP && !order.size.is_positive() {
153            log::warn!(
154                "Attempted to add order with non-positive size: order_id={order_id}, size={size}, ignoring",
155                order_id = order.order_id,
156                size = order.size
157            );
158            return;
159        }
160
161        let book_price = order.to_book_price();
162        self.cache.insert(order.order_id, book_price);
163
164        match self.levels.get_mut(&book_price) {
165            Some(level) => {
166                level.add(order);
167            }
168            None => {
169                let level = BookLevel::from_order(order);
170                self.levels.insert(book_price, level);
171            }
172        }
173    }
174
175    /// Handles L1_MBP-specific add logic.
176    ///
177    /// Returns `true` to continue with normal add flow, `false` to abort.
178    ///
179    /// Special cases:
180    /// 1. Zero-size orders clear the top of book (common venue behavior)
181    /// 2. Successive updates at different prices remove the old level
182    fn handle_l1_add(&mut self, order: &BookOrder) -> bool {
183        // Zero-size L1 update means "clear the top of book"
184        if !order.size.is_positive() {
185            if let Some(&old_price) = self.cache.get(&order.order_id) {
186                if let Some(old_level) = self.levels.get_mut(&old_price) {
187                    old_level.delete(order);
188                    if old_level.is_empty() {
189                        self.levels.remove(&old_price);
190                    }
191                }
192                self.cache.remove(&order.order_id);
193            }
194            log::debug!(
195                "L1 zero-size add cleared top of book: order_id={order_id}, side={side:?}",
196                order_id = order.order_id,
197                side = self.side
198            );
199            return false;
200        }
201
202        // Check if L1 order exists at a different price and remove old level
203        if let Some(&old_price) = self.cache.get(&order.order_id) {
204            let book_price = order.to_book_price();
205            if old_price != book_price {
206                // Remove the old level to prevent ghost levels
207                if let Some(old_level) = self.levels.get_mut(&old_price) {
208                    old_level.delete(order);
209                    if old_level.is_empty() {
210                        self.levels.remove(&old_price);
211                    }
212                }
213            }
214        }
215
216        true
217    }
218
219    /// Updates an existing order in the ladder, moving it to a new price level if needed.
220    pub fn update(&mut self, order: BookOrder) {
221        let price = self.cache.get(&order.order_id).copied();
222        if let Some(price) = price
223            && let Some(level) = self.levels.get_mut(&price)
224        {
225            if order.price == level.price.value {
226                let level_len_before = level.len();
227                level.update(order);
228
229                // If level.update removed the order due to zero size, remove from cache too
230                if order.size.raw == 0 {
231                    self.cache.remove(&order.order_id);
232                    debug_assert_eq!(
233                        level.len(),
234                        level_len_before - 1,
235                        "Level should have one less order after zero-size update"
236                    );
237                } else {
238                    debug_assert!(
239                        self.cache.contains_key(&order.order_id),
240                        "Cache should still contain order {0} after update",
241                        order.order_id
242                    );
243                }
244
245                if level.is_empty() {
246                    self.levels.remove(&price);
247                    debug_assert!(
248                        !self.cache.values().any(|p| *p == price),
249                        "Cache should not contain removed price level {price:?}"
250                    );
251                }
252
253                debug_assert_eq!(
254                    self.cache.len(),
255                    self.levels.values().map(|level| level.len()).sum::<usize>(),
256                    "Cache size should equal total orders across all levels"
257                );
258                return;
259            }
260
261            // Price update: delete and insert at new level
262            self.cache.remove(&order.order_id);
263            level.delete(&order);
264
265            if level.is_empty() {
266                self.levels.remove(&price);
267                debug_assert!(
268                    !self.cache.values().any(|p| *p == price),
269                    "Cache should not contain removed price level {price:?}"
270                );
271            }
272        }
273
274        // Only add if the order has positive size
275        if order.size.is_positive() {
276            self.add(order);
277        }
278
279        // Validate cache consistency after update
280        debug_assert_eq!(
281            self.cache.len(),
282            self.levels.values().map(|level| level.len()).sum::<usize>(),
283            "Cache size should equal total orders across all levels"
284        );
285    }
286
287    /// Deletes an order from the ladder.
288    pub fn delete(&mut self, order: BookOrder, sequence: u64, ts_event: UnixNanos) {
289        self.remove_order(order.order_id, sequence, ts_event);
290    }
291
292    /// Removes an order by its ID from the ladder.
293    pub fn remove_order(&mut self, order_id: OrderId, sequence: u64, ts_event: UnixNanos) {
294        if let Some(price) = self.cache.get(&order_id).copied()
295            && let Some(level) = self.levels.get_mut(&price)
296        {
297            // Check if order exists in level before modifying cache
298            if level.orders.contains_key(&order_id) {
299                let level_len_before = level.len();
300
301                // Now safe to remove from cache since we know order exists in level
302                self.cache.remove(&order_id);
303                level.remove_by_id(order_id, sequence, ts_event);
304
305                debug_assert_eq!(
306                    level.len(),
307                    level_len_before - 1,
308                    "Level should have exactly one less order after removal"
309                );
310
311                if level.is_empty() {
312                    self.levels.remove(&price);
313                    debug_assert!(
314                        !self.cache.values().any(|p| *p == price),
315                        "Cache should not contain removed price level {price:?}"
316                    );
317                }
318            }
319        }
320
321        // Validate cache consistency after removal
322        debug_assert_eq!(
323            self.cache.len(),
324            self.levels.values().map(|level| level.len()).sum::<usize>(),
325            "Cache size should equal total orders across all levels"
326        );
327    }
328
329    /// Removes an entire price level from the ladder and returns it.
330    pub fn remove_level(&mut self, price: BookPrice) -> Option<BookLevel> {
331        if let Some(level) = self.levels.remove(&price) {
332            // Remove all orders in this level from the cache
333            for order_id in level.orders.keys() {
334                self.cache.remove(order_id);
335            }
336
337            debug_assert_eq!(
338                self.cache.len(),
339                self.levels.values().map(|level| level.len()).sum::<usize>(),
340                "Cache size should equal total orders across all levels"
341            );
342
343            Some(level)
344        } else {
345            None
346        }
347    }
348
349    /// Returns the total size of all orders in the ladder.
350    #[must_use]
351    #[allow(dead_code, reason = "Used in tests")]
352    pub fn sizes(&self) -> f64 {
353        self.levels.values().map(BookLevel::size).sum()
354    }
355
356    /// Returns the total value exposure (price * size) of all orders in the ladder.
357    #[must_use]
358    #[allow(dead_code, reason = "Used in tests")]
359    pub fn exposures(&self) -> f64 {
360        self.levels.values().map(BookLevel::exposure).sum()
361    }
362
363    /// Returns the best price level in the ladder.
364    #[must_use]
365    pub fn top(&self) -> Option<&BookLevel> {
366        match self.levels.iter().next() {
367            Some((_, l)) => Option::Some(l),
368            None => Option::None,
369        }
370    }
371
372    /// Simulates fills for an order against this ladder's liquidity.
373    /// Returns a list of (price, size) tuples representing the simulated fills.
374    #[must_use]
375    pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
376        let is_reversed = self.side == OrderSideSpecified::Buy;
377        let mut fills = Vec::new();
378        let mut cumulative_denominator = Quantity::zero(order.size.precision);
379        let target = order.size;
380
381        for level in self.levels.values() {
382            if (is_reversed && level.price.value < order.price)
383                || (!is_reversed && level.price.value > order.price)
384            {
385                break;
386            }
387
388            for book_order in level.orders.values() {
389                let current = book_order.size;
390                if cumulative_denominator + current >= target {
391                    // This order has filled us, add fill and return
392                    let remainder = target - cumulative_denominator;
393                    if remainder.is_positive() {
394                        fills.push((book_order.price, remainder));
395                    }
396                    return fills;
397                }
398
399                // Add this fill and continue
400                fills.push((book_order.price, current));
401                cumulative_denominator += current;
402            }
403        }
404
405        fills
406    }
407}
408
409impl Display for BookLadder {
410    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411        writeln!(f, "{}(side={})", stringify!(BookLadder), self.side)?;
412        for (price, level) in &self.levels {
413            writeln!(f, "  {} -> {} orders", price, level.len())?;
414        }
415        Ok(())
416    }
417}
418
419////////////////////////////////////////////////////////////////////////////////
420// Tests
421////////////////////////////////////////////////////////////////////////////////
422#[cfg(test)]
423mod tests {
424    use rstest::rstest;
425
426    use crate::{
427        data::order::BookOrder,
428        enums::{BookType, OrderSide, OrderSideSpecified},
429        orderbook::ladder::{BookLadder, BookPrice},
430        types::{Price, Quantity},
431    };
432
433    #[rstest]
434    fn test_is_empty() {
435        let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
436        assert!(ladder.is_empty(), "A new ladder should be empty");
437    }
438
439    #[rstest]
440    fn test_is_empty_after_add() {
441        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
442        assert!(ladder.is_empty(), "Ladder should start empty");
443        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(100), 1);
444        ladder.add(order);
445        assert!(
446            !ladder.is_empty(),
447            "Ladder should not be empty after adding an order"
448        );
449    }
450
451    #[rstest]
452    fn test_add_bulk_empty() {
453        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
454        ladder.add_bulk(vec![]);
455        assert!(
456            ladder.is_empty(),
457            "Adding an empty vector should leave the ladder empty"
458        );
459    }
460
461    #[rstest]
462    fn test_add_bulk_orders() {
463        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
464        let orders = vec![
465            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1),
466            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2),
467            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(50), 3),
468        ];
469        ladder.add_bulk(orders);
470        // All orders share the same price, so there should be one price level.
471        assert_eq!(ladder.len(), 1, "Ladder should have one price level");
472        let orders_in_level = ladder.top().unwrap().get_orders();
473        assert_eq!(
474            orders_in_level.len(),
475            3,
476            "Price level should contain all bulk orders"
477        );
478    }
479
480    #[rstest]
481    fn test_book_price_bid_sorting() {
482        let mut bid_prices = [
483            BookPrice::new(Price::from("2.0"), OrderSideSpecified::Buy),
484            BookPrice::new(Price::from("4.0"), OrderSideSpecified::Buy),
485            BookPrice::new(Price::from("1.0"), OrderSideSpecified::Buy),
486            BookPrice::new(Price::from("3.0"), OrderSideSpecified::Buy),
487        ];
488        bid_prices.sort();
489        assert_eq!(bid_prices[0].value, Price::from("4.0"));
490    }
491
492    #[rstest]
493    fn test_book_price_ask_sorting() {
494        let mut ask_prices = [
495            BookPrice::new(Price::from("2.0"), OrderSideSpecified::Sell),
496            BookPrice::new(Price::from("4.0"), OrderSideSpecified::Sell),
497            BookPrice::new(Price::from("1.0"), OrderSideSpecified::Sell),
498            BookPrice::new(Price::from("3.0"), OrderSideSpecified::Sell),
499        ];
500
501        ask_prices.sort();
502        assert_eq!(ask_prices[0].value, Price::from("1.0"));
503    }
504
505    #[rstest]
506    fn test_add_single_order() {
507        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
508        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 0);
509
510        ladder.add(order);
511        assert_eq!(ladder.len(), 1);
512        assert_eq!(ladder.sizes(), 20.0);
513        assert_eq!(ladder.exposures(), 200.0);
514        assert_eq!(ladder.top().unwrap().price.value, Price::from("10.0"));
515    }
516
517    #[rstest]
518    fn test_add_multiple_buy_orders() {
519        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
520        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 0);
521        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(30), 1);
522        let order3 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(50), 2);
523        let order4 = BookOrder::new(OrderSide::Buy, Price::from("8.00"), Quantity::from(200), 3);
524
525        ladder.add_bulk(vec![order1, order2, order3, order4]);
526        assert_eq!(ladder.len(), 3);
527        assert_eq!(ladder.sizes(), 300.0);
528        assert_eq!(ladder.exposures(), 2520.0);
529        assert_eq!(ladder.top().unwrap().price.value, Price::from("10.0"));
530    }
531
532    #[rstest]
533    fn test_add_multiple_sell_orders() {
534        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
535        let order1 = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 0);
536        let order2 = BookOrder::new(OrderSide::Sell, Price::from("12.00"), Quantity::from(30), 1);
537        let order3 = BookOrder::new(OrderSide::Sell, Price::from("12.00"), Quantity::from(50), 2);
538        let order4 = BookOrder::new(
539            OrderSide::Sell,
540            Price::from("13.00"),
541            Quantity::from(200),
542            0,
543        );
544
545        ladder.add_bulk(vec![order1, order2, order3, order4]);
546        assert_eq!(ladder.len(), 3);
547        assert_eq!(ladder.sizes(), 300.0);
548        assert_eq!(ladder.exposures(), 3780.0);
549        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
550    }
551
552    #[rstest]
553    fn test_add_to_same_price_level() {
554        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
555        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
556        let order2 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2);
557
558        ladder.add(order1);
559        ladder.add(order2);
560
561        assert_eq!(ladder.len(), 1);
562        assert_eq!(ladder.sizes(), 50.0);
563        assert_eq!(ladder.exposures(), 500.0);
564    }
565
566    #[rstest]
567    fn test_add_descending_buy_orders() {
568        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
569        let order1 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(20), 1);
570        let order2 = BookOrder::new(OrderSide::Buy, Price::from("8.00"), Quantity::from(30), 2);
571
572        ladder.add(order1);
573        ladder.add(order2);
574
575        assert_eq!(ladder.top().unwrap().price.value, Price::from("9.00"));
576    }
577
578    #[rstest]
579    fn test_add_ascending_sell_orders() {
580        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
581        let order1 = BookOrder::new(OrderSide::Sell, Price::from("8.00"), Quantity::from(20), 1);
582        let order2 = BookOrder::new(OrderSide::Sell, Price::from("9.00"), Quantity::from(30), 2);
583
584        ladder.add(order1);
585        ladder.add(order2);
586
587        assert_eq!(ladder.top().unwrap().price.value, Price::from("8.00"));
588    }
589
590    #[rstest]
591    fn test_update_buy_order_price() {
592        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
593        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
594
595        ladder.add(order);
596        let order = BookOrder::new(OrderSide::Buy, Price::from("11.10"), Quantity::from(20), 1);
597
598        ladder.update(order);
599        assert_eq!(ladder.len(), 1);
600        assert_eq!(ladder.sizes(), 20.0);
601        assert_eq!(ladder.exposures(), 222.0);
602        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.1"));
603    }
604
605    #[rstest]
606    fn test_update_sell_order_price() {
607        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
608        let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 1);
609
610        ladder.add(order);
611
612        let order = BookOrder::new(OrderSide::Sell, Price::from("11.10"), Quantity::from(20), 1);
613
614        ladder.update(order);
615        assert_eq!(ladder.len(), 1);
616        assert_eq!(ladder.sizes(), 20.0);
617        assert_eq!(ladder.exposures(), 222.0);
618        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.1"));
619    }
620
621    #[rstest]
622    fn test_update_buy_order_size() {
623        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
624        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
625
626        ladder.add(order);
627
628        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(10), 1);
629
630        ladder.update(order);
631        assert_eq!(ladder.len(), 1);
632        assert_eq!(ladder.sizes(), 10.0);
633        assert_eq!(ladder.exposures(), 110.0);
634        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
635    }
636
637    #[rstest]
638    fn test_update_sell_order_size() {
639        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
640        let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 1);
641
642        ladder.add(order);
643
644        let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(10), 1);
645
646        ladder.update(order);
647        assert_eq!(ladder.len(), 1);
648        assert_eq!(ladder.sizes(), 10.0);
649        assert_eq!(ladder.exposures(), 110.0);
650        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
651    }
652
653    #[rstest]
654    fn test_delete_non_existing_order() {
655        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
656        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
657
658        ladder.delete(order, 0, 0.into());
659
660        assert_eq!(ladder.len(), 0);
661    }
662
663    #[rstest]
664    fn test_delete_buy_order() {
665        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
666        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
667
668        ladder.add(order);
669
670        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(10), 1);
671
672        ladder.delete(order, 0, 0.into());
673        assert_eq!(ladder.len(), 0);
674        assert_eq!(ladder.sizes(), 0.0);
675        assert_eq!(ladder.exposures(), 0.0);
676        assert_eq!(ladder.top(), None);
677    }
678
679    #[rstest]
680    fn test_delete_sell_order() {
681        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
682        let order = BookOrder::new(OrderSide::Sell, Price::from("10.00"), Quantity::from(10), 1);
683
684        ladder.add(order);
685
686        let order = BookOrder::new(OrderSide::Sell, Price::from("10.00"), Quantity::from(10), 1);
687
688        ladder.delete(order, 0, 0.into());
689        assert_eq!(ladder.len(), 0);
690        assert_eq!(ladder.sizes(), 0.0);
691        assert_eq!(ladder.exposures(), 0.0);
692        assert_eq!(ladder.top(), None);
693    }
694
695    #[rstest]
696    fn test_ladder_sizes_empty() {
697        let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
698        assert_eq!(
699            ladder.sizes(),
700            0.0,
701            "An empty ladder should have total size 0.0"
702        );
703    }
704
705    #[rstest]
706    fn test_ladder_exposures_empty() {
707        let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
708        assert_eq!(
709            ladder.exposures(),
710            0.0,
711            "An empty ladder should have total exposure 0.0"
712        );
713    }
714
715    #[rstest]
716    fn test_ladder_sizes() {
717        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
718        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
719        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.50"), Quantity::from(30), 2);
720        ladder.add(order1);
721        ladder.add(order2);
722
723        let expected_size = 20.0 + 30.0;
724        assert_eq!(
725            ladder.sizes(),
726            expected_size,
727            "Ladder total size should match the sum of order sizes"
728        );
729    }
730
731    #[rstest]
732    fn test_ladder_exposures() {
733        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
734        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
735        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.50"), Quantity::from(30), 2);
736        ladder.add(order1);
737        ladder.add(order2);
738
739        let expected_exposure = 10.00 * 20.0 + 9.50 * 30.0;
740        assert_eq!(
741            ladder.exposures(),
742            expected_exposure,
743            "Ladder total exposure should match the sum of individual exposures"
744        );
745    }
746
747    #[rstest]
748    fn test_iter_returns_fifo() {
749        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
750        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
751        let order2 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2);
752        ladder.add(order1);
753        ladder.add(order2);
754        let orders: Vec<BookOrder> = ladder.top().unwrap().iter().copied().collect();
755        assert_eq!(
756            orders,
757            vec![order1, order2],
758            "Iterator should return orders in FIFO order"
759        );
760    }
761
762    #[rstest]
763    fn test_update_missing_order_inserts() {
764        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
765        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
766        // Call update on an order that hasn't been added yet (upsert behavior)
767        ladder.update(order);
768        assert_eq!(
769            ladder.len(),
770            1,
771            "Ladder should have one level after upsert update"
772        );
773        let orders = ladder.top().unwrap().get_orders();
774        assert_eq!(
775            orders.len(),
776            1,
777            "Price level should contain the inserted order"
778        );
779        assert_eq!(orders[0], order, "The inserted order should match");
780    }
781
782    #[rstest]
783    fn test_cache_consistency_after_operations() {
784        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
785        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
786        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(30), 2);
787        ladder.add(order1);
788        ladder.add(order2);
789
790        // Ensure that each order in the cache is present in the corresponding price level.
791        for (order_id, price) in &ladder.cache {
792            let level = ladder
793                .levels
794                .get(price)
795                .expect("Every price in the cache should have a corresponding level");
796            assert!(
797                level.orders.contains_key(order_id),
798                "Order id {order_id} should be present in the level for price {price}",
799            );
800        }
801    }
802
803    #[rstest]
804    fn test_simulate_fills_with_empty_book() {
805        let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
806        let order = BookOrder::new(OrderSide::Buy, Price::max(2), Quantity::from(500), 1);
807
808        let fills = ladder.simulate_fills(&order);
809
810        assert!(fills.is_empty());
811    }
812
813    #[rstest]
814    #[case(OrderSide::Buy, Price::max(2), OrderSideSpecified::Sell)]
815    #[case(OrderSide::Sell, Price::min(2), OrderSideSpecified::Buy)]
816    fn test_simulate_order_fills_with_no_size(
817        #[case] side: OrderSide,
818        #[case] price: Price,
819        #[case] ladder_side: OrderSideSpecified,
820    ) {
821        let ladder = BookLadder::new(ladder_side, BookType::L3_MBO);
822        let order = BookOrder {
823            price, // <-- Simulate a MARKET order
824            size: Quantity::from(500),
825            side,
826            order_id: 2,
827        };
828
829        let fills = ladder.simulate_fills(&order);
830
831        assert!(fills.is_empty());
832    }
833
834    #[rstest]
835    #[case(OrderSide::Buy, OrderSideSpecified::Sell, Price::from("60.0"))]
836    #[case(OrderSide::Sell, OrderSideSpecified::Buy, Price::from("40.0"))]
837    fn test_simulate_order_fills_buy_when_far_from_market(
838        #[case] order_side: OrderSide,
839        #[case] ladder_side: OrderSideSpecified,
840        #[case] ladder_price: Price,
841    ) {
842        let mut ladder = BookLadder::new(ladder_side, BookType::L3_MBO);
843
844        ladder.add(BookOrder {
845            price: ladder_price,
846            size: Quantity::from(100),
847            side: ladder_side.as_order_side(),
848            order_id: 1,
849        });
850
851        let order = BookOrder {
852            price: Price::from("50.00"),
853            size: Quantity::from(500),
854            side: order_side,
855            order_id: 2,
856        };
857
858        let fills = ladder.simulate_fills(&order);
859
860        assert!(fills.is_empty());
861    }
862
863    #[rstest]
864    fn test_simulate_order_fills_sell_when_far_from_market() {
865        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
866
867        ladder.add(BookOrder {
868            price: Price::from("100.00"),
869            size: Quantity::from(100),
870            side: OrderSide::Buy,
871            order_id: 1,
872        });
873
874        let order = BookOrder {
875            price: Price::from("150.00"), // <-- Simulate a MARKET order
876            size: Quantity::from(500),
877            side: OrderSide::Buy,
878            order_id: 2,
879        };
880
881        let fills = ladder.simulate_fills(&order);
882
883        assert!(fills.is_empty());
884    }
885
886    #[rstest]
887    fn test_simulate_order_fills_buy() {
888        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
889
890        ladder.add_bulk(vec![
891            BookOrder {
892                price: Price::from("100.00"),
893                size: Quantity::from(100),
894                side: OrderSide::Sell,
895                order_id: 1,
896            },
897            BookOrder {
898                price: Price::from("101.00"),
899                size: Quantity::from(200),
900                side: OrderSide::Sell,
901                order_id: 2,
902            },
903            BookOrder {
904                price: Price::from("102.00"),
905                size: Quantity::from(400),
906                side: OrderSide::Sell,
907                order_id: 3,
908            },
909        ]);
910
911        let order = BookOrder {
912            price: Price::max(2), // <-- Simulate a MARKET order
913            size: Quantity::from(500),
914            side: OrderSide::Buy,
915            order_id: 4,
916        };
917
918        let fills = ladder.simulate_fills(&order);
919
920        assert_eq!(fills.len(), 3);
921
922        let (price1, size1) = fills[0];
923        assert_eq!(price1, Price::from("100.00"));
924        assert_eq!(size1, Quantity::from(100));
925
926        let (price2, size2) = fills[1];
927        assert_eq!(price2, Price::from("101.00"));
928        assert_eq!(size2, Quantity::from(200));
929
930        let (price3, size3) = fills[2];
931        assert_eq!(price3, Price::from("102.00"));
932        assert_eq!(size3, Quantity::from(200));
933    }
934
935    #[rstest]
936    fn test_simulate_order_fills_sell() {
937        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
938
939        ladder.add_bulk(vec![
940            BookOrder {
941                price: Price::from("102.00"),
942                size: Quantity::from(100),
943                side: OrderSide::Buy,
944                order_id: 1,
945            },
946            BookOrder {
947                price: Price::from("101.00"),
948                size: Quantity::from(200),
949                side: OrderSide::Buy,
950                order_id: 2,
951            },
952            BookOrder {
953                price: Price::from("100.00"),
954                size: Quantity::from(400),
955                side: OrderSide::Buy,
956                order_id: 3,
957            },
958        ]);
959
960        let order = BookOrder {
961            price: Price::min(2), // <-- Simulate a MARKET order
962            size: Quantity::from(500),
963            side: OrderSide::Sell,
964            order_id: 4,
965        };
966
967        let fills = ladder.simulate_fills(&order);
968
969        assert_eq!(fills.len(), 3);
970
971        let (price1, size1) = fills[0];
972        assert_eq!(price1, Price::from("102.00"));
973        assert_eq!(size1, Quantity::from(100));
974
975        let (price2, size2) = fills[1];
976        assert_eq!(price2, Price::from("101.00"));
977        assert_eq!(size2, Quantity::from(200));
978
979        let (price3, size3) = fills[2];
980        assert_eq!(price3, Price::from("100.00"));
981        assert_eq!(size3, Quantity::from(200));
982    }
983
984    #[rstest]
985    fn test_simulate_order_fills_sell_with_size_at_limit_of_precision() {
986        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
987
988        ladder.add_bulk(vec![
989            BookOrder {
990                price: Price::from("102.00"),
991                size: Quantity::from("100.000000000"),
992                side: OrderSide::Buy,
993                order_id: 1,
994            },
995            BookOrder {
996                price: Price::from("101.00"),
997                size: Quantity::from("200.000000000"),
998                side: OrderSide::Buy,
999                order_id: 2,
1000            },
1001            BookOrder {
1002                price: Price::from("100.00"),
1003                size: Quantity::from("400.000000000"),
1004                side: OrderSide::Buy,
1005                order_id: 3,
1006            },
1007        ]);
1008
1009        let order = BookOrder {
1010            price: Price::min(2),                  // <-- Simulate a MARKET order
1011            size: Quantity::from("699.999999999"), // <-- Size slightly less than total size in ladder
1012            side: OrderSide::Sell,
1013            order_id: 4,
1014        };
1015
1016        let fills = ladder.simulate_fills(&order);
1017
1018        assert_eq!(fills.len(), 3);
1019
1020        let (price1, size1) = fills[0];
1021        assert_eq!(price1, Price::from("102.00"));
1022        assert_eq!(size1, Quantity::from("100.000000000"));
1023
1024        let (price2, size2) = fills[1];
1025        assert_eq!(price2, Price::from("101.00"));
1026        assert_eq!(size2, Quantity::from("200.000000000"));
1027
1028        let (price3, size3) = fills[2];
1029        assert_eq!(price3, Price::from("100.00"));
1030        assert_eq!(size3, Quantity::from("399.999999999"));
1031    }
1032
1033    #[rstest]
1034    fn test_boundary_prices() {
1035        let max_price = Price::max(1);
1036        let min_price = Price::min(1);
1037
1038        let mut ladder_buy = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1039        let mut ladder_sell = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
1040
1041        let order_buy = BookOrder::new(OrderSide::Buy, min_price, Quantity::from(1), 1);
1042        let order_sell = BookOrder::new(OrderSide::Sell, max_price, Quantity::from(1), 1);
1043
1044        ladder_buy.add(order_buy);
1045        ladder_sell.add(order_sell);
1046
1047        assert_eq!(ladder_buy.top().unwrap().price.value, min_price);
1048        assert_eq!(ladder_sell.top().unwrap().price.value, max_price);
1049    }
1050
1051    #[rstest]
1052    fn test_l1_ghost_levels_regression() {
1053        // Regression test for L1 ghost levels bug.
1054        // When L1 orders are added at different prices,
1055        // the old level should be removed to prevent ghost levels.
1056        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1057        let side_constant = OrderSide::Buy as u64;
1058
1059        // Add first L1 order at price 100.00
1060        let order1 = BookOrder {
1061            side: OrderSide::Buy,
1062            price: Price::from("100.00"),
1063            size: Quantity::from(50),
1064            order_id: side_constant,
1065        };
1066        ladder.add(order1);
1067
1068        assert_eq!(ladder.len(), 1, "Should have one level after first add");
1069        assert_eq!(
1070            ladder.top().unwrap().price.value,
1071            Price::from("100.00"),
1072            "Top level should be at 100.00"
1073        );
1074
1075        // Add second L1 order at price 101.00 (price moved up)
1076        // This simulates a venue sending BookAction::Add for new top-of-book
1077        let order2 = BookOrder {
1078            side: OrderSide::Buy,
1079            price: Price::from("101.00"),
1080            size: Quantity::from(60),
1081            order_id: side_constant, // Same order_id (L1 constant)
1082        };
1083        ladder.add(order2);
1084
1085        // Bug: Without the fix, we'd have 2 levels (ghost level at 100.00)
1086        assert_eq!(
1087            ladder.len(),
1088            1,
1089            "Should still have only one level after L1 update"
1090        );
1091        assert_eq!(
1092            ladder.top().unwrap().price.value,
1093            Price::from("101.00"),
1094            "Top level should be at new price 101.00"
1095        );
1096
1097        // Verify no ghost level at old price
1098        let prices: Vec<Price> = ladder.levels.keys().map(|bp| bp.value).collect();
1099        assert_eq!(
1100            prices,
1101            vec![Price::from("101.00")],
1102            "Should only have the new price level"
1103        );
1104
1105        // Add third L1 order at price 100.50 (price moved down)
1106        let order3 = BookOrder {
1107            side: OrderSide::Buy,
1108            price: Price::from("100.50"),
1109            size: Quantity::from(70),
1110            order_id: side_constant,
1111        };
1112        ladder.add(order3);
1113
1114        assert_eq!(
1115            ladder.len(),
1116            1,
1117            "Should still have only one level after second update"
1118        );
1119        assert_eq!(
1120            ladder.top().unwrap().price.value,
1121            Price::from("100.50"),
1122            "Top level should be at new price 100.50"
1123        );
1124    }
1125
1126    #[rstest]
1127    fn test_l2_orders_not_affected_by_l1_fix() {
1128        // Ensure that L2/L3 orders (non-L1) can still exist at multiple levels
1129        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1130
1131        // L2 orders have order_id = price.raw, not side constant
1132        let order1 = BookOrder {
1133            side: OrderSide::Buy,
1134            price: Price::from("100.00"),
1135            size: Quantity::from(50),
1136            order_id: Price::from("100.00").raw as u64,
1137        };
1138        ladder.add(order1);
1139
1140        let order2 = BookOrder {
1141            side: OrderSide::Buy,
1142            price: Price::from("99.00"),
1143            size: Quantity::from(60),
1144            order_id: Price::from("99.00").raw as u64,
1145        };
1146        ladder.add(order2);
1147
1148        // Both levels should exist
1149        assert_eq!(ladder.len(), 2, "L2 orders should create multiple levels");
1150        assert_eq!(
1151            ladder.top().unwrap().price.value,
1152            Price::from("100.00"),
1153            "Top level should be best bid"
1154        );
1155    }
1156
1157    #[rstest]
1158    fn test_zero_size_l1_order_clears_top() {
1159        // Regression test: Zero-size L1 orders should clear the top of book
1160        // Common scenario: venues send Add with size=0 to clear the top
1161        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1162        let side_constant = OrderSide::Buy as u64;
1163
1164        // Add valid L1 order first
1165        let order1 = BookOrder {
1166            side: OrderSide::Buy,
1167            price: Price::from("100.00"),
1168            size: Quantity::from(50),
1169            order_id: side_constant,
1170        };
1171        ladder.add(order1);
1172
1173        assert_eq!(ladder.len(), 1);
1174        assert_eq!(ladder.top().unwrap().price.value, Price::from("100.00"));
1175        assert!(ladder.top().unwrap().first().is_some());
1176
1177        // Try to add zero-size L1 order (venue clearing the book)
1178        let order2 = BookOrder {
1179            side: OrderSide::Buy,
1180            price: Price::from("101.00"),
1181            size: Quantity::zero(9), // Zero size
1182            order_id: side_constant,
1183        };
1184        ladder.add(order2);
1185
1186        // L1 zero-size should clear the top of book
1187        assert_eq!(ladder.len(), 0, "Zero-size L1 add should clear the book");
1188        assert!(ladder.top().is_none(), "Book should be empty after clear");
1189
1190        // Cache should be empty
1191        assert!(
1192            ladder.cache.is_empty(),
1193            "Cache should be empty after L1 clear"
1194        );
1195    }
1196
1197    #[rstest]
1198    fn test_zero_size_order_to_empty_ladder() {
1199        // Edge case: Adding zero-size L1 order to empty ladder should remain empty
1200        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L1_MBP);
1201        let side_constant = OrderSide::Sell as u64;
1202
1203        let order = BookOrder {
1204            side: OrderSide::Sell,
1205            price: Price::from("100.00"),
1206            size: Quantity::zero(9),
1207            order_id: side_constant,
1208        };
1209        ladder.add(order);
1210
1211        assert_eq!(ladder.len(), 0, "Empty ladder should remain empty");
1212        assert!(ladder.top().is_none(), "Top should be None");
1213        assert!(
1214            ladder.cache.is_empty(),
1215            "Cache should remain empty for zero-size add"
1216        );
1217    }
1218
1219    #[rstest]
1220    fn test_l3_order_id_collision_no_ghost_levels() {
1221        // Regression test: L3 venue order IDs 1 and 2 should not trigger L1 ghost level removal
1222        // Real L3 feeds routinely use order IDs 1 or 2, which match the side constants
1223        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1224
1225        // Add order with ID 1 at 100.00 (matches Buy side constant)
1226        let order1 = BookOrder {
1227            side: OrderSide::Buy,
1228            price: Price::from("100.00"),
1229            size: Quantity::from(50),
1230            order_id: 1, // Matches OrderSide::Buy as u64
1231        };
1232        ladder.add(order1);
1233
1234        assert_eq!(ladder.len(), 1);
1235
1236        // Add another order with ID 1 at a different price 99.00
1237        // For L3, this is a DIFFERENT order (different price), should create second level
1238        let order2 = BookOrder {
1239            side: OrderSide::Buy,
1240            price: Price::from("99.00"),
1241            size: Quantity::from(60),
1242            order_id: 1, // Same ID, different price - valid in L3
1243        };
1244        ladder.add(order2);
1245
1246        // Should have both levels - L3 allows duplicate order IDs at different prices
1247        assert_eq!(
1248            ladder.len(),
1249            2,
1250            "L3 should allow order ID 1 at multiple price levels"
1251        );
1252
1253        let prices: Vec<Price> = ladder.levels.keys().map(|bp| bp.value).collect();
1254        assert!(
1255            prices.contains(&Price::from("100.00")),
1256            "Level at 100.00 should still exist"
1257        );
1258        assert!(
1259            prices.contains(&Price::from("99.00")),
1260            "Level at 99.00 should exist"
1261        );
1262    }
1263
1264    #[rstest]
1265    fn test_l1_vs_l3_different_behavior_same_order_id() {
1266        // Demonstrates the difference between L1 and L3 behavior for same order ID
1267
1268        // L1 behavior: order ID = side constant, successive adds at different prices replace
1269        let mut l1_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1270        let side_constant = OrderSide::Buy as u64;
1271
1272        let order1 = BookOrder {
1273            side: OrderSide::Buy,
1274            price: Price::from("100.00"),
1275            size: Quantity::from(50),
1276            order_id: side_constant,
1277        };
1278        l1_ladder.add(order1);
1279
1280        let order2 = BookOrder {
1281            side: OrderSide::Buy,
1282            price: Price::from("101.00"),
1283            size: Quantity::from(60),
1284            order_id: side_constant, // Same ID
1285        };
1286        l1_ladder.add(order2);
1287
1288        assert_eq!(l1_ladder.len(), 1, "L1 should have only 1 level");
1289        assert_eq!(
1290            l1_ladder.top().unwrap().price.value,
1291            Price::from("101.00"),
1292            "L1 should have replaced the old level"
1293        );
1294
1295        // L3 behavior: order ID can be reused at different prices (different orders)
1296        let mut l3_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1297
1298        let order3 = BookOrder {
1299            side: OrderSide::Buy,
1300            price: Price::from("100.00"),
1301            size: Quantity::from(50),
1302            order_id: 1, // Happens to match side constant
1303        };
1304        l3_ladder.add(order3);
1305
1306        let order4 = BookOrder {
1307            side: OrderSide::Buy,
1308            price: Price::from("101.00"),
1309            size: Quantity::from(60),
1310            order_id: 1, // Same ID but different order
1311        };
1312        l3_ladder.add(order4);
1313
1314        assert_eq!(l3_ladder.len(), 2, "L3 should have 2 levels");
1315    }
1316}