nautilus_model/data/
depth.rs

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