Skip to main content

nautilus_model/data/
depth.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//! An [`OrderBookDepth`] aggregated order book snapshot with runtime depth.
17
18use std::{collections::HashMap, fmt::Display};
19
20use indexmap::IndexMap;
21use nautilus_core::{UnixNanos, serialization::Serializable};
22use serde::{Deserialize, Serialize};
23use smallvec::SmallVec;
24
25use super::{ARROW_TIMESTAMP_NANOSECOND, HasTsInit, order::BookOrder};
26use crate::{
27    enums::OrderSide,
28    identifiers::InstrumentId,
29    types::{PRICE_ERROR, PRICE_UNDEF},
30};
31
32/// Number of levels in a standard depth-10 snapshot.
33pub const DEPTH10_LEN: usize = 10;
34/// Number of levels stored inline before a depth side spills to the heap.
35pub const DEPTH_INLINE_LEN: usize = DEPTH10_LEN;
36
37const DEPTH_SIDE_LIST: &str = "List(Struct(price: Decimal128(38, 16), size: Decimal128(38, 16), count: UInt32, order_id: UInt64))";
38
39/// Represents one aggregated order book snapshot with any number of levels per side.
40///
41/// The plural name denotes the many levels in one snapshot. In contrast, [`super::OrderBookDeltas`]
42/// is a container of multiple update events. Up to ten levels per side remain inline; deeper venue
43/// snapshots spill transparently without changing the data type.
44///
45/// Per-level [`BookOrder::order_id`] values are retained when supplied by the venue.
46#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[serde(try_from = "OrderBookDepthRaw")]
48#[cfg_attr(
49    feature = "python",
50    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
51)]
52#[cfg_attr(
53    feature = "python",
54    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
55)]
56pub struct OrderBookDepth {
57    /// The instrument ID for the book.
58    pub instrument_id: InstrumentId,
59    /// The bid orders for the depth update.
60    pub bids: SmallVec<[BookOrder; DEPTH_INLINE_LEN]>,
61    /// The ask orders for the depth update.
62    pub asks: SmallVec<[BookOrder; DEPTH_INLINE_LEN]>,
63    /// The count of bid orders per level for the depth update.
64    pub bid_counts: SmallVec<[u32; DEPTH_INLINE_LEN]>,
65    /// The count of ask orders per level for the depth update.
66    pub ask_counts: SmallVec<[u32; DEPTH_INLINE_LEN]>,
67    /// The record flags bit field, indicating event end and data information.
68    pub flags: u8,
69    /// The message sequence number assigned at the venue.
70    pub sequence: u64,
71    /// UNIX timestamp (nanoseconds) when the book event occurred.
72    pub ts_event: UnixNanos,
73    /// UNIX timestamp (nanoseconds) when the instance was created.
74    pub ts_init: UnixNanos,
75}
76
77#[derive(Deserialize)]
78struct OrderBookDepthRaw {
79    instrument_id: InstrumentId,
80    bids: SmallVec<[BookOrder; DEPTH_INLINE_LEN]>,
81    asks: SmallVec<[BookOrder; DEPTH_INLINE_LEN]>,
82    bid_counts: SmallVec<[u32; DEPTH_INLINE_LEN]>,
83    ask_counts: SmallVec<[u32; DEPTH_INLINE_LEN]>,
84    flags: u8,
85    sequence: u64,
86    ts_event: UnixNanos,
87    ts_init: UnixNanos,
88}
89
90impl TryFrom<OrderBookDepthRaw> for OrderBookDepth {
91    type Error = anyhow::Error;
92
93    fn try_from(value: OrderBookDepthRaw) -> Result<Self, Self::Error> {
94        Self::new_checked(
95            value.instrument_id,
96            value.bids,
97            value.asks,
98            value.bid_counts,
99            value.ask_counts,
100            value.flags,
101            value.sequence,
102            value.ts_event,
103            value.ts_init,
104        )
105    }
106}
107
108impl OrderBookDepth {
109    /// Creates a new [`OrderBookDepth`] instance.
110    ///
111    /// # Panics
112    ///
113    /// Panics if either order side and its count vector have different lengths.
114    #[expect(clippy::too_many_arguments)]
115    #[must_use]
116    pub fn new<B, A, BC, AC>(
117        instrument_id: InstrumentId,
118        bids: B,
119        asks: A,
120        bid_counts: BC,
121        ask_counts: AC,
122        flags: u8,
123        sequence: u64,
124        ts_event: UnixNanos,
125        ts_init: UnixNanos,
126    ) -> Self
127    where
128        B: IntoIterator<Item = BookOrder>,
129        A: IntoIterator<Item = BookOrder>,
130        BC: IntoIterator<Item = u32>,
131        AC: IntoIterator<Item = u32>,
132    {
133        Self::new_checked(
134            instrument_id,
135            bids,
136            asks,
137            bid_counts,
138            ask_counts,
139            flags,
140            sequence,
141            ts_event,
142            ts_init,
143        )
144        .unwrap_or_else(|e| panic!("{e}"))
145    }
146
147    /// Creates a new [`OrderBookDepth`] instance after validating its levels.
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if an order side and its count vector have different lengths, or if a
152    /// retained level has the wrong side. Levels with no side, a non-positive or undefined size,
153    /// or an undefined or error price are omitted with their count entries.
154    #[expect(clippy::too_many_arguments)]
155    pub fn new_checked<B, A, BC, AC>(
156        instrument_id: InstrumentId,
157        bids: B,
158        asks: A,
159        bid_counts: BC,
160        ask_counts: AC,
161        flags: u8,
162        sequence: u64,
163        ts_event: UnixNanos,
164        ts_init: UnixNanos,
165    ) -> anyhow::Result<Self>
166    where
167        B: IntoIterator<Item = BookOrder>,
168        A: IntoIterator<Item = BookOrder>,
169        BC: IntoIterator<Item = u32>,
170        AC: IntoIterator<Item = u32>,
171    {
172        let bids = bids
173            .into_iter()
174            .collect::<SmallVec<[BookOrder; DEPTH_INLINE_LEN]>>();
175        let asks = asks
176            .into_iter()
177            .collect::<SmallVec<[BookOrder; DEPTH_INLINE_LEN]>>();
178        let bid_counts = bid_counts
179            .into_iter()
180            .collect::<SmallVec<[u32; DEPTH_INLINE_LEN]>>();
181        let ask_counts = ask_counts
182            .into_iter()
183            .collect::<SmallVec<[u32; DEPTH_INLINE_LEN]>>();
184        anyhow::ensure!(
185            bids.len() == bid_counts.len(),
186            "bid order and count lengths must match"
187        );
188        anyhow::ensure!(
189            asks.len() == ask_counts.len(),
190            "ask order and count lengths must match"
191        );
192        let (bids, bid_counts): (
193            SmallVec<[BookOrder; DEPTH_INLINE_LEN]>,
194            SmallVec<[u32; DEPTH_INLINE_LEN]>,
195        ) = bids
196            .into_iter()
197            .zip(bid_counts)
198            .filter(|(order, _)| is_depth_level_present(order))
199            .unzip();
200        let (asks, ask_counts): (
201            SmallVec<[BookOrder; DEPTH_INLINE_LEN]>,
202            SmallVec<[u32; DEPTH_INLINE_LEN]>,
203        ) = asks
204            .into_iter()
205            .zip(ask_counts)
206            .filter(|(order, _)| is_depth_level_present(order))
207            .unzip();
208        anyhow::ensure!(
209            bids.iter().all(|order| order.side == Some(OrderSide::Buy)),
210            "bid levels must have Buy side"
211        );
212        anyhow::ensure!(
213            asks.iter().all(|order| order.side == Some(OrderSide::Sell)),
214            "ask levels must have Sell side"
215        );
216
217        Ok(Self {
218            instrument_id,
219            bids,
220            asks,
221            bid_counts,
222            ask_counts,
223            flags,
224            sequence,
225            ts_event,
226            ts_init,
227        })
228    }
229
230    /// Returns the metadata for the type, for use with serialization formats.
231    #[must_use]
232    pub fn get_metadata(
233        instrument_id: &InstrumentId,
234        price_precision: u8,
235        size_precision: u8,
236    ) -> HashMap<String, String> {
237        let mut metadata = HashMap::new();
238        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
239        metadata.insert("price_precision".to_string(), price_precision.to_string());
240        metadata.insert("size_precision".to_string(), size_precision.to_string());
241        metadata
242    }
243
244    /// Returns the field map for the type, for use with Arrow schemas.
245    #[must_use]
246    pub fn get_fields() -> IndexMap<String, String> {
247        let mut metadata = IndexMap::new();
248        metadata.insert("bids".to_string(), DEPTH_SIDE_LIST.to_string());
249        metadata.insert("asks".to_string(), DEPTH_SIDE_LIST.to_string());
250        metadata.insert("flags".to_string(), "UInt8".to_string());
251        metadata.insert("sequence".to_string(), "UInt64".to_string());
252        metadata.insert(
253            "ts_event".to_string(),
254            ARROW_TIMESTAMP_NANOSECOND.to_string(),
255        );
256        metadata.insert(
257            "ts_init".to_string(),
258            ARROW_TIMESTAMP_NANOSECOND.to_string(),
259        );
260        metadata
261    }
262}
263
264/// Returns whether an order represents a populated depth level.
265#[must_use]
266pub fn is_depth_level_present(order: &BookOrder) -> bool {
267    order.side.is_some()
268        && order.price.raw != PRICE_UNDEF
269        && order.price.raw != PRICE_ERROR
270        && order.size.is_positive()
271}
272
273// TODO: Exact format for Debug and Display TBD
274impl Display for OrderBookDepth {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        write!(
277            f,
278            "{},flags={},sequence={},ts_event={},ts_init={}",
279            self.instrument_id, self.flags, self.sequence, self.ts_event, self.ts_init
280        )
281    }
282}
283
284impl Serializable for OrderBookDepth {}
285
286impl HasTsInit for OrderBookDepth {
287    fn ts_init(&self) -> UnixNanos {
288        self.ts_init
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use std::{
295        collections::hash_map::DefaultHasher,
296        hash::{Hash, Hasher},
297    };
298
299    use rstest::rstest;
300    use serde_json;
301
302    use super::*;
303    use crate::{
304        data::{
305            order::{BookOrder, NULL_ORDER},
306            stubs::*,
307        },
308        enums::OrderSide,
309        types::{Price, QUANTITY_UNDEF, Quantity, price::PriceRaw},
310    };
311
312    fn create_test_book_order(
313        side: OrderSide,
314        price: &str,
315        size: &str,
316        order_id: u64,
317    ) -> BookOrder {
318        BookOrder::new(side, Price::from(price), Quantity::from(size), order_id)
319    }
320
321    fn create_test_depth() -> OrderBookDepth {
322        let instrument_id = InstrumentId::from("EURUSD.SIM");
323
324        // Create bid orders (descending prices)
325        let bids = [
326            create_test_book_order(OrderSide::Buy, "1.0500", "100000", 1),
327            create_test_book_order(OrderSide::Buy, "1.0499", "150000", 2),
328            create_test_book_order(OrderSide::Buy, "1.0498", "200000", 3),
329            create_test_book_order(OrderSide::Buy, "1.0497", "125000", 4),
330            create_test_book_order(OrderSide::Buy, "1.0496", "175000", 5),
331            create_test_book_order(OrderSide::Buy, "1.0495", "100000", 6),
332            create_test_book_order(OrderSide::Buy, "1.0494", "225000", 7),
333            create_test_book_order(OrderSide::Buy, "1.0493", "150000", 8),
334            create_test_book_order(OrderSide::Buy, "1.0492", "300000", 9),
335            create_test_book_order(OrderSide::Buy, "1.0491", "175000", 10),
336        ];
337
338        // Create ask orders (ascending prices)
339        let asks = [
340            create_test_book_order(OrderSide::Sell, "1.0501", "100000", 11),
341            create_test_book_order(OrderSide::Sell, "1.0502", "125000", 12),
342            create_test_book_order(OrderSide::Sell, "1.0503", "150000", 13),
343            create_test_book_order(OrderSide::Sell, "1.0504", "175000", 14),
344            create_test_book_order(OrderSide::Sell, "1.0505", "200000", 15),
345            create_test_book_order(OrderSide::Sell, "1.0506", "100000", 16),
346            create_test_book_order(OrderSide::Sell, "1.0507", "250000", 17),
347            create_test_book_order(OrderSide::Sell, "1.0508", "125000", 18),
348            create_test_book_order(OrderSide::Sell, "1.0509", "300000", 19),
349            create_test_book_order(OrderSide::Sell, "1.0510", "175000", 20),
350        ];
351
352        let bid_counts = [1, 2, 1, 3, 1, 2, 1, 4, 1, 2];
353        let ask_counts = [2, 1, 3, 1, 2, 1, 4, 1, 2, 3];
354
355        OrderBookDepth::new(
356            instrument_id,
357            bids,
358            asks,
359            bid_counts,
360            ask_counts,
361            32,                             // flags
362            12345,                          // sequence
363            UnixNanos::from(1_000_000_000), // ts_event
364            UnixNanos::from(2_000_000_000), // ts_init
365        )
366    }
367
368    fn create_empty_depth() -> OrderBookDepth {
369        let instrument_id = InstrumentId::from("EMPTY.TEST");
370
371        // Create empty orders with zero prices and quantities
372        let empty_bid = create_test_book_order(OrderSide::Buy, "0.0", "0", 0);
373        let empty_ask = create_test_book_order(OrderSide::Sell, "0.0", "0", 0);
374
375        OrderBookDepth::new(
376            instrument_id,
377            [empty_bid; DEPTH10_LEN],
378            [empty_ask; DEPTH10_LEN],
379            [0; DEPTH10_LEN],
380            [0; DEPTH10_LEN],
381            0,
382            0,
383            UnixNanos::from(0),
384            UnixNanos::from(0),
385        )
386    }
387
388    #[rstest]
389    fn test_order_book_depths_new() {
390        let depth = create_test_depth();
391
392        assert_eq!(depth.instrument_id, InstrumentId::from("EURUSD.SIM"));
393        assert_eq!(depth.bids.len(), DEPTH10_LEN);
394        assert_eq!(depth.asks.len(), DEPTH10_LEN);
395        assert_eq!(depth.bid_counts.len(), DEPTH10_LEN);
396        assert_eq!(depth.ask_counts.len(), DEPTH10_LEN);
397        assert_eq!(depth.flags, 32);
398        assert_eq!(depth.sequence, 12345);
399        assert_eq!(depth.ts_event, UnixNanos::from(1_000_000_000));
400        assert_eq!(depth.ts_init, UnixNanos::from(2_000_000_000));
401    }
402
403    #[rstest]
404    #[case::price(true)]
405    #[case::size(false)]
406    fn test_depths_drop_partial_undefined_level(#[case] price_undefined: bool) {
407        let instrument_id = InstrumentId::from("AUD/USD.SIM");
408        let bid = BookOrder::new(
409            OrderSide::Buy,
410            Price::from("1.23"),
411            Quantity::from("100.00"),
412            1,
413        );
414        let ask = BookOrder::new(
415            OrderSide::Sell,
416            Price::from("1.24"),
417            Quantity::from("100.00"),
418            2,
419        );
420        let mut asks = [ask; DEPTH10_LEN];
421        if price_undefined {
422            asks[1].price = Price::from_raw(PRICE_UNDEF, 0);
423        } else {
424            asks[1].size = Quantity::from_raw(QUANTITY_UNDEF, 0);
425        }
426        let depth = OrderBookDepth::new(
427            instrument_id,
428            [bid; DEPTH10_LEN],
429            asks,
430            [1; DEPTH10_LEN],
431            [1; DEPTH10_LEN],
432            0,
433            1,
434            1.into(),
435            1.into(),
436        );
437        let expected = OrderBookDepth::new(
438            instrument_id,
439            [bid; DEPTH10_LEN],
440            [ask; DEPTH10_LEN - 1],
441            [1; DEPTH10_LEN],
442            [1; DEPTH10_LEN - 1],
443            0,
444            1,
445            1.into(),
446            1.into(),
447        );
448
449        assert_eq!(depth, expected);
450    }
451
452    #[rstest]
453    fn test_order_book_depths_new_checked_rejects_mismatched_counts() {
454        let result = OrderBookDepth::new_checked(
455            InstrumentId::from("EURUSD.SIM"),
456            Vec::<BookOrder>::new(),
457            Vec::<BookOrder>::new(),
458            vec![1],
459            Vec::<u32>::new(),
460            0,
461            0,
462            UnixNanos::from(1),
463            UnixNanos::from(2),
464        );
465
466        assert_eq!(
467            result.unwrap_err().to_string(),
468            "bid order and count lengths must match",
469        );
470    }
471
472    #[rstest]
473    fn test_order_book_depths_new_checked_rejects_sell_bid() {
474        let result = OrderBookDepth::new_checked(
475            InstrumentId::from("EURUSD.SIM"),
476            [create_test_book_order(
477                OrderSide::Sell,
478                "1.0500",
479                "100000",
480                1,
481            )],
482            [],
483            [1],
484            [],
485            0,
486            0,
487            UnixNanos::from(1),
488            UnixNanos::from(2),
489        );
490
491        assert_eq!(
492            result.unwrap_err().to_string(),
493            "bid levels must have Buy side",
494        );
495    }
496
497    #[rstest]
498    fn test_order_book_depths_new_checked_rejects_buy_ask() {
499        let result = OrderBookDepth::new_checked(
500            InstrumentId::from("EURUSD.SIM"),
501            [],
502            [create_test_book_order(
503                OrderSide::Buy,
504                "1.0501",
505                "100000",
506                2,
507            )],
508            [],
509            [1],
510            0,
511            0,
512            UnixNanos::from(1),
513            UnixNanos::from(2),
514        );
515
516        assert_eq!(
517            result.unwrap_err().to_string(),
518            "ask levels must have Sell side",
519        );
520    }
521
522    #[rstest]
523    #[case(PRICE_UNDEF)]
524    #[case(PRICE_ERROR)]
525    fn test_depth_level_with_sentinel_price_is_absent(#[case] raw: PriceRaw) {
526        let order = BookOrder::new(
527            OrderSide::Buy,
528            Price::from_raw(raw, 0),
529            Quantity::from("1"),
530            1,
531        );
532
533        assert!(!is_depth_level_present(&order));
534    }
535
536    #[rstest]
537    #[case(PRICE_UNDEF)]
538    #[case(PRICE_ERROR)]
539    fn test_order_book_depths_new_checked_drops_sentinel_price(#[case] raw: PriceRaw) {
540        let order = BookOrder::new(
541            OrderSide::Buy,
542            Price::from_raw(raw, 0),
543            Quantity::from("1"),
544            1,
545        );
546
547        let depth = OrderBookDepth::new_checked(
548            InstrumentId::from("EURUSD.SIM"),
549            [order],
550            [],
551            [7],
552            [],
553            0,
554            0,
555            UnixNanos::from(1),
556            UnixNanos::from(2),
557        )
558        .unwrap();
559
560        assert!(depth.bids.is_empty());
561        assert!(depth.bid_counts.is_empty());
562    }
563
564    #[rstest]
565    fn test_order_book_depths_deserialize_rejects_mismatched_counts() {
566        let mut value = serde_json::to_value(create_test_depth()).unwrap();
567        value["bid_counts"].as_array_mut().unwrap().pop();
568
569        let error = serde_json::from_value::<OrderBookDepth>(value).unwrap_err();
570
571        assert!(
572            error
573                .to_string()
574                .contains("bid order and count lengths must match")
575        );
576    }
577
578    #[rstest]
579    fn test_order_book_depths_deserialize_drops_legacy_padding_and_zero_size_levels() {
580        let mut legacy = create_test_depth();
581        legacy.bids[1] = NULL_ORDER;
582        legacy.bids[2].size = Quantity::zero(legacy.bids[2].size.precision);
583        let payload = serde_json::to_string(&legacy).unwrap();
584
585        let decoded = serde_json::from_str::<OrderBookDepth>(&payload).unwrap();
586        let bid_order_ids = decoded
587            .bids
588            .iter()
589            .map(|order| order.order_id)
590            .collect::<Vec<_>>();
591
592        assert_eq!(bid_order_ids, vec![1, 4, 5, 6, 7, 8, 9, 10]);
593        assert_eq!(decoded.bid_counts.as_slice(), &[1, 3, 1, 2, 1, 4, 1, 2]);
594        assert_eq!(decoded.asks, legacy.asks);
595        assert_eq!(decoded.ask_counts, legacy.ask_counts);
596        assert_eq!(decoded.flags, legacy.flags);
597        assert_eq!(decoded.sequence, legacy.sequence);
598        assert_eq!(decoded.ts_event, legacy.ts_event);
599        assert_eq!(decoded.ts_init, legacy.ts_init);
600    }
601
602    #[rstest]
603    fn test_order_book_depths_msgpack_deserialize_drops_legacy_padding_and_zero_size_levels() {
604        let mut legacy = create_test_depth();
605        legacy.bids[1] = NULL_ORDER;
606        legacy.bids[2].size = Quantity::zero(legacy.bids[2].size.precision);
607        let payload = rmp_serde::to_vec_named(&legacy).unwrap();
608
609        let decoded = rmp_serde::from_slice::<OrderBookDepth>(&payload).unwrap();
610
611        assert_eq!(
612            decoded
613                .bids
614                .iter()
615                .map(|order| order.order_id)
616                .collect::<Vec<_>>(),
617            vec![1, 4, 5, 6, 7, 8, 9, 10],
618        );
619        assert_eq!(decoded.bid_counts.as_slice(), &[1, 3, 1, 2, 1, 4, 1, 2]);
620        assert_eq!(decoded.asks, legacy.asks);
621        assert_eq!(decoded.ask_counts, legacy.ask_counts);
622        assert_eq!(decoded.flags, legacy.flags);
623        assert_eq!(decoded.sequence, legacy.sequence);
624        assert_eq!(decoded.ts_event, legacy.ts_event);
625        assert_eq!(decoded.ts_init, legacy.ts_init);
626    }
627
628    #[rstest]
629    fn test_order_book_depths_new_with_all_parameters() {
630        let instrument_id = InstrumentId::from("GBPUSD.SIM");
631        let bid = create_test_book_order(OrderSide::Buy, "1.2500", "50000", 1);
632        let ask = create_test_book_order(OrderSide::Sell, "1.2501", "75000", 2);
633        let flags = 64u8;
634        let sequence = 999u64;
635        let ts_event = UnixNanos::from(5_000_000_000);
636        let ts_init = UnixNanos::from(6_000_000_000);
637
638        let depth = OrderBookDepth::new(
639            instrument_id,
640            [bid; DEPTH10_LEN],
641            [ask; DEPTH10_LEN],
642            [5; DEPTH10_LEN],
643            [3; DEPTH10_LEN],
644            flags,
645            sequence,
646            ts_event,
647            ts_init,
648        );
649
650        assert_eq!(depth.instrument_id, instrument_id);
651        assert_eq!(depth.bids[0], bid);
652        assert_eq!(depth.asks[0], ask);
653        assert_eq!(depth.bid_counts[0], 5);
654        assert_eq!(depth.ask_counts[0], 3);
655        assert_eq!(depth.flags, flags);
656        assert_eq!(depth.sequence, sequence);
657        assert_eq!(depth.ts_event, ts_event);
658        assert_eq!(depth.ts_init, ts_init);
659    }
660
661    #[rstest]
662    fn test_order_book_depths_lengths() {
663        let depth = create_test_depth();
664
665        // The legacy depth-10 fixture retains all ten levels.
666        assert_eq!(depth.bids.len(), 10);
667        assert_eq!(depth.asks.len(), 10);
668        assert_eq!(depth.bid_counts.len(), 10);
669        assert_eq!(depth.ask_counts.len(), 10);
670    }
671
672    #[rstest]
673    fn test_order_book_depths_indexing() {
674        let depth = create_test_depth();
675
676        // Test first and last elements of each array
677        assert_eq!(depth.bids[0].price, Price::from("1.0500"));
678        assert_eq!(depth.bids[9].price, Price::from("1.0491"));
679        assert_eq!(depth.asks[0].price, Price::from("1.0501"));
680        assert_eq!(depth.asks[9].price, Price::from("1.0510"));
681        assert_eq!(depth.bid_counts[0], 1);
682        assert_eq!(depth.bid_counts[9], 2);
683        assert_eq!(depth.ask_counts[0], 2);
684        assert_eq!(depth.ask_counts[9], 3);
685    }
686
687    #[rstest]
688    fn test_order_book_depths_bid_ask_ordering() {
689        let depth = create_test_depth();
690
691        // Verify bid prices are in descending order (highest to lowest)
692        for i in 0..9 {
693            assert!(
694                depth.bids[i].price >= depth.bids[i + 1].price,
695                "Bid prices should be in descending order: {} >= {}",
696                depth.bids[i].price,
697                depth.bids[i + 1].price
698            );
699        }
700
701        // Verify ask prices are in ascending order (lowest to highest)
702        for i in 0..9 {
703            assert!(
704                depth.asks[i].price <= depth.asks[i + 1].price,
705                "Ask prices should be in ascending order: {} <= {}",
706                depth.asks[i].price,
707                depth.asks[i + 1].price
708            );
709        }
710
711        // Verify bid-ask spread (best bid < best ask)
712        assert!(
713            depth.bids[0].price < depth.asks[0].price,
714            "Best bid {} should be less than best ask {}",
715            depth.bids[0].price,
716            depth.asks[0].price
717        );
718    }
719
720    #[rstest]
721    fn test_order_book_depths_clone() {
722        let depth1 = create_test_depth();
723        let depth2 = depth1.clone();
724
725        assert_eq!(depth1.instrument_id, depth2.instrument_id);
726        assert_eq!(depth1.bids, depth2.bids);
727        assert_eq!(depth1.asks, depth2.asks);
728        assert_eq!(depth1.bid_counts, depth2.bid_counts);
729        assert_eq!(depth1.ask_counts, depth2.ask_counts);
730        assert_eq!(depth1.flags, depth2.flags);
731        assert_eq!(depth1.sequence, depth2.sequence);
732        assert_eq!(depth1.ts_event, depth2.ts_event);
733        assert_eq!(depth1.ts_init, depth2.ts_init);
734    }
735
736    #[rstest]
737    fn test_order_book_depths_inline_and_spilled_storage() {
738        let inline = create_test_depth();
739        let bid = inline.bids[0];
740        let ask = inline.asks[0];
741        let spilled = OrderBookDepth::new(
742            inline.instrument_id,
743            vec![bid; 25],
744            vec![ask; 25],
745            vec![1; 25],
746            vec![1; 25],
747            inline.flags,
748            inline.sequence,
749            inline.ts_event,
750            inline.ts_init,
751        );
752
753        assert!(!inline.bids.spilled());
754        assert!(!inline.asks.spilled());
755        assert!(!inline.bid_counts.spilled());
756        assert!(!inline.ask_counts.spilled());
757        assert!(spilled.bids.spilled());
758        assert!(spilled.asks.spilled());
759        assert!(spilled.bid_counts.spilled());
760        assert!(spilled.ask_counts.spilled());
761        assert_eq!(spilled.bids.len(), 25);
762        assert_eq!(spilled.asks.len(), 25);
763    }
764
765    #[rstest]
766    fn test_order_book_depths_debug() {
767        let depth = create_test_depth();
768        let debug_str = format!("{depth:?}");
769
770        assert!(debug_str.contains("OrderBookDepth"));
771        assert!(debug_str.contains("EURUSD.SIM"));
772        assert!(debug_str.contains("flags: 32"));
773        assert!(debug_str.contains("sequence: 12345"));
774    }
775
776    #[rstest]
777    fn test_order_book_depths_partial_eq() {
778        let depth1 = create_test_depth();
779        let depth2 = create_test_depth();
780        let depth3 = create_empty_depth();
781
782        assert_eq!(depth1, depth2); // Same data
783        assert_ne!(depth1, depth3); // Different data
784        assert_ne!(depth2, depth3); // Different data
785    }
786
787    #[rstest]
788    fn test_order_book_depths_eq_consistency() {
789        let depth1 = create_test_depth();
790        let depth2 = create_test_depth();
791
792        assert_eq!(depth1, depth2);
793        assert_eq!(depth2, depth1); // Symmetry
794        assert_eq!(depth1, depth1); // Reflexivity
795    }
796
797    #[rstest]
798    fn test_order_book_depths_hash() {
799        let depth1 = create_test_depth();
800        let depth2 = create_test_depth();
801
802        let mut hasher1 = DefaultHasher::new();
803        let mut hasher2 = DefaultHasher::new();
804
805        depth1.hash(&mut hasher1);
806        depth2.hash(&mut hasher2);
807
808        assert_eq!(hasher1.finish(), hasher2.finish()); // Equal objects have equal hashes
809    }
810
811    #[rstest]
812    fn test_order_book_depths_hash_different_objects() {
813        let depth1 = create_test_depth();
814        let depth2 = create_empty_depth();
815
816        let mut hasher1 = DefaultHasher::new();
817        let mut hasher2 = DefaultHasher::new();
818
819        depth1.hash(&mut hasher1);
820        depth2.hash(&mut hasher2);
821
822        assert_ne!(hasher1.finish(), hasher2.finish()); // Different objects should have different hashes
823    }
824
825    #[rstest]
826    fn test_order_book_depths_display() {
827        let depth = create_test_depth();
828        let display_str = format!("{depth}");
829
830        assert!(display_str.contains("EURUSD.SIM"));
831        assert!(display_str.contains("flags=32"));
832        assert!(display_str.contains("sequence=12345"));
833        assert!(display_str.contains("ts_event=1000000000"));
834        assert!(display_str.contains("ts_init=2000000000"));
835    }
836
837    #[rstest]
838    fn test_order_book_depths_display_format() {
839        let depth = create_test_depth();
840        let expected = "EURUSD.SIM,flags=32,sequence=12345,ts_event=1000000000,ts_init=2000000000";
841
842        assert_eq!(format!("{depth}"), expected);
843    }
844
845    #[rstest]
846    fn test_order_book_depths_serialization() {
847        let depth = create_test_depth();
848
849        // Test JSON serialization
850        let json = serde_json::to_string(&depth).unwrap();
851        let deserialized: OrderBookDepth = serde_json::from_str(&json).unwrap();
852
853        assert_eq!(depth, deserialized);
854    }
855
856    #[rstest]
857    fn test_order_book_depths_serializable_trait() {
858        fn assert_serializable<T: Serializable>(_: &T) {}
859
860        let depth = create_test_depth();
861
862        // Verify Serializable trait is implemented (compile-time check)
863        assert_serializable(&depth);
864    }
865
866    #[rstest]
867    fn test_order_book_depths_has_ts_init() {
868        let depth = create_test_depth();
869
870        assert_eq!(depth.ts_init(), UnixNanos::from(2_000_000_000));
871    }
872
873    #[rstest]
874    fn test_order_book_depths_get_metadata() {
875        let instrument_id = InstrumentId::from("EURUSD.SIM");
876        let price_precision = 5u8;
877        let size_precision = 0u8;
878
879        let metadata =
880            OrderBookDepth::get_metadata(&instrument_id, price_precision, size_precision);
881
882        assert_eq!(
883            metadata.get("instrument_id"),
884            Some(&"EURUSD.SIM".to_string())
885        );
886        assert_eq!(metadata.get("price_precision"), Some(&"5".to_string()));
887        assert_eq!(metadata.get("size_precision"), Some(&"0".to_string()));
888        assert_eq!(metadata.len(), 3);
889    }
890
891    #[rstest]
892    fn test_order_book_depths_get_fields() {
893        let fields = OrderBookDepth::get_fields();
894
895        assert_eq!(fields.get("bids"), Some(&DEPTH_SIDE_LIST.to_string()));
896        assert_eq!(fields.get("asks"), Some(&DEPTH_SIDE_LIST.to_string()));
897        assert_eq!(fields.get("flags"), Some(&"UInt8".to_string()));
898        assert_eq!(fields.get("sequence"), Some(&"UInt64".to_string()));
899        assert_eq!(
900            fields.get("ts_event"),
901            Some(&ARROW_TIMESTAMP_NANOSECOND.to_string())
902        );
903        assert_eq!(
904            fields.get("ts_init"),
905            Some(&ARROW_TIMESTAMP_NANOSECOND.to_string())
906        );
907        assert_eq!(fields.get("identifier"), None);
908        assert_eq!(fields.len(), 6);
909    }
910
911    #[rstest]
912    fn test_order_book_depths_get_fields_order() {
913        let fields = OrderBookDepth::get_fields();
914        let keys: Vec<&String> = fields.keys().collect();
915
916        assert_eq!(
917            keys,
918            vec!["bids", "asks", "flags", "sequence", "ts_event", "ts_init",]
919        );
920    }
921
922    #[rstest]
923    fn test_order_book_depths_empty_values() {
924        let depth = create_empty_depth();
925
926        assert_eq!(depth.instrument_id, InstrumentId::from("EMPTY.TEST"));
927        assert_eq!(depth.flags, 0);
928        assert_eq!(depth.sequence, 0);
929        assert_eq!(depth.ts_event, UnixNanos::from(0));
930        assert_eq!(depth.ts_init, UnixNanos::from(0));
931
932        assert!(depth.bids.is_empty());
933        assert!(depth.asks.is_empty());
934        assert!(depth.bid_counts.is_empty());
935        assert!(depth.ask_counts.is_empty());
936    }
937
938    #[rstest]
939    fn test_order_book_depths_max_values() {
940        let instrument_id = InstrumentId::from("MAX.TEST");
941        let max_bid = create_test_book_order(OrderSide::Buy, "999999.99", "999999999", u64::MAX);
942        let max_ask = create_test_book_order(OrderSide::Sell, "1000000.00", "999999999", u64::MAX);
943
944        let depth = OrderBookDepth::new(
945            instrument_id,
946            [max_bid; DEPTH10_LEN],
947            [max_ask; DEPTH10_LEN],
948            [u32::MAX; DEPTH10_LEN],
949            [u32::MAX; DEPTH10_LEN],
950            u8::MAX,
951            u64::MAX,
952            UnixNanos::from(u64::MAX),
953            UnixNanos::from(u64::MAX),
954        );
955
956        assert_eq!(depth.flags, u8::MAX);
957        assert_eq!(depth.sequence, u64::MAX);
958        assert_eq!(depth.ts_event, UnixNanos::from(u64::MAX));
959        assert_eq!(depth.ts_init, UnixNanos::from(u64::MAX));
960
961        for &count in &depth.bid_counts {
962            assert_eq!(count, u32::MAX);
963        }
964
965        for &count in &depth.ask_counts {
966            assert_eq!(count, u32::MAX);
967        }
968    }
969
970    #[rstest]
971    fn test_order_book_depths_different_instruments() {
972        let instruments = [
973            "EURUSD.SIM",
974            "GBPUSD.SIM",
975            "USDJPY.SIM",
976            "AUDUSD.SIM",
977            "USDCHF.SIM",
978        ];
979
980        for instrument_str in &instruments {
981            let instrument_id = InstrumentId::from(*instrument_str);
982            let bid = create_test_book_order(OrderSide::Buy, "1.0000", "100000", 1);
983            let ask = create_test_book_order(OrderSide::Sell, "1.0001", "100000", 2);
984
985            let depth = OrderBookDepth::new(
986                instrument_id,
987                [bid; DEPTH10_LEN],
988                [ask; DEPTH10_LEN],
989                [1; DEPTH10_LEN],
990                [1; DEPTH10_LEN],
991                0,
992                1,
993                UnixNanos::from(1_000_000_000),
994                UnixNanos::from(2_000_000_000),
995            );
996
997            assert_eq!(depth.instrument_id, instrument_id);
998            assert!(format!("{depth}").contains(instrument_str));
999        }
1000    }
1001
1002    #[rstest]
1003    fn test_order_book_depths_realistic_forex_spread() {
1004        let instrument_id = InstrumentId::from("EURUSD.SIM");
1005
1006        // Realistic EUR/USD spread with 0.1 pip spread
1007        let best_bid = create_test_book_order(OrderSide::Buy, "1.08500", "1000000", 1);
1008        let best_ask = create_test_book_order(OrderSide::Sell, "1.08501", "1000000", 2);
1009
1010        let depth = OrderBookDepth::new(
1011            instrument_id,
1012            [best_bid; DEPTH10_LEN],
1013            [best_ask; DEPTH10_LEN],
1014            [5; DEPTH10_LEN], // Realistic order count
1015            [3; DEPTH10_LEN],
1016            16,                                         // Realistic flags
1017            123_456,                                    // Realistic sequence
1018            UnixNanos::from(1_672_531_200_000_000_000), // Jan 1, 2023 timestamp
1019            UnixNanos::from(1_672_531_200_000_100_000),
1020        );
1021
1022        assert_eq!(depth.bids[0].price, Price::from("1.08500"));
1023        assert_eq!(depth.asks[0].price, Price::from("1.08501"));
1024        assert!(depth.bids[0].price < depth.asks[0].price); // Positive spread
1025
1026        // Verify realistic quantities and counts
1027        assert_eq!(depth.bids[0].size, Quantity::from("1000000"));
1028        assert_eq!(depth.bid_counts[0], 5);
1029        assert_eq!(depth.ask_counts[0], 3);
1030    }
1031
1032    #[rstest]
1033    fn test_order_book_depth_with_stub(stub_depth10: OrderBookDepth) {
1034        let depth = stub_depth10;
1035
1036        assert_eq!(depth.instrument_id, InstrumentId::from("AAPL.XNAS"));
1037        assert_eq!(depth.bids.len(), 10);
1038        assert_eq!(depth.asks.len(), 10);
1039        assert_eq!(depth.asks[9].price, Price::from("109.0"));
1040        assert_eq!(depth.asks[0].price, Price::from("100.0"));
1041        assert_eq!(depth.bids[0].price, Price::from("99.0"));
1042        assert_eq!(depth.bids[9].price, Price::from("90.0"));
1043        assert_eq!(depth.bid_counts.len(), 10);
1044        assert_eq!(depth.ask_counts.len(), 10);
1045        assert_eq!(depth.bid_counts[0], 1);
1046        assert_eq!(depth.ask_counts[0], 1);
1047        assert_eq!(depth.flags, 0);
1048        assert_eq!(depth.sequence, 0);
1049        assert_eq!(depth.ts_event, UnixNanos::from(1));
1050        assert_eq!(depth.ts_init, UnixNanos::from(2));
1051    }
1052
1053    #[rstest]
1054    fn test_new(stub_depth10: OrderBookDepth) {
1055        let depth = stub_depth10;
1056        let instrument_id = InstrumentId::from("AAPL.XNAS");
1057        let flags = 0;
1058        let sequence = 0;
1059        let ts_event = 1;
1060        let ts_init = 2;
1061
1062        assert_eq!(depth.instrument_id, instrument_id);
1063        assert_eq!(depth.bids.len(), 10);
1064        assert_eq!(depth.asks.len(), 10);
1065        assert_eq!(depth.asks[9].price, Price::from("109.0"));
1066        assert_eq!(depth.asks[0].price, Price::from("100.0"));
1067        assert_eq!(depth.bids[0].price, Price::from("99.0"));
1068        assert_eq!(depth.bids[9].price, Price::from("90.0"));
1069        assert_eq!(depth.bid_counts.len(), 10);
1070        assert_eq!(depth.ask_counts.len(), 10);
1071        assert_eq!(depth.bid_counts[0], 1);
1072        assert_eq!(depth.ask_counts[0], 1);
1073        assert_eq!(depth.flags, flags);
1074        assert_eq!(depth.sequence, sequence);
1075        assert_eq!(depth.ts_event, ts_event);
1076        assert_eq!(depth.ts_init, ts_init);
1077    }
1078
1079    #[rstest]
1080    fn test_display(stub_depth10: OrderBookDepth) {
1081        let depth = stub_depth10;
1082        assert_eq!(
1083            format!("{depth}"),
1084            "AAPL.XNAS,flags=0,sequence=0,ts_event=1,ts_init=2".to_string()
1085        );
1086    }
1087}