Skip to main content

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