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