// ml/tests/barrier_optimization_test.rs // // TDD Test Suite for Barrier Optimization Engine // Tests MUST be written BEFORE implementation use approx::assert_relative_eq; use std::time::Instant; // Import types that will be implemented use ml::features::barrier_optimization::{BarrierOptimizer, BarrierParams, OptimizationResult}; #[test] fn test_barrier_params_validation() { // Test valid parameters let params = BarrierParams::new(2.0, 1.0, 10); assert_relative_eq!(params.profit_factor, 2.0, epsilon = 1e-6); assert_relative_eq!(params.stop_factor, 1.0, epsilon = 1e-6); assert_eq!(params.time_horizon, 10); } #[test] #[should_panic(expected = "profit_factor must be positive")] fn test_barrier_params_negative_profit() { BarrierParams::new(-1.0, 1.0, 10); } #[test] #[should_panic(expected = "stop_factor must be positive")] fn test_barrier_params_negative_stop() { BarrierParams::new(2.0, -1.0, 10); } #[test] #[should_panic(expected = "time_horizon must be at least 1")] fn test_barrier_params_zero_horizon() { BarrierParams::new(2.0, 1.0, 0); } #[test] fn test_optimizer_creation_default() { let optimizer = BarrierOptimizer::new(); // Default ranges assert_eq!(optimizer.profit_range().len(), 5); // [1.0, 1.5, 2.0, 2.5, 3.0] assert_eq!(optimizer.stop_range().len(), 4); // [0.5, 1.0, 1.5, 2.0] assert_eq!(optimizer.horizon_range().len(), 4); // [5, 10, 20, 30] // Total combinations: 5 * 4 * 4 = 80 assert_eq!(optimizer.total_combinations(), 80); } #[test] fn test_optimizer_creation_custom() { let profit_range = vec![1.5, 2.0, 2.5]; let stop_range = vec![0.5, 1.0]; let horizon_range = vec![10, 20]; let optimizer = BarrierOptimizer::with_ranges( profit_range.clone(), stop_range.clone(), horizon_range.clone(), ); assert_eq!(optimizer.profit_range(), &profit_range); assert_eq!(optimizer.stop_range(), &stop_range); assert_eq!(optimizer.horizon_range(), &horizon_range); assert_eq!(optimizer.total_combinations(), 3 * 2 * 2); // 12 } #[test] fn test_sharpe_ratio_calculation_positive_returns() { let optimizer = BarrierOptimizer::new(); // Positive returns with some volatility let returns = vec![0.01, 0.02, -0.005, 0.015, 0.008]; let sharpe = optimizer.calculate_sharpe(&returns); // Should be positive (profitable strategy) assert!(sharpe > 0.0); assert!(sharpe.is_finite()); } #[test] fn test_sharpe_ratio_calculation_negative_returns() { let optimizer = BarrierOptimizer::new(); // Negative returns (losing strategy) let returns = vec![-0.01, -0.02, 0.005, -0.015, -0.008]; let sharpe = optimizer.calculate_sharpe(&returns); // Should be negative assert!(sharpe < 0.0); assert!(sharpe.is_finite()); } #[test] fn test_sharpe_ratio_zero_volatility() { let optimizer = BarrierOptimizer::new(); // All returns are identical (zero volatility) let returns = vec![0.01, 0.01, 0.01, 0.01, 0.01]; let sharpe = optimizer.calculate_sharpe(&returns); // Should handle gracefully (return 0.0 or large value) assert!(sharpe.is_finite()); } #[test] fn test_sharpe_ratio_empty_returns() { let optimizer = BarrierOptimizer::new(); let returns = vec![]; let sharpe = optimizer.calculate_sharpe(&returns); // Should return 0.0 for empty data assert_relative_eq!(sharpe, 0.0, epsilon = 1e-6); } #[test] fn test_backtest_params_simple_uptrend() { let optimizer = BarrierOptimizer::new(); // Simple uptrend: prices increase steadily let prices = vec![100.0, 101.0, 102.0, 103.0, 104.0, 105.0]; let params = BarrierParams::new(2.0, 1.0, 3); let sharpe = optimizer.backtest_params(¶ms, &prices); // Uptrend should produce positive Sharpe assert!(sharpe > 0.0); assert!(sharpe.is_finite()); } #[test] fn test_backtest_params_simple_downtrend() { let optimizer = BarrierOptimizer::new(); // Simple downtrend: prices decrease steadily let prices = vec![105.0, 104.0, 103.0, 102.0, 101.0, 100.0]; let params = BarrierParams::new(2.0, 1.0, 3); let sharpe = optimizer.backtest_params(¶ms, &prices); // Downtrend should produce negative or low Sharpe assert!(sharpe.is_finite()); } #[test] fn test_backtest_params_volatile_market() { let optimizer = BarrierOptimizer::new(); // Volatile market: prices oscillate let prices = vec![100.0, 105.0, 98.0, 107.0, 95.0, 110.0]; let params = BarrierParams::new(2.0, 1.0, 3); let sharpe = optimizer.backtest_params(¶ms, &prices); // Should handle volatility without crashing assert!(sharpe.is_finite()); } #[test] fn test_backtest_params_insufficient_data() { let optimizer = BarrierOptimizer::new(); // Too few prices for meaningful backtest let prices = vec![100.0, 101.0]; let params = BarrierParams::new(2.0, 1.0, 10); // horizon longer than data let sharpe = optimizer.backtest_params(¶ms, &prices); // Should return 0.0 or handle gracefully assert!(sharpe.is_finite()); } #[test] fn test_optimize_simple_data() { let optimizer = BarrierOptimizer::new(); // Simple uptrend data let prices = vec![ 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, 110.0, ]; let result = optimizer.optimize(&prices); // Should find optimal parameters assert!(result.best_params.profit_factor > 0.0); assert!(result.best_params.stop_factor > 0.0); assert!(result.best_params.time_horizon > 0); assert!(result.best_sharpe.is_finite()); assert!(result.evaluations > 0); assert!(result.duration_ms > 0); } #[test] fn test_optimize_returns_best_sharpe() { let optimizer = BarrierOptimizer::new(); // Generate synthetic data with known pattern let prices: Vec = (0..50).map(|i| 100.0 + (i as f64) * 0.5).collect(); let result = optimizer.optimize(&prices); // Best Sharpe should be better than worst case assert!(result.best_sharpe > -10.0); // Sanity check // Verify the selected parameters are within search space let profit_range = optimizer.profit_range(); let stop_range = optimizer.stop_range(); let horizon_range = optimizer.horizon_range(); assert!(profit_range.contains(&result.best_params.profit_factor)); assert!(stop_range.contains(&result.best_params.stop_factor)); assert!(horizon_range.contains(&result.best_params.time_horizon)); } #[test] fn test_optimize_evaluates_all_combinations() { let optimizer = BarrierOptimizer::new(); // Small dataset let prices: Vec = (0..20).map(|i| 100.0 + i as f64).collect(); let result = optimizer.optimize(&prices); // Should evaluate all combinations (5 * 4 * 4 = 80) assert_eq!(result.evaluations, 80); } #[test] fn test_optimize_consistent_results() { let optimizer = BarrierOptimizer::new(); // Same data should produce same results let prices: Vec = (0..30).map(|i| 100.0 + (i as f64) * 0.3).collect(); let result1 = optimizer.optimize(&prices); let result2 = optimizer.optimize(&prices); assert_relative_eq!(result1.best_sharpe, result2.best_sharpe, epsilon = 1e-6); assert_eq!( result1.best_params.profit_factor, result2.best_params.profit_factor ); assert_eq!( result1.best_params.stop_factor, result2.best_params.stop_factor ); assert_eq!( result1.best_params.time_horizon, result2.best_params.time_horizon ); } #[test] fn test_optimize_performance_100_combinations() { // Custom optimizer with fewer combinations for performance test let profit_range = vec![1.0, 1.5, 2.0, 2.5, 3.0]; // 5 let stop_range = vec![0.5, 1.0, 1.5, 2.0]; // 4 let horizon_range = vec![5, 10, 20, 30, 40]; // 5 // Total: 5 * 4 * 5 = 100 combinations let optimizer = BarrierOptimizer::with_ranges(profit_range, stop_range, horizon_range); // Generate sufficient data let prices: Vec = (0..100).map(|i| 100.0 + (i as f64) * 0.2).collect(); let start = Instant::now(); let result = optimizer.optimize(&prices); let duration = start.elapsed(); // Must complete in under 10 seconds assert!( duration.as_secs() < 10, "Optimization took {:?}, expected < 10s", duration ); assert_eq!(result.evaluations, 100); assert!(result.duration_ms > 0); } #[test] fn test_optimize_cross_validation_walk_forward() { let optimizer = BarrierOptimizer::new(); // Generate data with trend reversal let mut prices = Vec::new(); // First half: uptrend for i in 0..25 { prices.push(100.0 + i as f64); } // Second half: downtrend for i in 0..25 { prices.push(125.0 - i as f64); } // Split into train/test let split_idx = prices.len() / 2; let train_prices = &prices[..split_idx]; let test_prices = &prices[split_idx..]; // Optimize on training data let train_result = optimizer.optimize(train_prices); // Backtest on test data with optimal params let test_sharpe = optimizer.backtest_params(&train_result.best_params, test_prices); // Test Sharpe should be finite (may be negative due to reversal) assert!(test_sharpe.is_finite()); } #[test] fn test_optimization_result_display() { let params = BarrierParams::new(2.0, 1.0, 10); let result = OptimizationResult { best_params: params, best_sharpe: 1.5, evaluations: 80, duration_ms: 1234, }; // Should implement Display trait let display_str = format!("{}", result); assert!(display_str.contains("2.0")); assert!(display_str.contains("1.0")); assert!(display_str.contains("10")); assert!(display_str.contains("1.5")); } #[test] fn test_barrier_params_clone() { let params = BarrierParams::new(2.0, 1.0, 10); let cloned = params.clone(); assert_relative_eq!(params.profit_factor, cloned.profit_factor, epsilon = 1e-6); assert_relative_eq!(params.stop_factor, cloned.stop_factor, epsilon = 1e-6); assert_eq!(params.time_horizon, cloned.time_horizon); } #[test] fn test_optimization_with_nan_prices() { let optimizer = BarrierOptimizer::new(); // Prices with NaN values let prices = vec![100.0, f64::NAN, 102.0, 103.0]; let result = optimizer.optimize(&prices); // Should handle NaN gracefully (skip or filter) assert!(result.best_sharpe.is_finite()); } #[test] fn test_optimization_with_infinite_prices() { let optimizer = BarrierOptimizer::new(); // Prices with infinity let prices = vec![100.0, f64::INFINITY, 102.0, 103.0]; let result = optimizer.optimize(&prices); // Should handle infinity gracefully assert!(result.best_sharpe.is_finite()); } #[test] fn test_optimize_parallel_consistency() { // Test that optimization is deterministic (no race conditions) let optimizer = BarrierOptimizer::new(); let prices: Vec = (0..50).map(|i| 100.0 + (i as f64) * 0.5).collect(); let results: Vec<_> = (0..5).map(|_| optimizer.optimize(&prices)).collect(); // All results should be identical let first_sharpe = results[0].best_sharpe; for result in &results { assert_relative_eq!(result.best_sharpe, first_sharpe, epsilon = 1e-6); } } #[test] fn test_backtest_params_respects_time_horizon() { let optimizer = BarrierOptimizer::new(); let prices: Vec = (0..100).map(|i| 100.0 + (i as f64) * 0.1).collect(); // Short horizon vs long horizon should produce different results let short_params = BarrierParams::new(2.0, 1.0, 5); let long_params = BarrierParams::new(2.0, 1.0, 30); let short_sharpe = optimizer.backtest_params(&short_params, &prices); let long_sharpe = optimizer.backtest_params(&long_params, &prices); // Results should differ (unless market is perfectly linear) assert!(short_sharpe.is_finite()); assert!(long_sharpe.is_finite()); } #[test] fn test_optimize_empty_prices() { let optimizer = BarrierOptimizer::new(); let prices = vec![]; let result = optimizer.optimize(&prices); // Should handle gracefully, return default or zero Sharpe assert!(result.best_sharpe.is_finite()); assert_eq!(result.evaluations, 80); // Still evaluates all combinations } #[test] fn test_optimize_single_price() { let optimizer = BarrierOptimizer::new(); let prices = vec![100.0]; let result = optimizer.optimize(&prices); // Should handle gracefully assert!(result.best_sharpe.is_finite()); } #[test] fn test_barrier_params_default() { let params = BarrierParams::default(); // Default should be reasonable assert!(params.profit_factor > 0.0); assert!(params.stop_factor > 0.0); assert!(params.time_horizon > 0); }