nautilus_model/orderbook/
ladder.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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},
22};
23
24use nautilus_core::UnixNanos;
25
26use crate::{
27    data::order::{BookOrder, OrderId},
28    enums::{BookType, OrderSideSpecified, RecordFlag},
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 std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{}", self.value)
95    }
96}
97
98/// Tracks the type of L1 batch currently being accumulated.
99///
100/// Separating MBP and snapshot batches prevents cross-contamination where
101/// stale MBP data could pollute a new snapshot. Without this distinction,
102/// an incomplete MBP stream (missing F_LAST) would leave batch state that
103/// incorrectly affects subsequent snapshot processing.
104#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
105enum L1BatchState {
106    /// Not in any batch.
107    #[default]
108    None,
109    /// Accumulating an F_MBP batch (final two deltas accumulate).
110    MbpBatch,
111    /// Accumulating an F_SNAPSHOT batch (all deltas accumulate).
112    SnapshotBatch,
113}
114
115/// Represents a ladder of price levels for one side of an order book.
116#[derive(Clone, Debug)]
117pub(crate) struct BookLadder {
118    pub side: OrderSideSpecified,
119    pub book_type: BookType,
120    pub levels: BTreeMap<BookPrice, BookLevel>,
121    pub cache: HashMap<u64, BookPrice>,
122    batch_state: L1BatchState,
123}
124
125impl BookLadder {
126    /// Creates a new [`Ladder`] instance.
127    #[must_use]
128    pub fn new(side: OrderSideSpecified, book_type: BookType) -> Self {
129        Self {
130            side,
131            book_type,
132            levels: BTreeMap::new(),
133            cache: HashMap::new(),
134            batch_state: L1BatchState::None,
135        }
136    }
137
138    /// Returns the number of price levels in the ladder.
139    #[must_use]
140    pub fn len(&self) -> usize {
141        self.levels.len()
142    }
143
144    /// Returns true if the ladder has no price levels.
145    #[must_use]
146    #[allow(dead_code)]
147    pub fn is_empty(&self) -> bool {
148        self.levels.is_empty()
149    }
150
151    /// Removes all orders and price levels from the ladder.
152    ///
153    /// Also resets the batch state to ensure clean handling of subsequent batches.
154    pub fn clear(&mut self) {
155        self.levels.clear();
156        self.cache.clear();
157        self.batch_state = L1BatchState::None;
158    }
159
160    /// Adds an order to the ladder at its price level.
161    ///
162    /// For L1_MBP books, behavior depends on flags:
163    /// - F_MBP or F_SNAPSHOT (multi-level batch): Retains best after each add to prevent
164    ///   accumulation even if F_LAST is never sent.
165    /// - F_TOB or no batch flags (single replacement): Clears existing levels first,
166    ///   allowing price to degrade.
167    pub fn add(&mut self, order: BookOrder, flags: u8) {
168        if self.book_type == BookType::L1_MBP && !self.handle_l1_add(&order, flags) {
169            return;
170        }
171
172        if self.book_type != BookType::L1_MBP && !order.size.is_positive() {
173            log::warn!(
174                "Attempted to add order with non-positive size: order_id={}, size={}, ignoring",
175                order.order_id,
176                order.size,
177            );
178            return;
179        }
180
181        let book_price = order.to_book_price();
182        self.cache.insert(order.order_id, book_price);
183
184        match self.levels.get_mut(&book_price) {
185            Some(level) => {
186                level.add(order);
187            }
188            None => {
189                let level = BookLevel::from_order(order);
190                self.levels.insert(book_price, level);
191            }
192        }
193
194        // For L1_MBP with F_MBP or F_SNAPSHOT, always retain best to prevent unbounded
195        // accumulation if F_LAST is never sent
196        let is_batch = RecordFlag::F_MBP.matches(flags) || RecordFlag::F_SNAPSHOT.matches(flags);
197        if self.book_type == BookType::L1_MBP && is_batch {
198            self.retain_best_only();
199            if RecordFlag::F_LAST.matches(flags) {
200                self.batch_state = L1BatchState::None;
201            }
202        }
203    }
204
205    /// Handles L1_MBP-specific add logic.
206    ///
207    /// Returns `true` to continue with normal add flow, `false` to abort.
208    ///
209    /// Behavior depends on flags:
210    /// - F_SNAPSHOT with F_LAST: End of snapshot batch. If in snapshot batch, accumulate;
211    ///   otherwise clear (single-delta snapshot or cross-contamination from MBP).
212    /// - F_SNAPSHOT without F_LAST: Start/continue snapshot batch. Clears if not already
213    ///   in a snapshot batch (handles stale MBP data).
214    /// - F_MBP with F_LAST: End of MBP batch. If in MBP batch, accumulate final two;
215    ///   otherwise clear.
216    /// - F_MBP without F_LAST: Always clear (streaming mode, prevents stale prices).
217    /// - F_TOB or no batch flags: Single replacement (clears first).
218    ///
219    /// Zero-size orders clear the entire L1 ladder.
220    fn handle_l1_add(&mut self, order: &BookOrder, flags: u8) -> bool {
221        if !order.size.is_positive() {
222            self.clear();
223            let side = self.side;
224            log::debug!("L1 zero-size add cleared ladder: side={side:?}");
225            return false;
226        }
227
228        let is_mbp = RecordFlag::F_MBP.matches(flags);
229        let is_snapshot = RecordFlag::F_SNAPSHOT.matches(flags);
230        let is_last = RecordFlag::F_LAST.matches(flags);
231
232        if is_snapshot && is_last {
233            // F_SNAPSHOT|F_LAST: end of snapshot batch
234            // Only accumulate if we're in a snapshot batch; otherwise clear to prevent
235            // cross-contamination from stale MBP data
236            if self.batch_state != L1BatchState::SnapshotBatch {
237                self.clear();
238            }
239        } else if is_snapshot {
240            // F_SNAPSHOT without F_LAST: start/continue snapshot batch
241            if self.batch_state != L1BatchState::SnapshotBatch {
242                self.clear();
243                self.batch_state = L1BatchState::SnapshotBatch;
244            }
245        } else if is_mbp && is_last {
246            // F_MBP|F_LAST: end of MBP batch, accumulate if already in MBP batch
247            if self.batch_state != L1BatchState::MbpBatch {
248                self.clear();
249            }
250        } else if is_mbp {
251            // F_MBP without F_LAST: always clear (streaming mode)
252            self.clear();
253            self.batch_state = L1BatchState::MbpBatch;
254        } else {
255            // Non-batch: replacement mode
256            self.clear();
257        }
258
259        true
260    }
261
262    /// Updates an existing order in the ladder, moving it to a new price level if needed.
263    pub fn update(&mut self, order: BookOrder, flags: u8) {
264        let price = self.cache.get(&order.order_id).copied();
265        if let Some(price) = price
266            && let Some(level) = self.levels.get_mut(&price)
267        {
268            if order.price == level.price.value {
269                let level_len_before = level.len();
270                level.update(order);
271
272                // If level.update removed the order due to zero size, remove from cache too
273                if order.size.raw == 0 {
274                    self.cache.remove(&order.order_id);
275                    debug_assert_eq!(
276                        level.len(),
277                        level_len_before - 1,
278                        "Level should have one less order after zero-size update"
279                    );
280                } else {
281                    debug_assert!(
282                        self.cache.contains_key(&order.order_id),
283                        "Cache should still contain order {0} after update",
284                        order.order_id
285                    );
286                }
287
288                if level.is_empty() {
289                    self.levels.remove(&price);
290                    debug_assert!(
291                        !self.cache.values().any(|p| *p == price),
292                        "Cache should not contain removed price level {price:?}"
293                    );
294                }
295
296                debug_assert_eq!(
297                    self.cache.len(),
298                    self.levels.values().map(|level| level.len()).sum::<usize>(),
299                    "Cache size should equal total orders across all levels"
300                );
301                return;
302            }
303
304            // Price update: delete and insert at new level
305            self.cache.remove(&order.order_id);
306            level.delete(&order);
307
308            if level.is_empty() {
309                self.levels.remove(&price);
310                debug_assert!(
311                    !self.cache.values().any(|p| *p == price),
312                    "Cache should not contain removed price level {price:?}"
313                );
314            }
315        }
316
317        // Only add if the order has positive size
318        if order.size.is_positive() {
319            self.add(order, flags);
320        }
321
322        // Validate cache consistency after update
323        debug_assert_eq!(
324            self.cache.len(),
325            self.levels.values().map(|level| level.len()).sum::<usize>(),
326            "Cache size should equal total orders across all levels"
327        );
328    }
329
330    /// Deletes an order from the ladder.
331    pub fn delete(&mut self, order: BookOrder, sequence: u64, ts_event: UnixNanos) {
332        self.remove_order(order.order_id, sequence, ts_event);
333    }
334
335    /// Removes an order by its ID from the ladder.
336    pub fn remove_order(&mut self, order_id: OrderId, sequence: u64, ts_event: UnixNanos) {
337        if let Some(price) = self.cache.get(&order_id).copied()
338            && let Some(level) = self.levels.get_mut(&price)
339        {
340            // Check if order exists in level before modifying cache
341            if level.orders.contains_key(&order_id) {
342                let level_len_before = level.len();
343
344                // Now safe to remove from cache since we know order exists in level
345                self.cache.remove(&order_id);
346                level.remove_by_id(order_id, sequence, ts_event);
347
348                debug_assert_eq!(
349                    level.len(),
350                    level_len_before - 1,
351                    "Level should have exactly one less order after removal"
352                );
353
354                if level.is_empty() {
355                    self.levels.remove(&price);
356                    debug_assert!(
357                        !self.cache.values().any(|p| *p == price),
358                        "Cache should not contain removed price level {price:?}"
359                    );
360                }
361            }
362        }
363
364        // Validate cache consistency after removal
365        debug_assert_eq!(
366            self.cache.len(),
367            self.levels.values().map(|level| level.len()).sum::<usize>(),
368            "Cache size should equal total orders across all levels"
369        );
370    }
371
372    /// Removes an entire price level from the ladder and returns it.
373    pub fn remove_level(&mut self, price: BookPrice) -> Option<BookLevel> {
374        if let Some(level) = self.levels.remove(&price) {
375            // Remove all orders in this level from the cache
376            for order_id in level.orders.keys() {
377                self.cache.remove(order_id);
378            }
379
380            debug_assert_eq!(
381                self.cache.len(),
382                self.levels.values().map(|level| level.len()).sum::<usize>(),
383                "Cache size should equal total orders across all levels"
384            );
385
386            Some(level)
387        } else {
388            None
389        }
390    }
391
392    /// Retains only the best price level, removing all others.
393    ///
394    /// For L1_MBP books, this ensures only the top-of-book level is kept after
395    /// processing multi-level data. The BTreeMap ordering ensures the first
396    /// entry is always the best price (highest for bids, lowest for asks).
397    fn retain_best_only(&mut self) {
398        if self.levels.len() <= 1 {
399            return;
400        }
401
402        let best_price = match self.levels.keys().next().copied() {
403            Some(price) => price,
404            None => return,
405        };
406
407        // Remove all levels except the best (don't use remove_level as it
408        // incorrectly handles cache for L1 where all orders share order_id)
409        self.levels.retain(|price, _| *price == best_price);
410
411        // Rebuild cache from remaining level (necessary for L1 where
412        // all orders use the same order_id and remove_level would corrupt cache)
413        self.cache.clear();
414        for (book_price, level) in &self.levels {
415            for order_id in level.orders.keys() {
416                self.cache.insert(*order_id, *book_price);
417            }
418        }
419
420        debug_assert!(
421            self.levels.len() <= 1,
422            "L1 ladder should have at most 1 level after retain_best_only"
423        );
424        debug_assert_eq!(
425            self.cache.len(),
426            self.levels.values().map(|l| l.len()).sum::<usize>(),
427            "Cache size should equal total orders across all levels"
428        );
429    }
430
431    /// Returns the total size of all orders in the ladder.
432    #[must_use]
433    #[allow(dead_code)]
434    pub fn sizes(&self) -> f64 {
435        self.levels.values().map(BookLevel::size).sum()
436    }
437
438    /// Returns the total value exposure (price * size) of all orders in the ladder.
439    #[must_use]
440    #[allow(dead_code)]
441    pub fn exposures(&self) -> f64 {
442        self.levels.values().map(BookLevel::exposure).sum()
443    }
444
445    /// Returns the best price level in the ladder.
446    #[must_use]
447    pub fn top(&self) -> Option<&BookLevel> {
448        match self.levels.iter().next() {
449            Some((_, l)) => Option::Some(l),
450            None => Option::None,
451        }
452    }
453
454    /// Simulates fills for an order against this ladder's liquidity.
455    /// Returns a list of (price, size) tuples representing the simulated fills.
456    #[must_use]
457    pub fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
458        let is_reversed = self.side == OrderSideSpecified::Buy;
459        let mut fills = Vec::new();
460        let mut cumulative_denominator = Quantity::zero(order.size.precision);
461        let target = order.size;
462
463        for level in self.levels.values() {
464            if (is_reversed && level.price.value < order.price)
465                || (!is_reversed && level.price.value > order.price)
466            {
467                break;
468            }
469
470            for book_order in level.orders.values() {
471                let current = book_order.size;
472                if cumulative_denominator + current >= target {
473                    // This order has filled us, add fill and return
474                    let remainder = target - cumulative_denominator;
475                    if remainder.is_positive() {
476                        fills.push((book_order.price, remainder));
477                    }
478                    return fills;
479                }
480
481                // Add this fill and continue
482                fills.push((book_order.price, current));
483                cumulative_denominator += current;
484            }
485        }
486
487        fills
488    }
489}
490
491impl Display for BookLadder {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        writeln!(f, "{}(side={})", stringify!(BookLadder), self.side)?;
494        for (price, level) in &self.levels {
495            writeln!(f, "  {} -> {} orders", price, level.len())?;
496        }
497        Ok(())
498    }
499}
500
501#[cfg(test)]
502impl BookLadder {
503    /// Adds multiple orders to the ladder.
504    pub fn add_bulk(&mut self, orders: Vec<BookOrder>) {
505        for order in orders {
506            self.add(order, 0);
507        }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use rstest::rstest;
514
515    use crate::{
516        data::order::BookOrder,
517        enums::{BookType, OrderSide, OrderSideSpecified, RecordFlag},
518        orderbook::ladder::{BookLadder, BookPrice},
519        types::{Price, Quantity},
520    };
521
522    #[rstest]
523    fn test_is_empty() {
524        let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
525        assert!(ladder.is_empty(), "A new ladder should be empty");
526    }
527
528    #[rstest]
529    fn test_is_empty_after_add() {
530        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
531        assert!(ladder.is_empty(), "Ladder should start empty");
532        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(100), 1);
533        ladder.add(order, 0);
534        assert!(
535            !ladder.is_empty(),
536            "Ladder should not be empty after adding an order"
537        );
538    }
539
540    #[rstest]
541    fn test_add_bulk_empty() {
542        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
543        ladder.add_bulk(vec![]);
544        assert!(
545            ladder.is_empty(),
546            "Adding an empty vector should leave the ladder empty"
547        );
548    }
549
550    #[rstest]
551    fn test_add_bulk_orders() {
552        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
553        let orders = vec![
554            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1),
555            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2),
556            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(50), 3),
557        ];
558        ladder.add_bulk(orders);
559        // All orders share the same price, so there should be one price level.
560        assert_eq!(ladder.len(), 1, "Ladder should have one price level");
561        let orders_in_level = ladder.top().unwrap().get_orders();
562        assert_eq!(
563            orders_in_level.len(),
564            3,
565            "Price level should contain all bulk orders"
566        );
567    }
568
569    #[rstest]
570    fn test_book_price_bid_sorting() {
571        let mut bid_prices = [
572            BookPrice::new(Price::from("2.0"), OrderSideSpecified::Buy),
573            BookPrice::new(Price::from("4.0"), OrderSideSpecified::Buy),
574            BookPrice::new(Price::from("1.0"), OrderSideSpecified::Buy),
575            BookPrice::new(Price::from("3.0"), OrderSideSpecified::Buy),
576        ];
577        bid_prices.sort();
578        assert_eq!(bid_prices[0].value, Price::from("4.0"));
579    }
580
581    #[rstest]
582    fn test_book_price_ask_sorting() {
583        let mut ask_prices = [
584            BookPrice::new(Price::from("2.0"), OrderSideSpecified::Sell),
585            BookPrice::new(Price::from("4.0"), OrderSideSpecified::Sell),
586            BookPrice::new(Price::from("1.0"), OrderSideSpecified::Sell),
587            BookPrice::new(Price::from("3.0"), OrderSideSpecified::Sell),
588        ];
589
590        ask_prices.sort();
591        assert_eq!(ask_prices[0].value, Price::from("1.0"));
592    }
593
594    #[rstest]
595    fn test_add_single_order() {
596        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
597        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 0);
598
599        ladder.add(order, 0);
600        assert_eq!(ladder.len(), 1);
601        assert_eq!(ladder.sizes(), 20.0);
602        assert_eq!(ladder.exposures(), 200.0);
603        assert_eq!(ladder.top().unwrap().price.value, Price::from("10.0"));
604    }
605
606    #[rstest]
607    fn test_add_multiple_buy_orders() {
608        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
609        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 0);
610        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(30), 1);
611        let order3 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(50), 2);
612        let order4 = BookOrder::new(OrderSide::Buy, Price::from("8.00"), Quantity::from(200), 3);
613
614        ladder.add_bulk(vec![order1, order2, order3, order4]);
615        assert_eq!(ladder.len(), 3);
616        assert_eq!(ladder.sizes(), 300.0);
617        assert_eq!(ladder.exposures(), 2520.0);
618        assert_eq!(ladder.top().unwrap().price.value, Price::from("10.0"));
619    }
620
621    #[rstest]
622    fn test_add_multiple_sell_orders() {
623        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
624        let order1 = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 0);
625        let order2 = BookOrder::new(OrderSide::Sell, Price::from("12.00"), Quantity::from(30), 1);
626        let order3 = BookOrder::new(OrderSide::Sell, Price::from("12.00"), Quantity::from(50), 2);
627        let order4 = BookOrder::new(
628            OrderSide::Sell,
629            Price::from("13.00"),
630            Quantity::from(200),
631            0,
632        );
633
634        ladder.add_bulk(vec![order1, order2, order3, order4]);
635        assert_eq!(ladder.len(), 3);
636        assert_eq!(ladder.sizes(), 300.0);
637        assert_eq!(ladder.exposures(), 3780.0);
638        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
639    }
640
641    #[rstest]
642    fn test_add_to_same_price_level() {
643        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
644        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
645        let order2 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2);
646
647        ladder.add(order1, 0);
648        ladder.add(order2, 0);
649
650        assert_eq!(ladder.len(), 1);
651        assert_eq!(ladder.sizes(), 50.0);
652        assert_eq!(ladder.exposures(), 500.0);
653    }
654
655    #[rstest]
656    fn test_add_descending_buy_orders() {
657        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
658        let order1 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(20), 1);
659        let order2 = BookOrder::new(OrderSide::Buy, Price::from("8.00"), Quantity::from(30), 2);
660
661        ladder.add(order1, 0);
662        ladder.add(order2, 0);
663
664        assert_eq!(ladder.top().unwrap().price.value, Price::from("9.00"));
665    }
666
667    #[rstest]
668    fn test_add_ascending_sell_orders() {
669        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
670        let order1 = BookOrder::new(OrderSide::Sell, Price::from("8.00"), Quantity::from(20), 1);
671        let order2 = BookOrder::new(OrderSide::Sell, Price::from("9.00"), Quantity::from(30), 2);
672
673        ladder.add(order1, 0);
674        ladder.add(order2, 0);
675
676        assert_eq!(ladder.top().unwrap().price.value, Price::from("8.00"));
677    }
678
679    #[rstest]
680    fn test_update_buy_order_price() {
681        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
682        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
683
684        ladder.add(order, 0);
685        let order = BookOrder::new(OrderSide::Buy, Price::from("11.10"), Quantity::from(20), 1);
686
687        ladder.update(order, 0);
688        assert_eq!(ladder.len(), 1);
689        assert_eq!(ladder.sizes(), 20.0);
690        assert_eq!(ladder.exposures(), 222.0);
691        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.1"));
692    }
693
694    #[rstest]
695    fn test_update_sell_order_price() {
696        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
697        let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 1);
698
699        ladder.add(order, 0);
700
701        let order = BookOrder::new(OrderSide::Sell, Price::from("11.10"), Quantity::from(20), 1);
702
703        ladder.update(order, 0);
704        assert_eq!(ladder.len(), 1);
705        assert_eq!(ladder.sizes(), 20.0);
706        assert_eq!(ladder.exposures(), 222.0);
707        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.1"));
708    }
709
710    #[rstest]
711    fn test_update_buy_order_size() {
712        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
713        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
714
715        ladder.add(order, 0);
716
717        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(10), 1);
718
719        ladder.update(order, 0);
720        assert_eq!(ladder.len(), 1);
721        assert_eq!(ladder.sizes(), 10.0);
722        assert_eq!(ladder.exposures(), 110.0);
723        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
724    }
725
726    #[rstest]
727    fn test_update_sell_order_size() {
728        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
729        let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 1);
730
731        ladder.add(order, 0);
732
733        let order = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(10), 1);
734
735        ladder.update(order, 0);
736        assert_eq!(ladder.len(), 1);
737        assert_eq!(ladder.sizes(), 10.0);
738        assert_eq!(ladder.exposures(), 110.0);
739        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
740    }
741
742    #[rstest]
743    fn test_delete_non_existing_order() {
744        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
745        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
746
747        ladder.delete(order, 0, 0.into());
748
749        assert_eq!(ladder.len(), 0);
750    }
751
752    #[rstest]
753    fn test_delete_buy_order() {
754        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
755        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(20), 1);
756
757        ladder.add(order, 0);
758
759        let order = BookOrder::new(OrderSide::Buy, Price::from("11.00"), Quantity::from(10), 1);
760
761        ladder.delete(order, 0, 0.into());
762        assert_eq!(ladder.len(), 0);
763        assert_eq!(ladder.sizes(), 0.0);
764        assert_eq!(ladder.exposures(), 0.0);
765        assert_eq!(ladder.top(), None);
766    }
767
768    #[rstest]
769    fn test_delete_sell_order() {
770        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
771        let order = BookOrder::new(OrderSide::Sell, Price::from("10.00"), Quantity::from(10), 1);
772
773        ladder.add(order, 0);
774
775        let order = BookOrder::new(OrderSide::Sell, Price::from("10.00"), Quantity::from(10), 1);
776
777        ladder.delete(order, 0, 0.into());
778        assert_eq!(ladder.len(), 0);
779        assert_eq!(ladder.sizes(), 0.0);
780        assert_eq!(ladder.exposures(), 0.0);
781        assert_eq!(ladder.top(), None);
782    }
783
784    #[rstest]
785    fn test_ladder_sizes_empty() {
786        let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
787        assert_eq!(
788            ladder.sizes(),
789            0.0,
790            "An empty ladder should have total size 0.0"
791        );
792    }
793
794    #[rstest]
795    fn test_ladder_exposures_empty() {
796        let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
797        assert_eq!(
798            ladder.exposures(),
799            0.0,
800            "An empty ladder should have total exposure 0.0"
801        );
802    }
803
804    #[rstest]
805    fn test_ladder_sizes() {
806        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
807        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
808        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.50"), Quantity::from(30), 2);
809        ladder.add(order1, 0);
810        ladder.add(order2, 0);
811
812        let expected_size = 20.0 + 30.0;
813        assert_eq!(
814            ladder.sizes(),
815            expected_size,
816            "Ladder total size should match the sum of order sizes"
817        );
818    }
819
820    #[rstest]
821    fn test_ladder_exposures() {
822        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
823        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
824        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.50"), Quantity::from(30), 2);
825        ladder.add(order1, 0);
826        ladder.add(order2, 0);
827
828        let expected_exposure = 10.00 * 20.0 + 9.50 * 30.0;
829        assert_eq!(
830            ladder.exposures(),
831            expected_exposure,
832            "Ladder total exposure should match the sum of individual exposures"
833        );
834    }
835
836    #[rstest]
837    fn test_iter_returns_fifo() {
838        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
839        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
840        let order2 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2);
841        ladder.add(order1, 0);
842        ladder.add(order2, 0);
843        let orders: Vec<BookOrder> = ladder.top().unwrap().iter().copied().collect();
844        assert_eq!(
845            orders,
846            vec![order1, order2],
847            "Iterator should return orders in FIFO order"
848        );
849    }
850
851    #[rstest]
852    fn test_update_missing_order_inserts() {
853        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
854        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
855        // Call update on an order that hasn't been added yet (upsert behavior)
856        ladder.update(order, 0);
857        assert_eq!(
858            ladder.len(),
859            1,
860            "Ladder should have one level after upsert update"
861        );
862        let orders = ladder.top().unwrap().get_orders();
863        assert_eq!(
864            orders.len(),
865            1,
866            "Price level should contain the inserted order"
867        );
868        assert_eq!(orders[0], order, "The inserted order should match");
869    }
870
871    #[rstest]
872    fn test_cache_consistency_after_operations() {
873        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
874        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
875        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(30), 2);
876        ladder.add(order1, 0);
877        ladder.add(order2, 0);
878
879        // Ensure that each order in the cache is present in the corresponding price level.
880        for (order_id, price) in &ladder.cache {
881            let level = ladder
882                .levels
883                .get(price)
884                .expect("Every price in the cache should have a corresponding level");
885            assert!(
886                level.orders.contains_key(order_id),
887                "Order id {order_id} should be present in the level for price {price}",
888            );
889        }
890    }
891
892    #[rstest]
893    fn test_simulate_fills_with_empty_book() {
894        let ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
895        let order = BookOrder::new(OrderSide::Buy, Price::max(2), Quantity::from(500), 1);
896
897        let fills = ladder.simulate_fills(&order);
898
899        assert!(fills.is_empty());
900    }
901
902    #[rstest]
903    #[case(OrderSide::Buy, Price::max(2), OrderSideSpecified::Sell)]
904    #[case(OrderSide::Sell, Price::min(2), OrderSideSpecified::Buy)]
905    fn test_simulate_order_fills_with_no_size(
906        #[case] side: OrderSide,
907        #[case] price: Price,
908        #[case] ladder_side: OrderSideSpecified,
909    ) {
910        let ladder = BookLadder::new(ladder_side, BookType::L3_MBO);
911        let order = BookOrder {
912            price, // <-- Simulate a MARKET order
913            size: Quantity::from(500),
914            side,
915            order_id: 2,
916        };
917
918        let fills = ladder.simulate_fills(&order);
919
920        assert!(fills.is_empty());
921    }
922
923    #[rstest]
924    #[case(OrderSide::Buy, OrderSideSpecified::Sell, Price::from("60.0"))]
925    #[case(OrderSide::Sell, OrderSideSpecified::Buy, Price::from("40.0"))]
926    fn test_simulate_order_fills_buy_when_far_from_market(
927        #[case] order_side: OrderSide,
928        #[case] ladder_side: OrderSideSpecified,
929        #[case] ladder_price: Price,
930    ) {
931        let mut ladder = BookLadder::new(ladder_side, BookType::L3_MBO);
932
933        ladder.add(
934            BookOrder {
935                price: ladder_price,
936                size: Quantity::from(100),
937                side: ladder_side.as_order_side(),
938                order_id: 1,
939            },
940            0,
941        );
942
943        let order = BookOrder {
944            price: Price::from("50.00"),
945            size: Quantity::from(500),
946            side: order_side,
947            order_id: 2,
948        };
949
950        let fills = ladder.simulate_fills(&order);
951
952        assert!(fills.is_empty());
953    }
954
955    #[rstest]
956    fn test_simulate_order_fills_sell_when_far_from_market() {
957        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
958
959        ladder.add(
960            BookOrder {
961                price: Price::from("100.00"),
962                size: Quantity::from(100),
963                side: OrderSide::Buy,
964                order_id: 1,
965            },
966            0,
967        );
968
969        let order = BookOrder {
970            price: Price::from("150.00"), // <-- Simulate a MARKET order
971            size: Quantity::from(500),
972            side: OrderSide::Buy,
973            order_id: 2,
974        };
975
976        let fills = ladder.simulate_fills(&order);
977
978        assert!(fills.is_empty());
979    }
980
981    #[rstest]
982    fn test_simulate_order_fills_buy() {
983        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
984
985        ladder.add_bulk(vec![
986            BookOrder {
987                price: Price::from("100.00"),
988                size: Quantity::from(100),
989                side: OrderSide::Sell,
990                order_id: 1,
991            },
992            BookOrder {
993                price: Price::from("101.00"),
994                size: Quantity::from(200),
995                side: OrderSide::Sell,
996                order_id: 2,
997            },
998            BookOrder {
999                price: Price::from("102.00"),
1000                size: Quantity::from(400),
1001                side: OrderSide::Sell,
1002                order_id: 3,
1003            },
1004        ]);
1005
1006        let order = BookOrder {
1007            price: Price::max(2), // <-- Simulate a MARKET order
1008            size: Quantity::from(500),
1009            side: OrderSide::Buy,
1010            order_id: 4,
1011        };
1012
1013        let fills = ladder.simulate_fills(&order);
1014
1015        assert_eq!(fills.len(), 3);
1016
1017        let (price1, size1) = fills[0];
1018        assert_eq!(price1, Price::from("100.00"));
1019        assert_eq!(size1, Quantity::from(100));
1020
1021        let (price2, size2) = fills[1];
1022        assert_eq!(price2, Price::from("101.00"));
1023        assert_eq!(size2, Quantity::from(200));
1024
1025        let (price3, size3) = fills[2];
1026        assert_eq!(price3, Price::from("102.00"));
1027        assert_eq!(size3, Quantity::from(200));
1028    }
1029
1030    #[rstest]
1031    fn test_simulate_order_fills_sell() {
1032        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1033
1034        ladder.add_bulk(vec![
1035            BookOrder {
1036                price: Price::from("102.00"),
1037                size: Quantity::from(100),
1038                side: OrderSide::Buy,
1039                order_id: 1,
1040            },
1041            BookOrder {
1042                price: Price::from("101.00"),
1043                size: Quantity::from(200),
1044                side: OrderSide::Buy,
1045                order_id: 2,
1046            },
1047            BookOrder {
1048                price: Price::from("100.00"),
1049                size: Quantity::from(400),
1050                side: OrderSide::Buy,
1051                order_id: 3,
1052            },
1053        ]);
1054
1055        let order = BookOrder {
1056            price: Price::min(2), // <-- Simulate a MARKET order
1057            size: Quantity::from(500),
1058            side: OrderSide::Sell,
1059            order_id: 4,
1060        };
1061
1062        let fills = ladder.simulate_fills(&order);
1063
1064        assert_eq!(fills.len(), 3);
1065
1066        let (price1, size1) = fills[0];
1067        assert_eq!(price1, Price::from("102.00"));
1068        assert_eq!(size1, Quantity::from(100));
1069
1070        let (price2, size2) = fills[1];
1071        assert_eq!(price2, Price::from("101.00"));
1072        assert_eq!(size2, Quantity::from(200));
1073
1074        let (price3, size3) = fills[2];
1075        assert_eq!(price3, Price::from("100.00"));
1076        assert_eq!(size3, Quantity::from(200));
1077    }
1078
1079    #[rstest]
1080    fn test_simulate_order_fills_sell_with_size_at_limit_of_precision() {
1081        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1082
1083        ladder.add_bulk(vec![
1084            BookOrder {
1085                price: Price::from("102.00"),
1086                size: Quantity::from("100.000000000"),
1087                side: OrderSide::Buy,
1088                order_id: 1,
1089            },
1090            BookOrder {
1091                price: Price::from("101.00"),
1092                size: Quantity::from("200.000000000"),
1093                side: OrderSide::Buy,
1094                order_id: 2,
1095            },
1096            BookOrder {
1097                price: Price::from("100.00"),
1098                size: Quantity::from("400.000000000"),
1099                side: OrderSide::Buy,
1100                order_id: 3,
1101            },
1102        ]);
1103
1104        let order = BookOrder {
1105            price: Price::min(2),                  // <-- Simulate a MARKET order
1106            size: Quantity::from("699.999999999"), // <-- Size slightly less than total size in ladder
1107            side: OrderSide::Sell,
1108            order_id: 4,
1109        };
1110
1111        let fills = ladder.simulate_fills(&order);
1112
1113        assert_eq!(fills.len(), 3);
1114
1115        let (price1, size1) = fills[0];
1116        assert_eq!(price1, Price::from("102.00"));
1117        assert_eq!(size1, Quantity::from("100.000000000"));
1118
1119        let (price2, size2) = fills[1];
1120        assert_eq!(price2, Price::from("101.00"));
1121        assert_eq!(size2, Quantity::from("200.000000000"));
1122
1123        let (price3, size3) = fills[2];
1124        assert_eq!(price3, Price::from("100.00"));
1125        assert_eq!(size3, Quantity::from("399.999999999"));
1126    }
1127
1128    #[rstest]
1129    fn test_boundary_prices() {
1130        let max_price = Price::max(1);
1131        let min_price = Price::min(1);
1132
1133        let mut ladder_buy = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1134        let mut ladder_sell = BookLadder::new(OrderSideSpecified::Sell, BookType::L3_MBO);
1135
1136        let order_buy = BookOrder::new(OrderSide::Buy, min_price, Quantity::from(1), 1);
1137        let order_sell = BookOrder::new(OrderSide::Sell, max_price, Quantity::from(1), 1);
1138
1139        ladder_buy.add(order_buy, 0);
1140        ladder_sell.add(order_sell, 0);
1141
1142        assert_eq!(ladder_buy.top().unwrap().price.value, min_price);
1143        assert_eq!(ladder_sell.top().unwrap().price.value, max_price);
1144    }
1145
1146    #[rstest]
1147    fn test_l1_single_delta_batches_replace_each_other() {
1148        // Test that single-delta batches (each add has F_LAST) replace each other.
1149        // Each batch represents the current top-of-book, not a running best.
1150        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1151        let side_constant = OrderSide::Buy as u64;
1152
1153        // Using F_MBP | F_LAST simulates receiving single-delta batches
1154        let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1155
1156        // Add first L1 order at price 100.00
1157        let order1 = BookOrder {
1158            side: OrderSide::Buy,
1159            price: Price::from("100.00"),
1160            size: Quantity::from(50),
1161            order_id: side_constant,
1162        };
1163        ladder.add(order1, batch_flags);
1164
1165        assert_eq!(ladder.len(), 1, "Should have one level after first add");
1166        assert_eq!(
1167            ladder.top().unwrap().price.value,
1168            Price::from("100.00"),
1169            "Top level should be at 100.00"
1170        );
1171
1172        let order2 = BookOrder {
1173            side: OrderSide::Buy,
1174            price: Price::from("101.00"),
1175            size: Quantity::from(60),
1176            order_id: side_constant,
1177        };
1178        ladder.add(order2, batch_flags);
1179
1180        assert_eq!(ladder.len(), 1, "Should have only one level");
1181        assert_eq!(
1182            ladder.top().unwrap().price.value,
1183            Price::from("101.00"),
1184            "Top level should be at 101.00"
1185        );
1186
1187        // Price CAN degrade between batches
1188        let order3 = BookOrder {
1189            side: OrderSide::Buy,
1190            price: Price::from("100.50"),
1191            size: Quantity::from(70),
1192            order_id: side_constant,
1193        };
1194        ladder.add(order3, batch_flags);
1195
1196        assert_eq!(ladder.len(), 1, "Should have only one level");
1197        assert_eq!(
1198            ladder.top().unwrap().price.value,
1199            Price::from("100.50"),
1200            "Top level should be at 100.50 (new batch replaced old)"
1201        );
1202    }
1203
1204    #[rstest]
1205    fn test_l2_orders_not_affected_by_l1_fix() {
1206        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1207
1208        let order1 = BookOrder {
1209            side: OrderSide::Buy,
1210            price: Price::from("100.00"),
1211            size: Quantity::from(50),
1212            order_id: Price::from("100.00").raw as u64,
1213        };
1214        ladder.add(order1, 0);
1215
1216        let order2 = BookOrder {
1217            side: OrderSide::Buy,
1218            price: Price::from("99.00"),
1219            size: Quantity::from(60),
1220            order_id: Price::from("99.00").raw as u64,
1221        };
1222        ladder.add(order2, 0);
1223
1224        assert_eq!(ladder.len(), 2, "L2 orders should create multiple levels");
1225        assert_eq!(
1226            ladder.top().unwrap().price.value,
1227            Price::from("100.00"),
1228            "Top level should be best bid"
1229        );
1230    }
1231
1232    #[rstest]
1233    fn test_zero_size_l1_order_clears_top() {
1234        // Venues send Add with size=0 to clear top-of-book
1235        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1236        let side_constant = OrderSide::Buy as u64;
1237
1238        let order1 = BookOrder {
1239            side: OrderSide::Buy,
1240            price: Price::from("100.00"),
1241            size: Quantity::from(50),
1242            order_id: side_constant,
1243        };
1244        ladder.add(order1, 0);
1245
1246        assert_eq!(ladder.len(), 1);
1247        assert_eq!(ladder.top().unwrap().price.value, Price::from("100.00"));
1248        assert!(ladder.top().unwrap().first().is_some());
1249
1250        // Try to add zero-size L1 order (venue clearing the book)
1251        let order2 = BookOrder {
1252            side: OrderSide::Buy,
1253            price: Price::from("101.00"),
1254            size: Quantity::zero(9), // Zero size
1255            order_id: side_constant,
1256        };
1257        ladder.add(order2, 0);
1258
1259        // L1 zero-size should clear the top of book
1260        assert_eq!(ladder.len(), 0, "Zero-size L1 add should clear the book");
1261        assert!(ladder.top().is_none(), "Book should be empty after clear");
1262
1263        // Cache should be empty
1264        assert!(
1265            ladder.cache.is_empty(),
1266            "Cache should be empty after L1 clear"
1267        );
1268    }
1269
1270    #[rstest]
1271    fn test_zero_size_order_to_empty_ladder() {
1272        // Edge case: Adding zero-size L1 order to empty ladder should remain empty
1273        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L1_MBP);
1274        let side_constant = OrderSide::Sell as u64;
1275
1276        let order = BookOrder {
1277            side: OrderSide::Sell,
1278            price: Price::from("100.00"),
1279            size: Quantity::zero(9),
1280            order_id: side_constant,
1281        };
1282        ladder.add(order, 0);
1283
1284        assert_eq!(ladder.len(), 0, "Empty ladder should remain empty");
1285        assert!(ladder.top().is_none(), "Top should be None");
1286        assert!(
1287            ladder.cache.is_empty(),
1288            "Cache should remain empty for zero-size add"
1289        );
1290    }
1291
1292    #[rstest]
1293    fn test_l3_order_id_collision_no_ghost_levels() {
1294        // Regression test: L3 venue order IDs 1 and 2 should not trigger L1 ghost level removal
1295        // Real L3 feeds routinely use order IDs 1 or 2, which match the side constants
1296        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1297
1298        // Add order with ID 1 at 100.00 (matches Buy side constant)
1299        let order1 = BookOrder {
1300            side: OrderSide::Buy,
1301            price: Price::from("100.00"),
1302            size: Quantity::from(50),
1303            order_id: 1, // Matches OrderSide::Buy as u64
1304        };
1305        ladder.add(order1, 0);
1306
1307        assert_eq!(ladder.len(), 1);
1308
1309        // Add another order with ID 1 at a different price 99.00
1310        // For L3, this is a DIFFERENT order (different price), should create second level
1311        let order2 = BookOrder {
1312            side: OrderSide::Buy,
1313            price: Price::from("99.00"),
1314            size: Quantity::from(60),
1315            order_id: 1, // Same ID, different price - valid in L3
1316        };
1317        ladder.add(order2, 0);
1318
1319        // Should have both levels - L3 allows duplicate order IDs at different prices
1320        assert_eq!(
1321            ladder.len(),
1322            2,
1323            "L3 should allow order ID 1 at multiple price levels"
1324        );
1325
1326        let prices: Vec<Price> = ladder.levels.keys().map(|bp| bp.value).collect();
1327        assert!(
1328            prices.contains(&Price::from("100.00")),
1329            "Level at 100.00 should still exist"
1330        );
1331        assert!(
1332            prices.contains(&Price::from("99.00")),
1333            "Level at 99.00 should exist"
1334        );
1335    }
1336
1337    #[rstest]
1338    fn test_l1_vs_l3_different_behavior_same_order_id() {
1339        // Demonstrates the difference between L1 and L3 behavior for same order ID
1340
1341        // L1 behavior with replacement (flags=0): successive adds replace
1342        let mut l1_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1343        let side_constant = OrderSide::Buy as u64;
1344
1345        let order1 = BookOrder {
1346            side: OrderSide::Buy,
1347            price: Price::from("100.00"),
1348            size: Quantity::from(50),
1349            order_id: side_constant,
1350        };
1351        l1_ladder.add(order1, 0);
1352
1353        let order2 = BookOrder {
1354            side: OrderSide::Buy,
1355            price: Price::from("101.00"),
1356            size: Quantity::from(60),
1357            order_id: side_constant, // Same ID
1358        };
1359        l1_ladder.add(order2, 0);
1360
1361        assert_eq!(l1_ladder.len(), 1, "L1 should have only 1 level");
1362        assert_eq!(
1363            l1_ladder.top().unwrap().price.value,
1364            Price::from("101.00"),
1365            "L1 should have replaced the old level"
1366        );
1367
1368        // L3 behavior: order ID can be reused at different prices (different orders)
1369        let mut l3_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L3_MBO);
1370
1371        let order3 = BookOrder {
1372            side: OrderSide::Buy,
1373            price: Price::from("100.00"),
1374            size: Quantity::from(50),
1375            order_id: 1, // Happens to match side constant
1376        };
1377        l3_ladder.add(order3, 0);
1378
1379        let order4 = BookOrder {
1380            side: OrderSide::Buy,
1381            price: Price::from("101.00"),
1382            size: Quantity::from(60),
1383            order_id: 1, // Same ID but different order
1384        };
1385        l3_ladder.add(order4, 0);
1386
1387        assert_eq!(l3_ladder.len(), 2, "L3 should have 2 levels");
1388    }
1389
1390    #[rstest]
1391    #[case::bids_worst_to_best(OrderSideSpecified::Buy, OrderSide::Buy, &["99.00", "100.00", "101.00", "102.00"], "102.00")]
1392    #[case::bids_best_to_worst(OrderSideSpecified::Buy, OrderSide::Buy, &["102.00", "101.00", "100.00", "99.00"], "100.00")]
1393    #[case::asks_worst_to_best(OrderSideSpecified::Sell, OrderSide::Sell, &["105.00", "104.00", "103.00", "102.00"], "102.00")]
1394    #[case::asks_best_to_worst(OrderSideSpecified::Sell, OrderSide::Sell, &["102.00", "103.00", "104.00", "105.00"], "104.00")]
1395    fn test_l1_multi_delta_batch_keeps_best_of_final_two(
1396        #[case] side_spec: OrderSideSpecified,
1397        #[case] side: OrderSide,
1398        #[case] prices: &[&str],
1399        #[case] expected_best: &str,
1400    ) {
1401        // Multi-delta batch: F_MBP without F_LAST clears each time.
1402        // Only the delta before F_LAST + F_LAST delta accumulate.
1403        let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1404
1405        let batch_size = prices.len();
1406        for (i, price_str) in prices.iter().enumerate() {
1407            let order = BookOrder {
1408                side,
1409                price: Price::from(*price_str),
1410                size: Quantity::from((i + 1) as u64 * 10),
1411                order_id: (i + 100) as u64,
1412            };
1413            let flags = if i == batch_size - 1 {
1414                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1415            } else {
1416                RecordFlag::F_MBP as u8
1417            };
1418            ladder.add(order, flags);
1419        }
1420
1421        assert_eq!(ladder.len(), 1, "L1 should have only 1 level");
1422        assert_eq!(
1423            ladder.top().unwrap().price.value,
1424            Price::from(expected_best),
1425            "Should keep best of final two deltas"
1426        );
1427    }
1428
1429    #[rstest]
1430    fn test_l1_retain_best_only_cache_consistency() {
1431        // Verify cache is properly cleaned up when retaining only the best level
1432        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1433        let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1434        let prices = ["100.00", "101.00", "102.00", "103.00", "104.00"];
1435
1436        for (i, price_str) in prices.iter().enumerate() {
1437            let order = BookOrder {
1438                side: OrderSide::Buy,
1439                price: Price::from(*price_str),
1440                size: Quantity::from(10),
1441                order_id: (i + 1) as u64,
1442            };
1443            ladder.add(order, batch_flags);
1444        }
1445
1446        assert_eq!(ladder.len(), 1);
1447        assert_eq!(
1448            ladder.cache.len(),
1449            1,
1450            "Cache should have exactly 1 entry for L1"
1451        );
1452
1453        let total_orders: usize = ladder.levels.values().map(|l| l.len()).sum();
1454        assert_eq!(
1455            ladder.cache.len(),
1456            total_orders,
1457            "Cache should be consistent with levels"
1458        );
1459    }
1460
1461    #[rstest]
1462    fn test_l1_sequential_replacement_allows_price_degradation() {
1463        // Test that sequential L1 replacements (without F_MBP) allow price degradation
1464        // This is the expected behavior for top-of-book feeds like F_TOB
1465        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1466        let side_constant = OrderSide::Buy as u64;
1467
1468        // Add first L1 order at price 101.00 (best bid)
1469        let order1 = BookOrder {
1470            side: OrderSide::Buy,
1471            price: Price::from("101.00"),
1472            size: Quantity::from(50),
1473            order_id: side_constant,
1474        };
1475        ladder.add(order1, 0); // flags=0 means replacement mode
1476
1477        assert_eq!(ladder.len(), 1);
1478        assert_eq!(
1479            ladder.top().unwrap().price.value,
1480            Price::from("101.00"),
1481            "Should have bid at 101.00"
1482        );
1483
1484        // Add second L1 order at worse price 100.00 (replacement mode)
1485        // This should REPLACE the previous level, allowing price degradation
1486        let order2 = BookOrder {
1487            side: OrderSide::Buy,
1488            price: Price::from("100.00"),
1489            size: Quantity::from(60),
1490            order_id: side_constant,
1491        };
1492        ladder.add(order2, 0); // flags=0 means replacement mode
1493
1494        assert_eq!(ladder.len(), 1);
1495        assert_eq!(
1496            ladder.top().unwrap().price.value,
1497            Price::from("100.00"),
1498            "Sequential replacement should allow price to degrade from 101 to 100"
1499        );
1500
1501        // Verify the size was updated too
1502        assert_eq!(
1503            ladder.top().unwrap().first().unwrap().size,
1504            Quantity::from(60),
1505            "Size should be from the new order"
1506        );
1507    }
1508
1509    #[rstest]
1510    #[case::bids(OrderSideSpecified::Buy, OrderSide::Buy, &["100.00", "101.00", "102.00"], "102.00", &["97.00", "98.00", "99.00"], "99.00")]
1511    #[case::asks(OrderSideSpecified::Sell, OrderSide::Sell, &["100.00", "101.00", "102.00"], "101.00", &["103.00", "104.00", "105.00"], "104.00")]
1512    fn test_l1_consecutive_batches_clear_between(
1513        #[case] side_spec: OrderSideSpecified,
1514        #[case] side: OrderSide,
1515        #[case] batch1_prices: &[&str],
1516        #[case] expected1: &str,
1517        #[case] batch2_prices: &[&str],
1518        #[case] expected2: &str,
1519    ) {
1520        // Consecutive batches clear old data when a new batch starts
1521        let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1522
1523        // Batch 1
1524        for (i, price_str) in batch1_prices.iter().enumerate() {
1525            let order = BookOrder {
1526                side,
1527                price: Price::from(*price_str),
1528                size: Quantity::from(10),
1529                order_id: (i + 100) as u64,
1530            };
1531            let flags = if i == batch1_prices.len() - 1 {
1532                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1533            } else {
1534                RecordFlag::F_MBP as u8
1535            };
1536            ladder.add(order, flags);
1537        }
1538
1539        assert_eq!(ladder.len(), 1);
1540        assert_eq!(
1541            ladder.top().unwrap().price.value,
1542            Price::from(expected1),
1543            "After batch 1"
1544        );
1545
1546        // Batch 2 (worse prices for bids, higher prices for asks)
1547        for (i, price_str) in batch2_prices.iter().enumerate() {
1548            let order = BookOrder {
1549                side,
1550                price: Price::from(*price_str),
1551                size: Quantity::from(20),
1552                order_id: (i + 200) as u64,
1553            };
1554            let flags = if i == batch2_prices.len() - 1 {
1555                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1556            } else {
1557                RecordFlag::F_MBP as u8
1558            };
1559            ladder.add(order, flags);
1560        }
1561
1562        assert_eq!(ladder.len(), 1);
1563        assert_eq!(
1564            ladder.top().unwrap().price.value,
1565            Price::from(expected2),
1566            "After batch 2: batch 1 data cleared"
1567        );
1568    }
1569
1570    #[rstest]
1571    fn test_l1_zero_size_clears_regardless_of_order_id() {
1572        // Regression test: Zero-size clears must work even when order_id
1573        // differs between F_MBP batch (price-hash ID) and clear (side-constant ID)
1574        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1575
1576        // Add order with F_MBP flags (uses price-hash order_id via pre_process_order)
1577        let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1578        let order = BookOrder {
1579            side: OrderSide::Buy,
1580            price: Price::from("100.00"),
1581            size: Quantity::from(50),
1582            order_id: 12345, // Price-hash ID
1583        };
1584        ladder.add(order, batch_flags);
1585        assert_eq!(ladder.len(), 1);
1586
1587        // Clear with zero-size and different order_id (side-constant)
1588        let clear_order = BookOrder {
1589            side: OrderSide::Buy,
1590            price: Price::from("100.00"),
1591            size: Quantity::zero(9),
1592            order_id: OrderSide::Buy as u64, // Side-constant ID (different!)
1593        };
1594        ladder.add(clear_order, 0);
1595
1596        // Should be cleared despite order_id mismatch
1597        assert_eq!(
1598            ladder.len(),
1599            0,
1600            "Zero-size should clear L1 regardless of order_id"
1601        );
1602        assert!(ladder.cache.is_empty(), "Cache should be empty after clear");
1603    }
1604
1605    #[rstest]
1606    fn test_l1_f_mbp_without_f_last_does_not_accumulate() {
1607        // F_MBP without F_LAST: each message clears, preventing stale prices.
1608        // This allows prices to degrade when the market moves.
1609        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1610        let flags = RecordFlag::F_MBP as u8; // No F_LAST
1611
1612        // Prices descending from 100 to 91 (simulates degrading market)
1613        let prices = [
1614            "100.00", "99.00", "98.00", "97.00", "96.00", "95.00", "94.00", "93.00", "92.00",
1615            "91.00",
1616        ];
1617
1618        for (i, price_str) in prices.iter().enumerate() {
1619            let order = BookOrder {
1620                side: OrderSide::Buy,
1621                price: Price::from(*price_str),
1622                size: Quantity::from(10),
1623                order_id: (i + 100) as u64,
1624            };
1625            ladder.add(order, flags);
1626
1627            assert_eq!(
1628                ladder.len(),
1629                1,
1630                "L1 should always have at most 1 level, iteration {i}"
1631            );
1632        }
1633
1634        // Final price should be 91 (the last added), not 100 (the best ever seen)
1635        assert_eq!(
1636            ladder.top().unwrap().price.value,
1637            Price::from("91.00"),
1638            "Should show last price (91), allowing degradation"
1639        );
1640    }
1641
1642    #[rstest]
1643    fn test_l1_f_mbp_two_delta_batch_retains_best() {
1644        // A 2-delta batch (F_MBP then F_MBP|F_LAST) accumulates both and keeps best
1645        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L1_MBP);
1646
1647        // Delta 1 (F_MBP only): clears, adds 100, sets in_l1_batch=true
1648        let order1 = BookOrder {
1649            side: OrderSide::Sell,
1650            price: Price::from("100.00"),
1651            size: Quantity::from(10),
1652            order_id: 100,
1653        };
1654        ladder.add(order1, RecordFlag::F_MBP as u8);
1655
1656        // Delta 2 (F_MBP|F_LAST): in_l1_batch=true so doesn't clear,
1657        // adds 101, now has 100+101, retain_best → 100
1658        let order2 = BookOrder {
1659            side: OrderSide::Sell,
1660            price: Price::from("101.00"),
1661            size: Quantity::from(20),
1662            order_id: 101,
1663        };
1664        ladder.add(order2, RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8);
1665
1666        assert_eq!(ladder.len(), 1);
1667        assert_eq!(
1668            ladder.top().unwrap().price.value,
1669            Price::from("100.00"),
1670            "2-delta batch keeps best ask (100) from both deltas"
1671        );
1672    }
1673
1674    #[rstest]
1675    fn test_l1_snapshot_batch_accumulates_all_levels_bids() {
1676        // F_SNAPSHOT batch accumulates ALL levels and keeps best bid
1677        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1678        let prices = ["98.00", "99.00", "100.00", "101.00"];
1679        let batch_size = prices.len();
1680
1681        for (i, price_str) in prices.iter().enumerate() {
1682            let order = BookOrder {
1683                side: OrderSide::Buy,
1684                price: Price::from(*price_str),
1685                size: Quantity::from(10),
1686                order_id: (i + 100) as u64,
1687            };
1688            let flags = if i == batch_size - 1 {
1689                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1690            } else {
1691                RecordFlag::F_SNAPSHOT as u8
1692            };
1693            ladder.add(order, flags);
1694        }
1695
1696        assert_eq!(
1697            ladder.len(),
1698            1,
1699            "L1 should have only 1 level after snapshot"
1700        );
1701        assert_eq!(
1702            ladder.top().unwrap().price.value,
1703            Price::from("101.00"),
1704            "F_SNAPSHOT batch should keep best bid (101) from ALL deltas"
1705        );
1706    }
1707
1708    #[rstest]
1709    fn test_l1_snapshot_batch_accumulates_all_levels_asks() {
1710        // F_SNAPSHOT batch accumulates ALL levels and keeps best ask
1711        let mut ladder = BookLadder::new(OrderSideSpecified::Sell, BookType::L1_MBP);
1712        let prices = ["104.00", "103.00", "102.00", "101.00"];
1713        let batch_size = prices.len();
1714
1715        for (i, price_str) in prices.iter().enumerate() {
1716            let order = BookOrder {
1717                side: OrderSide::Sell,
1718                price: Price::from(*price_str),
1719                size: Quantity::from(10),
1720                order_id: (i + 100) as u64,
1721            };
1722            let flags = if i == batch_size - 1 {
1723                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1724            } else {
1725                RecordFlag::F_SNAPSHOT as u8
1726            };
1727            ladder.add(order, flags);
1728        }
1729
1730        assert_eq!(
1731            ladder.len(),
1732            1,
1733            "L1 should have only 1 level after snapshot"
1734        );
1735        assert_eq!(
1736            ladder.top().unwrap().price.value,
1737            Price::from("101.00"),
1738            "F_SNAPSHOT batch should keep best ask (101) from ALL deltas"
1739        );
1740    }
1741
1742    #[rstest]
1743    fn test_l1_snapshot_vs_mbp_different_accumulation_behavior() {
1744        // F_SNAPSHOT accumulates all levels, F_MBP only accumulates final two
1745        let mut mbp_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1746        let prices = ["98.00", "99.00", "100.00", "101.00"];
1747        for (i, price_str) in prices.iter().enumerate() {
1748            let order = BookOrder {
1749                side: OrderSide::Buy,
1750                price: Price::from(*price_str),
1751                size: Quantity::from(10),
1752                order_id: (i + 100) as u64,
1753            };
1754            let flags = if i == prices.len() - 1 {
1755                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1756            } else {
1757                RecordFlag::F_MBP as u8
1758            };
1759            mbp_ladder.add(order, flags);
1760        }
1761        assert_eq!(
1762            mbp_ladder.top().unwrap().price.value,
1763            Price::from("101.00"),
1764            "F_MBP keeps best of final two (100, 101)"
1765        );
1766
1767        let mut snapshot_ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1768        for (i, price_str) in prices.iter().enumerate() {
1769            let order = BookOrder {
1770                side: OrderSide::Buy,
1771                price: Price::from(*price_str),
1772                size: Quantity::from(10),
1773                order_id: (i + 200) as u64,
1774            };
1775            let flags = if i == prices.len() - 1 {
1776                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1777            } else {
1778                RecordFlag::F_SNAPSHOT as u8
1779            };
1780            snapshot_ladder.add(order, flags);
1781        }
1782        assert_eq!(
1783            snapshot_ladder.top().unwrap().price.value,
1784            Price::from("101.00"),
1785            "F_SNAPSHOT keeps best of ALL deltas (98, 99, 100, 101)"
1786        );
1787    }
1788
1789    #[rstest]
1790    fn test_l1_snapshot_after_incomplete_mbp_stream() {
1791        // Snapshot must clear stale state from incomplete F_MBP stream (no F_LAST sent)
1792        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1793
1794        // Incomplete F_MBP stream leaves stale batch state
1795        let stale_order = BookOrder {
1796            side: OrderSide::Buy,
1797            price: Price::from("101.00"),
1798            size: Quantity::from(10),
1799            order_id: 100,
1800        };
1801        ladder.add(stale_order, RecordFlag::F_MBP as u8);
1802        assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
1803
1804        // Snapshot arrives with Clear delta first
1805        ladder.clear();
1806
1807        // Snapshot prices worse than stale 101
1808        for (i, price_str) in ["98.00", "99.00", "100.00"].iter().enumerate() {
1809            let order = BookOrder {
1810                side: OrderSide::Buy,
1811                price: Price::from(*price_str),
1812                size: Quantity::from(10),
1813                order_id: (i + 200) as u64,
1814            };
1815            let flags = if i == 2 {
1816                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1817            } else {
1818                RecordFlag::F_SNAPSHOT as u8
1819            };
1820            ladder.add(order, flags);
1821        }
1822
1823        assert_eq!(
1824            ladder.top().unwrap().price.value,
1825            Price::from("100.00"),
1826            "Snapshot replaces stale MBP state: best is 100, not stale 101"
1827        );
1828    }
1829
1830    #[rstest]
1831    fn test_l1_snapshot_clears_previous_batch() {
1832        // New F_SNAPSHOT batch clears previous batch
1833        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1834
1835        for (i, price_str) in ["100.00", "101.00", "102.00"].iter().enumerate() {
1836            let order = BookOrder {
1837                side: OrderSide::Buy,
1838                price: Price::from(*price_str),
1839                size: Quantity::from(10),
1840                order_id: (i + 100) as u64,
1841            };
1842            let flags = if i == 2 {
1843                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1844            } else {
1845                RecordFlag::F_SNAPSHOT as u8
1846            };
1847            ladder.add(order, flags);
1848        }
1849        assert_eq!(ladder.top().unwrap().price.value, Price::from("102.00"));
1850
1851        // Second batch with worse prices
1852        for (i, price_str) in ["95.00", "96.00", "97.00"].iter().enumerate() {
1853            let order = BookOrder {
1854                side: OrderSide::Buy,
1855                price: Price::from(*price_str),
1856                size: Quantity::from(20),
1857                order_id: (i + 200) as u64,
1858            };
1859            let flags = if i == 2 {
1860                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1861            } else {
1862                RecordFlag::F_SNAPSHOT as u8
1863            };
1864            ladder.add(order, flags);
1865        }
1866        assert_eq!(
1867            ladder.top().unwrap().price.value,
1868            Price::from("97.00"),
1869            "Second batch clears first: best is 97, not 102"
1870        );
1871    }
1872
1873    #[rstest]
1874    fn test_l1_single_delta_snapshot_after_mbp_batch() {
1875        // Single-delta snapshot (F_SNAPSHOT|F_LAST) must clear stale MBP batch state
1876        let mut ladder = BookLadder::new(OrderSideSpecified::Buy, BookType::L1_MBP);
1877
1878        let mbp_order1 = BookOrder {
1879            side: OrderSide::Buy,
1880            price: Price::from("100.00"),
1881            size: Quantity::from(10),
1882            order_id: 1,
1883        };
1884        let mbp_order2 = BookOrder {
1885            side: OrderSide::Buy,
1886            price: Price::from("101.00"),
1887            size: Quantity::from(10),
1888            order_id: 2,
1889        };
1890        ladder.add(mbp_order1, RecordFlag::F_MBP as u8);
1891        ladder.add(
1892            mbp_order2,
1893            RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8,
1894        );
1895
1896        assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
1897
1898        // Single-delta snapshot at worse price (no preceding Clear)
1899        let snapshot_order = BookOrder {
1900            side: OrderSide::Buy,
1901            price: Price::from("95.00"),
1902            size: Quantity::from(20),
1903            order_id: 100,
1904        };
1905        ladder.add(
1906            snapshot_order,
1907            RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
1908        );
1909
1910        assert_eq!(
1911            ladder.top().unwrap().price.value,
1912            Price::from("95.00"),
1913            "Single-delta snapshot clears MBP state: best is 95, not stale 101"
1914        );
1915        assert_eq!(ladder.len(), 1);
1916    }
1917}