nautilus_model/orderbook/
level.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 discrete price level in an order book.
17
18use std::cmp::Ordering;
19
20use indexmap::IndexMap;
21use nautilus_core::UnixNanos;
22use rust_decimal::Decimal;
23
24use crate::{
25    data::order::{BookOrder, OrderId},
26    enums::OrderSideSpecified,
27    orderbook::{BookIntegrityError, BookPrice},
28    types::{fixed::FIXED_SCALAR, quantity::QuantityRaw},
29};
30
31/// Represents a discrete price level in an order book.
32///
33/// Orders are stored in an [`IndexMap`] which preserves FIFO (insertion) order.
34#[derive(Clone, Debug, Eq)]
35#[cfg_attr(
36    feature = "python",
37    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
38)]
39pub struct BookLevel {
40    pub price: BookPrice,
41    pub(crate) orders: IndexMap<OrderId, BookOrder>,
42}
43
44impl BookLevel {
45    /// Creates a new [`BookLevel`] instance.
46    #[must_use]
47    pub fn new(price: BookPrice) -> Self {
48        Self {
49            price,
50            orders: IndexMap::new(),
51        }
52    }
53
54    /// Creates a new [`BookLevel`] from an order, using the order's price and side.
55    #[must_use]
56    pub fn from_order(order: BookOrder) -> Self {
57        let mut level = Self {
58            price: order.to_book_price(),
59            orders: IndexMap::new(),
60        };
61        level.add(order);
62        level
63    }
64
65    pub fn side(&self) -> OrderSideSpecified {
66        self.price.side
67    }
68
69    /// Returns the number of orders at this price level.
70    #[must_use]
71    pub fn len(&self) -> usize {
72        self.orders.len()
73    }
74
75    /// Returns true if this price level has no orders.
76    #[must_use]
77    pub fn is_empty(&self) -> bool {
78        self.orders.is_empty()
79    }
80
81    /// Returns a reference to the first order at this price level in FIFO order.
82    #[inline]
83    #[must_use]
84    pub fn first(&self) -> Option<&BookOrder> {
85        self.orders.get_index(0).map(|(_key, order)| order)
86    }
87
88    /// Returns an iterator over the orders at this price level in FIFO order.
89    pub fn iter(&self) -> impl Iterator<Item = &BookOrder> {
90        self.orders.values()
91    }
92
93    /// Returns all orders at this price level in FIFO insertion order.
94    #[must_use]
95    pub fn get_orders(&self) -> Vec<BookOrder> {
96        self.orders.values().copied().collect()
97    }
98
99    /// Returns the total size of all orders at this price level as a float.
100    #[must_use]
101    pub fn size(&self) -> f64 {
102        self.orders.values().map(|o| o.size.as_f64()).sum()
103    }
104
105    /// Returns the total size of all orders at this price level as raw integer units.
106    #[must_use]
107    pub fn size_raw(&self) -> QuantityRaw {
108        self.orders.values().map(|o| o.size.raw).sum()
109    }
110
111    /// Returns the total size of all orders at this price level as a decimal.
112    #[must_use]
113    pub fn size_decimal(&self) -> Decimal {
114        self.orders.values().map(|o| o.size.as_decimal()).sum()
115    }
116
117    /// Returns the total exposure (price * size) of all orders at this price level as a float.
118    #[must_use]
119    pub fn exposure(&self) -> f64 {
120        self.orders
121            .values()
122            .map(|o| o.price.as_f64() * o.size.as_f64())
123            .sum()
124    }
125
126    /// Returns the total exposure (price * size) of all orders at this price level as raw integer units.
127    ///
128    /// Saturates at `QuantityRaw::MAX` if the total exposure would overflow.
129    #[must_use]
130    pub fn exposure_raw(&self) -> QuantityRaw {
131        self.orders
132            .values()
133            .map(|o| {
134                let exposure_f64 = o.price.as_f64() * o.size.as_f64();
135                debug_assert!(
136                    exposure_f64.is_finite(),
137                    "Exposure calculation resulted in non-finite value for order {}: price={}, size={}",
138                    o.order_id,
139                    o.price,
140                    o.size
141                );
142
143                let scaled = exposure_f64 * FIXED_SCALAR;
144                if scaled >= QuantityRaw::MAX as f64 {
145                    QuantityRaw::MAX
146                } else if scaled < 0.0 {
147                    0
148                } else {
149                    scaled as QuantityRaw
150                }
151            })
152            .fold(0, |acc, val| acc.saturating_add(val))
153    }
154
155    /// Adds multiple orders to this price level in FIFO order. Orders must match the level's price.
156    pub fn add_bulk(&mut self, orders: Vec<BookOrder>) {
157        for order in orders {
158            self.add(order);
159        }
160    }
161
162    /// Adds an order to this price level. Order must match the level's price.
163    pub fn add(&mut self, order: BookOrder) {
164        debug_assert_eq!(order.price, self.price.value);
165
166        if !order.size.is_positive() {
167            log::warn!(
168                "Attempted to add order with non-positive size: order_id={order_id}, size={size}, ignoring",
169                order_id = order.order_id,
170                size = order.size
171            );
172            return;
173        }
174
175        self.orders.insert(order.order_id, order);
176    }
177
178    /// Updates an existing order at this price level. Updated order must match the level's price.
179    /// Removes the order if size becomes zero.
180    pub fn update(&mut self, order: BookOrder) {
181        debug_assert_eq!(order.price, self.price.value);
182
183        if order.size.raw == 0 {
184            // Updating non-existent order to zero size is a no-op, which is valid
185            self.orders.shift_remove(&order.order_id);
186        } else {
187            debug_assert!(
188                order.size.is_positive(),
189                "Order size must be positive: {}",
190                order.size
191            );
192            self.orders.insert(order.order_id, order);
193        }
194    }
195
196    /// Deletes an order from this price level.
197    pub fn delete(&mut self, order: &BookOrder) {
198        self.orders.shift_remove(&order.order_id);
199    }
200
201    /// Removes an order by its ID.
202    ///
203    /// # Panics
204    ///
205    /// Panics if no order with the given `order_id` exists at this level.
206    pub fn remove_by_id(&mut self, order_id: OrderId, sequence: u64, ts_event: UnixNanos) {
207        assert!(
208            self.orders.shift_remove(&order_id).is_some(),
209            "{}",
210            &BookIntegrityError::OrderNotFound(order_id, sequence, ts_event)
211        );
212    }
213}
214
215impl PartialEq for BookLevel {
216    fn eq(&self, other: &Self) -> bool {
217        self.price == other.price
218    }
219}
220
221impl PartialOrd for BookLevel {
222    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
223        Some(self.cmp(other))
224    }
225}
226
227impl Ord for BookLevel {
228    fn cmp(&self, other: &Self) -> Ordering {
229        self.price.cmp(&other.price)
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use rstest::rstest;
236    use rust_decimal_macros::dec;
237
238    use crate::{
239        data::order::BookOrder,
240        enums::{OrderSide, OrderSideSpecified},
241        orderbook::{BookLevel, BookPrice},
242        types::{Price, Quantity, fixed::FIXED_SCALAR, quantity::QuantityRaw},
243    };
244
245    #[rstest]
246    fn test_empty_level() {
247        let level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
248        assert!(level.first().is_none());
249        assert_eq!(level.side(), OrderSideSpecified::Buy);
250    }
251
252    #[rstest]
253    fn test_level_from_order() {
254        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
255        let level = BookLevel::from_order(order);
256
257        assert_eq!(level.price.value, Price::from("1.00"));
258        assert_eq!(level.price.side, OrderSideSpecified::Buy);
259        assert_eq!(level.len(), 1);
260        assert_eq!(level.first().unwrap(), &order);
261        assert_eq!(level.size(), 10.0);
262    }
263
264    #[rstest]
265    #[should_panic(expected = "assertion `left == right` failed")]
266    fn test_add_order_incorrect_price_level() {
267        let mut level =
268            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
269        let incorrect_price_order =
270            BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 1);
271        level.add(incorrect_price_order);
272    }
273
274    #[rstest]
275    #[should_panic(expected = "assertion `left == right` failed")]
276    fn test_add_bulk_orders_incorrect_price() {
277        let mut level =
278            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
279        let orders = vec![
280            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1),
281            BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 2), // Incorrect price
282        ];
283        level.add_bulk(orders);
284    }
285
286    #[rstest]
287    fn test_add_bulk_empty() {
288        let mut level =
289            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
290        level.add_bulk(vec![]);
291        assert!(level.is_empty());
292    }
293
294    #[rstest]
295    fn test_comparisons_bid_side() {
296        let level0 = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
297        let level1 = BookLevel::new(BookPrice::new(Price::from("1.01"), OrderSideSpecified::Buy));
298        assert_eq!(level0, level0);
299        assert!(level0 > level1);
300    }
301
302    #[rstest]
303    fn test_comparisons_ask_side() {
304        let level0 = BookLevel::new(BookPrice::new(
305            Price::from("1.00"),
306            OrderSideSpecified::Sell,
307        ));
308        let level1 = BookLevel::new(BookPrice::new(
309            Price::from("1.01"),
310            OrderSideSpecified::Sell,
311        ));
312        assert_eq!(level0, level0);
313        assert!(level0 < level1);
314    }
315
316    #[rstest]
317    fn test_book_level_sorting() {
318        let mut levels = [
319            BookLevel::new(BookPrice::new(
320                Price::from("1.00"),
321                OrderSideSpecified::Sell,
322            )),
323            BookLevel::new(BookPrice::new(
324                Price::from("1.02"),
325                OrderSideSpecified::Sell,
326            )),
327            BookLevel::new(BookPrice::new(
328                Price::from("1.01"),
329                OrderSideSpecified::Sell,
330            )),
331        ];
332        levels.sort();
333        assert_eq!(levels[0].price.value, Price::from("1.00"));
334        assert_eq!(levels[1].price.value, Price::from("1.01"));
335        assert_eq!(levels[2].price.value, Price::from("1.02"));
336    }
337
338    #[rstest]
339    fn test_add_single_order() {
340        let mut level =
341            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
342        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
343
344        level.add(order);
345        assert!(!level.is_empty());
346        assert_eq!(level.len(), 1);
347        assert_eq!(level.size(), 10.0);
348        assert_eq!(level.first().unwrap(), &order);
349    }
350
351    #[rstest]
352    fn test_add_multiple_orders() {
353        let mut level =
354            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
355        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
356        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
357
358        level.add(order1);
359        level.add(order2);
360        assert_eq!(level.len(), 2);
361        assert_eq!(level.size(), 30.0);
362        assert_eq!(level.exposure(), 60.0);
363        assert_eq!(level.first().unwrap(), &order1);
364    }
365
366    #[rstest]
367    fn test_get_orders() {
368        let mut level =
369            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
370        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
371        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
372
373        level.add(order1);
374        level.add(order2);
375
376        let orders = level.get_orders();
377        assert_eq!(orders.len(), 2);
378        assert_eq!(orders[0], order1); // Checks FIFO order maintained
379        assert_eq!(orders[1], order2);
380    }
381
382    #[rstest]
383    fn test_iter_returns_fifo() {
384        let mut level =
385            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
386        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
387        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
388        level.add(order1);
389        level.add(order2);
390
391        let orders: Vec<_> = level.iter().copied().collect();
392        assert_eq!(orders, vec![order1, order2]);
393    }
394
395    #[rstest]
396    fn test_update_order() {
397        let mut level =
398            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
399        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
400        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 0);
401
402        level.add(order1);
403        level.update(order2);
404        assert_eq!(level.len(), 1);
405        assert_eq!(level.size(), 20.0);
406        assert_eq!(level.exposure(), 20.0);
407    }
408
409    #[rstest]
410    fn test_update_inserts_if_missing() {
411        let mut level =
412            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
413        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
414        level.update(order);
415        assert_eq!(level.len(), 1);
416        assert_eq!(level.first().unwrap(), &order);
417    }
418
419    #[rstest]
420    fn test_update_zero_size_nonexistent() {
421        let mut level =
422            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
423        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::zero(0), 1);
424        level.update(order);
425        assert_eq!(level.len(), 0);
426    }
427
428    #[rstest]
429    fn test_fifo_order_after_updates() {
430        let mut level =
431            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
432
433        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
434        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
435
436        level.add(order1);
437        level.add(order2);
438
439        // Update order1 size
440        let updated_order1 =
441            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(15), 1);
442        level.update(updated_order1);
443
444        let orders = level.get_orders();
445        assert_eq!(orders.len(), 2);
446        assert_eq!(orders[0], updated_order1); // First order still first
447        assert_eq!(orders[1], order2); // Second order still second
448    }
449
450    #[rstest]
451    fn test_insertion_order_after_mixed_operations() {
452        let mut level =
453            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
454        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
455        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
456        let order3 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(30), 3);
457
458        level.add(order1);
459        level.add(order2);
460        level.add(order3);
461
462        // Update order2 (should keep its position)
463        let updated_order2 =
464            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(25), 2);
465        level.update(updated_order2);
466
467        // Remove order1; order2 (updated) should now be first
468        level.delete(&order1);
469
470        let orders = level.get_orders();
471        assert_eq!(orders, vec![updated_order2, order3]);
472    }
473
474    #[rstest]
475    #[should_panic(expected = "assertion `left == right` failed")]
476    fn test_update_order_incorrect_price() {
477        let mut level =
478            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
479
480        // Add initial order at correct price level
481        let initial_order =
482            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
483        level.add(initial_order);
484
485        // Attempt to update with order at incorrect price level
486        let updated_order =
487            BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
488        level.update(updated_order);
489    }
490
491    #[rstest]
492    fn test_update_order_with_zero_size() {
493        let mut level =
494            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
495        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
496        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::zero(0), 0);
497
498        level.add(order1);
499        level.update(order2);
500        assert_eq!(level.len(), 0);
501        assert_eq!(level.size(), 0.0);
502        assert_eq!(level.exposure(), 0.0);
503    }
504
505    #[rstest]
506    fn test_delete_nonexistent_order() {
507        let mut level =
508            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
509        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
510        level.delete(&order);
511        assert_eq!(level.len(), 0);
512    }
513
514    #[rstest]
515    fn test_delete_order() {
516        let mut level =
517            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
518        let order1_id = 0;
519        let order1 = BookOrder::new(
520            OrderSide::Buy,
521            Price::from("1.00"),
522            Quantity::from(10),
523            order1_id,
524        );
525        let order2_id = 1;
526        let order2 = BookOrder::new(
527            OrderSide::Buy,
528            Price::from("1.00"),
529            Quantity::from(20),
530            order2_id,
531        );
532
533        level.add(order1);
534        level.add(order2);
535        level.delete(&order1);
536        assert_eq!(level.len(), 1);
537        assert_eq!(level.size(), 20.0);
538        assert!(level.orders.contains_key(&order2_id));
539        assert_eq!(level.exposure(), 20.0);
540    }
541
542    #[rstest]
543    fn test_remove_order_by_id() {
544        let mut level =
545            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
546        let order1_id = 0;
547        let order1 = BookOrder::new(
548            OrderSide::Buy,
549            Price::from("1.00"),
550            Quantity::from(10),
551            order1_id,
552        );
553        let order2_id = 1;
554        let order2 = BookOrder::new(
555            OrderSide::Buy,
556            Price::from("1.00"),
557            Quantity::from(20),
558            order2_id,
559        );
560
561        level.add(order1);
562        level.add(order2);
563        level.remove_by_id(order2_id, 0, 0.into());
564        assert_eq!(level.len(), 1);
565        assert!(level.orders.contains_key(&order1_id));
566        assert_eq!(level.size(), 10.0);
567        assert_eq!(level.exposure(), 10.0);
568    }
569
570    #[rstest]
571    fn test_add_bulk_orders() {
572        let mut level =
573            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
574        let order1_id = 0;
575        let order1 = BookOrder::new(
576            OrderSide::Buy,
577            Price::from("2.00"),
578            Quantity::from(10),
579            order1_id,
580        );
581        let order2_id = 1;
582        let order2 = BookOrder::new(
583            OrderSide::Buy,
584            Price::from("2.00"),
585            Quantity::from(20),
586            order2_id,
587        );
588
589        let orders = vec![order1, order2];
590        level.add_bulk(orders);
591        assert_eq!(level.len(), 2);
592        assert_eq!(level.size(), 30.0);
593        assert_eq!(level.exposure(), 60.0);
594    }
595
596    #[rstest]
597    fn test_maximum_order_id() {
598        let mut level =
599            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
600
601        let order = BookOrder::new(
602            OrderSide::Buy,
603            Price::from("1.00"),
604            Quantity::from(10),
605            u64::MAX,
606        );
607        level.add(order);
608
609        assert_eq!(level.len(), 1);
610        assert_eq!(level.first().unwrap(), &order);
611    }
612
613    #[rstest]
614    #[should_panic(
615        expected = "Integrity error: order not found: order_id=1, sequence=2, ts_event=3"
616    )]
617    fn test_remove_nonexistent_order() {
618        let mut level =
619            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
620        level.remove_by_id(1, 2, 3.into());
621    }
622
623    #[rstest]
624    fn test_size() {
625        let mut level =
626            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
627        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
628        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(15), 1);
629
630        level.add(order1);
631        level.add(order2);
632        assert_eq!(level.size(), 25.0);
633    }
634
635    #[rstest]
636    fn test_size_raw() {
637        let mut level =
638            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
639        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
640        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
641
642        level.add(order1);
643        level.add(order2);
644        assert_eq!(
645            level.size_raw(),
646            (30.0 * FIXED_SCALAR).round() as QuantityRaw
647        );
648    }
649
650    #[rstest]
651    fn test_size_decimal() {
652        let mut level =
653            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
654        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
655        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
656
657        level.add(order1);
658        level.add(order2);
659        assert_eq!(level.size_decimal(), dec!(30.0));
660    }
661
662    #[rstest]
663    fn test_exposure() {
664        let mut level =
665            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
666        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
667        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
668
669        level.add(order1);
670        level.add(order2);
671        assert_eq!(level.exposure(), 60.0);
672    }
673
674    #[rstest]
675    fn test_exposure_raw() {
676        let mut level =
677            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
678        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
679        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
680
681        level.add(order1);
682        level.add(order2);
683        assert_eq!(
684            level.exposure_raw(),
685            (60.0 * FIXED_SCALAR).round() as QuantityRaw
686        );
687    }
688
689    #[rstest]
690    fn test_exposure_raw_saturates_on_overflow() {
691        // Test that exposure_raw saturates at QuantityRaw::MAX instead of wrapping
692        // Use values whose product * FIXED_SCALAR overflows QuantityRaw
693        #[cfg(feature = "high-precision")]
694        let (price_str, qty_str) = ("1000000000000.00", "1000000000000.00");
695        #[cfg(not(feature = "high-precision"))]
696        let (price_str, qty_str) = ("100000000.00", "1000000000.00");
697
698        let mut level = BookLevel::new(BookPrice::new(
699            Price::from(price_str),
700            OrderSideSpecified::Buy,
701        ));
702
703        // Create an order with large price and quantity that would overflow QuantityRaw
704        let order = BookOrder::new(
705            OrderSide::Buy,
706            Price::from(price_str),
707            Quantity::from(qty_str),
708            0,
709        );
710
711        level.add(order);
712
713        // Should saturate at max value instead of wrapping around
714        let result = level.exposure_raw();
715        assert_eq!(result, QuantityRaw::MAX);
716    }
717
718    #[rstest]
719    fn test_exposure_raw_sum_saturates_on_overflow() {
720        // Test that summing exposures saturates instead of wrapping
721        #[cfg(feature = "high-precision")]
722        let (price_str, qty_str, count) = ("10000000000000.00", "10000000000000.00", 100);
723        #[cfg(not(feature = "high-precision"))]
724        let (price_str, qty_str, count) = ("1000000000.00", "1000000000.00", 100);
725
726        let mut level = BookLevel::new(BookPrice::new(
727            Price::from(price_str),
728            OrderSideSpecified::Buy,
729        ));
730
731        // Add multiple large orders that together would overflow when summed
732        for i in 0..count {
733            let order = BookOrder::new(
734                OrderSide::Buy,
735                Price::from(price_str),
736                Quantity::from(qty_str),
737                i,
738            );
739            level.add(order);
740        }
741
742        // Should saturate at max value instead of wrapping around
743        let result = level.exposure_raw();
744        assert_eq!(result, QuantityRaw::MAX);
745    }
746}