//! Integration tests for volume-based technical indicators (OBV, MFI, VWAP) //! //! This test suite validates the implementation of volume indicators added in Wave 19.1.3 use chrono::Utc; use common::ml_strategy::MLFeatureExtractor; #[test] fn test_obv_accumulation_uptrend() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Simulate strong uptrend with increasing volume for i in 0..20 { let price = 100.0 + (i as f64 * 2.0); let volume = 1000.0 + (i as f64 * 50.0); let features = extractor.extract_features(price, volume, timestamp); if i >= 1 { // OBV is at index 10 (7 base + 3 oscillators) let obv = features[10]; // OBV should be positive in sustained uptrend assert!( obv > 0.0 || i == 1, "OBV should be positive in uptrend at iteration {}, got {}", i, obv ); } } } #[test] fn test_obv_distribution_downtrend() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Simulate strong downtrend for i in 0..20 { let price = 140.0 - (i as f64 * 2.0); let volume = 1000.0 + (i as f64 * 50.0); let features = extractor.extract_features(price, volume, timestamp); if i >= 1 { let obv = features[10]; // OBV should be negative in sustained downtrend assert!( obv < 0.0 || i == 1, "OBV should be negative in downtrend at iteration {}, got {}", i, obv ); } } } #[test] fn test_obv_unchanged_on_flat_price() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Extract first feature to initialize extractor.extract_features(100.0, 1000.0, timestamp); // Same price, different volumes - OBV should remain unchanged let features1 = extractor.extract_features(100.0, 1500.0, timestamp); let features2 = extractor.extract_features(100.0, 2000.0, timestamp); let features3 = extractor.extract_features(100.0, 500.0, timestamp); let obv1 = features1[10]; let obv2 = features2[10]; let obv3 = features3[10]; // All OBV values should be equal when price is flat assert_eq!(obv1, obv2, "OBV should not change when price is unchanged"); assert_eq!(obv2, obv3, "OBV should not change when price is unchanged"); } #[test] fn test_mfi_overbought_condition() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Generate 15+ bars for MFI calculation // Strong sustained uptrend with high volume = overbought for i in 0..16 { let price = 100.0 + (i as f64 * 3.0); let volume = 1000.0 + (i as f64 * 200.0); extractor.extract_features(price, volume, timestamp); } // Final strong up move let features = extractor.extract_features(148.0, 4000.0, timestamp); // MFI is at index 11 (7 base + 3 oscillators + OBV) let mfi = features[11]; // MFI should be strongly positive (overbought, normalized from high MFI value) assert!( mfi > 0.3, "MFI should indicate overbought condition (positive), got {}", mfi ); } #[test] fn test_mfi_oversold_condition() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Generate 15+ bars for MFI calculation // Strong sustained downtrend with high volume = oversold for i in 0..16 { let price = 148.0 - (i as f64 * 3.0); let volume = 1000.0 + (i as f64 * 200.0); extractor.extract_features(price, volume, timestamp); } // Final strong down move let features = extractor.extract_features(100.0, 4000.0, timestamp); let mfi = features[11]; // MFI should be strongly negative (oversold, normalized from low MFI value) assert!( mfi < -0.3, "MFI should indicate oversold condition (negative), got {}", mfi ); } #[test] fn test_mfi_neutral_condition() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Generate mixed market with equal buying/selling pressure for i in 0..15 { let price = if i % 2 == 0 { 100.0 } else { 101.0 }; let volume = 1000.0; extractor.extract_features(price, volume, timestamp); } let features = extractor.extract_features(100.5, 1000.0, timestamp); let mfi = features[11]; // MFI should be near neutral (close to 0) assert!( mfi.abs() < 0.5, "MFI should be near neutral with mixed signals, got {}", mfi ); } #[test] fn test_vwap_benchmark_oscillating_market() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Trade around a base price with varying volumes let base_price = 100.0; let prices = vec![100.0, 102.0, 98.0, 101.0, 99.0, 100.0, 103.0, 97.0]; let volumes = vec![1000.0, 500.0, 1500.0, 800.0, 1200.0, 1000.0, 600.0, 1400.0]; for (price, volume) in prices.iter().zip(volumes.iter()) { extractor.extract_features(*price, *volume, timestamp); } let features = extractor.extract_features(100.0, 1000.0, timestamp); // VWAP is at index 12 (7 base + 3 oscillators + OBV + MFI) let vwap_ratio = features[12]; // VWAP ratio should be near 0 when price oscillates around average assert!( vwap_ratio.abs() < 0.2, "VWAP ratio should be near 0 for oscillating prices, got {}", vwap_ratio ); } #[test] fn test_vwap_below_current_price() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // High volume at low prices, then price rises with low volume extractor.extract_features(100.0, 5000.0, timestamp); extractor.extract_features(101.0, 4000.0, timestamp); extractor.extract_features(102.0, 3000.0, timestamp); // Price jumps up with low volume let features = extractor.extract_features(110.0, 500.0, timestamp); let vwap_ratio = features[12]; // Price > VWAP, so ratio should be positive (bullish) assert!( vwap_ratio > 0.0, "VWAP ratio should be positive when price > VWAP, got {}", vwap_ratio ); } #[test] fn test_vwap_above_current_price() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // High volume at high prices, then price drops with low volume extractor.extract_features(110.0, 5000.0, timestamp); extractor.extract_features(109.0, 4000.0, timestamp); extractor.extract_features(108.0, 3000.0, timestamp); // Price drops with low volume let features = extractor.extract_features(100.0, 500.0, timestamp); let vwap_ratio = features[12]; // Price < VWAP, so ratio should be negative (bearish) assert!( vwap_ratio < 0.0, "VWAP ratio should be negative when price < VWAP, got {}", vwap_ratio ); } #[test] fn test_all_volume_indicators_normalized() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Generate diverse market conditions to test normalization for i in 0..20 { let price = 100.0 + ((i as f64 * 5.0).sin() * 20.0); // Volatile sine wave let volume = 500.0 + (i as f64 * 100.0); // Increasing volume extractor.extract_features(price, volume, timestamp); } let features = extractor.extract_features(105.0, 2500.0, timestamp); let obv = features[10]; let mfi = features[11]; let vwap = features[12]; // All volume indicators should be in [-1, 1] range assert!( obv >= -1.0 && obv <= 1.0, "OBV should be normalized to [-1, 1], got {}", obv ); assert!( mfi >= -1.0 && mfi <= 1.0, "MFI should be normalized to [-1, 1], got {}", mfi ); assert!( vwap >= -1.0 && vwap <= 1.0, "VWAP should be normalized to [-1, 1], got {}", vwap ); } #[test] fn test_volume_indicators_with_extreme_values() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Test with extreme volume spikes and price movements for i in 0..15 { let price = if i == 10 { 150.0 } else { 100.0 }; // Price spike let volume = if i == 10 { 50000.0 } else { 1000.0 }; // Volume spike extractor.extract_features(price, volume, timestamp); } let features = extractor.extract_features(102.0, 1200.0, timestamp); let obv = features[10]; let mfi = features[11]; let vwap = features[12]; // Even with extreme values, indicators should remain normalized assert!( obv >= -1.0 && obv <= 1.0, "OBV should handle extreme values, got {}", obv ); assert!( mfi >= -1.0 && mfi <= 1.0, "MFI should handle extreme values, got {}", mfi ); assert!( vwap >= -1.0 && vwap <= 1.0, "VWAP should handle extreme values, got {}", vwap ); } #[test] fn test_volume_indicators_insufficient_data() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Test with minimal data points let features1 = extractor.extract_features(100.0, 1000.0, timestamp); let features2 = extractor.extract_features(101.0, 1100.0, timestamp); // OBV should work with 2 data points assert_eq!(features1[10], 0.0, "OBV should be 0 for first data point"); // MFI should default to 0 with insufficient data (needs 15 points) assert_eq!(features1[11], 0.0, "MFI should be 0 with insufficient data"); assert_eq!(features2[11], 0.0, "MFI should be 0 with insufficient data"); // VWAP should work with any amount of data assert!( features1[12] >= -1.0 && features1[12] <= 1.0, "VWAP should be calculated even with minimal data" ); } #[test] fn test_feature_vector_includes_volume_indicators() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Generate sufficient data for all indicators for i in 0..30 { let price = 100.0 + (i as f64 * 0.5); let volume = 1000.0 + (i as f64 * 10.0); extractor.extract_features(price, volume, timestamp); } let features = extractor.extract_features(115.0, 1300.0, timestamp); // Total features: 30 (Wave A + Wave C) // Wave A: 26 features (7 base + 3 oscillators + 3 volume + 5 EMA + 1 ADX + 1 BB + 2 Stoch + 1 CCI + 1 RSI + 2 MACD) // Wave C: 4 features (OBV Momentum, Volume Oscillator, A/D Line, EMA Ratio) assert_eq!( features.len(), 30, "Feature vector should include all 30 features (Wave A + Wave C)" ); // Verify volume indicators are at correct indices let obv = features[10]; let mfi = features[11]; let vwap = features[12]; assert!(obv.abs() <= 1.0, "OBV at index 10"); assert!(mfi.abs() <= 1.0, "MFI at index 11"); assert!(vwap.abs() <= 1.0, "VWAP at index 12"); } #[test] fn test_volume_indicators_provide_unique_signals() { let mut extractor = MLFeatureExtractor::new(30); let timestamp = Utc::now(); // Create scenario where volume indicators should diverge // Phase 1: High volume accumulation at low prices for i in 0..10 { let price = 100.0 - (i as f64 * 0.5); let volume = 1000.0 + (i as f64 * 300.0); // Increasing volume extractor.extract_features(price, volume, timestamp); } // Phase 2: Price recovery with moderate volume for i in 0..10 { let price = 95.0 + (i as f64 * 1.0); let volume = 1500.0; // Consistent moderate volume extractor.extract_features(price, volume, timestamp); } let features = extractor.extract_features(105.0, 1600.0, timestamp); let obv = features[10]; let mfi = features[11]; let vwap = features[12]; // All three indicators should provide different perspectives // OBV: Should reflect volume accumulation during downturn + recovery // MFI: Should show recent buying pressure (14-period window) // VWAP: Should show price relative to volume-weighted average // Verify they're not all the same (they provide unique information) let indicators_equal = (obv - mfi).abs() < 0.01 && (mfi - vwap).abs() < 0.01; assert!( !indicators_equal, "Volume indicators should provide different signals: OBV={}, MFI={}, VWAP={}", obv, mfi, vwap ); }