//! DBN Alternative Bars Integration Test //! //! Tests the integration of DBN data loader with alternative bar samplers. //! Validates tick extraction from DBN files and feeding to samplers. //! //! Wave B Agent B13: DBN Data Adapter for Alternative Bars (TDD) use chrono::Utc; use ml::data_loaders::dbn_tick_adapter::{DBNTickAdapter, Tick}; use ml::features::alternative_bars::{DollarBarSampler, TickBarSampler, VolumeBarSampler}; use std::collections::HashMap; use std::path::PathBuf; #[tokio::test] async fn test_dbn_tick_adapter_creation() { // Test: DBNTickAdapter can be created with file mapping let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await; assert!(adapter.is_ok(), "Failed to create DBNTickAdapter"); } #[tokio::test] async fn test_load_ticks_from_dbn() { // Test: Can load ticks from ES.FUT DBN file let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let ticks = adapter.load_ticks("ES.FUT").await; assert!(ticks.is_ok(), "Failed to load ticks"); let ticks = ticks.unwrap(); // ES.FUT has 1,674 OHLCV bars → should generate ~6,696 ticks (4 per bar) assert!( ticks.len() >= 1000, "Expected at least 1000 ticks, got {}", ticks.len() ); assert!( ticks.len() <= 10000, "Expected at most 10000 ticks, got {}", ticks.len() ); // Validate first tick let first_tick = &ticks[0]; assert!( first_tick.price > 0.0, "First tick price should be positive" ); assert!( first_tick.volume > 0.0, "First tick volume should be positive" ); assert!( first_tick.timestamp.timestamp() > 0, "First tick timestamp should be valid" ); } #[tokio::test] async fn test_tick_structure() { // Test: Tick structure has correct fields let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let ticks = adapter.load_ticks("ES.FUT").await.unwrap(); for (idx, tick) in ticks.iter().take(100).enumerate() { assert!( tick.price > 0.0, "Tick {} price should be positive: {}", idx, tick.price ); assert!( tick.volume >= 0.0, "Tick {} volume should be non-negative: {}", idx, tick.volume ); assert!( tick.timestamp.timestamp() > 0, "Tick {} timestamp should be valid", idx ); } } #[tokio::test] async fn test_feed_ticks_to_tick_bar_sampler() { // Test: Can feed DBN ticks to TickBarSampler and generate bars let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let ticks = adapter.load_ticks("ES.FUT").await.unwrap(); // Create tick bar sampler (100 ticks per bar) let mut sampler = TickBarSampler::new(100); let mut bars_generated = 0; for tick in ticks.iter() { if let Some(_bar) = sampler.update(tick.price, tick.volume, tick.timestamp) { bars_generated += 1; } } // With ~6,696 ticks and 100 ticks/bar, expect ~66 bars assert!( bars_generated >= 50, "Expected at least 50 tick bars, got {}", bars_generated ); assert!( bars_generated <= 100, "Expected at most 100 tick bars, got {}", bars_generated ); } #[tokio::test] async fn test_feed_ticks_to_volume_bar_sampler() { // Test: Can feed DBN ticks to VolumeBarSampler and generate bars let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let ticks = adapter.load_ticks("ES.FUT").await.unwrap(); // Create volume bar sampler (1000 volume per bar) let mut sampler = VolumeBarSampler::new(1000); let mut bars_generated = 0; for tick in ticks.iter() { if let Some(_bar) = sampler.update(tick.price, tick.volume, tick.timestamp) { bars_generated += 1; } } // Volume bars depend on total volume in dataset assert!( bars_generated >= 10, "Expected at least 10 volume bars, got {}", bars_generated ); } #[tokio::test] async fn test_feed_ticks_to_dollar_bar_sampler() { // Test: Can feed DBN ticks to DollarBarSampler and generate bars let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let ticks = adapter.load_ticks("ES.FUT").await.unwrap(); // Create dollar bar sampler ($1,000,000 per bar - ES.FUT trades at ~4700-4800) let mut sampler = DollarBarSampler::new(1_000_000.0); let mut bars_generated = 0; for tick in ticks.iter() { if let Some(_bar) = sampler.update(tick.price, tick.volume, tick.timestamp) { bars_generated += 1; } } // Dollar bars depend on total dollar volume in dataset assert!( bars_generated >= 5, "Expected at least 5 dollar bars, got {}", bars_generated ); } #[tokio::test] async fn test_bar_count_consistency() { // Test: Bar counts are consistent across multiple runs let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let ticks = adapter.load_ticks("ES.FUT").await.unwrap(); // Run 1: Generate tick bars let mut sampler1 = TickBarSampler::new(100); let mut bars1 = 0; for tick in ticks.iter() { if sampler1 .update(tick.price, tick.volume, tick.timestamp) .is_some() { bars1 += 1; } } // Run 2: Generate tick bars (should be identical) let mut sampler2 = TickBarSampler::new(100); let mut bars2 = 0; for tick in ticks.iter() { if sampler2 .update(tick.price, tick.volume, tick.timestamp) .is_some() { bars2 += 1; } } assert_eq!( bars1, bars2, "Bar counts should be consistent: {} vs {}", bars1, bars2 ); } #[tokio::test] async fn test_es_fut_real_data() { // Test: ES.FUT generates expected number of ticks and bars let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let ticks = adapter.load_ticks("ES.FUT").await.unwrap(); println!("ES.FUT ticks: {}", ticks.len()); // ES.FUT has 1,674 bars → ~6,696 ticks (4 per bar) assert!( ticks.len() >= 5000, "ES.FUT should have at least 5000 ticks, got {}", ticks.len() ); // Generate tick bars (100 ticks per bar) let mut sampler = TickBarSampler::new(100); let mut bars = Vec::new(); for tick in ticks.iter() { if let Some(bar) = sampler.update(tick.price, tick.volume, tick.timestamp) { bars.push(bar); } } println!("ES.FUT tick bars: {}", bars.len()); // Expect ~66 bars (6,696 ticks / 100 ticks per bar) assert!( bars.len() >= 50, "ES.FUT should generate at least 50 tick bars, got {}", bars.len() ); assert!( bars.len() <= 100, "ES.FUT should generate at most 100 tick bars, got {}", bars.len() ); } #[tokio::test] async fn test_tick_adapter_with_missing_file() { // Test: Error handling for missing DBN file let mut file_mapping = HashMap::new(); file_mapping.insert( "MISSING.FUT".to_string(), PathBuf::from("nonexistent/path/missing.dbn"), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let result = adapter.load_ticks("MISSING.FUT").await; assert!( result.is_err(), "Should return error for missing file, got Ok" ); } #[tokio::test] async fn test_tick_adapter_with_unknown_symbol() { // Test: Error handling for unknown symbol let mut file_mapping = HashMap::new(); file_mapping.insert( "ES.FUT".to_string(), PathBuf::from( "/home/jgrusewski/Work/foxhunt/test_data/real/databento/ES.FUT_ohlcv-1m_2024-01-02.dbn", ), ); let adapter = DBNTickAdapter::new(file_mapping).await.unwrap(); let result = adapter.load_ticks("UNKNOWN.FUT").await; assert!( result.is_err(), "Should return error for unknown symbol, got Ok" ); }