//! Volume-based technical indicators validation tests //! //! Tests for OBV (On-Balance Volume), MFI (Money Flow Index), and VWAP //! (Volume-Weighted Average Price) implementation in ML feature extraction. use chrono::Utc; use common::ml_strategy::MLFeatureExtractor; #[test] fn test_obv_accumulation_on_uptrend() { let mut extractor = MLFeatureExtractor::new(20); // Simulate uptrend with increasing prices and volume let prices = vec![100.0, 101.0, 102.0, 103.0, 104.0]; let volumes = vec![1000.0, 1100.0, 1200.0, 1300.0, 1400.0]; let mut features_list = Vec::new(); for (price, volume) in prices.iter().zip(volumes.iter()) { let features = extractor.extract_features(*price, *volume, Utc::now()); features_list.push(features); } // OBV should be increasing (positive accumulation) // Feature index for OBV is 10 in Wave A feature set let obv_feature_idx = 10; // First data point has no previous price, so OBV should be 0 assert_eq!(features_list[0][obv_feature_idx], 0.0); // Subsequent OBV values should be positive and increasing for i in 1..features_list.len() { let obv = features_list[i][obv_feature_idx]; assert!( obv > 0.0, "OBV should be positive in uptrend at index {}", i ); if i > 1 { // Each OBV should be greater than or equal to previous (accumulation) assert!( obv >= features_list[i - 1][obv_feature_idx], "OBV should increase in uptrend: {} < {}", obv, features_list[i - 1][obv_feature_idx] ); } } } #[test] fn test_obv_distribution_on_downtrend() { let mut extractor = MLFeatureExtractor::new(20); // Simulate downtrend with decreasing prices let prices = vec![104.0, 103.0, 102.0, 101.0, 100.0]; let volumes = vec![1000.0, 1100.0, 1200.0, 1300.0, 1400.0]; let mut features_list = Vec::new(); for (price, volume) in prices.iter().zip(volumes.iter()) { let features = extractor.extract_features(*price, *volume, Utc::now()); features_list.push(features); } // OBV is at index 10 in Wave A feature set let obv_feature_idx = 10; // OBV should be decreasing (negative accumulation/distribution) for i in 1..features_list.len() { let obv = features_list[i][obv_feature_idx]; assert!( obv < 0.0, "OBV should be negative in downtrend at index {}", i ); if i > 1 { // Each OBV should be less than or equal to previous (distribution) assert!( obv <= features_list[i - 1][obv_feature_idx], "OBV should decrease in downtrend" ); } } } #[test] fn test_mfi_overbought_signal() { let mut extractor = MLFeatureExtractor::new(20); // Generate 15 bars (need 15 for MFI 14-period calculation) // Strong uptrend with high volume = overbought condition for i in 0..15 { let price = 100.0 + (i as f64 * 2.0); // Strong uptrend let volume = 1000.0 + (i as f64 * 100.0); // Increasing volume extractor.extract_features(price, volume, Utc::now()); } // Last feature extraction should have MFI calculated let features = extractor.extract_features(130.0, 2500.0, Utc::now()); // MFI is at index 11 in Wave A feature set let mfi_feature_idx = 11; let mfi_normalized = features[mfi_feature_idx]; // MFI normalized from [0, 100] to [-1, 1] via ((mfi/50) - 1).tanh() // High MFI (>70 = overbought) should map to positive normalized value // MFI of 100 -> (100/50 - 1).tanh() = 1.0.tanh() = 0.76 assert!( mfi_normalized > 0.5, "MFI should indicate overbought condition (positive normalized value): {}", mfi_normalized ); } #[test] fn test_mfi_oversold_signal() { let mut extractor = MLFeatureExtractor::new(20); // Generate 15 bars with strong downtrend = oversold condition for i in 0..15 { let price = 130.0 - (i as f64 * 2.0); // Strong downtrend let volume = 1000.0 + (i as f64 * 100.0); // Increasing volume on decline extractor.extract_features(price, volume, Utc::now()); } // Last feature extraction let features = extractor.extract_features(100.0, 2500.0, Utc::now()); // MFI is at index 11 in Wave A feature set let mfi_feature_idx = 11; let mfi_normalized = features[mfi_feature_idx]; // MFI normalized from [0, 100] to [-1, 1] // Low MFI (<30 = oversold) should map to negative normalized value // MFI of 0 -> (0/50 - 1).tanh() = -1.0.tanh() = -0.76 assert!( mfi_normalized < -0.3, "MFI should indicate oversold condition (negative normalized value): {}", mfi_normalized ); } #[test] fn test_vwap_price_benchmark() { let mut extractor = MLFeatureExtractor::new(20); // Trade at consistent price with varying volume let base_price = 100.0; let prices = vec![100.0, 102.0, 98.0, 101.0, 99.0, 100.0]; let volumes = vec![1000.0, 500.0, 1500.0, 800.0, 1200.0, 1000.0]; let mut features_list = Vec::new(); for (price, volume) in prices.iter().zip(volumes.iter()) { let features = extractor.extract_features(*price, *volume, Utc::now()); features_list.push(features); } // VWAP is at index 12 in Wave A feature set let vwap_feature_idx = 12; // Last VWAP should be close to base price (oscillating around it) let vwap_ratio = features_list.last().unwrap()[vwap_feature_idx]; // VWAP ratio = (current_price - VWAP) / VWAP, normalized with tanh // Since prices oscillate around 100, VWAP should be near 100, ratio near 0 assert!( vwap_ratio.abs() < 0.3, "VWAP ratio should be near 0 when price oscillates around average: {}", vwap_ratio ); } #[test] fn test_vwap_above_price_signal() { let mut extractor = MLFeatureExtractor::new(20); // Start with high volume at high prices, then drop price with low volume // This will create VWAP above current price (bearish signal) extractor.extract_features(110.0, 5000.0, Utc::now()); // High price, high volume extractor.extract_features(109.0, 4000.0, Utc::now()); extractor.extract_features(108.0, 3000.0, Utc::now()); // Drop price with low volume let features = extractor.extract_features(100.0, 500.0, Utc::now()); // VWAP is at index 12 in Wave A feature set let vwap_feature_idx = 12; let vwap_ratio = features[vwap_feature_idx]; // Price dropped below VWAP -> negative ratio assert!( vwap_ratio < 0.0, "VWAP ratio should be negative when price drops below VWAP: {}", vwap_ratio ); } #[test] fn test_vwap_below_price_signal() { let mut extractor = MLFeatureExtractor::new(20); // Start with high volume at low prices, then raise price with low volume // This will create VWAP below current price (bullish signal) extractor.extract_features(100.0, 5000.0, Utc::now()); // Low price, high volume extractor.extract_features(101.0, 4000.0, Utc::now()); extractor.extract_features(102.0, 3000.0, Utc::now()); // Raise price with low volume let features = extractor.extract_features(110.0, 500.0, Utc::now()); // VWAP is at index 12 in Wave A feature set let vwap_feature_idx = 12; let vwap_ratio = features[vwap_feature_idx]; // Price rose above VWAP -> positive ratio assert!( vwap_ratio > 0.0, "VWAP ratio should be positive when price rises above VWAP: {}", vwap_ratio ); } #[test] fn test_all_volume_indicators_normalized() { let mut extractor = MLFeatureExtractor::new(20); // Generate sufficient data for all indicators (15+ bars for MFI) for i in 0..20 { let price = 100.0 + (i as f64 * 0.5); let volume = 1000.0 + (i as f64 * 50.0); extractor.extract_features(price, volume, Utc::now()); } // Final feature extraction let features = extractor.extract_features(110.0, 2000.0, Utc::now()); // Check that OBV, MFI, VWAP are all normalized to [-1, 1] // Volume indicators are at indices 10, 11, 12 in Wave A feature set let obv_idx = 10; let mfi_idx = 11; let vwap_idx = 12; assert!( features[obv_idx] >= -1.0 && features[obv_idx] <= 1.0, "OBV should be normalized to [-1, 1]: {}", features[obv_idx] ); assert!( features[mfi_idx] >= -1.0 && features[mfi_idx] <= 1.0, "MFI should be normalized to [-1, 1]: {}", features[mfi_idx] ); assert!( features[vwap_idx] >= -1.0 && features[vwap_idx] <= 1.0, "VWAP should be normalized to [-1, 1]: {}", features[vwap_idx] ); } #[test] fn test_feature_vector_length_increased() { let mut extractor = MLFeatureExtractor::new(20); // Generate sufficient data for i in 0..20 { let price = 100.0 + i as f64; let volume = 1000.0 + (i as f64 * 10.0); extractor.extract_features(price, volume, Utc::now()); } let features = extractor.extract_features(120.0, 1200.0, Utc::now()); // 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 have 30 elements (Wave A + Wave C)" ); } #[test] fn test_insufficient_data_graceful_handling() { let mut extractor = MLFeatureExtractor::new(20); // Only 1-2 data points (insufficient for MFI which needs 15) let features1 = extractor.extract_features(100.0, 1000.0, Utc::now()); let features2 = extractor.extract_features(101.0, 1100.0, Utc::now()); // Volume indicators are at indices 10, 11, 12 in Wave A feature set let obv_idx = 10; let mfi_idx = 11; let vwap_idx = 12; // OBV should work with 2 data points assert_eq!( features1[obv_idx], 0.0, "OBV should be 0 for first data point" ); assert!( features2[obv_idx] != 0.0 || features2[obv_idx] == 0.0, "OBV should be calculated or 0 for second data point" ); // MFI should default to 0 with insufficient data assert_eq!( features1[mfi_idx], 0.0, "MFI should be 0 with insufficient data" ); assert_eq!( features2[mfi_idx], 0.0, "MFI should be 0 with insufficient data" ); // VWAP should work with any amount of data assert!( features1[vwap_idx] != 0.0 || features1[vwap_idx] == 0.0, "VWAP should be calculated or 0" ); }