//! TDD Tests for Volume Bar Sampling //! //! Tests volume-based bar formation (bars emitted when volume threshold reached) //! //! ## Test Coverage //! 1. Basic volume accumulation and bar formation //! 2. OHLCV calculation correctness //! 3. Adaptive threshold (EWMA of recent bar volumes) //! 4. Edge cases: single large trade, zero volume periods //! 5. Performance: <50μs per bar formation //! 6. Consistency: volume per bar should match threshold use chrono::Utc; use ml::features::alternative_bars::{OHLCVBar as AltBar, VolumeBarSampler}; use std::time::Instant; #[test] fn test_volume_bar_basic_formation() { // Fixed threshold: 1000 contracts let mut sampler = VolumeBarSampler::new(1000.0, false); // Trade 1: 300 contracts at $4500 (11:00:00) let ts1 = Utc::now(); let bar1 = sampler.update(4500.0, 300.0, ts1); assert!(bar1.is_none(), "Bar should not form yet (300/1000)"); // Trade 2: 400 contracts at $4505 (11:00:05) let ts2 = ts1 + chrono::Duration::seconds(5); let bar2 = sampler.update(4505.0, 400.0, ts2); assert!(bar2.is_none(), "Bar should not form yet (700/1000)"); // Trade 3: 350 contracts at $4510 (11:00:10) -> Exceeds 1000 let ts3 = ts2 + chrono::Duration::seconds(5); let bar3 = sampler.update(4510.0, 350.0, ts3); assert!(bar3.is_some(), "Bar should form (1050 >= 1000)"); let bar = bar3.unwrap(); assert_eq!(bar.open, 4500.0, "Open should be first trade price"); assert_eq!(bar.high, 4510.0, "High should be max price"); assert_eq!(bar.low, 4500.0, "Low should be min price"); assert_eq!(bar.close, 4510.0, "Close should be last trade price"); assert_eq!(bar.volume, 1050.0, "Volume should be sum of trades"); assert_eq!(bar.timestamp, ts1, "Timestamp should be bar start"); println!("✅ Basic volume bar formation validated"); } #[test] fn test_volume_bar_ohlcv_correctness() { let mut sampler = VolumeBarSampler::new(500.0, false); // Scenario: 5 trades forming 2 complete bars let ts_base = Utc::now(); let trades = vec![ // Bar 1 (600 volume) (4500.0, 100.0, ts_base), // Open=4500 (4510.0, 150.0, ts_base + chrono::Duration::seconds(1)), // High=4510 (4495.0, 200.0, ts_base + chrono::Duration::seconds(2)), // Low=4495 (4505.0, 150.0, ts_base + chrono::Duration::seconds(3)), // Close=4505 // Bar 2 (550 volume) (4508.0, 100.0, ts_base + chrono::Duration::seconds(4)), // Open=4508 (4520.0, 250.0, ts_base + chrono::Duration::seconds(5)), // High=4520 (4507.0, 100.0, ts_base + chrono::Duration::seconds(6)), // Low=4507 (4515.0, 100.0, ts_base + chrono::Duration::seconds(7)), // Close=4515 ]; let mut bars = Vec::new(); for (price, volume, ts) in trades { if let Some(bar) = sampler.update(price, volume, ts) { bars.push(bar); } } assert_eq!(bars.len(), 2, "Should form 2 complete bars"); // Bar 1 validation assert_eq!(bars[0].open, 4500.0, "Bar 1: Wrong open"); assert_eq!(bars[0].high, 4510.0, "Bar 1: Wrong high"); assert_eq!(bars[0].low, 4495.0, "Bar 1: Wrong low"); assert_eq!(bars[0].close, 4505.0, "Bar 1: Wrong close"); assert_eq!(bars[0].volume, 600.0, "Bar 1: Wrong volume"); // Bar 2 validation assert_eq!(bars[1].open, 4508.0, "Bar 2: Wrong open"); assert_eq!(bars[1].high, 4520.0, "Bar 2: Wrong high"); assert_eq!(bars[1].low, 4507.0, "Bar 2: Wrong low"); assert_eq!(bars[1].close, 4515.0, "Bar 2: Wrong close"); assert_eq!(bars[1].volume, 550.0, "Bar 2: Wrong volume"); println!("✅ OHLCV calculation correctness validated"); } #[test] fn test_volume_bar_adaptive_threshold() { // Adaptive threshold: uses EWMA of recent bar volumes let mut sampler = VolumeBarSampler::new(1000.0, true); // Enable adaptive // First bar: 1200 volume (exceeds initial threshold) let ts_base = Utc::now(); let bar1 = sampler.update(4500.0, 600.0, ts_base); assert!(bar1.is_none()); let bar1 = sampler.update(4505.0, 600.0, ts_base + chrono::Duration::seconds(1)); assert!(bar1.is_some()); assert_eq!(bar1.as_ref().unwrap().volume, 1200.0); // Second bar: threshold should adapt towards 1200 // EWMA(α=0.2): new_threshold = 0.2 * 1200 + 0.8 * 1000 = 1040 let bar2 = sampler.update(4510.0, 520.0, ts_base + chrono::Duration::seconds(2)); assert!( bar2.is_none(), "Should not form bar yet with adaptive threshold" ); let bar2 = sampler.update(4515.0, 530.0, ts_base + chrono::Duration::seconds(3)); assert!(bar2.is_some(), "Should form bar (1050 > ~1040)"); println!("✅ Adaptive threshold EWMA validated"); } #[test] fn test_volume_bar_single_large_trade() { // Edge case: Single trade exceeds threshold let mut sampler = VolumeBarSampler::new(1000.0, false); let ts = Utc::now(); let bar = sampler.update(4500.0, 5000.0, ts); assert!(bar.is_some(), "Should form bar immediately"); let bar = bar.unwrap(); assert_eq!(bar.open, 4500.0); assert_eq!(bar.high, 4500.0); assert_eq!(bar.low, 4500.0); assert_eq!(bar.close, 4500.0); assert_eq!(bar.volume, 5000.0, "Should capture full large trade"); println!("✅ Single large trade edge case validated"); } #[test] fn test_volume_bar_zero_volume_handling() { // Edge case: Zero volume trades (should be ignored or handled gracefully) let mut sampler = VolumeBarSampler::new(1000.0, false); let ts = Utc::now(); // Zero volume trade let bar1 = sampler.update(4500.0, 0.0, ts); assert!(bar1.is_none(), "Zero volume should not contribute"); // Normal trades after zero volume let bar2 = sampler.update(4505.0, 500.0, ts + chrono::Duration::seconds(1)); assert!(bar2.is_none()); let bar3 = sampler.update(4510.0, 500.0, ts + chrono::Duration::seconds(2)); assert!(bar3.is_some()); let bar = bar3.unwrap(); assert_eq!(bar.volume, 1000.0, "Should only count non-zero volumes"); assert_eq!(bar.open, 4505.0, "Should ignore zero-volume price in OHLC"); println!("✅ Zero volume handling validated"); } #[test] fn test_volume_bar_performance() { // Performance: <50μs per bar formation let mut sampler = VolumeBarSampler::new(10000.0, false); let ts_base = Utc::now(); let num_trades = 10000; let start = Instant::now(); let mut bars_formed = 0; for i in 0..num_trades { let price = 4500.0 + (i as f64 % 100.0) * 0.1; let volume = 5.0; // Small increments to test many updates let ts = ts_base + chrono::Duration::milliseconds(i); if sampler.update(price, volume, ts).is_some() { bars_formed += 1; } } let elapsed = start.elapsed(); let avg_per_update = elapsed.as_nanos() as f64 / num_trades as f64; let avg_per_bar = if bars_formed > 0 { elapsed.as_nanos() as f64 / bars_formed as f64 } else { 0.0 }; println!("✅ Performance test completed:"); println!(" - Total trades: {}", num_trades); println!(" - Bars formed: {}", bars_formed); println!(" - Avg per update: {:.2}ns", avg_per_update); println!( " - Avg per bar: {:.2}ns ({:.2}μs)", avg_per_bar, avg_per_bar / 1000.0 ); // Target: <50μs per bar formation assert!( avg_per_bar < 50_000.0, "Performance target missed: {:.2}μs > 50μs", avg_per_bar / 1000.0 ); } #[test] fn test_volume_bar_consistency() { // Consistency: volume per bar should match threshold (±1 trade) let threshold = 1000.0; let mut sampler = VolumeBarSampler::new(threshold, false); let ts_base = Utc::now(); let mut bars = Vec::new(); // Simulate 5000 trades, each 50 contracts for i in 0..5000 { let price = 4500.0 + (i as f64).sin() * 10.0; let volume = 50.0; let ts = ts_base + chrono::Duration::milliseconds(i); if let Some(bar) = sampler.update(price, volume, ts) { bars.push(bar); } } // Each bar should have volume close to threshold for (i, bar) in bars.iter().enumerate() { assert!( bar.volume >= threshold && bar.volume <= threshold + 50.0, "Bar {} volume {} outside expected range [{}, {}]", i, bar.volume, threshold, threshold + 50.0 ); } println!( "✅ Volume consistency validated: {} bars formed", bars.len() ); println!( " - Volume range: {:.2} - {:.2}", bars.iter().map(|b| b.volume).fold(f64::INFINITY, f64::min), bars.iter() .map(|b| b.volume) .fold(f64::NEG_INFINITY, f64::max) ); } #[test] fn test_volume_bar_time_interval_variance() { // Volume bars should have varying time intervals (high activity = faster bars) let mut sampler = VolumeBarSampler::new(1000.0, false); let ts_base = Utc::now(); let mut bars = Vec::new(); // Simulate varying activity: first 10 bars fast, next 10 bars slow let mut ts = ts_base; // Fast activity: 100 contracts per second (10s per bar) for _ in 0..10 { for _ in 0..10 { ts = ts + chrono::Duration::seconds(1); if let Some(bar) = sampler.update(4500.0, 100.0, ts) { bars.push((bar, ts)); break; } } } // Slow activity: 10 contracts per second (100s per bar) for _ in 0..10 { for _ in 0..100 { ts = ts + chrono::Duration::seconds(1); if let Some(bar) = sampler.update(4500.0, 10.0, ts) { bars.push((bar, ts)); break; } } } // Validate time intervals vary assert_eq!(bars.len(), 20, "Should form 20 bars"); let fast_intervals: Vec<_> = bars .iter() .take(10) .zip(bars.iter().skip(1).take(9)) .map(|((_, ts1), (_, ts2))| (*ts2 - *ts1).num_seconds()) .collect(); let slow_intervals: Vec<_> = bars .iter() .skip(10) .take(9) .zip(bars.iter().skip(11).take(9)) .map(|((_, ts1), (_, ts2))| (*ts2 - *ts1).num_seconds()) .collect(); let avg_fast = fast_intervals.iter().sum::() as f64 / fast_intervals.len() as f64; let avg_slow = slow_intervals.iter().sum::() as f64 / slow_intervals.len() as f64; println!("✅ Time interval variance validated:"); println!(" - Fast activity: avg {:.2}s per bar", avg_fast); println!(" - Slow activity: avg {:.2}s per bar", avg_slow); assert!( avg_slow > avg_fast * 5.0, "Slow bars should take significantly longer than fast bars" ); } #[test] fn test_volume_bar_multiple_bar_sequence() { // Integration test: Process 1000 trades, validate all bars let mut sampler = VolumeBarSampler::new(500.0, false); let ts_base = Utc::now(); let mut bars = Vec::new(); for i in 0..1000 { let price = 4500.0 + (i as f64 * 0.1).sin() * 50.0; let volume = 10.0 + (i as f64 * 0.05).cos() * 5.0; // Varying volume let ts = ts_base + chrono::Duration::milliseconds(i * 100); if let Some(bar) = sampler.update(price, volume, ts) { bars.push(bar); } } // Should form ~100 bars (1000 trades * ~10-15 volume / 500 threshold) assert!( bars.len() >= 20 && bars.len() <= 35, "Expected 20-35 bars, got {}", bars.len() ); // All bars should be valid for (i, bar) in bars.iter().enumerate() { assert!(bar.open > 0.0, "Bar {} has invalid open", i); assert!(bar.high >= bar.low, "Bar {} has high < low", i); assert!(bar.high >= bar.open, "Bar {} has high < open", i); assert!(bar.high >= bar.close, "Bar {} has high < close", i); assert!(bar.low <= bar.open, "Bar {} has low > open", i); assert!(bar.low <= bar.close, "Bar {} has low > close", i); assert!(bar.volume > 0.0, "Bar {} has zero volume", i); } println!("✅ Multiple bar sequence validated: {} bars", bars.len()); }