//! EWMA (Exponentially Weighted Moving Average) Threshold Tests //! //! Test Suite for adaptive threshold calculation using EWMA. //! Tests cover initialization, value updates, span parameter effects, //! and edge cases like volatility spikes. use approx::assert_relative_eq; use ml::features::ewma::{AdaptiveThreshold, EWMACalculator}; #[cfg(test)] mod ewma_basic_tests { use super::*; #[test] fn test_ewma_initialization() { let calculator = EWMACalculator::new(100); // Verify alpha calculation: α = 2 / (span + 1) assert_relative_eq!(calculator.alpha, 2.0 / 101.0, epsilon = 1e-10); // Initial state should be None assert!(calculator.ewma.is_none()); assert!(calculator.current().is_none()); } #[test] fn test_ewma_first_value() { let mut calculator = EWMACalculator::new(100); // First value should initialize EWMA to that value let first_value = 100.0; let result = calculator.update(first_value); assert_relative_eq!(result, first_value, epsilon = 1e-10); assert_relative_eq!(calculator.current().unwrap(), first_value, epsilon = 1e-10); } #[test] fn test_ewma_constant_values() { let mut calculator = EWMACalculator::new(100); let constant_value = 50.0; // Update with constant value multiple times for _ in 0..10 { calculator.update(constant_value); } // EWMA should converge to constant value assert_relative_eq!( calculator.current().unwrap(), constant_value, epsilon = 1e-6 ); } #[test] fn test_ewma_span_parameter() { // Test different span values let spans = vec![10, 50, 100, 200]; let values = vec![100.0, 110.0, 120.0, 130.0, 140.0]; for span in spans { let mut calculator = EWMACalculator::new(span); let expected_alpha = 2.0 / (span as f64 + 1.0); assert_relative_eq!(calculator.alpha, expected_alpha, epsilon = 1e-10); // Smaller span = more responsive = higher alpha // Update with increasing values for value in &values { calculator.update(*value); } // Verify EWMA is computed assert!(calculator.current().is_some()); } } } #[cfg(test)] mod ewma_computation_tests { use super::*; #[test] fn test_ewma_formula() { let span = 10; let alpha = 2.0 / (span as f64 + 1.0); // = 2/11 ≈ 0.1818 let mut calculator = EWMACalculator::new(span); // First value let v1 = 100.0; let ewma1 = calculator.update(v1); assert_relative_eq!(ewma1, v1, epsilon = 1e-10); // Second value: EWMA = α * v2 + (1 - α) * EWMA_prev let v2 = 110.0; let expected_ewma2 = alpha * v2 + (1.0 - alpha) * ewma1; let ewma2 = calculator.update(v2); assert_relative_eq!(ewma2, expected_ewma2, epsilon = 1e-10); // Third value let v3 = 105.0; let expected_ewma3 = alpha * v3 + (1.0 - alpha) * ewma2; let ewma3 = calculator.update(v3); assert_relative_eq!(ewma3, expected_ewma3, epsilon = 1e-10); } #[test] fn test_ewma_trend_tracking() { let mut calculator = EWMACalculator::new(20); // Upward trend let upward_values: Vec = (100..120).map(|x| x as f64).collect(); let mut last_ewma = 0.0; for value in upward_values { let ewma = calculator.update(value); if last_ewma > 0.0 { // EWMA should increase with upward trend assert!(ewma > last_ewma, "EWMA should track upward trend"); } last_ewma = ewma; } } #[test] fn test_ewma_mean_reversion() { let mut calculator = EWMACalculator::new(50); // Initialize at 100 calculator.update(100.0); // Spike to 150 calculator.update(150.0); let spike_ewma = calculator.current().unwrap(); // Revert to 100 for _ in 0..20 { calculator.update(100.0); } let reverted_ewma = calculator.current().unwrap(); // EWMA should decrease back towards 100 assert!(reverted_ewma < spike_ewma); assert!(reverted_ewma > 100.0); // But not fully there yet (50 span is slow) } } #[cfg(test)] mod ewma_threshold_adaptation_tests { use super::*; #[test] fn test_adaptive_threshold_normal_volatility() { let mut calculator = EWMACalculator::new(100); // Simulate normal market conditions (low volatility) let base_value = 1000.0; let volatility = 5.0; // ±0.5% for i in 0..50 { let noise = (i as f64 * 0.1).sin() * volatility; calculator.update(base_value + noise); } let ewma = calculator.current().unwrap(); // EWMA should be close to base value assert!((ewma - base_value).abs() < volatility * 2.0); } #[test] fn test_adaptive_threshold_high_volatility() { let mut calculator = EWMACalculator::new(100); // Simulate high volatility market let values = vec![ 1000.0, 1050.0, 980.0, 1020.0, 950.0, 1030.0, 970.0, 1040.0, 990.0, 1010.0, ]; let mut ewma_values = Vec::new(); for value in values { ewma_values.push(calculator.update(value)); } // EWMA should smooth out volatility let ewma_volatility = calculate_std_dev(&ewma_values); let raw_volatility = calculate_std_dev(&vec![ 1000.0, 1050.0, 980.0, 1020.0, 950.0, 1030.0, 970.0, 1040.0, 990.0, 1010.0, ]); // EWMA volatility should be lower than raw volatility assert!(ewma_volatility < raw_volatility); } #[test] fn test_adaptive_threshold_regime_change() { let mut calculator = EWMACalculator::new(50); // Low volatility regime (100-105) for _ in 0..20 { calculator.update(100.0 + (rand::random::() * 5.0)); } let low_vol_ewma = calculator.current().unwrap(); // Regime change to high volatility (100-120) for _ in 0..20 { calculator.update(100.0 + (rand::random::() * 20.0)); } let high_vol_ewma = calculator.current().unwrap(); // EWMA should adapt to new regime assert!((high_vol_ewma - low_vol_ewma).abs() > 0.0); } fn calculate_std_dev(values: &[f64]) -> f64 { let mean = values.iter().sum::() / values.len() as f64; let variance = values.iter().map(|x| (x - mean).powi(2)).sum::() / values.len() as f64; variance.sqrt() } } #[cfg(test)] mod ewma_edge_cases_tests { use super::*; #[test] fn test_ewma_zero_values() { let mut calculator = EWMACalculator::new(100); // Initialize with zero calculator.update(0.0); assert_relative_eq!(calculator.current().unwrap(), 0.0, epsilon = 1e-10); // Add more zeros for _ in 0..10 { calculator.update(0.0); } assert_relative_eq!(calculator.current().unwrap(), 0.0, epsilon = 1e-10); } #[test] fn test_ewma_negative_values() { let mut calculator = EWMACalculator::new(100); // Use negative values (e.g., returns) calculator.update(-5.0); calculator.update(-3.0); calculator.update(-7.0); let ewma = calculator.current().unwrap(); assert!(ewma < 0.0, "EWMA should handle negative values"); } #[test] fn test_ewma_large_values() { let mut calculator = EWMACalculator::new(100); // Use large values (e.g., Bitcoin prices) let large_values = vec![50000.0, 51000.0, 49000.0, 52000.0]; for value in large_values { calculator.update(value); } let ewma = calculator.current().unwrap(); assert!(ewma > 0.0 && ewma < 100000.0); } #[test] fn test_ewma_extreme_volatility_spike() { let mut calculator = EWMACalculator::new(100); // Normal values for _ in 0..50 { calculator.update(100.0); } let normal_ewma = calculator.current().unwrap(); // Extreme spike (10x) calculator.update(1000.0); let spike_ewma = calculator.current().unwrap(); // EWMA should increase but be dampened by history assert!(spike_ewma > normal_ewma); assert!(spike_ewma < 1000.0); // Not fully track the spike // Should be closer to previous EWMA due to long span let expected_spike_ewma = (2.0 / 101.0) * 1000.0 + (99.0 / 101.0) * normal_ewma; assert_relative_eq!(spike_ewma, expected_spike_ewma, epsilon = 1e-6); } #[test] fn test_ewma_reset() { let mut calculator = EWMACalculator::new(100); // Build up history for i in 0..20 { calculator.update(100.0 + i as f64); } assert!(calculator.current().is_some()); // Reset calculator.reset(); assert!(calculator.current().is_none()); // Should reinitialize on next update calculator.update(50.0); assert_relative_eq!(calculator.current().unwrap(), 50.0, epsilon = 1e-10); } #[test] fn test_ewma_very_small_span() { let mut calculator = EWMACalculator::new(2); // Very small span means high alpha (2/3 ≈ 0.667) let alpha = 2.0 / 3.0; assert_relative_eq!(calculator.alpha, alpha, epsilon = 1e-10); // Should be very responsive calculator.update(100.0); calculator.update(200.0); let expected = alpha * 200.0 + (1.0 - alpha) * 100.0; assert_relative_eq!(calculator.current().unwrap(), expected, epsilon = 1e-10); } #[test] fn test_ewma_very_large_span() { let mut calculator = EWMACalculator::new(1000); // Very large span means low alpha (2/1001 ≈ 0.002) let alpha = 2.0 / 1001.0; assert_relative_eq!(calculator.alpha, alpha, epsilon = 1e-10); // Should be very slow to respond calculator.update(100.0); calculator.update(200.0); let expected = alpha * 200.0 + (1.0 - alpha) * 100.0; assert_relative_eq!(calculator.current().unwrap(), expected, epsilon = 1e-10); // Should be close to first value due to low alpha assert!((calculator.current().unwrap() - 100.0).abs() < 5.0); } } #[cfg(test)] mod ewma_span_comparison_tests { use super::*; #[test] fn test_span_responsiveness_comparison() { let spans = vec![10, 50, 100, 200]; let values = vec![100.0, 150.0]; // Sudden jump let mut final_ewmas = Vec::new(); for span in &spans { let mut calculator = EWMACalculator::new(*span); for value in &values { calculator.update(*value); } final_ewmas.push(calculator.current().unwrap()); } // Smaller span should be more responsive (closer to 150.0) for i in 0..final_ewmas.len() - 1 { assert!( (final_ewmas[i] - 150.0).abs() < (final_ewmas[i + 1] - 150.0).abs(), "Smaller span should be more responsive to changes" ); } } #[test] fn test_optimal_span_selection() { // Test different spans on realistic data let market_data = generate_realistic_market_data(100); let spans = vec![10, 20, 50, 100]; for span in spans { let mut calculator = EWMACalculator::new(span); for value in &market_data { calculator.update(*value); } // All spans should produce valid EWMA let ewma = calculator.current().unwrap(); assert!(ewma > 0.0); assert!(ewma.is_finite()); } } fn generate_realistic_market_data(count: usize) -> Vec { let mut data = Vec::new(); let mut price = 1000.0; for i in 0..count { // Add trend + noise let trend = 0.1 * (i as f64 / 10.0); let noise = (i as f64 * 0.3).sin() * 5.0; price += trend + noise; data.push(price); } data } }