Skip to main content

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