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 for an unknown state code or a batch state 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    use rstest::rstest;
594
595    use crate::{
596        data::order::BookOrder,
597        enums::{BookType, OrderSide, RecordFlag},
598        orderbook::{
599            ladder::{BookLadder, BookPrice},
600            level::BookLevel,
601        },
602        types::{Price, Quantity},
603    };
604
605    #[rstest]
606    fn test_is_empty_after_add() {
607        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
608        assert!(ladder.is_empty(), "Ladder should start empty");
609        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(100), 1);
610        ladder.add(order, 0);
611        assert!(
612            !ladder.is_empty(),
613            "Ladder should not be empty after adding an order"
614        );
615    }
616
617    #[rstest]
618    fn test_add_bulk_empty() {
619        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
620        ladder.add_bulk(&[]);
621        assert!(
622            ladder.is_empty(),
623            "Adding an empty vector should leave the ladder empty"
624        );
625    }
626
627    #[rstest]
628    fn test_add_bulk_orders() {
629        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
630        let orders = [
631            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1),
632            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2),
633            BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(50), 3),
634        ];
635        ladder.add_bulk(&orders);
636        // All orders share the same price, so there should be one price level.
637        assert_eq!(ladder.len(), 1, "Ladder should have one price level");
638        let orders_in_level = ladder.top().unwrap().get_orders();
639        assert_eq!(
640            orders_in_level.len(),
641            3,
642            "Price level should contain all bulk orders"
643        );
644    }
645
646    #[rstest]
647    #[case::bid(OrderSide::Buy, "4.0")]
648    #[case::ask(OrderSide::Sell, "1.0")]
649    fn test_book_price_sorting(#[case] side: OrderSide, #[case] expected_top: &str) {
650        let mut prices = [
651            BookPrice::new(Price::from("2.0"), side),
652            BookPrice::new(Price::from("4.0"), side),
653            BookPrice::new(Price::from("1.0"), side),
654            BookPrice::new(Price::from("3.0"), side),
655        ];
656
657        prices.sort();
658
659        assert_eq!(prices[0].value, Price::from(expected_top));
660    }
661
662    #[rstest]
663    fn test_add_single_order() {
664        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
665        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 0);
666
667        ladder.add(order, 0);
668        assert_eq!(ladder.len(), 1);
669        assert_eq!(ladder.sizes(), 20.0);
670        assert_eq!(ladder.exposures(), 200.0);
671        assert_eq!(ladder.top().unwrap().price.value, Price::from("10.0"));
672    }
673
674    #[rstest]
675    fn test_add_multiple_buy_orders() {
676        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
677        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 0);
678        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(30), 1);
679        let order3 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(50), 2);
680        let order4 = BookOrder::new(OrderSide::Buy, Price::from("8.00"), Quantity::from(200), 3);
681
682        ladder.add_bulk(&[order1, order2, order3, order4]);
683        assert_eq!(ladder.len(), 3);
684        assert_eq!(ladder.sizes(), 300.0);
685        assert_eq!(ladder.exposures(), 2520.0);
686        assert_eq!(ladder.top().unwrap().price.value, Price::from("10.0"));
687    }
688
689    #[rstest]
690    fn test_add_multiple_sell_orders() {
691        let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L3_MBO);
692        let order1 = BookOrder::new(OrderSide::Sell, Price::from("11.00"), Quantity::from(20), 0);
693        let order2 = BookOrder::new(OrderSide::Sell, Price::from("12.00"), Quantity::from(30), 1);
694        let order3 = BookOrder::new(OrderSide::Sell, Price::from("12.00"), Quantity::from(50), 2);
695        let order4 = BookOrder::new(
696            OrderSide::Sell,
697            Price::from("13.00"),
698            Quantity::from(200),
699            3,
700        );
701
702        ladder.add_bulk(&[order1, order2, order3, order4]);
703        assert_eq!(ladder.len(), 3);
704        assert_eq!(ladder.sizes(), 300.0);
705        assert_eq!(ladder.exposures(), 3780.0);
706        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
707    }
708
709    #[rstest]
710    fn test_add_to_same_price_level() {
711        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
712        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
713        let order2 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2);
714
715        ladder.add(order1, 0);
716        ladder.add(order2, 0);
717
718        assert_eq!(ladder.len(), 1);
719        assert_eq!(ladder.sizes(), 50.0);
720        assert_eq!(ladder.exposures(), 500.0);
721    }
722
723    #[rstest]
724    #[case::bid(OrderSide::Buy, "9.00", "8.00", "9.00")]
725    #[case::ask(OrderSide::Sell, "8.00", "9.00", "8.00")]
726    fn test_add_orders_preserves_top(
727        #[case] side: OrderSide,
728        #[case] first_price: &str,
729        #[case] second_price: &str,
730        #[case] expected_top: &str,
731    ) {
732        let mut ladder = BookLadder::new(side, BookType::L3_MBO);
733        let order1 = BookOrder::new(side, Price::from(first_price), Quantity::from(20), 1);
734        let order2 = BookOrder::new(side, Price::from(second_price), Quantity::from(30), 2);
735
736        ladder.add(order1, 0);
737        ladder.add(order2, 0);
738
739        assert_eq!(ladder.top().unwrap().price.value, Price::from(expected_top));
740    }
741
742    #[rstest]
743    #[case::bid(OrderSide::Buy)]
744    #[case::ask(OrderSide::Sell)]
745    fn test_update_order_price(#[case] side: OrderSide) {
746        let mut ladder = BookLadder::new(side, BookType::L3_MBO);
747        let order = BookOrder::new(side, Price::from("11.00"), Quantity::from(20), 1);
748
749        ladder.add(order, 0);
750        let order = BookOrder::new(side, Price::from("11.10"), Quantity::from(20), 1);
751
752        ladder.update(order, 0);
753        assert_eq!(ladder.len(), 1);
754        assert_eq!(ladder.sizes(), 20.0);
755        assert_eq!(ladder.exposures(), 222.0);
756        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.1"));
757    }
758
759    #[rstest]
760    #[case::bid(OrderSide::Buy)]
761    #[case::ask(OrderSide::Sell)]
762    fn test_update_order_size(#[case] side: OrderSide) {
763        let mut ladder = BookLadder::new(side, BookType::L3_MBO);
764        let order = BookOrder::new(side, Price::from("11.00"), Quantity::from(20), 1);
765
766        ladder.add(order, 0);
767        let order = BookOrder::new(side, Price::from("11.00"), Quantity::from(10), 1);
768
769        ladder.update(order, 0);
770        assert_eq!(ladder.len(), 1);
771        assert_eq!(ladder.sizes(), 10.0);
772        assert_eq!(ladder.exposures(), 110.0);
773        assert_eq!(ladder.top().unwrap().price.value, Price::from("11.0"));
774    }
775
776    #[rstest]
777    fn test_delete_non_existing_order() {
778        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
779        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
780
781        ladder.delete(order, 0, 0.into());
782
783        assert_eq!(ladder.len(), 0);
784    }
785
786    #[rstest]
787    #[case::bid(OrderSide::Buy, "11.00", "20", "10")]
788    #[case::ask(OrderSide::Sell, "10.00", "10", "10")]
789    fn test_delete_order(
790        #[case] side: OrderSide,
791        #[case] price: &str,
792        #[case] stored_size: &str,
793        #[case] delete_size: &str,
794    ) {
795        let mut ladder = BookLadder::new(side, BookType::L3_MBO);
796        let order = BookOrder::new(side, Price::from(price), Quantity::from(stored_size), 1);
797
798        ladder.add(order, 0);
799        let delete = BookOrder::new(side, Price::from(price), Quantity::from(delete_size), 1);
800
801        ladder.delete(delete, 0, 0.into());
802        assert_eq!(ladder.len(), 0);
803        assert_eq!(ladder.sizes(), 0.0);
804        assert_eq!(ladder.exposures(), 0.0);
805        assert_eq!(ladder.top(), None);
806    }
807
808    #[rstest]
809    fn test_ladder_totals_empty() {
810        let ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
811        assert_eq!(ladder.sizes(), 0.0);
812        assert_eq!(ladder.exposures(), 0.0);
813    }
814
815    #[rstest]
816    fn test_ladder_totals() {
817        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
818        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
819        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.50"), Quantity::from(30), 2);
820        ladder.add(order1, 0);
821        ladder.add(order2, 0);
822
823        let expected_size = 20.0 + 30.0;
824        let expected_exposure = 10.00 * 20.0 + 9.50 * 30.0;
825
826        assert_eq!(ladder.sizes(), expected_size);
827        assert_eq!(ladder.exposures(), expected_exposure);
828    }
829
830    #[rstest]
831    fn test_iter_returns_fifo() {
832        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
833        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
834        let order2 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(30), 2);
835        ladder.add(order1, 0);
836        ladder.add(order2, 0);
837        let orders: Vec<BookOrder> = ladder.top().unwrap().iter().copied().collect();
838        assert_eq!(
839            orders,
840            vec![order1, order2],
841            "Iterator should return orders in FIFO order"
842        );
843    }
844
845    #[rstest]
846    fn test_update_missing_order_inserts() {
847        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
848        let order = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
849        // Call update on an order that hasn't been added yet (upsert behavior)
850        ladder.update(order, 0);
851        assert_eq!(
852            ladder.len(),
853            1,
854            "Ladder should have one level after upsert update"
855        );
856        let orders = ladder.top().unwrap().get_orders();
857        assert_eq!(
858            orders.len(),
859            1,
860            "Price level should contain the inserted order"
861        );
862        assert_eq!(orders[0], order, "The inserted order should match");
863    }
864
865    #[rstest]
866    fn test_cache_consistency_after_operations() {
867        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
868        let order1 = BookOrder::new(OrderSide::Buy, Price::from("10.00"), Quantity::from(20), 1);
869        let order2 = BookOrder::new(OrderSide::Buy, Price::from("9.00"), Quantity::from(30), 2);
870        ladder.add(order1, 0);
871        ladder.add(order2, 0);
872
873        // Ensure that each order in the cache is present in the corresponding price level.
874        for (order_id, price) in &ladder.cache {
875            let level = ladder
876                .levels
877                .get(price)
878                .expect("Every price in the cache should have a corresponding level");
879            assert!(
880                level.orders.contains_key(order_id),
881                "Order id {order_id} should be present in the level for price {price}",
882            );
883        }
884    }
885
886    #[rstest]
887    fn test_simulate_fills_with_empty_book() {
888        let ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
889        let order = BookOrder::new(OrderSide::Buy, Price::max(2), Quantity::from(500), 1);
890
891        let fills = ladder.simulate_fills(&order);
892
893        assert!(fills.is_empty());
894    }
895
896    #[rstest]
897    #[case(OrderSide::Buy, Price::max(2), OrderSide::Sell)]
898    #[case(OrderSide::Sell, Price::min(2), OrderSide::Buy)]
899    fn test_simulate_order_fills_with_no_size(
900        #[case] side: OrderSide,
901        #[case] price: Price,
902        #[case] ladder_side: OrderSide,
903    ) {
904        let ladder = BookLadder::new(ladder_side, BookType::L3_MBO);
905        let order = BookOrder {
906            price, // <-- Simulate a MARKET order
907            size: Quantity::from(500),
908            side: side.into(),
909            order_id: 2,
910        };
911
912        let fills = ladder.simulate_fills(&order);
913
914        assert!(fills.is_empty());
915    }
916
917    #[rstest]
918    #[case(OrderSide::Buy, OrderSide::Sell, Price::from("60.0"))]
919    #[case(OrderSide::Sell, OrderSide::Buy, Price::from("40.0"))]
920    fn test_simulate_order_fills_buy_when_far_from_market(
921        #[case] order_side: OrderSide,
922        #[case] ladder_side: OrderSide,
923        #[case] ladder_price: Price,
924    ) {
925        let mut ladder = BookLadder::new(ladder_side, BookType::L3_MBO);
926
927        ladder.add(
928            BookOrder {
929                price: ladder_price,
930                size: Quantity::from(100),
931                side: ladder_side.into(),
932                order_id: 1,
933            },
934            0,
935        );
936
937        let order = BookOrder {
938            price: Price::from("50.00"),
939            size: Quantity::from(500),
940            side: order_side.into(),
941            order_id: 2,
942        };
943
944        let fills = ladder.simulate_fills(&order);
945
946        assert!(fills.is_empty());
947    }
948
949    #[rstest]
950    fn test_simulate_order_fills_sell_when_far_from_market() {
951        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
952
953        ladder.add(
954            BookOrder {
955                price: Price::from("100.00"),
956                size: Quantity::from(100),
957                side: OrderSide::Buy.into(),
958                order_id: 1,
959            },
960            0,
961        );
962
963        let order = BookOrder {
964            price: Price::from("150.00"), // <-- Simulate a MARKET order
965            size: Quantity::from(500),
966            side: OrderSide::Buy.into(),
967            order_id: 2,
968        };
969
970        let fills = ladder.simulate_fills(&order);
971
972        assert!(fills.is_empty());
973    }
974
975    #[rstest]
976    fn test_simulate_order_fills_buy() {
977        let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L3_MBO);
978
979        ladder.add_bulk(&[
980            BookOrder {
981                price: Price::from("100.00"),
982                size: Quantity::from(100),
983                side: OrderSide::Sell.into(),
984                order_id: 1,
985            },
986            BookOrder {
987                price: Price::from("101.00"),
988                size: Quantity::from(200),
989                side: OrderSide::Sell.into(),
990                order_id: 2,
991            },
992            BookOrder {
993                price: Price::from("102.00"),
994                size: Quantity::from(400),
995                side: OrderSide::Sell.into(),
996                order_id: 3,
997            },
998        ]);
999
1000        let order = BookOrder {
1001            price: Price::max(2), // <-- Simulate a MARKET order
1002            size: Quantity::from(500),
1003            side: OrderSide::Buy.into(),
1004            order_id: 4,
1005        };
1006
1007        let fills = ladder.simulate_fills(&order);
1008
1009        assert_eq!(fills.len(), 3);
1010
1011        let (price1, size1) = fills[0];
1012        assert_eq!(price1, Price::from("100.00"));
1013        assert_eq!(size1, Quantity::from(100));
1014
1015        let (price2, size2) = fills[1];
1016        assert_eq!(price2, Price::from("101.00"));
1017        assert_eq!(size2, Quantity::from(200));
1018
1019        let (price3, size3) = fills[2];
1020        assert_eq!(price3, Price::from("102.00"));
1021        assert_eq!(size3, Quantity::from(200));
1022    }
1023
1024    #[rstest]
1025    fn test_simulate_order_fills_sell() {
1026        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1027
1028        ladder.add_bulk(&[
1029            BookOrder {
1030                price: Price::from("102.00"),
1031                size: Quantity::from(100),
1032                side: OrderSide::Buy.into(),
1033                order_id: 1,
1034            },
1035            BookOrder {
1036                price: Price::from("101.00"),
1037                size: Quantity::from(200),
1038                side: OrderSide::Buy.into(),
1039                order_id: 2,
1040            },
1041            BookOrder {
1042                price: Price::from("100.00"),
1043                size: Quantity::from(400),
1044                side: OrderSide::Buy.into(),
1045                order_id: 3,
1046            },
1047        ]);
1048
1049        let order = BookOrder {
1050            price: Price::min(2), // <-- Simulate a MARKET order
1051            size: Quantity::from(500),
1052            side: OrderSide::Sell.into(),
1053            order_id: 4,
1054        };
1055
1056        let fills = ladder.simulate_fills(&order);
1057
1058        assert_eq!(fills.len(), 3);
1059
1060        let (price1, size1) = fills[0];
1061        assert_eq!(price1, Price::from("102.00"));
1062        assert_eq!(size1, Quantity::from(100));
1063
1064        let (price2, size2) = fills[1];
1065        assert_eq!(price2, Price::from("101.00"));
1066        assert_eq!(size2, Quantity::from(200));
1067
1068        let (price3, size3) = fills[2];
1069        assert_eq!(price3, Price::from("100.00"));
1070        assert_eq!(size3, Quantity::from(200));
1071    }
1072
1073    #[rstest]
1074    fn test_simulate_order_fills_sell_with_size_at_limit_of_precision() {
1075        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1076
1077        ladder.add_bulk(&[
1078            BookOrder {
1079                price: Price::from("102.00"),
1080                size: Quantity::from("100.000000000"),
1081                side: OrderSide::Buy.into(),
1082                order_id: 1,
1083            },
1084            BookOrder {
1085                price: Price::from("101.00"),
1086                size: Quantity::from("200.000000000"),
1087                side: OrderSide::Buy.into(),
1088                order_id: 2,
1089            },
1090            BookOrder {
1091                price: Price::from("100.00"),
1092                size: Quantity::from("400.000000000"),
1093                side: OrderSide::Buy.into(),
1094                order_id: 3,
1095            },
1096        ]);
1097
1098        let order = BookOrder {
1099            price: Price::min(2),                  // <-- Simulate a MARKET order
1100            size: Quantity::from("699.999999999"), // <-- Size slightly less than total size in ladder
1101            side: OrderSide::Sell.into(),
1102            order_id: 4,
1103        };
1104
1105        let fills = ladder.simulate_fills(&order);
1106
1107        assert_eq!(fills.len(), 3);
1108
1109        let (price1, size1) = fills[0];
1110        assert_eq!(price1, Price::from("102.00"));
1111        assert_eq!(size1, Quantity::from("100.000000000"));
1112
1113        let (price2, size2) = fills[1];
1114        assert_eq!(price2, Price::from("101.00"));
1115        assert_eq!(size2, Quantity::from("200.000000000"));
1116
1117        let (price3, size3) = fills[2];
1118        assert_eq!(price3, Price::from("100.00"));
1119        assert_eq!(size3, Quantity::from("399.999999999"));
1120    }
1121
1122    #[rstest]
1123    fn test_boundary_prices() {
1124        let max_price = Price::max(1);
1125        let min_price = Price::min(1);
1126
1127        let mut ladder_buy = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1128        let mut ladder_sell = BookLadder::new(OrderSide::Sell, BookType::L3_MBO);
1129
1130        let order_buy = BookOrder::new(OrderSide::Buy, min_price, Quantity::from(1), 1);
1131        let order_sell = BookOrder::new(OrderSide::Sell, max_price, Quantity::from(1), 1);
1132
1133        ladder_buy.add(order_buy, 0);
1134        ladder_sell.add(order_sell, 0);
1135
1136        assert_eq!(ladder_buy.top().unwrap().price.value, min_price);
1137        assert_eq!(ladder_sell.top().unwrap().price.value, max_price);
1138    }
1139
1140    #[rstest]
1141    fn test_l1_single_delta_batches_replace_each_other() {
1142        // Test that single-delta batches (each add has F_LAST) replace each other.
1143        // Each batch represents the current top-of-book, not a running best.
1144        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1145        let side_constant = OrderSide::Buy as u64;
1146
1147        // Using F_MBP | F_LAST simulates receiving single-delta batches
1148        let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1149
1150        // Add first L1 order at price 100.00
1151        let order1 = BookOrder {
1152            side: OrderSide::Buy.into(),
1153            price: Price::from("100.00"),
1154            size: Quantity::from(50),
1155            order_id: side_constant,
1156        };
1157        ladder.add(order1, batch_flags);
1158
1159        assert_eq!(ladder.len(), 1, "Should have one level after first add");
1160        assert_eq!(
1161            ladder.top().unwrap().price.value,
1162            Price::from("100.00"),
1163            "Top level should be at 100.00"
1164        );
1165
1166        let order2 = BookOrder {
1167            side: OrderSide::Buy.into(),
1168            price: Price::from("101.00"),
1169            size: Quantity::from(60),
1170            order_id: side_constant,
1171        };
1172        ladder.add(order2, batch_flags);
1173
1174        assert_eq!(ladder.len(), 1, "Should have only one level");
1175        assert_eq!(
1176            ladder.top().unwrap().price.value,
1177            Price::from("101.00"),
1178            "Top level should be at 101.00"
1179        );
1180
1181        // Price CAN degrade between batches
1182        let order3 = BookOrder {
1183            side: OrderSide::Buy.into(),
1184            price: Price::from("100.50"),
1185            size: Quantity::from(70),
1186            order_id: side_constant,
1187        };
1188        ladder.add(order3, batch_flags);
1189
1190        assert_eq!(ladder.len(), 1, "Should have only one level");
1191        assert_eq!(
1192            ladder.top().unwrap().price.value,
1193            Price::from("100.50"),
1194            "Top level should be at 100.50 (new batch replaced old)"
1195        );
1196    }
1197
1198    #[rstest]
1199    fn test_l2_orders_create_multiple_levels() {
1200        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L2_MBP);
1201
1202        let order1 = BookOrder {
1203            side: OrderSide::Buy.into(),
1204            price: Price::from("100.00"),
1205            size: Quantity::from(50),
1206            order_id: Price::from("100.00").raw() as u64,
1207        };
1208        ladder.add(order1, 0);
1209
1210        let order2 = BookOrder {
1211            side: OrderSide::Buy.into(),
1212            price: Price::from("99.00"),
1213            size: Quantity::from(60),
1214            order_id: Price::from("99.00").raw() as u64,
1215        };
1216        ladder.add(order2, 0);
1217
1218        assert_eq!(ladder.len(), 2, "L2 orders should create multiple levels");
1219        assert_eq!(
1220            ladder.top().unwrap().price.value,
1221            Price::from("100.00"),
1222            "Top level should be best bid"
1223        );
1224    }
1225
1226    #[rstest]
1227    fn test_zero_size_l1_order_clears_top() {
1228        // Venues send Add with size=0 to clear top-of-book
1229        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1230        let side_constant = OrderSide::Buy as u64;
1231
1232        let order1 = BookOrder {
1233            side: OrderSide::Buy.into(),
1234            price: Price::from("100.00"),
1235            size: Quantity::from(50),
1236            order_id: side_constant,
1237        };
1238        ladder.add(order1, 0);
1239
1240        assert_eq!(ladder.len(), 1);
1241        assert_eq!(ladder.top().unwrap().price.value, Price::from("100.00"));
1242        assert!(ladder.top().unwrap().first().is_some());
1243
1244        // Try to add zero-size L1 order (venue clearing the book)
1245        let order2 = BookOrder {
1246            side: OrderSide::Buy.into(),
1247            price: Price::from("101.00"),
1248            size: Quantity::zero(9), // Zero size
1249            order_id: side_constant,
1250        };
1251        ladder.add(order2, 0);
1252
1253        // L1 zero-size should clear the top of book
1254        assert_eq!(ladder.len(), 0, "Zero-size L1 add should clear the book");
1255        assert!(ladder.top().is_none(), "Book should be empty after clear");
1256
1257        // Cache should be empty
1258        assert!(
1259            ladder.cache.is_empty(),
1260            "Cache should be empty after L1 clear"
1261        );
1262    }
1263
1264    #[rstest]
1265    fn test_zero_size_order_to_empty_ladder() {
1266        // Edge case: Adding zero-size L1 order to empty ladder should remain empty
1267        let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L1_MBP);
1268        let side_constant = OrderSide::Sell as u64;
1269
1270        let order = BookOrder {
1271            side: OrderSide::Sell.into(),
1272            price: Price::from("100.00"),
1273            size: Quantity::zero(9),
1274            order_id: side_constant,
1275        };
1276        ladder.add(order, 0);
1277
1278        assert_eq!(ladder.len(), 0, "Empty ladder should remain empty");
1279        assert!(ladder.top().is_none(), "Top should be None");
1280        assert!(
1281            ladder.cache.is_empty(),
1282            "Cache should remain empty for zero-size add"
1283        );
1284    }
1285
1286    #[rstest]
1287    fn test_l3_order_id_collision_moves_order() {
1288        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1289
1290        // Add order with ID 1 at 100.00 (matches Buy side constant)
1291        let order1 = BookOrder {
1292            side: OrderSide::Buy.into(),
1293            price: Price::from("100.00"),
1294            size: Quantity::from(50),
1295            order_id: 1, // Matches OrderSide::Buy as u64
1296        };
1297        ladder.add(order1, 0);
1298
1299        assert_eq!(ladder.len(), 1);
1300
1301        let order2 = BookOrder {
1302            side: OrderSide::Buy.into(),
1303            price: Price::from("99.00"),
1304            size: Quantity::from(60),
1305            order_id: 1,
1306        };
1307        ladder.add(order2, 0);
1308
1309        assert_eq!(ladder.len(), 1, "Order ID 1 must live at exactly one level");
1310        assert_eq!(
1311            ladder.top().unwrap().price.value,
1312            Price::from("99.00"),
1313            "Order should have moved to 99.00"
1314        );
1315        assert_eq!(
1316            ladder.top().unwrap().first().unwrap().size,
1317            Quantity::from(60),
1318            "Size should come from the re-added order"
1319        );
1320        assert_eq!(
1321            ladder.cache.len(),
1322            1,
1323            "Cache should track exactly one entry for the moved order"
1324        );
1325    }
1326
1327    #[rstest]
1328    fn test_l3_duplicate_order_id_update_and_delete_leave_no_ghost() {
1329        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1330
1331        let order1 = BookOrder {
1332            side: OrderSide::Buy.into(),
1333            price: Price::from("100.00"),
1334            size: Quantity::from(50),
1335            order_id: 1,
1336        };
1337        ladder.add(order1, 0);
1338
1339        let order2 = BookOrder {
1340            side: OrderSide::Buy.into(),
1341            price: Price::from("99.00"),
1342            size: Quantity::from(60),
1343            order_id: 1,
1344        };
1345        ladder.add(order2, 0);
1346
1347        let zero_update = BookOrder {
1348            side: OrderSide::Buy.into(),
1349            price: Price::from("99.00"),
1350            size: Quantity::zero(9),
1351            order_id: 1,
1352        };
1353        ladder.update(zero_update, 0);
1354
1355        assert!(
1356            ladder.is_empty(),
1357            "Zero-size update must not ghost the order"
1358        );
1359        assert!(ladder.cache.is_empty());
1360
1361        ladder.add(order1, 0);
1362        ladder.add(order2, 0);
1363        ladder.delete(order2, 0, 0.into());
1364
1365        assert!(ladder.is_empty(), "Delete must not ghost the order");
1366        assert!(ladder.cache.is_empty());
1367    }
1368
1369    #[rstest]
1370    #[case::bids(OrderSide::Buy, Some(OrderSide::Buy), "100.00", "99.00")]
1371    #[case::asks(OrderSide::Sell, Some(OrderSide::Sell), "100.00", "101.00")]
1372    fn test_move_leaves_other_orders_at_old_level(
1373        #[case] side_spec: OrderSide,
1374        #[case] side: Option<OrderSide>,
1375        #[case] old_price: &str,
1376        #[case] new_price: &str,
1377    ) {
1378        let mut ladder = BookLadder::new(side_spec, BookType::L3_MBO);
1379
1380        let moved = BookOrder {
1381            side,
1382            price: Price::from(old_price),
1383            size: Quantity::from(50),
1384            order_id: 1,
1385        };
1386        let staying = BookOrder {
1387            side,
1388            price: Price::from(old_price),
1389            size: Quantity::from(30),
1390            order_id: 2,
1391        };
1392        ladder.add(moved, 0);
1393        ladder.add(staying, 0);
1394
1395        let moved_new = BookOrder {
1396            side,
1397            price: Price::from(new_price),
1398            size: Quantity::from(60),
1399            order_id: 1,
1400        };
1401        ladder.add(moved_new, 0);
1402
1403        assert_eq!(
1404            ladder.len(),
1405            2,
1406            "Old level must survive with the remaining order"
1407        );
1408        let old_level = ladder
1409            .levels
1410            .get(&BookPrice::new(Price::from(old_price), side_spec))
1411            .expect("Old level should remain");
1412        assert_eq!(old_level.get_orders(), vec![staying]);
1413        let new_level = ladder
1414            .levels
1415            .get(&BookPrice::new(Price::from(new_price), side_spec))
1416            .expect("New level should exist");
1417        assert_eq!(new_level.get_orders(), vec![moved_new]);
1418    }
1419
1420    #[rstest]
1421    fn test_l1_vs_l3_duplicate_order_id_replacement() {
1422        // L1 behavior with replacement (flags=0): successive adds replace
1423        let mut l1_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1424        let side_constant = OrderSide::Buy as u64;
1425
1426        let order1 = BookOrder {
1427            side: OrderSide::Buy.into(),
1428            price: Price::from("100.00"),
1429            size: Quantity::from(50),
1430            order_id: side_constant,
1431        };
1432        l1_ladder.add(order1, 0);
1433
1434        let order2 = BookOrder {
1435            side: OrderSide::Buy.into(),
1436            price: Price::from("101.00"),
1437            size: Quantity::from(60),
1438            order_id: side_constant, // Same ID
1439        };
1440        l1_ladder.add(order2, 0);
1441
1442        assert_eq!(l1_ladder.len(), 1, "L1 should have only 1 level");
1443        assert_eq!(
1444            l1_ladder.top().unwrap().price.value,
1445            Price::from("101.00"),
1446            "L1 should have replaced the old level"
1447        );
1448
1449        let mut l3_ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1450
1451        let order3 = BookOrder {
1452            side: OrderSide::Buy.into(),
1453            price: Price::from("100.00"),
1454            size: Quantity::from(50),
1455            order_id: 1, // Happens to match side constant
1456        };
1457        l3_ladder.add(order3, 0);
1458
1459        let order4 = BookOrder {
1460            side: OrderSide::Buy.into(),
1461            price: Price::from("101.00"),
1462            size: Quantity::from(60),
1463            order_id: 1,
1464        };
1465        l3_ladder.add(order4, 0);
1466
1467        assert_eq!(
1468            l3_ladder.len(),
1469            1,
1470            "L3 should move the order to its new price level"
1471        );
1472        assert_eq!(
1473            l3_ladder.top().unwrap().price.value,
1474            Price::from("101.00"),
1475            "L3 order should have moved to 101.00"
1476        );
1477    }
1478
1479    #[rstest]
1480    #[case::bids_worst_to_best(OrderSide::Buy, Some(OrderSide::Buy), &["99.00", "100.00", "101.00", "102.00"], "102.00")]
1481    #[case::bids_best_to_worst(OrderSide::Buy, Some(OrderSide::Buy), &["102.00", "101.00", "100.00", "99.00"], "100.00")]
1482    #[case::asks_worst_to_best(OrderSide::Sell, Some(OrderSide::Sell), &["105.00", "104.00", "103.00", "102.00"], "102.00")]
1483    #[case::asks_best_to_worst(OrderSide::Sell, Some(OrderSide::Sell), &["102.00", "103.00", "104.00", "105.00"], "104.00")]
1484    fn test_l1_multi_delta_batch_keeps_best_of_final_two(
1485        #[case] side_spec: OrderSide,
1486        #[case] side: Option<OrderSide>,
1487        #[case] prices: &[&str],
1488        #[case] expected_best: &str,
1489    ) {
1490        // Multi-delta batch: F_MBP without F_LAST clears each time.
1491        // Only the delta before F_LAST + F_LAST delta accumulate.
1492        let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1493
1494        let batch_size = prices.len();
1495        for (i, price_str) in prices.iter().enumerate() {
1496            let order = BookOrder {
1497                side,
1498                price: Price::from(*price_str),
1499                size: Quantity::from((i + 1) as u64 * 10),
1500                order_id: (i + 100) as u64,
1501            };
1502            let flags = if i == batch_size - 1 {
1503                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1504            } else {
1505                RecordFlag::F_MBP as u8
1506            };
1507            ladder.add(order, flags);
1508        }
1509
1510        assert_eq!(ladder.len(), 1, "L1 should have only 1 level");
1511        assert_eq!(
1512            ladder.top().unwrap().price.value,
1513            Price::from(expected_best),
1514            "Should keep best of final two deltas"
1515        );
1516    }
1517
1518    #[rstest]
1519    fn test_l1_retain_best_only_cache_consistency() {
1520        // Verify cache is properly cleaned up when retaining only the best level
1521        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1522        let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1523        let prices = ["100.00", "101.00", "102.00", "103.00", "104.00"];
1524
1525        for (i, price_str) in prices.iter().enumerate() {
1526            let order = BookOrder {
1527                side: OrderSide::Buy.into(),
1528                price: Price::from(*price_str),
1529                size: Quantity::from(10),
1530                order_id: (i + 1) as u64,
1531            };
1532            ladder.add(order, batch_flags);
1533        }
1534
1535        assert_eq!(ladder.len(), 1);
1536        assert_eq!(
1537            ladder.cache.len(),
1538            1,
1539            "Cache should have exactly 1 entry for L1"
1540        );
1541
1542        let total_orders: usize = ladder.levels.values().map(BookLevel::len).sum();
1543        assert_eq!(
1544            ladder.cache.len(),
1545            total_orders,
1546            "Cache should be consistent with levels"
1547        );
1548    }
1549
1550    #[rstest]
1551    fn test_l1_sequential_replacement_allows_price_degradation() {
1552        // Test that sequential L1 replacements (without F_MBP) allow price degradation
1553        // This is the expected behavior for top-of-book feeds like F_TOB
1554        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1555        let side_constant = OrderSide::Buy as u64;
1556
1557        // Add first L1 order at price 101.00 (best bid)
1558        let order1 = BookOrder {
1559            side: OrderSide::Buy.into(),
1560            price: Price::from("101.00"),
1561            size: Quantity::from(50),
1562            order_id: side_constant,
1563        };
1564        ladder.add(order1, 0); // flags=0 means replacement mode
1565
1566        assert_eq!(ladder.len(), 1);
1567        assert_eq!(
1568            ladder.top().unwrap().price.value,
1569            Price::from("101.00"),
1570            "Should have bid at 101.00"
1571        );
1572
1573        // Add second L1 order at worse price 100.00 (replacement mode)
1574        // This should REPLACE the previous level, allowing price degradation
1575        let order2 = BookOrder {
1576            side: OrderSide::Buy.into(),
1577            price: Price::from("100.00"),
1578            size: Quantity::from(60),
1579            order_id: side_constant,
1580        };
1581        ladder.add(order2, 0); // flags=0 means replacement mode
1582
1583        assert_eq!(ladder.len(), 1);
1584        assert_eq!(
1585            ladder.top().unwrap().price.value,
1586            Price::from("100.00"),
1587            "Sequential replacement should allow price to degrade from 101 to 100"
1588        );
1589
1590        // Verify the size was updated too
1591        assert_eq!(
1592            ladder.top().unwrap().first().unwrap().size,
1593            Quantity::from(60),
1594            "Size should be from the new order"
1595        );
1596    }
1597
1598    #[rstest]
1599    #[case::bids(OrderSide::Buy, Some(OrderSide::Buy), &["100.00", "101.00", "102.00"], "102.00", &["97.00", "98.00", "99.00"], "99.00")]
1600    #[case::asks(OrderSide::Sell, Some(OrderSide::Sell), &["100.00", "101.00", "102.00"], "101.00", &["103.00", "104.00", "105.00"], "104.00")]
1601    fn test_l1_consecutive_batches_clear_between(
1602        #[case] side_spec: OrderSide,
1603        #[case] side: Option<OrderSide>,
1604        #[case] batch1_prices: &[&str],
1605        #[case] expected1: &str,
1606        #[case] batch2_prices: &[&str],
1607        #[case] expected2: &str,
1608    ) {
1609        // Consecutive batches clear old data when a new batch starts
1610        let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1611
1612        // Batch 1
1613        for (i, price_str) in batch1_prices.iter().enumerate() {
1614            let order = BookOrder {
1615                side,
1616                price: Price::from(*price_str),
1617                size: Quantity::from(10),
1618                order_id: (i + 100) as u64,
1619            };
1620            let flags = if i == batch1_prices.len() - 1 {
1621                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1622            } else {
1623                RecordFlag::F_MBP as u8
1624            };
1625            ladder.add(order, flags);
1626        }
1627
1628        assert_eq!(ladder.len(), 1);
1629        assert_eq!(
1630            ladder.top().unwrap().price.value,
1631            Price::from(expected1),
1632            "After batch 1"
1633        );
1634
1635        // Batch 2 (worse prices for bids, higher prices for asks)
1636        for (i, price_str) in batch2_prices.iter().enumerate() {
1637            let order = BookOrder {
1638                side,
1639                price: Price::from(*price_str),
1640                size: Quantity::from(20),
1641                order_id: (i + 200) as u64,
1642            };
1643            let flags = if i == batch2_prices.len() - 1 {
1644                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1645            } else {
1646                RecordFlag::F_MBP as u8
1647            };
1648            ladder.add(order, flags);
1649        }
1650
1651        assert_eq!(ladder.len(), 1);
1652        assert_eq!(
1653            ladder.top().unwrap().price.value,
1654            Price::from(expected2),
1655            "After batch 2: batch 1 data cleared"
1656        );
1657    }
1658
1659    #[rstest]
1660    fn test_l1_zero_size_clears_regardless_of_order_id() {
1661        // Regression test: Zero-size clears must work even when order_id
1662        // differs between F_MBP batch (price-hash ID) and clear (side-constant ID)
1663        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1664
1665        // Add order with F_MBP flags (uses price-hash order_id via pre_process_order)
1666        let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1667        let order = BookOrder {
1668            side: OrderSide::Buy.into(),
1669            price: Price::from("100.00"),
1670            size: Quantity::from(50),
1671            order_id: 12345, // Price-hash ID
1672        };
1673        ladder.add(order, batch_flags);
1674        assert_eq!(ladder.len(), 1);
1675
1676        // Clear with zero-size and different order_id (side-constant)
1677        let clear_order = BookOrder {
1678            side: OrderSide::Buy.into(),
1679            price: Price::from("100.00"),
1680            size: Quantity::zero(9),
1681            order_id: OrderSide::Buy as u64, // Side-constant ID (different!)
1682        };
1683        ladder.add(clear_order, 0);
1684
1685        // Should be cleared despite order_id mismatch
1686        assert_eq!(
1687            ladder.len(),
1688            0,
1689            "Zero-size should clear L1 regardless of order_id"
1690        );
1691        assert!(ladder.cache.is_empty(), "Cache should be empty after clear");
1692    }
1693
1694    #[rstest]
1695    fn test_l1_f_mbp_without_f_last_does_not_accumulate() {
1696        // F_MBP without F_LAST: each message clears, preventing stale prices.
1697        // This allows prices to degrade when the market moves.
1698        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1699        let flags = RecordFlag::F_MBP as u8; // No F_LAST
1700
1701        // Prices descending from 100 to 91 (simulates degrading market)
1702        let prices = [
1703            "100.00", "99.00", "98.00", "97.00", "96.00", "95.00", "94.00", "93.00", "92.00",
1704            "91.00",
1705        ];
1706
1707        for (i, price_str) in prices.iter().enumerate() {
1708            let order = BookOrder {
1709                side: OrderSide::Buy.into(),
1710                price: Price::from(*price_str),
1711                size: Quantity::from(10),
1712                order_id: (i + 100) as u64,
1713            };
1714            ladder.add(order, flags);
1715
1716            assert_eq!(
1717                ladder.len(),
1718                1,
1719                "L1 should always have at most 1 level, iteration {i}"
1720            );
1721        }
1722
1723        // Final price should be 91 (the last added), not 100 (the best ever seen)
1724        assert_eq!(
1725            ladder.top().unwrap().price.value,
1726            Price::from("91.00"),
1727            "Should show last price (91), allowing degradation"
1728        );
1729    }
1730
1731    #[rstest]
1732    fn test_l1_f_mbp_two_delta_batch_retains_best() {
1733        // A 2-delta batch (F_MBP then F_MBP|F_LAST) accumulates both and keeps best
1734        let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L1_MBP);
1735
1736        // Delta 1 (F_MBP only): clears, adds 100, sets in_l1_batch=true
1737        let order1 = BookOrder {
1738            side: OrderSide::Sell.into(),
1739            price: Price::from("100.00"),
1740            size: Quantity::from(10),
1741            order_id: 100,
1742        };
1743        ladder.add(order1, RecordFlag::F_MBP as u8);
1744
1745        // Delta 2 (F_MBP|F_LAST): in_l1_batch=true so doesn't clear,
1746        // adds 101, now has 100+101, retain_best → 100
1747        let order2 = BookOrder {
1748            side: OrderSide::Sell.into(),
1749            price: Price::from("101.00"),
1750            size: Quantity::from(20),
1751            order_id: 101,
1752        };
1753        ladder.add(order2, RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8);
1754
1755        assert_eq!(ladder.len(), 1);
1756        assert_eq!(
1757            ladder.top().unwrap().price.value,
1758            Price::from("100.00"),
1759            "2-delta batch keeps best ask (100) from both deltas"
1760        );
1761    }
1762
1763    #[rstest]
1764    fn test_l1_snapshot_batch_accumulates_all_levels_bids() {
1765        // F_SNAPSHOT batch accumulates ALL levels and keeps best bid
1766        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1767        let prices = ["98.00", "99.00", "100.00", "101.00"];
1768        let batch_size = prices.len();
1769
1770        for (i, price_str) in prices.iter().enumerate() {
1771            let order = BookOrder {
1772                side: OrderSide::Buy.into(),
1773                price: Price::from(*price_str),
1774                size: Quantity::from(10),
1775                order_id: (i + 100) as u64,
1776            };
1777            let flags = if i == batch_size - 1 {
1778                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1779            } else {
1780                RecordFlag::F_SNAPSHOT as u8
1781            };
1782            ladder.add(order, flags);
1783        }
1784
1785        assert_eq!(
1786            ladder.len(),
1787            1,
1788            "L1 should have only 1 level after snapshot"
1789        );
1790        assert_eq!(
1791            ladder.top().unwrap().price.value,
1792            Price::from("101.00"),
1793            "F_SNAPSHOT batch should keep best bid (101) from ALL deltas"
1794        );
1795    }
1796
1797    #[rstest]
1798    fn test_l1_snapshot_batch_accumulates_all_levels_asks() {
1799        // F_SNAPSHOT batch accumulates ALL levels and keeps best ask
1800        let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L1_MBP);
1801        let prices = ["104.00", "103.00", "102.00", "101.00"];
1802        let batch_size = prices.len();
1803
1804        for (i, price_str) in prices.iter().enumerate() {
1805            let order = BookOrder {
1806                side: OrderSide::Sell.into(),
1807                price: Price::from(*price_str),
1808                size: Quantity::from(10),
1809                order_id: (i + 100) as u64,
1810            };
1811            let flags = if i == batch_size - 1 {
1812                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1813            } else {
1814                RecordFlag::F_SNAPSHOT as u8
1815            };
1816            ladder.add(order, flags);
1817        }
1818
1819        assert_eq!(
1820            ladder.len(),
1821            1,
1822            "L1 should have only 1 level after snapshot"
1823        );
1824        assert_eq!(
1825            ladder.top().unwrap().price.value,
1826            Price::from("101.00"),
1827            "F_SNAPSHOT batch should keep best ask (101) from ALL deltas"
1828        );
1829    }
1830
1831    #[rstest]
1832    fn test_l1_snapshot_vs_mbp_different_accumulation_behavior() {
1833        // F_SNAPSHOT accumulates all levels, F_MBP only accumulates final two
1834        let mut mbp_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1835        let prices = ["98.00", "99.00", "100.00", "101.00"];
1836        for (i, price_str) in prices.iter().enumerate() {
1837            let order = BookOrder {
1838                side: OrderSide::Buy.into(),
1839                price: Price::from(*price_str),
1840                size: Quantity::from(10),
1841                order_id: (i + 100) as u64,
1842            };
1843            let flags = if i == prices.len() - 1 {
1844                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1845            } else {
1846                RecordFlag::F_MBP as u8
1847            };
1848            mbp_ladder.add(order, flags);
1849        }
1850        assert_eq!(
1851            mbp_ladder.top().unwrap().price.value,
1852            Price::from("101.00"),
1853            "F_MBP keeps best of final two (100, 101)"
1854        );
1855
1856        let mut snapshot_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1857
1858        for (i, price_str) in prices.iter().enumerate() {
1859            let order = BookOrder {
1860                side: OrderSide::Buy.into(),
1861                price: Price::from(*price_str),
1862                size: Quantity::from(10),
1863                order_id: (i + 200) as u64,
1864            };
1865            let flags = if i == prices.len() - 1 {
1866                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1867            } else {
1868                RecordFlag::F_SNAPSHOT as u8
1869            };
1870            snapshot_ladder.add(order, flags);
1871        }
1872        assert_eq!(
1873            snapshot_ladder.top().unwrap().price.value,
1874            Price::from("101.00"),
1875            "F_SNAPSHOT keeps best of ALL deltas (98, 99, 100, 101)"
1876        );
1877    }
1878
1879    #[rstest]
1880    fn test_l1_snapshot_after_incomplete_mbp_stream() {
1881        // Snapshot must clear stale state from incomplete F_MBP stream (no F_LAST sent)
1882        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1883
1884        // Incomplete F_MBP stream leaves stale batch state
1885        let stale_order = BookOrder {
1886            side: OrderSide::Buy.into(),
1887            price: Price::from("101.00"),
1888            size: Quantity::from(10),
1889            order_id: 100,
1890        };
1891        ladder.add(stale_order, RecordFlag::F_MBP as u8);
1892        assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
1893
1894        // Snapshot arrives with Clear delta first
1895        ladder.clear();
1896
1897        // Snapshot prices worse than stale 101
1898        for (i, price_str) in ["98.00", "99.00", "100.00"].iter().enumerate() {
1899            let order = BookOrder {
1900                side: OrderSide::Buy.into(),
1901                price: Price::from(*price_str),
1902                size: Quantity::from(10),
1903                order_id: (i + 200) as u64,
1904            };
1905            let flags = if i == 2 {
1906                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1907            } else {
1908                RecordFlag::F_SNAPSHOT as u8
1909            };
1910            ladder.add(order, flags);
1911        }
1912
1913        assert_eq!(
1914            ladder.top().unwrap().price.value,
1915            Price::from("100.00"),
1916            "Snapshot replaces stale MBP state: best is 100, not stale 101"
1917        );
1918    }
1919
1920    #[rstest]
1921    fn test_l1_snapshot_clears_previous_batch() {
1922        // New F_SNAPSHOT batch clears previous batch
1923        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1924
1925        for (i, price_str) in ["100.00", "101.00", "102.00"].iter().enumerate() {
1926            let order = BookOrder {
1927                side: OrderSide::Buy.into(),
1928                price: Price::from(*price_str),
1929                size: Quantity::from(10),
1930                order_id: (i + 100) as u64,
1931            };
1932            let flags = if i == 2 {
1933                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1934            } else {
1935                RecordFlag::F_SNAPSHOT as u8
1936            };
1937            ladder.add(order, flags);
1938        }
1939        assert_eq!(ladder.top().unwrap().price.value, Price::from("102.00"));
1940
1941        // Second batch with worse prices
1942        for (i, price_str) in ["95.00", "96.00", "97.00"].iter().enumerate() {
1943            let order = BookOrder {
1944                side: OrderSide::Buy.into(),
1945                price: Price::from(*price_str),
1946                size: Quantity::from(20),
1947                order_id: (i + 200) as u64,
1948            };
1949            let flags = if i == 2 {
1950                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1951            } else {
1952                RecordFlag::F_SNAPSHOT as u8
1953            };
1954            ladder.add(order, flags);
1955        }
1956        assert_eq!(
1957            ladder.top().unwrap().price.value,
1958            Price::from("97.00"),
1959            "Second batch clears first: best is 97, not 102"
1960        );
1961    }
1962
1963    #[rstest]
1964    fn test_l1_single_delta_snapshot_after_mbp_batch() {
1965        // Single-delta snapshot (F_SNAPSHOT|F_LAST) must clear stale MBP batch state
1966        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1967
1968        let mbp_order1 = BookOrder {
1969            side: OrderSide::Buy.into(),
1970            price: Price::from("100.00"),
1971            size: Quantity::from(10),
1972            order_id: 1,
1973        };
1974        let mbp_order2 = BookOrder {
1975            side: OrderSide::Buy.into(),
1976            price: Price::from("101.00"),
1977            size: Quantity::from(10),
1978            order_id: 2,
1979        };
1980        ladder.add(mbp_order1, RecordFlag::F_MBP as u8);
1981        ladder.add(
1982            mbp_order2,
1983            RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8,
1984        );
1985
1986        assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
1987
1988        // Single-delta snapshot at worse price (no preceding Clear)
1989        let snapshot_order = BookOrder {
1990            side: OrderSide::Buy.into(),
1991            price: Price::from("95.00"),
1992            size: Quantity::from(20),
1993            order_id: 100,
1994        };
1995        ladder.add(
1996            snapshot_order,
1997            RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
1998        );
1999
2000        assert_eq!(
2001            ladder.top().unwrap().price.value,
2002            Price::from("95.00"),
2003            "Single-delta snapshot clears MBP state: best is 95, not stale 101"
2004        );
2005        assert_eq!(ladder.len(), 1);
2006    }
2007}