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::{
25    UnixNanos,
26    correctness::{CorrectnessError, CorrectnessResult},
27};
28
29use crate::{
30    data::order::{BookOrder, OrderId},
31    enums::{BookType, OrderSide, RecordFlag},
32    orderbook::BookLevel,
33    types::{Price, Quantity},
34};
35
36/// Represents a price level with a specified side in an order books ladder.
37///
38/// # Comparison Semantics
39///
40/// `BookPrice` instances are only meaningfully compared within the same side
41/// (i.e., within a single `BookLadder`). Cross-side comparisons are not expected
42/// in normal use, as bid and ask ladders maintain separate `BTreeMap<BookPrice, BookLevel>`
43/// collections.
44///
45/// - Equality requires both `value` and `side` to match.
46/// - Ordering is side-dependent: Buy side sorts descending, Sell side ascending.
47#[derive(Clone, Copy, Debug, Eq)]
48#[cfg_attr(
49    feature = "python",
50    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
51)]
52pub struct BookPrice {
53    pub value: Price,
54    pub side: OrderSide,
55}
56
57impl BookPrice {
58    /// Creates a new [`BookPrice`] instance.
59    #[must_use]
60    pub fn new(value: Price, side: OrderSide) -> Self {
61        Self { value, side }
62    }
63}
64
65impl PartialOrd for BookPrice {
66    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
67        Some(self.cmp(other))
68    }
69}
70
71impl PartialEq for BookPrice {
72    fn eq(&self, other: &Self) -> bool {
73        self.side == other.side && self.value == other.value
74    }
75}
76
77impl Ord for BookPrice {
78    fn cmp(&self, other: &Self) -> Ordering {
79        assert_eq!(
80            self.side, other.side,
81            "BookPrice compared across sides: {:?} vs {:?}",
82            self.side, other.side
83        );
84
85        match self.side {
86            OrderSide::Buy => other.value.cmp(&self.value),
87            OrderSide::Sell => self.value.cmp(&other.value),
88        }
89    }
90}
91
92impl Display for BookPrice {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{}", self.value)
95    }
96}
97
98/// Tracks the type of L1 batch currently being accumulated.
99///
100/// Separating MBP and snapshot batches prevents cross-contamination where
101/// stale MBP data could pollute a new snapshot. Without this distinction,
102/// an incomplete MBP stream (missing `F_LAST`) would leave batch state that
103/// incorrectly affects subsequent snapshot processing.
104///
105/// The discriminants are serialized in `OrderBook` state and must remain stable.
106#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
107#[repr(u8)]
108enum L1BatchState {
109    /// Not in any batch.
110    #[default]
111    None = 0,
112    /// Accumulating an `F_MBP` batch (final two deltas accumulate).
113    MbpBatch = 1,
114    /// Accumulating an `F_SNAPSHOT` batch (all deltas accumulate).
115    SnapshotBatch = 2,
116}
117
118/// Represents a ladder of price levels for one side of an order book.
119#[derive(Clone, Debug)]
120pub(crate) struct BookLadder {
121    pub side: OrderSide,
122    pub book_type: BookType,
123    pub levels: BTreeMap<BookPrice, BookLevel>,
124    pub cache: HashMap<u64, BookPrice>,
125    batch_state: L1BatchState,
126}
127
128impl BookLadder {
129    /// Creates a new [`Ladder`] instance.
130    #[must_use]
131    pub(crate) fn new(side: OrderSide, book_type: BookType) -> Self {
132        Self {
133            side,
134            book_type,
135            levels: BTreeMap::new(),
136            cache: HashMap::new(),
137            batch_state: L1BatchState::None,
138        }
139    }
140
141    /// Returns the number of price levels in the ladder.
142    #[must_use]
143    pub(crate) fn len(&self) -> usize {
144        self.levels.len()
145    }
146
147    /// Returns true if the ladder has no price levels.
148    #[must_use]
149    #[allow(dead_code)]
150    pub(crate) fn is_empty(&self) -> bool {
151        self.levels.is_empty()
152    }
153
154    #[must_use]
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    pub(crate) fn set_batch_state_code(&mut self, code: u8) -> CorrectnessResult<()> {
165        let batch_state = match code {
166            0 => L1BatchState::None,
167            1 => L1BatchState::MbpBatch,
168            2 => L1BatchState::SnapshotBatch,
169            _ => {
170                return Err(CorrectnessError::PredicateViolation {
171                    message: format!("Invalid L1 batch state code: {code}"),
172                });
173            }
174        };
175
176        if self.book_type != BookType::L1_MBP && batch_state != L1BatchState::None {
177            return Err(CorrectnessError::PredicateViolation {
178                message: format!(
179                    "Cannot restore L1 batch state for book type {:?}",
180                    self.book_type
181                ),
182            });
183        }
184
185        self.batch_state = batch_state;
186        Ok(())
187    }
188
189    /// Removes all orders and price levels from the ladder.
190    ///
191    /// Also resets the batch state to ensure clean handling of subsequent batches.
192    pub(crate) fn clear(&mut self) {
193        self.levels.clear();
194        self.cache.clear();
195        self.batch_state = L1BatchState::None;
196    }
197
198    pub(crate) fn replace_l1(&mut self, order: BookOrder) {
199        debug_assert_eq!(self.book_type, BookType::L1_MBP);
200
201        let reusable_level = self.levels.pop_first().map(|(_, level)| level);
202        self.clear();
203
204        if !order.size.is_positive() {
205            let side = self.side;
206            log::debug!("L1 zero-size add cleared ladder: side={side:?}");
207            return;
208        }
209
210        let book_price = order.to_book_price();
211        self.cache.insert(order.order_id, book_price);
212
213        if let Some(mut level) = reusable_level {
214            level.price = book_price;
215            level.orders.clear();
216            level.add(order);
217            self.levels.insert(book_price, level);
218        } else {
219            self.levels.insert(book_price, BookLevel::from_order(order));
220        }
221    }
222
223    /// Adds an order to the ladder at its price level.
224    ///
225    /// For `L2_MBP` and `L3_MBO` books, an order ID lives at exactly one price
226    /// level within a ladder: re-adding an ID at a different price moves the
227    /// order to the new level's FIFO tail. This is a no-op for `L2_MBP`, whose
228    /// IDs are price hashes, and covers `L3_MBO` venue IDs as well as `F_TOB`
229    /// side-constant IDs.
230    ///
231    /// `L1_MBP` is exempt: batch accumulation must compare levels sharing the
232    /// side-constant ID before retaining only the best.
233    /// `L1_MBP` behavior depends on flags:
234    /// - `F_MBP` or `F_SNAPSHOT` (multi-level batch): Retains best after each add to prevent
235    ///   accumulation even if `F_LAST` is never sent.
236    /// - `F_TOB` or no batch flags (single replacement): Clears existing levels first,
237    ///   allowing price to degrade.
238    pub(crate) fn add(&mut self, order: BookOrder, flags: u8) {
239        if self.book_type == BookType::L1_MBP && !self.handle_l1_add(&order, flags) {
240            return;
241        }
242
243        if self.book_type != BookType::L1_MBP && !order.size.is_positive() {
244            log::warn!(
245                "Attempted to add order with non-positive size: order_id={}, size={}, ignoring",
246                order.order_id,
247                order.size,
248            );
249            return;
250        }
251
252        let book_price = order.to_book_price();
253
254        if self.book_type != BookType::L1_MBP
255            && let Some(existing_price) = self.cache.get(&order.order_id).copied()
256            && existing_price != book_price
257            && let Some(existing_level) = self.levels.get_mut(&existing_price)
258        {
259            existing_level.delete(&order);
260            if existing_level.is_empty() {
261                self.levels.remove(&existing_price);
262            }
263        }
264
265        self.cache.insert(order.order_id, book_price);
266
267        if let Some(level) = self.levels.get_mut(&book_price) {
268            level.add(order);
269        } else {
270            let level = BookLevel::from_order(order);
271            self.levels.insert(book_price, level);
272        }
273
274        // For L1_MBP with F_MBP or F_SNAPSHOT, always retain best to prevent unbounded
275        // accumulation if F_LAST is never sent
276        let is_batch = RecordFlag::F_MBP.matches(flags) || RecordFlag::F_SNAPSHOT.matches(flags);
277        if self.book_type == BookType::L1_MBP && is_batch {
278            self.retain_best_only();
279
280            if RecordFlag::F_LAST.matches(flags) {
281                self.batch_state = L1BatchState::None;
282            }
283        }
284    }
285
286    /// Handles L1_MBP-specific add logic.
287    ///
288    /// Returns `true` to continue with normal add flow, `false` to abort.
289    ///
290    /// Behavior depends on flags:
291    /// - `F_SNAPSHOT` with `F_LAST`: End of snapshot batch. If in snapshot batch, accumulate;
292    ///   otherwise clear (single-delta snapshot or cross-contamination from MBP).
293    /// - `F_SNAPSHOT` without `F_LAST`: Start/continue snapshot batch. Clears if not already
294    ///   in a snapshot batch (handles stale MBP data).
295    /// - `F_MBP` with `F_LAST`: End of MBP batch. If in MBP batch, accumulate final two;
296    ///   otherwise clear.
297    /// - `F_MBP` without `F_LAST`: Always clear (streaming mode, prevents stale prices).
298    /// - `F_TOB` or no batch flags: Single replacement (clears first).
299    ///
300    /// Zero-size orders clear the entire L1 ladder.
301    fn handle_l1_add(&mut self, order: &BookOrder, flags: u8) -> bool {
302        if !order.size.is_positive() {
303            self.clear();
304            let side = self.side;
305            log::debug!("L1 zero-size add cleared ladder: side={side:?}");
306            return false;
307        }
308
309        let is_mbp = RecordFlag::F_MBP.matches(flags);
310        let is_snapshot = RecordFlag::F_SNAPSHOT.matches(flags);
311        let is_last = RecordFlag::F_LAST.matches(flags);
312
313        if is_snapshot && is_last {
314            // F_SNAPSHOT|F_LAST: end of snapshot batch
315            // Only accumulate if we're in a snapshot batch; otherwise clear to prevent
316            // cross-contamination from stale MBP data
317            if self.batch_state != L1BatchState::SnapshotBatch {
318                self.clear();
319            }
320        } else if is_snapshot {
321            // F_SNAPSHOT without F_LAST: start/continue snapshot batch
322            if self.batch_state != L1BatchState::SnapshotBatch {
323                self.clear();
324                self.batch_state = L1BatchState::SnapshotBatch;
325            }
326        } else if is_mbp && is_last {
327            // F_MBP|F_LAST: end of MBP batch, accumulate if already in MBP batch
328            if self.batch_state != L1BatchState::MbpBatch {
329                self.clear();
330            }
331        } else if is_mbp {
332            // F_MBP without F_LAST: always clear (streaming mode)
333            self.clear();
334            self.batch_state = L1BatchState::MbpBatch;
335        } else {
336            // Non-batch: replacement mode
337            self.clear();
338        }
339
340        true
341    }
342
343    /// Updates an existing order in the ladder, moving it to a new price level if needed.
344    pub(crate) fn update(&mut self, order: BookOrder, flags: u8) {
345        let price = self.cache.get(&order.order_id).copied();
346        if let Some(price) = price
347            && let Some(level) = self.levels.get_mut(&price)
348        {
349            if order.price == level.price.value {
350                let level_len_before = level.len();
351                level.update(order);
352
353                // If level.update removed the order due to zero size, remove from cache too
354                if order.size.is_zero() {
355                    self.cache.remove(&order.order_id);
356                    debug_assert_eq!(
357                        level.len(),
358                        level_len_before - 1,
359                        "Level should have one less order after zero-size update"
360                    );
361                } else {
362                    debug_assert!(
363                        self.cache.contains_key(&order.order_id),
364                        "Cache should still contain order {0} after update",
365                        order.order_id
366                    );
367                }
368
369                if level.is_empty() {
370                    self.levels.remove(&price);
371                    debug_assert!(
372                        !self.cache.values().any(|p| *p == price),
373                        "Cache should not contain removed price level {price:?}"
374                    );
375                }
376
377                debug_assert_eq!(
378                    self.cache.len(),
379                    self.levels.values().map(BookLevel::len).sum::<usize>(),
380                    "Cache size should equal total orders across all levels"
381                );
382                return;
383            }
384
385            // Price update: delete and insert at new level
386            self.cache.remove(&order.order_id);
387            level.delete(&order);
388
389            if level.is_empty() {
390                self.levels.remove(&price);
391                debug_assert!(
392                    !self.cache.values().any(|p| *p == price),
393                    "Cache should not contain removed price level {price:?}"
394                );
395            }
396        }
397
398        // Only add if the order has positive size
399        if order.size.is_positive() {
400            self.add(order, flags);
401        }
402
403        // Validate cache consistency after update
404        debug_assert_eq!(
405            self.cache.len(),
406            self.levels.values().map(BookLevel::len).sum::<usize>(),
407            "Cache size should equal total orders across all levels"
408        );
409    }
410
411    /// Deletes an order from the ladder.
412    pub(crate) fn delete(&mut self, order: BookOrder, sequence: u64, ts_event: UnixNanos) {
413        self.remove_order(order.order_id, sequence, ts_event);
414    }
415
416    /// Removes an order by its ID from the ladder.
417    pub(crate) fn remove_order(&mut self, order_id: OrderId, sequence: u64, ts_event: UnixNanos) {
418        if let Some(price) = self.cache.get(&order_id).copied()
419            && let Some(level) = self.levels.get_mut(&price)
420        {
421            // Check if order exists in level before modifying cache
422            if level.orders.contains_key(&order_id) {
423                let level_len_before = level.len();
424
425                // Now safe to remove from cache since we know order exists in level
426                self.cache.remove(&order_id);
427                level.remove_by_id(order_id, sequence, ts_event);
428
429                debug_assert_eq!(
430                    level.len(),
431                    level_len_before - 1,
432                    "Level should have exactly one less order after removal"
433                );
434
435                if level.is_empty() {
436                    self.levels.remove(&price);
437                    debug_assert!(
438                        !self.cache.values().any(|p| *p == price),
439                        "Cache should not contain removed price level {price:?}"
440                    );
441                }
442            }
443        }
444
445        // Validate cache consistency after removal
446        debug_assert_eq!(
447            self.cache.len(),
448            self.levels.values().map(BookLevel::len).sum::<usize>(),
449            "Cache size should equal total orders across all levels"
450        );
451    }
452
453    /// Removes an entire price level from the ladder and returns it.
454    pub(crate) fn remove_level(&mut self, price: BookPrice) -> Option<BookLevel> {
455        if let Some(level) = self.levels.remove(&price) {
456            // Remove all orders in this level from the cache
457            for order_id in level.orders.keys() {
458                self.cache.remove(order_id);
459            }
460
461            debug_assert_eq!(
462                self.cache.len(),
463                self.levels.values().map(BookLevel::len).sum::<usize>(),
464                "Cache size should equal total orders across all levels"
465            );
466
467            Some(level)
468        } else {
469            None
470        }
471    }
472
473    /// Retains only the best price level, removing all others.
474    ///
475    /// For `L1_MBP` books, this ensures only the top-of-book level is kept after
476    /// processing multi-level data. The `BTreeMap` ordering ensures the first
477    /// entry is always the best price (highest for bids, lowest for asks).
478    fn retain_best_only(&mut self) {
479        if self.levels.len() <= 1 {
480            return;
481        }
482
483        let best_price = match self.levels.keys().next().copied() {
484            Some(price) => price,
485            None => return,
486        };
487
488        // Remove all levels except the best (don't use remove_level as it
489        // incorrectly handles cache for L1 where all orders share order_id)
490        self.levels.retain(|price, _| *price == best_price);
491
492        // Rebuild cache from remaining level (necessary for L1 where
493        // all orders use the same order_id and remove_level would corrupt cache)
494        self.cache.clear();
495
496        for (book_price, level) in &self.levels {
497            for order_id in level.orders.keys() {
498                self.cache.insert(*order_id, *book_price);
499            }
500        }
501
502        debug_assert!(
503            self.levels.len() <= 1,
504            "L1 ladder should have at most 1 level after retain_best_only"
505        );
506        debug_assert_eq!(
507            self.cache.len(),
508            self.levels.values().map(BookLevel::len).sum::<usize>(),
509            "Cache size should equal total orders across all levels"
510        );
511    }
512
513    /// Returns the total size of all orders in the ladder.
514    #[must_use]
515    #[allow(dead_code)]
516    pub(crate) fn sizes(&self) -> f64 {
517        self.levels.values().map(BookLevel::size).sum()
518    }
519
520    /// Returns the total value exposure (price * size) of all orders in the ladder.
521    #[must_use]
522    #[allow(dead_code)]
523    pub(crate) fn exposures(&self) -> f64 {
524        self.levels.values().map(BookLevel::exposure).sum()
525    }
526
527    /// Returns the best price level in the ladder.
528    #[must_use]
529    pub(crate) fn top(&self) -> Option<&BookLevel> {
530        self.levels.values().next()
531    }
532
533    /// Simulates fills for an order against this ladder's liquidity.
534    /// Returns a list of (price, size) tuples representing the simulated fills.
535    #[must_use]
536    pub(crate) fn simulate_fills(&self, order: &BookOrder) -> Vec<(Price, Quantity)> {
537        let is_reversed = self.side == OrderSide::Buy;
538        let mut fills = Vec::new();
539        let mut cumulative_denominator = Quantity::zero(order.size.precision);
540        let target = order.size;
541
542        for level in self.levels.values() {
543            if (is_reversed && level.price.value < order.price)
544                || (!is_reversed && level.price.value > order.price)
545            {
546                break;
547            }
548
549            for book_order in level.orders.values() {
550                let current = book_order.size;
551                let remainder = target - cumulative_denominator;
552                if current >= remainder {
553                    // This order has filled us, add fill and return
554                    if remainder.is_positive() {
555                        fills.push((book_order.price, remainder));
556                    }
557                    return fills;
558                }
559
560                // Add this fill and continue
561                fills.push((book_order.price, current));
562                cumulative_denominator = cumulative_denominator + current;
563            }
564        }
565
566        fills
567    }
568}
569
570impl Display for BookLadder {
571    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572        writeln!(f, "{}(side={})", stringify!(BookLadder), self.side)?;
573        for (price, level) in &self.levels {
574            writeln!(f, "  {} -> {} orders", price, level.len())?;
575        }
576        Ok(())
577    }
578}
579
580#[cfg(test)]
581impl BookLadder {
582    /// Adds multiple orders to the ladder.
583    pub(crate) fn add_bulk(&mut self, orders: &[BookOrder]) {
584        for order in orders {
585            self.add(*order, 0);
586        }
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use nautilus_core::correctness::CorrectnessError;
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        #[values(false, true)] via_update: bool,
1378    ) {
1379        let mut ladder = BookLadder::new(side_spec, BookType::L3_MBO);
1380
1381        let moved = BookOrder {
1382            side,
1383            price: Price::from(old_price),
1384            size: Quantity::from(50),
1385            order_id: 1,
1386        };
1387        let staying = BookOrder {
1388            side,
1389            price: Price::from(old_price),
1390            size: Quantity::from(30),
1391            order_id: 2,
1392        };
1393        ladder.add(moved, 0);
1394        ladder.add(staying, 0);
1395
1396        let moved_new = BookOrder {
1397            side,
1398            price: Price::from(new_price),
1399            size: Quantity::from(60),
1400            order_id: 1,
1401        };
1402
1403        if via_update {
1404            ladder.update(moved_new, 0);
1405        } else {
1406            ladder.add(moved_new, 0);
1407        }
1408
1409        assert_eq!(
1410            ladder.len(),
1411            2,
1412            "Old level must survive with the remaining order"
1413        );
1414        let old_level = ladder
1415            .levels
1416            .get(&BookPrice::new(Price::from(old_price), side_spec))
1417            .expect("Old level should remain");
1418        let new_level = ladder
1419            .levels
1420            .get(&BookPrice::new(Price::from(new_price), side_spec))
1421            .expect("New level should exist");
1422
1423        // `BookOrder` equality compares IDs only, so compare the fields that must move
1424        let fields = |level: &BookLevel| {
1425            level
1426                .iter()
1427                .map(|order| (order.order_id, order.price, order.size))
1428                .collect::<Vec<_>>()
1429        };
1430
1431        assert_eq!(
1432            fields(old_level),
1433            vec![(2, Price::from(old_price), Quantity::from(30))]
1434        );
1435        assert_eq!(
1436            fields(new_level),
1437            vec![(1, Price::from(new_price), Quantity::from(60))]
1438        );
1439        assert_eq!(ladder.cache.len(), 2);
1440        assert_eq!(ladder.cache[&1], new_level.price);
1441        assert_eq!(ladder.cache[&2], old_level.price);
1442    }
1443
1444    #[rstest]
1445    fn test_l1_vs_l3_duplicate_order_id_replacement() {
1446        // L1 behavior with replacement (flags=0): successive adds replace
1447        let mut l1_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1448        let side_constant = OrderSide::Buy as u64;
1449
1450        let order1 = BookOrder {
1451            side: OrderSide::Buy.into(),
1452            price: Price::from("100.00"),
1453            size: Quantity::from(50),
1454            order_id: side_constant,
1455        };
1456        l1_ladder.add(order1, 0);
1457
1458        let order2 = BookOrder {
1459            side: OrderSide::Buy.into(),
1460            price: Price::from("101.00"),
1461            size: Quantity::from(60),
1462            order_id: side_constant, // Same ID
1463        };
1464        l1_ladder.add(order2, 0);
1465
1466        assert_eq!(l1_ladder.len(), 1, "L1 should have only 1 level");
1467        assert_eq!(
1468            l1_ladder.top().unwrap().price.value,
1469            Price::from("101.00"),
1470            "L1 should have replaced the old level"
1471        );
1472
1473        let mut l3_ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1474
1475        let order3 = BookOrder {
1476            side: OrderSide::Buy.into(),
1477            price: Price::from("100.00"),
1478            size: Quantity::from(50),
1479            order_id: 1, // Happens to match side constant
1480        };
1481        l3_ladder.add(order3, 0);
1482
1483        let order4 = BookOrder {
1484            side: OrderSide::Buy.into(),
1485            price: Price::from("101.00"),
1486            size: Quantity::from(60),
1487            order_id: 1,
1488        };
1489        l3_ladder.add(order4, 0);
1490
1491        assert_eq!(
1492            l3_ladder.len(),
1493            1,
1494            "L3 should move the order to its new price level"
1495        );
1496        assert_eq!(
1497            l3_ladder.top().unwrap().price.value,
1498            Price::from("101.00"),
1499            "L3 order should have moved to 101.00"
1500        );
1501    }
1502
1503    #[rstest]
1504    #[case::bids_worst_to_best(OrderSide::Buy, Some(OrderSide::Buy), &["99.00", "100.00", "101.00", "102.00"], "102.00")]
1505    #[case::bids_best_to_worst(OrderSide::Buy, Some(OrderSide::Buy), &["102.00", "101.00", "100.00", "99.00"], "100.00")]
1506    #[case::asks_worst_to_best(OrderSide::Sell, Some(OrderSide::Sell), &["105.00", "104.00", "103.00", "102.00"], "102.00")]
1507    #[case::asks_best_to_worst(OrderSide::Sell, Some(OrderSide::Sell), &["102.00", "103.00", "104.00", "105.00"], "104.00")]
1508    fn test_l1_multi_delta_batch_keeps_best_of_final_two(
1509        #[case] side_spec: OrderSide,
1510        #[case] side: Option<OrderSide>,
1511        #[case] prices: &[&str],
1512        #[case] expected_best: &str,
1513    ) {
1514        // Multi-delta batch: F_MBP without F_LAST clears each time.
1515        // Only the delta before F_LAST + F_LAST delta accumulate.
1516        let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1517
1518        let batch_size = prices.len();
1519        for (i, price_str) in prices.iter().enumerate() {
1520            let order = BookOrder {
1521                side,
1522                price: Price::from(*price_str),
1523                size: Quantity::from((i + 1) as u64 * 10),
1524                order_id: (i + 100) as u64,
1525            };
1526            let flags = if i == batch_size - 1 {
1527                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1528            } else {
1529                RecordFlag::F_MBP as u8
1530            };
1531            ladder.add(order, flags);
1532        }
1533
1534        assert_eq!(ladder.len(), 1, "L1 should have only 1 level");
1535        assert_eq!(
1536            ladder.top().unwrap().price.value,
1537            Price::from(expected_best),
1538            "Should keep best of final two deltas"
1539        );
1540    }
1541
1542    #[rstest]
1543    fn test_l1_retain_best_only_cache_consistency() {
1544        // Verify cache is properly cleaned up when retaining only the best level
1545        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1546        let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1547        let prices = ["100.00", "101.00", "102.00", "103.00", "104.00"];
1548
1549        for (i, price_str) in prices.iter().enumerate() {
1550            let order = BookOrder {
1551                side: OrderSide::Buy.into(),
1552                price: Price::from(*price_str),
1553                size: Quantity::from(10),
1554                order_id: (i + 1) as u64,
1555            };
1556            ladder.add(order, batch_flags);
1557        }
1558
1559        assert_eq!(ladder.len(), 1);
1560        assert_eq!(
1561            ladder.cache.len(),
1562            1,
1563            "Cache should have exactly 1 entry for L1"
1564        );
1565
1566        let total_orders: usize = ladder.levels.values().map(BookLevel::len).sum();
1567        assert_eq!(
1568            ladder.cache.len(),
1569            total_orders,
1570            "Cache should be consistent with levels"
1571        );
1572    }
1573
1574    #[rstest]
1575    fn test_l1_sequential_replacement_allows_price_degradation() {
1576        // Test that sequential L1 replacements (without F_MBP) allow price degradation
1577        // This is the expected behavior for top-of-book feeds like F_TOB
1578        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1579        let side_constant = OrderSide::Buy as u64;
1580
1581        // Add first L1 order at price 101.00 (best bid)
1582        let order1 = BookOrder {
1583            side: OrderSide::Buy.into(),
1584            price: Price::from("101.00"),
1585            size: Quantity::from(50),
1586            order_id: side_constant,
1587        };
1588        ladder.add(order1, 0); // flags=0 means replacement mode
1589
1590        assert_eq!(ladder.len(), 1);
1591        assert_eq!(
1592            ladder.top().unwrap().price.value,
1593            Price::from("101.00"),
1594            "Should have bid at 101.00"
1595        );
1596
1597        // Add second L1 order at worse price 100.00 (replacement mode)
1598        // This should REPLACE the previous level, allowing price degradation
1599        let order2 = BookOrder {
1600            side: OrderSide::Buy.into(),
1601            price: Price::from("100.00"),
1602            size: Quantity::from(60),
1603            order_id: side_constant,
1604        };
1605        ladder.add(order2, 0); // flags=0 means replacement mode
1606
1607        assert_eq!(ladder.len(), 1);
1608        assert_eq!(
1609            ladder.top().unwrap().price.value,
1610            Price::from("100.00"),
1611            "Sequential replacement should allow price to degrade from 101 to 100"
1612        );
1613
1614        // Verify the size was updated too
1615        assert_eq!(
1616            ladder.top().unwrap().first().unwrap().size,
1617            Quantity::from(60),
1618            "Size should be from the new order"
1619        );
1620    }
1621
1622    #[rstest]
1623    #[case::bids(OrderSide::Buy, Some(OrderSide::Buy), &["100.00", "101.00", "102.00"], "102.00", &["97.00", "98.00", "99.00"], "99.00")]
1624    #[case::asks(OrderSide::Sell, Some(OrderSide::Sell), &["100.00", "101.00", "102.00"], "101.00", &["103.00", "104.00", "105.00"], "104.00")]
1625    fn test_l1_consecutive_batches_clear_between(
1626        #[case] side_spec: OrderSide,
1627        #[case] side: Option<OrderSide>,
1628        #[case] batch1_prices: &[&str],
1629        #[case] expected1: &str,
1630        #[case] batch2_prices: &[&str],
1631        #[case] expected2: &str,
1632    ) {
1633        // Consecutive batches clear old data when a new batch starts
1634        let mut ladder = BookLadder::new(side_spec, BookType::L1_MBP);
1635
1636        // Batch 1
1637        for (i, price_str) in batch1_prices.iter().enumerate() {
1638            let order = BookOrder {
1639                side,
1640                price: Price::from(*price_str),
1641                size: Quantity::from(10),
1642                order_id: (i + 100) as u64,
1643            };
1644            let flags = if i == batch1_prices.len() - 1 {
1645                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1646            } else {
1647                RecordFlag::F_MBP as u8
1648            };
1649            ladder.add(order, flags);
1650        }
1651
1652        assert_eq!(ladder.len(), 1);
1653        assert_eq!(
1654            ladder.top().unwrap().price.value,
1655            Price::from(expected1),
1656            "After batch 1"
1657        );
1658
1659        // Batch 2 (worse prices for bids, higher prices for asks)
1660        for (i, price_str) in batch2_prices.iter().enumerate() {
1661            let order = BookOrder {
1662                side,
1663                price: Price::from(*price_str),
1664                size: Quantity::from(20),
1665                order_id: (i + 200) as u64,
1666            };
1667            let flags = if i == batch2_prices.len() - 1 {
1668                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1669            } else {
1670                RecordFlag::F_MBP as u8
1671            };
1672            ladder.add(order, flags);
1673        }
1674
1675        assert_eq!(ladder.len(), 1);
1676        assert_eq!(
1677            ladder.top().unwrap().price.value,
1678            Price::from(expected2),
1679            "After batch 2: batch 1 data cleared"
1680        );
1681    }
1682
1683    #[rstest]
1684    fn test_l1_zero_size_clears_regardless_of_order_id() {
1685        // Regression test: Zero-size clears must work even when order_id
1686        // differs between F_MBP batch (price-hash ID) and clear (side-constant ID)
1687        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1688
1689        // Add order with F_MBP flags (uses price-hash order_id via pre_process_order)
1690        let batch_flags = RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8;
1691        let order = BookOrder {
1692            side: OrderSide::Buy.into(),
1693            price: Price::from("100.00"),
1694            size: Quantity::from(50),
1695            order_id: 12345, // Price-hash ID
1696        };
1697        ladder.add(order, batch_flags);
1698        assert_eq!(ladder.len(), 1);
1699
1700        // Clear with zero-size and different order_id (side-constant)
1701        let clear_order = BookOrder {
1702            side: OrderSide::Buy.into(),
1703            price: Price::from("100.00"),
1704            size: Quantity::zero(9),
1705            order_id: OrderSide::Buy as u64, // Side-constant ID (different!)
1706        };
1707        ladder.add(clear_order, 0);
1708
1709        // Should be cleared despite order_id mismatch
1710        assert_eq!(
1711            ladder.len(),
1712            0,
1713            "Zero-size should clear L1 regardless of order_id"
1714        );
1715        assert!(ladder.cache.is_empty(), "Cache should be empty after clear");
1716    }
1717
1718    #[rstest]
1719    fn test_l1_f_mbp_without_f_last_does_not_accumulate() {
1720        // F_MBP without F_LAST: each message clears, preventing stale prices.
1721        // This allows prices to degrade when the market moves.
1722        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1723        let flags = RecordFlag::F_MBP as u8; // No F_LAST
1724
1725        // Prices descending from 100 to 91 (simulates degrading market)
1726        let prices = [
1727            "100.00", "99.00", "98.00", "97.00", "96.00", "95.00", "94.00", "93.00", "92.00",
1728            "91.00",
1729        ];
1730
1731        for (i, price_str) in prices.iter().enumerate() {
1732            let order = BookOrder {
1733                side: OrderSide::Buy.into(),
1734                price: Price::from(*price_str),
1735                size: Quantity::from(10),
1736                order_id: (i + 100) as u64,
1737            };
1738            ladder.add(order, flags);
1739
1740            assert_eq!(
1741                ladder.len(),
1742                1,
1743                "L1 should always have at most 1 level, iteration {i}"
1744            );
1745        }
1746
1747        // Final price should be 91 (the last added), not 100 (the best ever seen)
1748        assert_eq!(
1749            ladder.top().unwrap().price.value,
1750            Price::from("91.00"),
1751            "Should show last price (91), allowing degradation"
1752        );
1753    }
1754
1755    #[rstest]
1756    fn test_l1_f_mbp_two_delta_batch_retains_best() {
1757        // A 2-delta batch (F_MBP then F_MBP|F_LAST) accumulates both and keeps best
1758        let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L1_MBP);
1759
1760        // Delta 1 (F_MBP only): clears, adds 100, sets in_l1_batch=true
1761        let order1 = BookOrder {
1762            side: OrderSide::Sell.into(),
1763            price: Price::from("100.00"),
1764            size: Quantity::from(10),
1765            order_id: 100,
1766        };
1767        ladder.add(order1, RecordFlag::F_MBP as u8);
1768
1769        // Delta 2 (F_MBP|F_LAST): in_l1_batch=true so doesn't clear,
1770        // adds 101, now has 100+101, retain_best → 100
1771        let order2 = BookOrder {
1772            side: OrderSide::Sell.into(),
1773            price: Price::from("101.00"),
1774            size: Quantity::from(20),
1775            order_id: 101,
1776        };
1777        ladder.add(order2, RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8);
1778
1779        assert_eq!(ladder.len(), 1);
1780        assert_eq!(
1781            ladder.top().unwrap().price.value,
1782            Price::from("100.00"),
1783            "2-delta batch keeps best ask (100) from both deltas"
1784        );
1785    }
1786
1787    #[rstest]
1788    fn test_l1_snapshot_batch_accumulates_all_levels_bids() {
1789        // F_SNAPSHOT batch accumulates ALL levels and keeps best bid
1790        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1791        let prices = ["98.00", "99.00", "100.00", "101.00"];
1792        let batch_size = prices.len();
1793
1794        for (i, price_str) in prices.iter().enumerate() {
1795            let order = BookOrder {
1796                side: OrderSide::Buy.into(),
1797                price: Price::from(*price_str),
1798                size: Quantity::from(10),
1799                order_id: (i + 100) as u64,
1800            };
1801            let flags = if i == batch_size - 1 {
1802                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1803            } else {
1804                RecordFlag::F_SNAPSHOT as u8
1805            };
1806            ladder.add(order, flags);
1807        }
1808
1809        assert_eq!(
1810            ladder.len(),
1811            1,
1812            "L1 should have only 1 level after snapshot"
1813        );
1814        assert_eq!(
1815            ladder.top().unwrap().price.value,
1816            Price::from("101.00"),
1817            "F_SNAPSHOT batch should keep best bid (101) from ALL deltas"
1818        );
1819    }
1820
1821    #[rstest]
1822    fn test_l1_snapshot_batch_accumulates_all_levels_asks() {
1823        // F_SNAPSHOT batch accumulates ALL levels and keeps best ask
1824        let mut ladder = BookLadder::new(OrderSide::Sell, BookType::L1_MBP);
1825        let prices = ["104.00", "103.00", "102.00", "101.00"];
1826        let batch_size = prices.len();
1827
1828        for (i, price_str) in prices.iter().enumerate() {
1829            let order = BookOrder {
1830                side: OrderSide::Sell.into(),
1831                price: Price::from(*price_str),
1832                size: Quantity::from(10),
1833                order_id: (i + 100) as u64,
1834            };
1835            let flags = if i == batch_size - 1 {
1836                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1837            } else {
1838                RecordFlag::F_SNAPSHOT as u8
1839            };
1840            ladder.add(order, flags);
1841        }
1842
1843        assert_eq!(
1844            ladder.len(),
1845            1,
1846            "L1 should have only 1 level after snapshot"
1847        );
1848        assert_eq!(
1849            ladder.top().unwrap().price.value,
1850            Price::from("101.00"),
1851            "F_SNAPSHOT batch should keep best ask (101) from ALL deltas"
1852        );
1853    }
1854
1855    #[rstest]
1856    #[case::bids(OrderSide::Buy, ["101.00", "100.00", "99.00"], "101.00")]
1857    #[case::asks(OrderSide::Sell, ["99.00", "100.00", "101.00"], "99.00")]
1858    fn test_l1_snapshot_batch_keeps_best_when_delivered_first(
1859        #[case] side: OrderSide,
1860        #[case] prices: [&str; 3],
1861        #[case] expected_best: &str,
1862    ) {
1863        let mut ladder = BookLadder::new(side, BookType::L1_MBP);
1864        let batch_size = prices.len();
1865
1866        for (i, price_str) in prices.iter().enumerate() {
1867            let order = BookOrder {
1868                side: side.into(),
1869                price: Price::from(*price_str),
1870                size: Quantity::from(10),
1871                order_id: side as u64,
1872            };
1873
1874            let flags = if i == batch_size - 1 {
1875                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1876            } else {
1877                RecordFlag::F_SNAPSHOT as u8
1878            };
1879
1880            ladder.add(order, flags);
1881        }
1882
1883        assert_eq!(ladder.len(), 1);
1884        assert_eq!(
1885            ladder.top().unwrap().price.value,
1886            Price::from(expected_best)
1887        );
1888        assert_eq!(ladder.cache.len(), 1);
1889    }
1890
1891    #[rstest]
1892    fn test_display() {
1893        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L3_MBO);
1894        ladder.add_bulk(&[
1895            BookOrder::new(OrderSide::Buy, Price::from("100.00"), Quantity::from(10), 1),
1896            BookOrder::new(OrderSide::Buy, Price::from("100.00"), Quantity::from(20), 2),
1897            BookOrder::new(OrderSide::Buy, Price::from("99.00"), Quantity::from(30), 3),
1898        ]);
1899
1900        assert_eq!(
1901            ladder.to_string(),
1902            "BookLadder(side=BUY)\n  100.00 -> 2 orders\n  99.00 -> 1 orders\n"
1903        );
1904    }
1905
1906    #[rstest]
1907    fn test_batch_state_code_round_trips_on_l1_ladder(#[values(0, 1, 2)] code: u8) {
1908        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1909
1910        ladder.set_batch_state_code(code).unwrap();
1911
1912        assert_eq!(ladder.batch_state_code(), code);
1913    }
1914
1915    #[rstest]
1916    #[case::unknown_code(BookType::L1_MBP, 3, "Invalid L1 batch state code: 3")]
1917    #[case::non_l1_batch(
1918        BookType::L2_MBP,
1919        1,
1920        "Cannot restore L1 batch state for book type L2_MBP"
1921    )]
1922    fn test_set_batch_state_code_rejects_invalid_state(
1923        #[case] book_type: BookType,
1924        #[case] code: u8,
1925        #[case] message: &str,
1926    ) {
1927        let mut ladder = BookLadder::new(OrderSide::Buy, book_type);
1928
1929        let result = ladder.set_batch_state_code(code);
1930
1931        assert_eq!(
1932            result,
1933            Err(CorrectnessError::PredicateViolation {
1934                message: message.to_string(),
1935            })
1936        );
1937        assert_eq!(ladder.batch_state_code(), 0);
1938    }
1939
1940    #[rstest]
1941    fn test_set_batch_state_code_accepts_no_batch_on_non_l1_ladder() {
1942        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L2_MBP);
1943
1944        assert_eq!(ladder.set_batch_state_code(0), Ok(()));
1945        assert_eq!(ladder.batch_state_code(), 0);
1946    }
1947
1948    #[rstest]
1949    fn test_l1_snapshot_vs_mbp_different_accumulation_behavior() {
1950        // F_SNAPSHOT accumulates all levels, F_MBP only accumulates final two
1951        let mut mbp_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1952        let prices = ["98.00", "99.00", "100.00", "101.00"];
1953        for (i, price_str) in prices.iter().enumerate() {
1954            let order = BookOrder {
1955                side: OrderSide::Buy.into(),
1956                price: Price::from(*price_str),
1957                size: Quantity::from(10),
1958                order_id: (i + 100) as u64,
1959            };
1960            let flags = if i == prices.len() - 1 {
1961                RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8
1962            } else {
1963                RecordFlag::F_MBP as u8
1964            };
1965            mbp_ladder.add(order, flags);
1966        }
1967        assert_eq!(
1968            mbp_ladder.top().unwrap().price.value,
1969            Price::from("101.00"),
1970            "F_MBP keeps best of final two (100, 101)"
1971        );
1972
1973        let mut snapshot_ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
1974
1975        for (i, price_str) in prices.iter().enumerate() {
1976            let order = BookOrder {
1977                side: OrderSide::Buy.into(),
1978                price: Price::from(*price_str),
1979                size: Quantity::from(10),
1980                order_id: (i + 200) as u64,
1981            };
1982            let flags = if i == prices.len() - 1 {
1983                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
1984            } else {
1985                RecordFlag::F_SNAPSHOT as u8
1986            };
1987            snapshot_ladder.add(order, flags);
1988        }
1989        assert_eq!(
1990            snapshot_ladder.top().unwrap().price.value,
1991            Price::from("101.00"),
1992            "F_SNAPSHOT keeps best of ALL deltas (98, 99, 100, 101)"
1993        );
1994    }
1995
1996    #[rstest]
1997    fn test_l1_snapshot_after_incomplete_mbp_stream() {
1998        // Snapshot must clear stale state from incomplete F_MBP stream (no F_LAST sent)
1999        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
2000
2001        // Incomplete F_MBP stream leaves stale batch state
2002        let stale_order = BookOrder {
2003            side: OrderSide::Buy.into(),
2004            price: Price::from("101.00"),
2005            size: Quantity::from(10),
2006            order_id: 100,
2007        };
2008        ladder.add(stale_order, RecordFlag::F_MBP as u8);
2009        assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
2010
2011        // Snapshot arrives with Clear delta first
2012        ladder.clear();
2013
2014        // Snapshot prices worse than stale 101
2015        for (i, price_str) in ["98.00", "99.00", "100.00"].iter().enumerate() {
2016            let order = BookOrder {
2017                side: OrderSide::Buy.into(),
2018                price: Price::from(*price_str),
2019                size: Quantity::from(10),
2020                order_id: (i + 200) as u64,
2021            };
2022            let flags = if i == 2 {
2023                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
2024            } else {
2025                RecordFlag::F_SNAPSHOT as u8
2026            };
2027            ladder.add(order, flags);
2028        }
2029
2030        assert_eq!(
2031            ladder.top().unwrap().price.value,
2032            Price::from("100.00"),
2033            "Snapshot replaces stale MBP state: best is 100, not stale 101"
2034        );
2035    }
2036
2037    #[rstest]
2038    fn test_l1_snapshot_clears_previous_batch() {
2039        // New F_SNAPSHOT batch clears previous batch
2040        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
2041
2042        for (i, price_str) in ["100.00", "101.00", "102.00"].iter().enumerate() {
2043            let order = BookOrder {
2044                side: OrderSide::Buy.into(),
2045                price: Price::from(*price_str),
2046                size: Quantity::from(10),
2047                order_id: (i + 100) as u64,
2048            };
2049            let flags = if i == 2 {
2050                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
2051            } else {
2052                RecordFlag::F_SNAPSHOT as u8
2053            };
2054            ladder.add(order, flags);
2055        }
2056        assert_eq!(ladder.top().unwrap().price.value, Price::from("102.00"));
2057
2058        // Second batch with worse prices
2059        for (i, price_str) in ["95.00", "96.00", "97.00"].iter().enumerate() {
2060            let order = BookOrder {
2061                side: OrderSide::Buy.into(),
2062                price: Price::from(*price_str),
2063                size: Quantity::from(20),
2064                order_id: (i + 200) as u64,
2065            };
2066            let flags = if i == 2 {
2067                RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8
2068            } else {
2069                RecordFlag::F_SNAPSHOT as u8
2070            };
2071            ladder.add(order, flags);
2072        }
2073        assert_eq!(
2074            ladder.top().unwrap().price.value,
2075            Price::from("97.00"),
2076            "Second batch clears first: best is 97, not 102"
2077        );
2078    }
2079
2080    #[rstest]
2081    fn test_l1_single_delta_snapshot_after_mbp_batch() {
2082        // Single-delta snapshot (F_SNAPSHOT|F_LAST) must clear stale MBP batch state
2083        let mut ladder = BookLadder::new(OrderSide::Buy, BookType::L1_MBP);
2084
2085        let mbp_order1 = BookOrder {
2086            side: OrderSide::Buy.into(),
2087            price: Price::from("100.00"),
2088            size: Quantity::from(10),
2089            order_id: 1,
2090        };
2091        let mbp_order2 = BookOrder {
2092            side: OrderSide::Buy.into(),
2093            price: Price::from("101.00"),
2094            size: Quantity::from(10),
2095            order_id: 2,
2096        };
2097        ladder.add(mbp_order1, RecordFlag::F_MBP as u8);
2098        ladder.add(
2099            mbp_order2,
2100            RecordFlag::F_MBP as u8 | RecordFlag::F_LAST as u8,
2101        );
2102
2103        assert_eq!(ladder.top().unwrap().price.value, Price::from("101.00"));
2104
2105        // Single-delta snapshot at worse price (no preceding Clear)
2106        let snapshot_order = BookOrder {
2107            side: OrderSide::Buy.into(),
2108            price: Price::from("95.00"),
2109            size: Quantity::from(20),
2110            order_id: 100,
2111        };
2112        ladder.add(
2113            snapshot_order,
2114            RecordFlag::F_SNAPSHOT as u8 | RecordFlag::F_LAST as u8,
2115        );
2116
2117        assert_eq!(
2118            ladder.top().unwrap().price.value,
2119            Price::from("95.00"),
2120            "Single-delta snapshot clears MBP state: best is 95, not stale 101"
2121        );
2122        assert_eq!(ladder.len(), 1);
2123    }
2124}