Skip to main content

nautilus_model/orderbook/
ladder.rs

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