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