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 `OrderBookDepth10` aggregated top-of-book data type with a fixed depth of 10 levels per side.
17
18use std::{collections::HashMap, fmt::Display};
19
20use indexmap::IndexMap;
21use nautilus_core::{UnixNanos, serialization::Serializable};
22use serde::{Deserialize, Serialize};
23
24use super::{HasTsInit, order::BookOrder};
25use crate::{identifiers::InstrumentId, types::fixed::FIXED_SIZE_BINARY};
26
27pub const DEPTH10_LEN: usize = 10;
28
29/// Represents an aggregated order book update with a fixed depth of 10 levels per side.
30///
31/// This structure is specifically designed for scenarios where a snapshot of the top 10 bid and
32/// ask levels in an order book is needed. It differs from `OrderBookDelta` or `OrderBookDeltas`
33/// in its fixed-depth nature and is optimized for cases where a full depth representation is not
34/// required or practical.
35///
36/// Note: This type is not compatible with `OrderBookDelta` or `OrderBookDeltas` due to
37/// its specialized structure and limited depth use case.
38///
39/// Per-level [`BookOrder::order_id`] values are non-semantic for this aggregated MBP data.
40/// Parquet catalog decoding canonicalizes them to zero.
41#[repr(C)]
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[cfg_attr(
44    feature = "python",
45    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
46)]
47#[cfg_attr(
48    feature = "python",
49    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
50)]
51pub struct OrderBookDepth10 {
52    /// The instrument ID for the book.
53    pub instrument_id: InstrumentId,
54    /// The bid orders for the depth update.
55    pub bids: [BookOrder; DEPTH10_LEN],
56    /// The ask orders for the depth update.
57    pub asks: [BookOrder; DEPTH10_LEN],
58    /// The count of bid orders per level for the depth update.
59    pub bid_counts: [u32; DEPTH10_LEN],
60    /// The count of ask orders per level for the depth update.
61    pub ask_counts: [u32; DEPTH10_LEN],
62    /// The record flags bit field, indicating event end and data information.
63    pub flags: u8,
64    /// The message sequence number assigned at the venue.
65    pub sequence: u64,
66    /// UNIX timestamp (nanoseconds) when the book event occurred.
67    pub ts_event: UnixNanos,
68    /// UNIX timestamp (nanoseconds) when the instance was created.
69    pub ts_init: UnixNanos,
70}
71
72impl OrderBookDepth10 {
73    /// Creates a new [`OrderBookDepth10`] instance.
74    #[expect(clippy::too_many_arguments)]
75    #[must_use]
76    pub fn new(
77        instrument_id: InstrumentId,
78        bids: [BookOrder; DEPTH10_LEN],
79        asks: [BookOrder; DEPTH10_LEN],
80        bid_counts: [u32; DEPTH10_LEN],
81        ask_counts: [u32; DEPTH10_LEN],
82        flags: u8,
83        sequence: u64,
84        ts_event: UnixNanos,
85        ts_init: UnixNanos,
86    ) -> Self {
87        Self {
88            instrument_id,
89            bids,
90            asks,
91            bid_counts,
92            ask_counts,
93            flags,
94            sequence,
95            ts_event,
96            ts_init,
97        }
98    }
99
100    /// Returns the metadata for the type, for use with serialization formats.
101    #[must_use]
102    pub fn get_metadata(
103        instrument_id: &InstrumentId,
104        price_precision: u8,
105        size_precision: u8,
106    ) -> HashMap<String, String> {
107        let mut metadata = HashMap::new();
108        metadata.insert("instrument_id".to_string(), instrument_id.to_string());
109        metadata.insert("price_precision".to_string(), price_precision.to_string());
110        metadata.insert("size_precision".to_string(), size_precision.to_string());
111        metadata
112    }
113
114    /// Returns the field map for the type, for use with Arrow schemas.
115    #[must_use]
116    pub fn get_fields() -> IndexMap<String, String> {
117        let mut metadata = IndexMap::new();
118        metadata.insert("bid_price_0".to_string(), FIXED_SIZE_BINARY.to_string());
119        metadata.insert("bid_price_1".to_string(), FIXED_SIZE_BINARY.to_string());
120        metadata.insert("bid_price_2".to_string(), FIXED_SIZE_BINARY.to_string());
121        metadata.insert("bid_price_3".to_string(), FIXED_SIZE_BINARY.to_string());
122        metadata.insert("bid_price_4".to_string(), FIXED_SIZE_BINARY.to_string());
123        metadata.insert("bid_price_5".to_string(), FIXED_SIZE_BINARY.to_string());
124        metadata.insert("bid_price_6".to_string(), FIXED_SIZE_BINARY.to_string());
125        metadata.insert("bid_price_7".to_string(), FIXED_SIZE_BINARY.to_string());
126        metadata.insert("bid_price_8".to_string(), FIXED_SIZE_BINARY.to_string());
127        metadata.insert("bid_price_9".to_string(), FIXED_SIZE_BINARY.to_string());
128        metadata.insert("ask_price_0".to_string(), FIXED_SIZE_BINARY.to_string());
129        metadata.insert("ask_price_1".to_string(), FIXED_SIZE_BINARY.to_string());
130        metadata.insert("ask_price_2".to_string(), FIXED_SIZE_BINARY.to_string());
131        metadata.insert("ask_price_3".to_string(), FIXED_SIZE_BINARY.to_string());
132        metadata.insert("ask_price_4".to_string(), FIXED_SIZE_BINARY.to_string());
133        metadata.insert("ask_price_5".to_string(), FIXED_SIZE_BINARY.to_string());
134        metadata.insert("ask_price_6".to_string(), FIXED_SIZE_BINARY.to_string());
135        metadata.insert("ask_price_7".to_string(), FIXED_SIZE_BINARY.to_string());
136        metadata.insert("ask_price_8".to_string(), FIXED_SIZE_BINARY.to_string());
137        metadata.insert("ask_price_9".to_string(), FIXED_SIZE_BINARY.to_string());
138        metadata.insert("bid_size_0".to_string(), FIXED_SIZE_BINARY.to_string());
139        metadata.insert("bid_size_1".to_string(), FIXED_SIZE_BINARY.to_string());
140        metadata.insert("bid_size_2".to_string(), FIXED_SIZE_BINARY.to_string());
141        metadata.insert("bid_size_3".to_string(), FIXED_SIZE_BINARY.to_string());
142        metadata.insert("bid_size_4".to_string(), FIXED_SIZE_BINARY.to_string());
143        metadata.insert("bid_size_5".to_string(), FIXED_SIZE_BINARY.to_string());
144        metadata.insert("bid_size_6".to_string(), FIXED_SIZE_BINARY.to_string());
145        metadata.insert("bid_size_7".to_string(), FIXED_SIZE_BINARY.to_string());
146        metadata.insert("bid_size_8".to_string(), FIXED_SIZE_BINARY.to_string());
147        metadata.insert("bid_size_9".to_string(), FIXED_SIZE_BINARY.to_string());
148        metadata.insert("ask_size_0".to_string(), FIXED_SIZE_BINARY.to_string());
149        metadata.insert("ask_size_1".to_string(), FIXED_SIZE_BINARY.to_string());
150        metadata.insert("ask_size_2".to_string(), FIXED_SIZE_BINARY.to_string());
151        metadata.insert("ask_size_3".to_string(), FIXED_SIZE_BINARY.to_string());
152        metadata.insert("ask_size_4".to_string(), FIXED_SIZE_BINARY.to_string());
153        metadata.insert("ask_size_5".to_string(), FIXED_SIZE_BINARY.to_string());
154        metadata.insert("ask_size_6".to_string(), FIXED_SIZE_BINARY.to_string());
155        metadata.insert("ask_size_7".to_string(), FIXED_SIZE_BINARY.to_string());
156        metadata.insert("ask_size_8".to_string(), FIXED_SIZE_BINARY.to_string());
157        metadata.insert("ask_size_9".to_string(), FIXED_SIZE_BINARY.to_string());
158        metadata.insert("bid_count_0".to_string(), "UInt32".to_string());
159        metadata.insert("bid_count_1".to_string(), "UInt32".to_string());
160        metadata.insert("bid_count_2".to_string(), "UInt32".to_string());
161        metadata.insert("bid_count_3".to_string(), "UInt32".to_string());
162        metadata.insert("bid_count_4".to_string(), "UInt32".to_string());
163        metadata.insert("bid_count_5".to_string(), "UInt32".to_string());
164        metadata.insert("bid_count_6".to_string(), "UInt32".to_string());
165        metadata.insert("bid_count_7".to_string(), "UInt32".to_string());
166        metadata.insert("bid_count_8".to_string(), "UInt32".to_string());
167        metadata.insert("bid_count_9".to_string(), "UInt32".to_string());
168        metadata.insert("ask_count_0".to_string(), "UInt32".to_string());
169        metadata.insert("ask_count_1".to_string(), "UInt32".to_string());
170        metadata.insert("ask_count_2".to_string(), "UInt32".to_string());
171        metadata.insert("ask_count_3".to_string(), "UInt32".to_string());
172        metadata.insert("ask_count_4".to_string(), "UInt32".to_string());
173        metadata.insert("ask_count_5".to_string(), "UInt32".to_string());
174        metadata.insert("ask_count_6".to_string(), "UInt32".to_string());
175        metadata.insert("ask_count_7".to_string(), "UInt32".to_string());
176        metadata.insert("ask_count_8".to_string(), "UInt32".to_string());
177        metadata.insert("ask_count_9".to_string(), "UInt32".to_string());
178        metadata.insert("flags".to_string(), "UInt8".to_string());
179        metadata.insert("sequence".to_string(), "UInt64".to_string());
180        metadata.insert("ts_event".to_string(), "UInt64".to_string());
181        metadata.insert("ts_init".to_string(), "UInt64".to_string());
182        metadata
183    }
184}
185
186// TODO: Exact format for Debug and Display TBD
187impl Display for OrderBookDepth10 {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        write!(
190            f,
191            "{},flags={},sequence={},ts_event={},ts_init={}",
192            self.instrument_id, self.flags, self.sequence, self.ts_event, self.ts_init
193        )
194    }
195}
196
197impl Serializable for OrderBookDepth10 {}
198
199impl HasTsInit for OrderBookDepth10 {
200    fn ts_init(&self) -> UnixNanos {
201        self.ts_init
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use std::{
208        collections::hash_map::DefaultHasher,
209        hash::{Hash, Hasher},
210    };
211
212    use rstest::rstest;
213    use serde_json;
214
215    use super::*;
216    use crate::{
217        data::{order::BookOrder, stubs::*},
218        enums::OrderSide,
219        types::{Price, Quantity},
220    };
221
222    fn create_test_book_order(
223        side: OrderSide,
224        price: &str,
225        size: &str,
226        order_id: u64,
227    ) -> BookOrder {
228        BookOrder::new(side, Price::from(price), Quantity::from(size), order_id)
229    }
230
231    fn create_test_depth10() -> OrderBookDepth10 {
232        let instrument_id = InstrumentId::from("EURUSD.SIM");
233
234        // Create bid orders (descending prices)
235        let bids = [
236            create_test_book_order(OrderSide::Buy, "1.0500", "100000", 1),
237            create_test_book_order(OrderSide::Buy, "1.0499", "150000", 2),
238            create_test_book_order(OrderSide::Buy, "1.0498", "200000", 3),
239            create_test_book_order(OrderSide::Buy, "1.0497", "125000", 4),
240            create_test_book_order(OrderSide::Buy, "1.0496", "175000", 5),
241            create_test_book_order(OrderSide::Buy, "1.0495", "100000", 6),
242            create_test_book_order(OrderSide::Buy, "1.0494", "225000", 7),
243            create_test_book_order(OrderSide::Buy, "1.0493", "150000", 8),
244            create_test_book_order(OrderSide::Buy, "1.0492", "300000", 9),
245            create_test_book_order(OrderSide::Buy, "1.0491", "175000", 10),
246        ];
247
248        // Create ask orders (ascending prices)
249        let asks = [
250            create_test_book_order(OrderSide::Sell, "1.0501", "100000", 11),
251            create_test_book_order(OrderSide::Sell, "1.0502", "125000", 12),
252            create_test_book_order(OrderSide::Sell, "1.0503", "150000", 13),
253            create_test_book_order(OrderSide::Sell, "1.0504", "175000", 14),
254            create_test_book_order(OrderSide::Sell, "1.0505", "200000", 15),
255            create_test_book_order(OrderSide::Sell, "1.0506", "100000", 16),
256            create_test_book_order(OrderSide::Sell, "1.0507", "250000", 17),
257            create_test_book_order(OrderSide::Sell, "1.0508", "125000", 18),
258            create_test_book_order(OrderSide::Sell, "1.0509", "300000", 19),
259            create_test_book_order(OrderSide::Sell, "1.0510", "175000", 20),
260        ];
261
262        let bid_counts = [1, 2, 1, 3, 1, 2, 1, 4, 1, 2];
263        let ask_counts = [2, 1, 3, 1, 2, 1, 4, 1, 2, 3];
264
265        OrderBookDepth10::new(
266            instrument_id,
267            bids,
268            asks,
269            bid_counts,
270            ask_counts,
271            32,                             // flags
272            12345,                          // sequence
273            UnixNanos::from(1_000_000_000), // ts_event
274            UnixNanos::from(2_000_000_000), // ts_init
275        )
276    }
277
278    fn create_empty_depth10() -> OrderBookDepth10 {
279        let instrument_id = InstrumentId::from("EMPTY.TEST");
280
281        // Create empty orders with zero prices and quantities
282        let empty_bid = create_test_book_order(OrderSide::Buy, "0.0", "0", 0);
283        let empty_ask = create_test_book_order(OrderSide::Sell, "0.0", "0", 0);
284
285        OrderBookDepth10::new(
286            instrument_id,
287            [empty_bid; DEPTH10_LEN],
288            [empty_ask; DEPTH10_LEN],
289            [0; DEPTH10_LEN],
290            [0; DEPTH10_LEN],
291            0,
292            0,
293            UnixNanos::from(0),
294            UnixNanos::from(0),
295        )
296    }
297
298    #[rstest]
299    fn test_order_book_depth10_new() {
300        let depth = create_test_depth10();
301
302        assert_eq!(depth.instrument_id, InstrumentId::from("EURUSD.SIM"));
303        assert_eq!(depth.bids.len(), DEPTH10_LEN);
304        assert_eq!(depth.asks.len(), DEPTH10_LEN);
305        assert_eq!(depth.bid_counts.len(), DEPTH10_LEN);
306        assert_eq!(depth.ask_counts.len(), DEPTH10_LEN);
307        assert_eq!(depth.flags, 32);
308        assert_eq!(depth.sequence, 12345);
309        assert_eq!(depth.ts_event, UnixNanos::from(1_000_000_000));
310        assert_eq!(depth.ts_init, UnixNanos::from(2_000_000_000));
311    }
312
313    #[rstest]
314    fn test_order_book_depth10_new_with_all_parameters() {
315        let instrument_id = InstrumentId::from("GBPUSD.SIM");
316        let bid = create_test_book_order(OrderSide::Buy, "1.2500", "50000", 1);
317        let ask = create_test_book_order(OrderSide::Sell, "1.2501", "75000", 2);
318        let flags = 64u8;
319        let sequence = 999u64;
320        let ts_event = UnixNanos::from(5_000_000_000);
321        let ts_init = UnixNanos::from(6_000_000_000);
322
323        let depth = OrderBookDepth10::new(
324            instrument_id,
325            [bid; DEPTH10_LEN],
326            [ask; DEPTH10_LEN],
327            [5; DEPTH10_LEN],
328            [3; DEPTH10_LEN],
329            flags,
330            sequence,
331            ts_event,
332            ts_init,
333        );
334
335        assert_eq!(depth.instrument_id, instrument_id);
336        assert_eq!(depth.bids[0], bid);
337        assert_eq!(depth.asks[0], ask);
338        assert_eq!(depth.bid_counts[0], 5);
339        assert_eq!(depth.ask_counts[0], 3);
340        assert_eq!(depth.flags, flags);
341        assert_eq!(depth.sequence, sequence);
342        assert_eq!(depth.ts_event, ts_event);
343        assert_eq!(depth.ts_init, ts_init);
344    }
345
346    #[rstest]
347    fn test_order_book_depth10_fixed_array_sizes() {
348        let depth = create_test_depth10();
349
350        // Verify arrays are exactly DEPTH10_LEN (10)
351        assert_eq!(depth.bids.len(), 10);
352        assert_eq!(depth.asks.len(), 10);
353        assert_eq!(depth.bid_counts.len(), 10);
354        assert_eq!(depth.ask_counts.len(), 10);
355
356        // Verify DEPTH10_LEN constant
357        assert_eq!(DEPTH10_LEN, 10);
358    }
359
360    #[rstest]
361    fn test_order_book_depth10_array_indexing() {
362        let depth = create_test_depth10();
363
364        // Test first and last elements of each array
365        assert_eq!(depth.bids[0].price, Price::from("1.0500"));
366        assert_eq!(depth.bids[9].price, Price::from("1.0491"));
367        assert_eq!(depth.asks[0].price, Price::from("1.0501"));
368        assert_eq!(depth.asks[9].price, Price::from("1.0510"));
369        assert_eq!(depth.bid_counts[0], 1);
370        assert_eq!(depth.bid_counts[9], 2);
371        assert_eq!(depth.ask_counts[0], 2);
372        assert_eq!(depth.ask_counts[9], 3);
373    }
374
375    #[rstest]
376    fn test_order_book_depth10_bid_ask_ordering() {
377        let depth = create_test_depth10();
378
379        // Verify bid prices are in descending order (highest to lowest)
380        for i in 0..9 {
381            assert!(
382                depth.bids[i].price >= depth.bids[i + 1].price,
383                "Bid prices should be in descending order: {} >= {}",
384                depth.bids[i].price,
385                depth.bids[i + 1].price
386            );
387        }
388
389        // Verify ask prices are in ascending order (lowest to highest)
390        for i in 0..9 {
391            assert!(
392                depth.asks[i].price <= depth.asks[i + 1].price,
393                "Ask prices should be in ascending order: {} <= {}",
394                depth.asks[i].price,
395                depth.asks[i + 1].price
396            );
397        }
398
399        // Verify bid-ask spread (best bid < best ask)
400        assert!(
401            depth.bids[0].price < depth.asks[0].price,
402            "Best bid {} should be less than best ask {}",
403            depth.bids[0].price,
404            depth.asks[0].price
405        );
406    }
407
408    #[rstest]
409    fn test_order_book_depth10_clone() {
410        let depth1 = create_test_depth10();
411        let depth2 = depth1;
412
413        assert_eq!(depth1.instrument_id, depth2.instrument_id);
414        assert_eq!(depth1.bids, depth2.bids);
415        assert_eq!(depth1.asks, depth2.asks);
416        assert_eq!(depth1.bid_counts, depth2.bid_counts);
417        assert_eq!(depth1.ask_counts, depth2.ask_counts);
418        assert_eq!(depth1.flags, depth2.flags);
419        assert_eq!(depth1.sequence, depth2.sequence);
420        assert_eq!(depth1.ts_event, depth2.ts_event);
421        assert_eq!(depth1.ts_init, depth2.ts_init);
422    }
423
424    #[rstest]
425    fn test_order_book_depth10_copy() {
426        let depth1 = create_test_depth10();
427        let depth2 = depth1;
428
429        // Verify Copy trait by modifying one and ensuring the other is unchanged
430        // Since we're using Copy, this should work without explicit clone
431        assert_eq!(depth1, depth2);
432    }
433
434    #[rstest]
435    fn test_order_book_depth10_debug() {
436        let depth = create_test_depth10();
437        let debug_str = format!("{depth:?}");
438
439        assert!(debug_str.contains("OrderBookDepth10"));
440        assert!(debug_str.contains("EURUSD.SIM"));
441        assert!(debug_str.contains("flags: 32"));
442        assert!(debug_str.contains("sequence: 12345"));
443    }
444
445    #[rstest]
446    fn test_order_book_depth10_partial_eq() {
447        let depth1 = create_test_depth10();
448        let depth2 = create_test_depth10();
449        let depth3 = create_empty_depth10();
450
451        assert_eq!(depth1, depth2); // Same data
452        assert_ne!(depth1, depth3); // Different data
453        assert_ne!(depth2, depth3); // Different data
454    }
455
456    #[rstest]
457    fn test_order_book_depth10_eq_consistency() {
458        let depth1 = create_test_depth10();
459        let depth2 = create_test_depth10();
460
461        assert_eq!(depth1, depth2);
462        assert_eq!(depth2, depth1); // Symmetry
463        assert_eq!(depth1, depth1); // Reflexivity
464    }
465
466    #[rstest]
467    fn test_order_book_depth10_hash() {
468        let depth1 = create_test_depth10();
469        let depth2 = create_test_depth10();
470
471        let mut hasher1 = DefaultHasher::new();
472        let mut hasher2 = DefaultHasher::new();
473
474        depth1.hash(&mut hasher1);
475        depth2.hash(&mut hasher2);
476
477        assert_eq!(hasher1.finish(), hasher2.finish()); // Equal objects have equal hashes
478    }
479
480    #[rstest]
481    fn test_order_book_depth10_hash_different_objects() {
482        let depth1 = create_test_depth10();
483        let depth2 = create_empty_depth10();
484
485        let mut hasher1 = DefaultHasher::new();
486        let mut hasher2 = DefaultHasher::new();
487
488        depth1.hash(&mut hasher1);
489        depth2.hash(&mut hasher2);
490
491        assert_ne!(hasher1.finish(), hasher2.finish()); // Different objects should have different hashes
492    }
493
494    #[rstest]
495    fn test_order_book_depth10_display() {
496        let depth = create_test_depth10();
497        let display_str = format!("{depth}");
498
499        assert!(display_str.contains("EURUSD.SIM"));
500        assert!(display_str.contains("flags=32"));
501        assert!(display_str.contains("sequence=12345"));
502        assert!(display_str.contains("ts_event=1000000000"));
503        assert!(display_str.contains("ts_init=2000000000"));
504    }
505
506    #[rstest]
507    fn test_order_book_depth10_display_format() {
508        let depth = create_test_depth10();
509        let expected = "EURUSD.SIM,flags=32,sequence=12345,ts_event=1000000000,ts_init=2000000000";
510
511        assert_eq!(format!("{depth}"), expected);
512    }
513
514    #[rstest]
515    fn test_order_book_depth10_serialization() {
516        let depth = create_test_depth10();
517
518        // Test JSON serialization
519        let json = serde_json::to_string(&depth).unwrap();
520        let deserialized: OrderBookDepth10 = serde_json::from_str(&json).unwrap();
521
522        assert_eq!(depth, deserialized);
523    }
524
525    #[rstest]
526    fn test_order_book_depth10_serializable_trait() {
527        fn assert_serializable<T: Serializable>(_: &T) {}
528
529        let depth = create_test_depth10();
530
531        // Verify Serializable trait is implemented (compile-time check)
532        assert_serializable(&depth);
533    }
534
535    #[rstest]
536    fn test_order_book_depth10_has_ts_init() {
537        let depth = create_test_depth10();
538
539        assert_eq!(depth.ts_init(), UnixNanos::from(2_000_000_000));
540    }
541
542    #[rstest]
543    fn test_order_book_depth10_get_metadata() {
544        let instrument_id = InstrumentId::from("EURUSD.SIM");
545        let price_precision = 5u8;
546        let size_precision = 0u8;
547
548        let metadata =
549            OrderBookDepth10::get_metadata(&instrument_id, price_precision, size_precision);
550
551        assert_eq!(
552            metadata.get("instrument_id"),
553            Some(&"EURUSD.SIM".to_string())
554        );
555        assert_eq!(metadata.get("price_precision"), Some(&"5".to_string()));
556        assert_eq!(metadata.get("size_precision"), Some(&"0".to_string()));
557        assert_eq!(metadata.len(), 3);
558    }
559
560    #[rstest]
561    fn test_order_book_depth10_get_fields() {
562        let fields = OrderBookDepth10::get_fields();
563
564        // Verify all 10 bid and ask price fields
565        for i in 0..10 {
566            assert_eq!(
567                fields.get(&format!("bid_price_{i}")),
568                Some(&FIXED_SIZE_BINARY.to_string())
569            );
570            assert_eq!(
571                fields.get(&format!("ask_price_{i}")),
572                Some(&FIXED_SIZE_BINARY.to_string())
573            );
574        }
575
576        // Verify all 10 bid and ask size fields
577        for i in 0..10 {
578            assert_eq!(
579                fields.get(&format!("bid_size_{i}")),
580                Some(&FIXED_SIZE_BINARY.to_string())
581            );
582            assert_eq!(
583                fields.get(&format!("ask_size_{i}")),
584                Some(&FIXED_SIZE_BINARY.to_string())
585            );
586        }
587
588        // Verify all 10 bid and ask count fields
589        for i in 0..10 {
590            assert_eq!(
591                fields.get(&format!("bid_count_{i}")),
592                Some(&"UInt32".to_string())
593            );
594            assert_eq!(
595                fields.get(&format!("ask_count_{i}")),
596                Some(&"UInt32".to_string())
597            );
598        }
599
600        // Verify metadata fields
601        assert_eq!(fields.get("flags"), Some(&"UInt8".to_string()));
602        assert_eq!(fields.get("sequence"), Some(&"UInt64".to_string()));
603        assert_eq!(fields.get("ts_event"), Some(&"UInt64".to_string()));
604        assert_eq!(fields.get("ts_init"), Some(&"UInt64".to_string()));
605
606        // Verify total field count:
607        // 10 bid_price + 10 ask_price + 10 bid_size + 10 ask_size + 10 bid_count + 10 ask_count + 4 metadata = 64
608        assert_eq!(fields.len(), 64);
609    }
610
611    #[rstest]
612    fn test_order_book_depth10_get_fields_order() {
613        let fields = OrderBookDepth10::get_fields();
614        let keys: Vec<&String> = fields.keys().collect();
615
616        // Verify the ordering of fields matches expectations
617        assert_eq!(keys[0], "bid_price_0");
618        assert_eq!(keys[9], "bid_price_9");
619        assert_eq!(keys[10], "ask_price_0");
620        assert_eq!(keys[19], "ask_price_9");
621        assert_eq!(keys[20], "bid_size_0");
622        assert_eq!(keys[29], "bid_size_9");
623        assert_eq!(keys[30], "ask_size_0");
624        assert_eq!(keys[39], "ask_size_9");
625        assert_eq!(keys[40], "bid_count_0");
626        assert_eq!(keys[41], "bid_count_1");
627    }
628
629    #[rstest]
630    fn test_order_book_depth10_empty_values() {
631        let depth = create_empty_depth10();
632
633        assert_eq!(depth.instrument_id, InstrumentId::from("EMPTY.TEST"));
634        assert_eq!(depth.flags, 0);
635        assert_eq!(depth.sequence, 0);
636        assert_eq!(depth.ts_event, UnixNanos::from(0));
637        assert_eq!(depth.ts_init, UnixNanos::from(0));
638
639        // Verify all orders have zero prices and quantities
640        for bid in &depth.bids {
641            assert_eq!(bid.price, Price::from("0.0"));
642            assert_eq!(bid.size, Quantity::from("0"));
643            assert_eq!(bid.order_id, 0);
644        }
645
646        for ask in &depth.asks {
647            assert_eq!(ask.price, Price::from("0.0"));
648            assert_eq!(ask.size, Quantity::from("0"));
649            assert_eq!(ask.order_id, 0);
650        }
651
652        // Verify all counts are zero
653        for &count in &depth.bid_counts {
654            assert_eq!(count, 0);
655        }
656
657        for &count in &depth.ask_counts {
658            assert_eq!(count, 0);
659        }
660    }
661
662    #[rstest]
663    fn test_order_book_depth10_max_values() {
664        let instrument_id = InstrumentId::from("MAX.TEST");
665        let max_bid = create_test_book_order(OrderSide::Buy, "999999.99", "999999999", u64::MAX);
666        let max_ask = create_test_book_order(OrderSide::Sell, "1000000.00", "999999999", u64::MAX);
667
668        let depth = OrderBookDepth10::new(
669            instrument_id,
670            [max_bid; DEPTH10_LEN],
671            [max_ask; DEPTH10_LEN],
672            [u32::MAX; DEPTH10_LEN],
673            [u32::MAX; DEPTH10_LEN],
674            u8::MAX,
675            u64::MAX,
676            UnixNanos::from(u64::MAX),
677            UnixNanos::from(u64::MAX),
678        );
679
680        assert_eq!(depth.flags, u8::MAX);
681        assert_eq!(depth.sequence, u64::MAX);
682        assert_eq!(depth.ts_event, UnixNanos::from(u64::MAX));
683        assert_eq!(depth.ts_init, UnixNanos::from(u64::MAX));
684
685        for &count in &depth.bid_counts {
686            assert_eq!(count, u32::MAX);
687        }
688
689        for &count in &depth.ask_counts {
690            assert_eq!(count, u32::MAX);
691        }
692    }
693
694    #[rstest]
695    fn test_order_book_depth10_different_instruments() {
696        let instruments = [
697            "EURUSD.SIM",
698            "GBPUSD.SIM",
699            "USDJPY.SIM",
700            "AUDUSD.SIM",
701            "USDCHF.SIM",
702        ];
703
704        for instrument_str in &instruments {
705            let instrument_id = InstrumentId::from(*instrument_str);
706            let bid = create_test_book_order(OrderSide::Buy, "1.0000", "100000", 1);
707            let ask = create_test_book_order(OrderSide::Sell, "1.0001", "100000", 2);
708
709            let depth = OrderBookDepth10::new(
710                instrument_id,
711                [bid; DEPTH10_LEN],
712                [ask; DEPTH10_LEN],
713                [1; DEPTH10_LEN],
714                [1; DEPTH10_LEN],
715                0,
716                1,
717                UnixNanos::from(1_000_000_000),
718                UnixNanos::from(2_000_000_000),
719            );
720
721            assert_eq!(depth.instrument_id, instrument_id);
722            assert!(format!("{depth}").contains(instrument_str));
723        }
724    }
725
726    #[rstest]
727    fn test_order_book_depth10_realistic_forex_spread() {
728        let instrument_id = InstrumentId::from("EURUSD.SIM");
729
730        // Realistic EUR/USD spread with 0.1 pip spread
731        let best_bid = create_test_book_order(OrderSide::Buy, "1.08500", "1000000", 1);
732        let best_ask = create_test_book_order(OrderSide::Sell, "1.08501", "1000000", 2);
733
734        let depth = OrderBookDepth10::new(
735            instrument_id,
736            [best_bid; DEPTH10_LEN],
737            [best_ask; DEPTH10_LEN],
738            [5; DEPTH10_LEN], // Realistic order count
739            [3; DEPTH10_LEN],
740            16,                                         // Realistic flags
741            123_456,                                    // Realistic sequence
742            UnixNanos::from(1_672_531_200_000_000_000), // Jan 1, 2023 timestamp
743            UnixNanos::from(1_672_531_200_000_100_000),
744        );
745
746        assert_eq!(depth.bids[0].price, Price::from("1.08500"));
747        assert_eq!(depth.asks[0].price, Price::from("1.08501"));
748        assert!(depth.bids[0].price < depth.asks[0].price); // Positive spread
749
750        // Verify realistic quantities and counts
751        assert_eq!(depth.bids[0].size, Quantity::from("1000000"));
752        assert_eq!(depth.bid_counts[0], 5);
753        assert_eq!(depth.ask_counts[0], 3);
754    }
755
756    #[rstest]
757    fn test_order_book_depth10_with_stub(stub_depth10: OrderBookDepth10) {
758        let depth = stub_depth10;
759
760        assert_eq!(depth.instrument_id, InstrumentId::from("AAPL.XNAS"));
761        assert_eq!(depth.bids.len(), 10);
762        assert_eq!(depth.asks.len(), 10);
763        assert_eq!(depth.asks[9].price, Price::from("109.0"));
764        assert_eq!(depth.asks[0].price, Price::from("100.0"));
765        assert_eq!(depth.bids[0].price, Price::from("99.0"));
766        assert_eq!(depth.bids[9].price, Price::from("90.0"));
767        assert_eq!(depth.bid_counts.len(), 10);
768        assert_eq!(depth.ask_counts.len(), 10);
769        assert_eq!(depth.bid_counts[0], 1);
770        assert_eq!(depth.ask_counts[0], 1);
771        assert_eq!(depth.flags, 0);
772        assert_eq!(depth.sequence, 0);
773        assert_eq!(depth.ts_event, UnixNanos::from(1));
774        assert_eq!(depth.ts_init, UnixNanos::from(2));
775    }
776
777    #[rstest]
778    fn test_new(stub_depth10: OrderBookDepth10) {
779        let depth = stub_depth10;
780        let instrument_id = InstrumentId::from("AAPL.XNAS");
781        let flags = 0;
782        let sequence = 0;
783        let ts_event = 1;
784        let ts_init = 2;
785
786        assert_eq!(depth.instrument_id, instrument_id);
787        assert_eq!(depth.bids.len(), 10);
788        assert_eq!(depth.asks.len(), 10);
789        assert_eq!(depth.asks[9].price, Price::from("109.0"));
790        assert_eq!(depth.asks[0].price, Price::from("100.0"));
791        assert_eq!(depth.bids[0].price, Price::from("99.0"));
792        assert_eq!(depth.bids[9].price, Price::from("90.0"));
793        assert_eq!(depth.bid_counts.len(), 10);
794        assert_eq!(depth.ask_counts.len(), 10);
795        assert_eq!(depth.bid_counts[0], 1);
796        assert_eq!(depth.ask_counts[0], 1);
797        assert_eq!(depth.flags, flags);
798        assert_eq!(depth.sequence, sequence);
799        assert_eq!(depth.ts_event, ts_event);
800        assert_eq!(depth.ts_init, ts_init);
801    }
802
803    #[rstest]
804    fn test_display(stub_depth10: OrderBookDepth10) {
805        let depth = stub_depth10;
806        assert_eq!(
807            format!("{depth}"),
808            "AAPL.XNAS,flags=0,sequence=0,ts_event=1,ts_init=2".to_string()
809        );
810    }
811}