/// WAVE 26 P1.5: TDD test for expanded hyperopt learning rate range /// /// CRITICAL BUG: Current range [2e-5, 8e-5] excludes production default 1e-4 /// This test verifies the fix to expand range to [1e-5, 3e-4] (30x range) /// /// Test Requirements: /// 1. Learning rate lower bound must be 1e-5 (log scale) /// 2. Learning rate upper bound must be 3e-4 (log scale) /// 3. Range must include production default 1e-4 /// 4. Warmup ratio must be added to search space [0.0, 0.2] #[cfg(test)] mod wave26_hyperopt_lr_range_tests { use ml::hyperopt::adapters::dqn::DQNParams; use ml::hyperopt::search_space::ParameterSpace; #[test] fn test_learning_rate_range_includes_production_default() { // CRITICAL: Production default is 1e-4 let production_lr = 1e-4; let bounds = DQNParams::continuous_bounds(); // Learning rate is at index 0 in continuous_bounds let (lr_min_log, lr_max_log) = bounds[0]; // Verify new expanded range let lr_min = lr_min_log.exp(); let lr_max = lr_max_log.exp(); // Test 1: Lower bound is 1e-5 assert!( (lr_min - 1e-5).abs() < 1e-7, "Learning rate lower bound should be 1e-5, got {}", lr_min ); // Test 2: Upper bound is 3e-4 assert!( (lr_max - 3e-4).abs() < 1e-6, "Learning rate upper bound should be 3e-4, got {}", lr_max ); // Test 3: Range includes production default assert!( lr_min <= production_lr && production_lr <= lr_max, "Production default {} must be within range [{}, {}]", production_lr, lr_min, lr_max ); // Test 4: Range is 30x (3e-4 / 1e-5 = 30) let range_multiplier = lr_max / lr_min; assert!( (range_multiplier - 30.0).abs() < 0.1, "Range multiplier should be 30x, got {}", range_multiplier ); } #[test] fn test_warmup_ratio_in_search_space() { let bounds = DQNParams::continuous_bounds(); // Warmup ratio should be added after Kelly parameters // Index 22: warmup_ratio (new parameter) assert_eq!( bounds.len(), 23, "Expected 23 parameters (22 existing + 1 warmup_ratio), got {}", bounds.len() ); let (warmup_min, warmup_max) = bounds[22]; // Test warmup ratio bounds [0.0, 0.2] (0-20% warmup) assert_eq!( warmup_min, 0.0, "Warmup ratio lower bound should be 0.0, got {}", warmup_min ); assert_eq!( warmup_max, 0.2, "Warmup ratio upper bound should be 0.2 (20%), got {}", warmup_max ); } #[test] fn test_from_continuous_validates_warmup_ratio() { // Create continuous vector with all 23 parameters let continuous = vec![ 1e-4_f64.ln(), // learning_rate (production default, within new range) 92.0, // batch_size 0.9588, // gamma 97_273_f64.ln(), // buffer_size 1.404, // hold_penalty_weight 5.563, // max_position_absolute 24.77_f64.ln(), // huber_delta 0.01, // entropy_coefficient 1.0, // transaction_cost_multiplier 0.6, // per_alpha 0.4, // per_beta_start -2.0, // v_min 2.0, // v_max 0.5_f64.ln(), // noisy_sigma_init 256.0, // dueling_hidden_dim 3.0, // n_steps 101.0, // num_atoms 1.5, // minimum_profit_factor 0.5, // kelly_fractional 0.25, // kelly_max_fraction 20.0, // kelly_min_trades 20.0, // volatility_window 0.1, // warmup_ratio (10% warmup) ]; let params = DQNParams::from_continuous(&continuous).unwrap(); // Verify learning rate is production default assert!( (params.learning_rate - 1e-4).abs() < 1e-6, "Learning rate should be 1e-4, got {}", params.learning_rate ); // Verify warmup ratio is extracted assert!( (params.warmup_ratio - 0.1).abs() < 1e-6, "Warmup ratio should be 0.1, got {}", params.warmup_ratio ); } #[test] fn test_warmup_ratio_bounds_clamping() { // Test minimum warmup ratio (0.0) let continuous_min = vec![ 1e-5_f64.ln(), 64.0, 0.95, 50_000_f64.ln(), 1.0, 4.0, 15.0_f64.ln(), 0.0, 0.5, 0.4, 0.2, -3.0, 1.0, 0.1_f64.ln(), 128.0, 1.0, 51.0, 1.1, 0.25, 0.1, 10.0, 10.0, 0.0, // warmup_ratio min ]; let params_min = DQNParams::from_continuous(&continuous_min).unwrap(); assert!( (params_min.warmup_ratio - 0.0).abs() < 1e-6, "Warmup ratio min should be 0.0, got {}", params_min.warmup_ratio ); // Test maximum warmup ratio (0.2) let continuous_max = vec![ 3e-4_f64.ln(), 160.0, 0.99, 100_000_f64.ln(), 2.0, 8.0, 40.0_f64.ln(), 0.1, 2.0, 0.8, 0.6, -1.0, 3.0, 1.0_f64.ln(), 512.0, 5.0, 201.0, 2.0, 1.0, 0.5, 50.0, 30.0, 0.2, // warmup_ratio max ]; let params_max = DQNParams::from_continuous(&continuous_max).unwrap(); assert!( (params_max.warmup_ratio - 0.2).abs() < 1e-6, "Warmup ratio max should be 0.2, got {}", params_max.warmup_ratio ); } #[test] fn test_param_names_includes_warmup_ratio() { let names = DQNParams::param_names(); assert_eq!( names.len(), 23, "Expected 23 parameter names, got {}", names.len() ); assert_eq!( names[22], "warmup_ratio", "Parameter 22 should be 'warmup_ratio', got '{}'", names[22] ); } }