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