// ml/tests/barrier_backtest_test.rs // Comprehensive tests for barrier parameter optimization backtesting use ml::backtesting::barrier_backtest::{BarrierBacktester, BarrierParams}; #[test] fn test_barrier_backtester_initialization() { let backtester = BarrierBacktester::new(10, 0.7); assert_eq!(backtester.walk_forward_windows(), 10); assert_eq!(backtester.train_test_split(), 0.7); } #[test] fn test_walk_forward_validation_single_window() { // Single window walk-forward validation let backtester = BarrierBacktester::new(1, 0.7); // Create synthetic price series (100 bars) let prices: Vec = (0..100) .map(|i| 100.0 + (i as f64) * 0.1 + ((i % 5) as f64) * 0.5) .collect(); let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Basic validation assert!(results.sharpe_ratio.is_finite()); assert!(results.win_rate >= 0.0 && results.win_rate <= 1.0); assert!(results.max_drawdown <= 0.0); // Drawdown is negative assert_eq!( results.label_distribution.0 + results.label_distribution.1 + results.label_distribution.2, prices.len() ); } #[test] fn test_walk_forward_validation_multiple_windows() { // Multiple windows walk-forward validation let backtester = BarrierBacktester::new(5, 0.7); // Create synthetic price series (500 bars for multiple windows) let prices: Vec = (0..500) .map(|i| 100.0 + (i as f64) * 0.02 + ((i as f64 / 10.0).sin() * 5.0)) .collect(); let params = BarrierParams { profit_target: 0.015, stop_loss: 0.01, max_holding_periods: 15, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Validate multi-window results assert!(results.sharpe_ratio.is_finite()); assert!(results.stability_score >= 0.0); // Variance should be non-negative assert!(results.win_rate >= 0.0 && results.win_rate <= 1.0); } #[test] fn test_sharpe_ratio_calculation() { let backtester = BarrierBacktester::new(1, 0.7); // Uptrending prices with volatility let prices: Vec = (0..200) .map(|i| 100.0 + (i as f64) * 0.1 + ((i as f64 / 5.0).sin() * 2.0)) .collect(); let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Sharpe ratio should be finite for trending market // Note: Annualized Sharpe can be extreme for small samples with low volatility assert!(results.sharpe_ratio.is_finite()); } #[test] fn test_parameter_stability_across_regimes() { // Test stability score across different market regimes let backtester = BarrierBacktester::new(3, 0.7); // Create price series with regime changes let mut prices = Vec::new(); // Regime 1: Uptrend (bars 0-150) for i in 0..150 { prices.push(100.0 + (i as f64) * 0.15); } // Regime 2: Downtrend (bars 150-300) for i in 0..150 { prices.push(122.5 - (i as f64) * 0.1); } // Regime 3: Sideways (bars 300-450) for i in 0..150 { prices.push(107.5 + ((i as f64 / 10.0).sin() * 3.0)); } let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Stability score should reflect regime changes assert!(results.stability_score >= 0.0); // Higher stability score means more variance across windows assert!(results.stability_score.is_finite()); } #[test] fn test_overfitting_detection_tight_barriers() { // Test for overfitting with very tight barriers let backtester = BarrierBacktester::new(5, 0.7); let prices: Vec = (0..500) .map(|i| 100.0 + (i as f64) * 0.01 + ((i as f64 / 20.0).sin() * 2.0)) .collect(); // Very tight barriers (likely to overfit to noise) let params = BarrierParams { profit_target: 0.001, // 0.1% stop_loss: 0.0005, // 0.05% max_holding_periods: 5, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Tight barriers should result in high stability score (high variance across windows) assert!(results.stability_score >= 0.0); // Label distribution should be heavily skewed (mostly holds or stops) let total_labels = results.label_distribution.0 + results.label_distribution.1 + results.label_distribution.2; assert_eq!(total_labels, prices.len()); } #[test] fn test_overfitting_detection_wide_barriers() { // Test for underfitting with very wide barriers let backtester = BarrierBacktester::new(5, 0.7); let prices: Vec = (0..500) .map(|i| 100.0 + (i as f64) * 0.01 + ((i as f64 / 20.0).sin() * 2.0)) .collect(); // Very wide barriers (may underfit) let params = BarrierParams { profit_target: 0.1, // 10% stop_loss: 0.05, // 5% max_holding_periods: 100, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Wide barriers should result in low stability score (consistent behavior) assert!(results.stability_score >= 0.0); // Most labels should timeout (max_holding_periods reached) } #[test] fn test_performance_full_dataset() { use std::time::Instant; let backtester = BarrierBacktester::new(10, 0.7); // Simulate ES.FUT-like dataset (1000 bars, typical intraday) let prices: Vec = (0..1000) .map(|i| 4500.0 + (i as f64) * 0.5 + ((i as f64 / 50.0).sin() * 20.0)) .collect(); let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let start = Instant::now(); let _results = backtester .run(&prices, params) .expect("Backtest should succeed"); let elapsed = start.elapsed(); // Performance requirement: <30s for full dataset assert!( elapsed.as_secs() < 30, "Backtest took {:?}, expected <30s", elapsed ); } #[test] fn test_label_distribution_balanced() { let backtester = BarrierBacktester::new(1, 0.7); // Create price series designed to hit both profit/stop targets let mut prices = Vec::new(); for i in 0..100 { if i % 2 == 0 { // Upswing (should hit profit target) prices.push(100.0 + (i as f64 / 10.0)); } else { // Downswing (should hit stop loss) prices.push(100.0 - (i as f64 / 10.0)); } } let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 5, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); let (buys, sells, holds) = results.label_distribution; let total = buys + sells + holds; assert_eq!(total, prices.len()); // With alternating up/down swings, we should have some balance assert!(buys > 0 || sells > 0); // At least some directional labels } #[test] fn test_win_rate_calculation() { let backtester = BarrierBacktester::new(1, 0.7); // Strong uptrend (should have high win rate with buy labels) let prices: Vec = (0..100) .map(|i| 100.0 + (i as f64) * 0.5) // Consistent uptrend .collect(); let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Win rate should be reasonable assert!(results.win_rate >= 0.0 && results.win_rate <= 1.0); assert!(results.win_rate.is_finite()); } #[test] fn test_max_drawdown_calculation() { let backtester = BarrierBacktester::new(1, 0.7); // Create price series with a known drawdown let mut prices = Vec::new(); // Initial rise for i in 0..30 { prices.push(100.0 + (i as f64) * 0.5); } // Sharp drop (creates drawdown) for i in 0..20 { prices.push(115.0 - (i as f64) * 0.3); } // Recovery for i in 0..30 { prices.push(109.0 + (i as f64) * 0.2); } let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Max drawdown should be negative and finite assert!(results.max_drawdown <= 0.0); assert!(results.max_drawdown.is_finite()); } #[test] fn test_empty_price_series() { let backtester = BarrierBacktester::new(1, 0.7); let prices: Vec = vec![]; let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let result = backtester.run(&prices, params); assert!(result.is_err(), "Should fail with empty prices"); } #[test] fn test_insufficient_data_for_windows() { let backtester = BarrierBacktester::new(10, 0.7); // Only 50 bars, not enough for 10 windows let prices: Vec = (0..50).map(|i| 100.0 + (i as f64) * 0.1).collect(); let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let result = backtester.run(&prices, params); assert!(result.is_err(), "Should fail with insufficient data"); } #[test] fn test_invalid_parameters() { let backtester = BarrierBacktester::new(1, 0.7); let prices: Vec = (0..100).map(|i| 100.0 + (i as f64) * 0.1).collect(); // Negative profit target let invalid_params = BarrierParams { profit_target: -0.02, stop_loss: 0.01, max_holding_periods: 10, }; let result = backtester.run(&prices, invalid_params); assert!(result.is_err(), "Should fail with negative profit target"); // Negative stop loss let invalid_params = BarrierParams { profit_target: 0.02, stop_loss: -0.01, max_holding_periods: 10, }; let result = backtester.run(&prices, invalid_params); assert!(result.is_err(), "Should fail with negative stop loss"); // Zero max holding periods let invalid_params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 0, }; let result = backtester.run(&prices, invalid_params); assert!(result.is_err(), "Should fail with zero max holding periods"); } #[test] fn test_stability_score_perfect_consistency() { let backtester = BarrierBacktester::new(5, 0.7); // Perfectly consistent price series (no regime changes) let prices: Vec = (0..500) .map(|i| 100.0 + (i as f64) * 0.1) // Linear trend .collect(); let params = BarrierParams { profit_target: 0.02, stop_loss: 0.01, max_holding_periods: 10, }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // Low stability score (low variance) for consistent market assert!(results.stability_score >= 0.0); assert!(results.stability_score.is_finite()); } #[test] fn test_real_world_scenario_es_fut() { // Simulate realistic ES.FUT price action let backtester = BarrierBacktester::new(10, 0.7); let mut prices = Vec::new(); let mut current_price = 4500.0; // Simulate 1000 bars with realistic volatility for i in 0..1000 { // Add trend component let trend = (i as f64 / 1000.0) * 50.0; // Add cyclical component let cycle = (i as f64 / 20.0).sin() * 15.0; // Add noise let noise = ((i * 7) % 13) as f64 - 6.0; let price = 4500.0 + trend + cycle + noise; prices.push(price); } let params = BarrierParams { profit_target: 0.015, // 1.5% (realistic for ES.FUT) stop_loss: 0.01, // 1% (risk management) max_holding_periods: 20, // ~20 minutes for 1min bars }; let results = backtester .run(&prices, params) .expect("Backtest should succeed"); // All metrics should be reasonable for real-world data assert!(results.sharpe_ratio.is_finite()); assert!(results.sharpe_ratio >= -3.0 && results.sharpe_ratio <= 3.0); assert!(results.win_rate >= 0.0 && results.win_rate <= 1.0); assert!(results.max_drawdown <= 0.0 && results.max_drawdown >= -0.5); assert!(results.stability_score >= 0.0 && results.stability_score.is_finite()); let total_labels = results.label_distribution.0 + results.label_distribution.1 + results.label_distribution.2; assert_eq!(total_labels, prices.len()); }