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////////////////////////////////////////////////////////////////////////////////
234// Tests
235////////////////////////////////////////////////////////////////////////////////
236#[cfg(test)]
237mod tests {
238    use rstest::rstest;
239    use rust_decimal_macros::dec;
240
241    use crate::{
242        data::order::BookOrder,
243        enums::{OrderSide, OrderSideSpecified},
244        orderbook::{BookLevel, BookPrice},
245        types::{Price, Quantity, fixed::FIXED_SCALAR, quantity::QuantityRaw},
246    };
247
248    #[rstest]
249    fn test_empty_level() {
250        let level = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
251        assert!(level.first().is_none());
252        assert_eq!(level.side(), OrderSideSpecified::Buy);
253    }
254
255    #[rstest]
256    fn test_level_from_order() {
257        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
258        let level = BookLevel::from_order(order);
259
260        assert_eq!(level.price.value, Price::from("1.00"));
261        assert_eq!(level.price.side, OrderSideSpecified::Buy);
262        assert_eq!(level.len(), 1);
263        assert_eq!(level.first().unwrap(), &order);
264        assert_eq!(level.size(), 10.0);
265    }
266
267    #[rstest]
268    #[should_panic(expected = "assertion `left == right` failed")]
269    fn test_add_order_incorrect_price_level() {
270        let mut level =
271            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
272        let incorrect_price_order =
273            BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 1);
274        level.add(incorrect_price_order);
275    }
276
277    #[rstest]
278    #[should_panic(expected = "assertion `left == right` failed")]
279    fn test_add_bulk_orders_incorrect_price() {
280        let mut level =
281            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
282        let orders = vec![
283            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1),
284            BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 2), // Incorrect price
285        ];
286        level.add_bulk(orders);
287    }
288
289    #[rstest]
290    fn test_add_bulk_empty() {
291        let mut level =
292            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
293        level.add_bulk(vec![]);
294        assert!(level.is_empty());
295    }
296
297    #[rstest]
298    fn test_comparisons_bid_side() {
299        let level0 = BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
300        let level1 = BookLevel::new(BookPrice::new(Price::from("1.01"), OrderSideSpecified::Buy));
301        assert_eq!(level0, level0);
302        assert!(level0 > level1);
303    }
304
305    #[rstest]
306    fn test_comparisons_ask_side() {
307        let level0 = BookLevel::new(BookPrice::new(
308            Price::from("1.00"),
309            OrderSideSpecified::Sell,
310        ));
311        let level1 = BookLevel::new(BookPrice::new(
312            Price::from("1.01"),
313            OrderSideSpecified::Sell,
314        ));
315        assert_eq!(level0, level0);
316        assert!(level0 < level1);
317    }
318
319    #[rstest]
320    fn test_book_level_sorting() {
321        let mut levels = [
322            BookLevel::new(BookPrice::new(
323                Price::from("1.00"),
324                OrderSideSpecified::Sell,
325            )),
326            BookLevel::new(BookPrice::new(
327                Price::from("1.02"),
328                OrderSideSpecified::Sell,
329            )),
330            BookLevel::new(BookPrice::new(
331                Price::from("1.01"),
332                OrderSideSpecified::Sell,
333            )),
334        ];
335        levels.sort();
336        assert_eq!(levels[0].price.value, Price::from("1.00"));
337        assert_eq!(levels[1].price.value, Price::from("1.01"));
338        assert_eq!(levels[2].price.value, Price::from("1.02"));
339    }
340
341    #[rstest]
342    fn test_add_single_order() {
343        let mut level =
344            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
345        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
346
347        level.add(order);
348        assert!(!level.is_empty());
349        assert_eq!(level.len(), 1);
350        assert_eq!(level.size(), 10.0);
351        assert_eq!(level.first().unwrap(), &order);
352    }
353
354    #[rstest]
355    fn test_add_multiple_orders() {
356        let mut level =
357            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
358        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
359        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
360
361        level.add(order1);
362        level.add(order2);
363        assert_eq!(level.len(), 2);
364        assert_eq!(level.size(), 30.0);
365        assert_eq!(level.exposure(), 60.0);
366        assert_eq!(level.first().unwrap(), &order1);
367    }
368
369    #[rstest]
370    fn test_get_orders() {
371        let mut level =
372            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
373        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
374        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
375
376        level.add(order1);
377        level.add(order2);
378
379        let orders = level.get_orders();
380        assert_eq!(orders.len(), 2);
381        assert_eq!(orders[0], order1); // Checks FIFO order maintained
382        assert_eq!(orders[1], order2);
383    }
384
385    #[rstest]
386    fn test_iter_returns_fifo() {
387        let mut level =
388            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
389        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
390        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
391        level.add(order1);
392        level.add(order2);
393
394        let orders: Vec<_> = level.iter().copied().collect();
395        assert_eq!(orders, vec![order1, order2]);
396    }
397
398    #[rstest]
399    fn test_update_order() {
400        let mut level =
401            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
402        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
403        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 0);
404
405        level.add(order1);
406        level.update(order2);
407        assert_eq!(level.len(), 1);
408        assert_eq!(level.size(), 20.0);
409        assert_eq!(level.exposure(), 20.0);
410    }
411
412    #[rstest]
413    fn test_update_inserts_if_missing() {
414        let mut level =
415            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
416        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
417        level.update(order);
418        assert_eq!(level.len(), 1);
419        assert_eq!(level.first().unwrap(), &order);
420    }
421
422    #[rstest]
423    fn test_update_zero_size_nonexistent() {
424        let mut level =
425            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
426        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::zero(0), 1);
427        level.update(order);
428        assert_eq!(level.len(), 0);
429    }
430
431    #[rstest]
432    fn test_fifo_order_after_updates() {
433        let mut level =
434            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
435
436        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
437        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
438
439        level.add(order1);
440        level.add(order2);
441
442        // Update order1 size
443        let updated_order1 =
444            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(15), 1);
445        level.update(updated_order1);
446
447        let orders = level.get_orders();
448        assert_eq!(orders.len(), 2);
449        assert_eq!(orders[0], updated_order1); // First order still first
450        assert_eq!(orders[1], order2); // Second order still second
451    }
452
453    #[rstest]
454    fn test_insertion_order_after_mixed_operations() {
455        let mut level =
456            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
457        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
458        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(20), 2);
459        let order3 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(30), 3);
460
461        level.add(order1);
462        level.add(order2);
463        level.add(order3);
464
465        // Update order2 (should keep its position)
466        let updated_order2 =
467            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(25), 2);
468        level.update(updated_order2);
469
470        // Remove order1; order2 (updated) should now be first
471        level.delete(&order1);
472
473        let orders = level.get_orders();
474        assert_eq!(orders, vec![updated_order2, order3]);
475    }
476
477    #[rstest]
478    #[should_panic(expected = "assertion `left == right` failed")]
479    fn test_update_order_incorrect_price() {
480        let mut level =
481            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
482
483        // Add initial order at correct price level
484        let initial_order =
485            BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
486        level.add(initial_order);
487
488        // Attempt to update with order at incorrect price level
489        let updated_order =
490            BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
491        level.update(updated_order);
492    }
493
494    #[rstest]
495    fn test_update_order_with_zero_size() {
496        let mut level =
497            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
498        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
499        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::zero(0), 0);
500
501        level.add(order1);
502        level.update(order2);
503        assert_eq!(level.len(), 0);
504        assert_eq!(level.size(), 0.0);
505        assert_eq!(level.exposure(), 0.0);
506    }
507
508    #[rstest]
509    fn test_delete_nonexistent_order() {
510        let mut level =
511            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
512        let order = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 1);
513        level.delete(&order);
514        assert_eq!(level.len(), 0);
515    }
516
517    #[rstest]
518    fn test_delete_order() {
519        let mut level =
520            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
521        let order1_id = 0;
522        let order1 = BookOrder::new(
523            OrderSide::Buy,
524            Price::from("1.00"),
525            Quantity::from(10),
526            order1_id,
527        );
528        let order2_id = 1;
529        let order2 = BookOrder::new(
530            OrderSide::Buy,
531            Price::from("1.00"),
532            Quantity::from(20),
533            order2_id,
534        );
535
536        level.add(order1);
537        level.add(order2);
538        level.delete(&order1);
539        assert_eq!(level.len(), 1);
540        assert_eq!(level.size(), 20.0);
541        assert!(level.orders.contains_key(&order2_id));
542        assert_eq!(level.exposure(), 20.0);
543    }
544
545    #[rstest]
546    fn test_remove_order_by_id() {
547        let mut level =
548            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
549        let order1_id = 0;
550        let order1 = BookOrder::new(
551            OrderSide::Buy,
552            Price::from("1.00"),
553            Quantity::from(10),
554            order1_id,
555        );
556        let order2_id = 1;
557        let order2 = BookOrder::new(
558            OrderSide::Buy,
559            Price::from("1.00"),
560            Quantity::from(20),
561            order2_id,
562        );
563
564        level.add(order1);
565        level.add(order2);
566        level.remove_by_id(order2_id, 0, 0.into());
567        assert_eq!(level.len(), 1);
568        assert!(level.orders.contains_key(&order1_id));
569        assert_eq!(level.size(), 10.0);
570        assert_eq!(level.exposure(), 10.0);
571    }
572
573    #[rstest]
574    fn test_add_bulk_orders() {
575        let mut level =
576            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
577        let order1_id = 0;
578        let order1 = BookOrder::new(
579            OrderSide::Buy,
580            Price::from("2.00"),
581            Quantity::from(10),
582            order1_id,
583        );
584        let order2_id = 1;
585        let order2 = BookOrder::new(
586            OrderSide::Buy,
587            Price::from("2.00"),
588            Quantity::from(20),
589            order2_id,
590        );
591
592        let orders = vec![order1, order2];
593        level.add_bulk(orders);
594        assert_eq!(level.len(), 2);
595        assert_eq!(level.size(), 30.0);
596        assert_eq!(level.exposure(), 60.0);
597    }
598
599    #[rstest]
600    fn test_maximum_order_id() {
601        let mut level =
602            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
603
604        let order = BookOrder::new(
605            OrderSide::Buy,
606            Price::from("1.00"),
607            Quantity::from(10),
608            u64::MAX,
609        );
610        level.add(order);
611
612        assert_eq!(level.len(), 1);
613        assert_eq!(level.first().unwrap(), &order);
614    }
615
616    #[rstest]
617    #[should_panic(
618        expected = "Integrity error: order not found: order_id=1, sequence=2, ts_event=3"
619    )]
620    fn test_remove_nonexistent_order() {
621        let mut level =
622            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
623        level.remove_by_id(1, 2, 3.into());
624    }
625
626    #[rstest]
627    fn test_size() {
628        let mut level =
629            BookLevel::new(BookPrice::new(Price::from("1.00"), OrderSideSpecified::Buy));
630        let order1 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(10), 0);
631        let order2 = BookOrder::new(OrderSide::Buy, Price::from("1.00"), Quantity::from(15), 1);
632
633        level.add(order1);
634        level.add(order2);
635        assert_eq!(level.size(), 25.0);
636    }
637
638    #[rstest]
639    fn test_size_raw() {
640        let mut level =
641            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
642        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
643        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
644
645        level.add(order1);
646        level.add(order2);
647        assert_eq!(
648            level.size_raw(),
649            (30.0 * FIXED_SCALAR).round() as QuantityRaw
650        );
651    }
652
653    #[rstest]
654    fn test_size_decimal() {
655        let mut level =
656            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
657        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
658        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
659
660        level.add(order1);
661        level.add(order2);
662        assert_eq!(level.size_decimal(), dec!(30.0));
663    }
664
665    #[rstest]
666    fn test_exposure() {
667        let mut level =
668            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
669        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
670        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
671
672        level.add(order1);
673        level.add(order2);
674        assert_eq!(level.exposure(), 60.0);
675    }
676
677    #[rstest]
678    fn test_exposure_raw() {
679        let mut level =
680            BookLevel::new(BookPrice::new(Price::from("2.00"), OrderSideSpecified::Buy));
681        let order1 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(10), 0);
682        let order2 = BookOrder::new(OrderSide::Buy, Price::from("2.00"), Quantity::from(20), 1);
683
684        level.add(order1);
685        level.add(order2);
686        assert_eq!(
687            level.exposure_raw(),
688            (60.0 * FIXED_SCALAR).round() as QuantityRaw
689        );
690    }
691
692    #[rstest]
693    fn test_exposure_raw_saturates_on_overflow() {
694        // Test that exposure_raw saturates at QuantityRaw::MAX instead of wrapping
695        // Use values whose product * FIXED_SCALAR overflows QuantityRaw
696        #[cfg(feature = "high-precision")]
697        let (price_str, qty_str) = ("1000000000000.00", "1000000000000.00");
698        #[cfg(not(feature = "high-precision"))]
699        let (price_str, qty_str) = ("100000000.00", "1000000000.00");
700
701        let mut level = BookLevel::new(BookPrice::new(
702            Price::from(price_str),
703            OrderSideSpecified::Buy,
704        ));
705
706        // Create an order with large price and quantity that would overflow QuantityRaw
707        let order = BookOrder::new(
708            OrderSide::Buy,
709            Price::from(price_str),
710            Quantity::from(qty_str),
711            0,
712        );
713
714        level.add(order);
715
716        // Should saturate at max value instead of wrapping around
717        let result = level.exposure_raw();
718        assert_eq!(result, QuantityRaw::MAX);
719    }
720
721    #[rstest]
722    fn test_exposure_raw_sum_saturates_on_overflow() {
723        // Test that summing exposures saturates instead of wrapping
724        #[cfg(feature = "high-precision")]
725        let (price_str, qty_str, count) = ("10000000000000.00", "10000000000000.00", 100);
726        #[cfg(not(feature = "high-precision"))]
727        let (price_str, qty_str, count) = ("1000000000.00", "1000000000.00", 100);
728
729        let mut level = BookLevel::new(BookPrice::new(
730            Price::from(price_str),
731            OrderSideSpecified::Buy,
732        ));
733
734        // Add multiple large orders that together would overflow when summed
735        for i in 0..count {
736            let order = BookOrder::new(
737                OrderSide::Buy,
738                Price::from(price_str),
739                Quantity::from(qty_str),
740                i,
741            );
742            level.add(order);
743        }
744
745        // Should saturate at max value instead of wrapping around
746        let result = level.exposure_raw();
747        assert_eq!(result, QuantityRaw::MAX);
748    }
749}