//! MBP-10 Parser Tests //! //! Test suite for Market By Price (10 levels) order book parsing and snapshot aggregation. //! Uses TDD methodology: tests written first, implementation follows. #![allow( clippy::absurd_extreme_comparisons, clippy::assertions_on_constants, clippy::assertions_on_result_states, clippy::clone_on_copy, clippy::decimal_literal_representation, clippy::doc_lazy_continuation, clippy::doc_markdown, clippy::double_comparisons, clippy::else_if_without_else, clippy::empty_drop, clippy::expect_used, clippy::field_reassign_with_default, clippy::format_push_string, clippy::if_then_some_else_none, clippy::indexing_slicing, clippy::integer_division, clippy::len_zero, clippy::let_and_return, clippy::let_underscore_must_use, clippy::manual_let_else, clippy::manual_range_contains, clippy::map_err_ignore, clippy::missing_const_for_fn, clippy::modulo_arithmetic, clippy::needless_range_loop, clippy::new_without_default, clippy::non_ascii_literal, clippy::nonminimal_bool, clippy::octal_escapes, clippy::overly_complex_bool_expr, clippy::redundant_clone, clippy::shadow_reuse, clippy::shadow_unrelated, clippy::similar_names, clippy::single_component_path_imports, clippy::single_match_else, clippy::str_to_string, clippy::string_slice, clippy::tests_outside_test_module, clippy::unnecessary_cast, clippy::unnecessary_get_then_check, clippy::unnecessary_unwrap, clippy::unseparated_literal_suffix, clippy::unwrap_or_default, clippy::unwrap_used, clippy::use_debug, clippy::useless_vec, clippy::wildcard_enum_match_arm, dead_code, deprecated, unreachable_pub, unused_assignments, unused_comparisons, unused_imports, unused_variables )] use anyhow::Result; use data::providers::databento::{ dbn_parser::{DbnParser, ProcessedMessage}, mbp10::{BidAskPair, Mbp10Snapshot, OrderBookAction}, }; /// Test MBP-10 snapshot creation from single update #[tokio::test] async fn test_mbp10_snapshot_creation() -> Result<()> { // Create test BidAskPair data let levels = vec![ BidAskPair { bid_px: 150000000000000, // 150.000000 (scaled by 1e9) bid_sz: 100, bid_ct: 5, ask_px: 150010000000000, // 150.010000 ask_sz: 120, ask_ct: 6, }, BidAskPair { bid_px: 149990000000000, bid_sz: 200, bid_ct: 8, ask_px: 150020000000000, ask_sz: 180, ask_ct: 7, }, ]; let snapshot = Mbp10Snapshot::new( "ES.FUT".to_string(), 1640995200000000000, // timestamp in nanoseconds levels.clone(), 0, 100, ); assert_eq!(snapshot.symbol, "ES.FUT"); assert_eq!(snapshot.levels.len(), 2); assert_eq!(snapshot.levels[0].bid_sz, 100); assert_eq!(snapshot.levels[0].ask_sz, 120); Ok(()) } /// Test fixed-point price conversion #[test] fn test_mbp10_price_conversion() { let pair = BidAskPair { bid_px: 150000000000000, // 150.000000 * 1e9 bid_sz: 100, bid_ct: 5, ask_px: 150010000000000, // 150.010000 * 1e9 ask_sz: 120, ask_ct: 6, }; let bid_price = BidAskPair::price_to_f64(pair.bid_px); let ask_price = BidAskPair::price_to_f64(pair.ask_px); assert!((bid_price - 150.0).abs() < 0.001); assert!((ask_price - 150.01).abs() < 0.001); } /// Test best bid/ask extraction #[test] fn test_mbp10_best_bid_ask() { let levels = vec![ BidAskPair { bid_px: 150000000000000, bid_sz: 100, bid_ct: 5, ask_px: 150010000000000, ask_sz: 120, ask_ct: 6, }, BidAskPair { bid_px: 149990000000000, // Lower bid bid_sz: 200, bid_ct: 8, ask_px: 150020000000000, // Higher ask ask_sz: 180, ask_ct: 7, }, ]; let snapshot = Mbp10Snapshot::new("ES.FUT".to_string(), 1640995200000000000, levels, 0, 100); let (best_bid, best_ask) = snapshot.get_best_bid_ask(); assert!((best_bid - 150.0).abs() < 0.001); assert!((best_ask - 150.01).abs() < 0.001); } /// Test mid price calculation #[test] fn test_mbp10_mid_price() { let levels = vec![BidAskPair { bid_px: 150000000000000, bid_sz: 100, bid_ct: 5, ask_px: 150010000000000, ask_sz: 120, ask_ct: 6, }]; let snapshot = Mbp10Snapshot::new("ES.FUT".to_string(), 1640995200000000000, levels, 0, 100); let mid = snapshot.mid_price(); assert!((mid - 150.005).abs() < 0.001); } /// Test spread calculation #[test] fn test_mbp10_spread() { let levels = vec![BidAskPair { bid_px: 150000000000000, bid_sz: 100, bid_ct: 5, ask_px: 150010000000000, ask_sz: 120, ask_ct: 6, }]; let snapshot = Mbp10Snapshot::new("ES.FUT".to_string(), 1640995200000000000, levels, 0, 100); let spread = snapshot.spread(); assert!((spread - 0.01).abs() < 0.001); } /// Test total volume calculations #[test] fn test_mbp10_total_volumes() { let levels = vec![ BidAskPair { bid_px: 150000000000000, bid_sz: 100, bid_ct: 5, ask_px: 150010000000000, ask_sz: 120, ask_ct: 6, }, BidAskPair { bid_px: 149990000000000, bid_sz: 200, bid_ct: 8, ask_px: 150020000000000, ask_sz: 180, ask_ct: 7, }, ]; let snapshot = Mbp10Snapshot::new("ES.FUT".to_string(), 1640995200000000000, levels, 0, 100); assert_eq!(snapshot.total_bid_volume(), 300); // 100 + 200 assert_eq!(snapshot.total_ask_volume(), 300); // 120 + 180 } /// Test volume imbalance calculation #[test] fn test_mbp10_volume_imbalance() { let levels = vec![BidAskPair { bid_px: 150000000000000, bid_sz: 200, // More bid volume bid_ct: 5, ask_px: 150010000000000, ask_sz: 100, ask_ct: 6, }]; let snapshot = Mbp10Snapshot::new("ES.FUT".to_string(), 1640995200000000000, levels, 0, 100); let imbalance = snapshot.volume_imbalance(); // (200 - 100) / (200 + 100) = 100 / 300 ≈ 0.333 assert!((imbalance - 0.333).abs() < 0.01); } /// Test order book depth #[test] fn test_mbp10_depth() { let levels = vec![ BidAskPair { bid_px: 150000000000000, bid_sz: 100, bid_ct: 5, ask_px: 150010000000000, ask_sz: 120, ask_ct: 6, }, BidAskPair { bid_px: 149990000000000, bid_sz: 200, bid_ct: 8, ask_px: 150020000000000, ask_sz: 180, ask_ct: 7, }, BidAskPair { bid_px: 149980000000000, bid_sz: 150, bid_ct: 4, ask_px: 150030000000000, ask_sz: 160, ask_ct: 5, }, ]; let snapshot = Mbp10Snapshot::new("ES.FUT".to_string(), 1640995200000000000, levels, 0, 100); assert_eq!(snapshot.depth(), 3); } /// Test DBN parser MBP-10 file loading (integration test) #[tokio::test] #[ignore = "Requires real MBP-10 DBN file"] async fn test_parse_mbp10_file() -> Result<()> { let parser = DbnParser::new()?; // This will be run with real MBP-10 test data let snapshots = parser .parse_mbp10_file("test_data/ES.FUT.mbp10.dbn") .await?; assert!(!snapshots.is_empty()); assert_eq!(snapshots[0].levels.len(), 10); // Verify first snapshot has valid data let first = &snapshots[0]; assert!(first.levels[0].bid_px > 0); assert!(first.levels[0].ask_px > first.levels[0].bid_px); assert!(first.levels[0].bid_sz > 0); assert!(first.levels[0].ask_sz > 0); Ok(()) } /// Test snapshot aggregation from incremental MBP-10 updates #[test] fn test_mbp10_snapshot_aggregation() { // Test that we can aggregate multiple MBP-10 update messages // into a full 10-level order book snapshot let mut snapshot = Mbp10Snapshot::empty("ES.FUT".to_string()); // Add first level (bid side) snapshot.update_level( 0, OrderBookAction::Add, 150000000000000, 100, 5, true, // is_bid ); // Add first level (ask side) snapshot.update_level( 0, OrderBookAction::Add, 150010000000000, 120, 6, false, // is_ask ); assert_eq!(snapshot.levels[0].bid_sz, 100); assert_eq!(snapshot.levels[0].ask_sz, 120); } /// Test performance: parse 1000 MBP-10 snapshots in <1ms #[test] #[ignore = "Performance benchmark"] fn test_mbp10_parsing_performance() { use std::time::Instant; // Create 1000 test snapshots let test_level = BidAskPair { bid_px: 150000000000000, bid_sz: 100, bid_ct: 5, ask_px: 150010000000000, ask_sz: 120, ask_ct: 6, }; let start = Instant::now(); for _ in 0..1000 { let _snapshot = Mbp10Snapshot::new( "ES.FUT".to_string(), 1640995200000000000, vec![test_level; 10], 0, 100, ); } let elapsed = start.elapsed(); println!("Parsed 1000 snapshots in {:?}", elapsed); // Target: <1ms total (1μs per snapshot) assert!(elapsed.as_micros() < 1000); }