//! 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. 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); }