//! TDD Tests for WAVE 26 P1.4: 6D Ensemble Uncertainty Hyperopt Integration //! //! Validates that ensemble uncertainty parameters are properly integrated into hyperopt search space: //! 1. DQNHyperparameters has 6 new fields //! 2. DQNParams has 5 new fields (ensemble_size as f64) //! 3. Search space includes 40 total parameters //! 4. train_with_params correctly converts parameters use ml::hyperopt::adapters::dqn::DQNParams; use ml::hyperopt::ParameterSpace; use ml::trainers::dqn::DQNHyperparameters; #[test] fn test_dqn_hyperparameters_has_ensemble_fields() { // WAVE 26 P1.4: Verify DQNHyperparameters struct has 6 new ensemble fields let hyperparams = DQNHyperparameters::conservative(); // Check default values exist (compilation test - if fields don't exist, this won't compile) let _use_ensemble = hyperparams.use_ensemble_uncertainty; let _ensemble_size = hyperparams.ensemble_size; let _beta_variance = hyperparams.beta_variance; let _beta_disagreement = hyperparams.beta_disagreement; let _beta_entropy = hyperparams.beta_entropy; let _variance_cap = hyperparams.variance_cap; // Verify conservative defaults are reasonable assert!(!hyperparams.use_ensemble_uncertainty, "Default should be disabled for conservative config"); assert_eq!(hyperparams.ensemble_size, 5, "Default ensemble size should be 5"); assert!(hyperparams.beta_variance >= 0.0 && hyperparams.beta_variance <= 1.0, "Beta variance should be in [0, 1]"); assert!(hyperparams.beta_disagreement >= 0.0 && hyperparams.beta_disagreement <= 1.0, "Beta disagreement should be in [0, 1]"); assert!(hyperparams.beta_entropy >= 0.0 && hyperparams.beta_entropy <= 1.0, "Beta entropy should be in [0, 1]"); assert!(hyperparams.variance_cap > 0.0, "Variance cap should be positive"); } #[test] fn test_dqn_params_has_ensemble_fields() { // WAVE 26 P1.4: Verify DQNParams struct has 5 new ensemble fields let params = DQNParams::default(); // Check default values exist (compilation test) let _use_ensemble = params.use_ensemble_uncertainty; let _ensemble_size = params.ensemble_size; let _beta_variance = params.beta_variance; let _beta_disagreement = params.beta_disagreement; let _beta_entropy = params.beta_entropy; // Verify defaults match conservative config assert!(!params.use_ensemble_uncertainty, "Default should be disabled"); assert_eq!(params.ensemble_size, 5.0, "Default ensemble size should be 5.0"); assert!(params.beta_variance > 0.0, "Beta variance should be positive"); assert!(params.beta_disagreement > 0.0, "Beta disagreement should be positive"); assert!(params.beta_entropy > 0.0, "Beta entropy should be positive"); } #[test] fn test_ensemble_search_space_bounds() { // WAVE 26 P1.4: Verify search space includes 5 ensemble continuous bounds let bounds = DQNParams::continuous_bounds(); // Search space now has 40 total parameters assert_eq!(bounds.len(), 40, "Search space should have 40 continuous parameters total"); // Extract the ensemble bounds (indices 23-27) let ensemble_size_bounds = bounds[23]; let beta_variance_bounds = bounds[24]; let beta_disagreement_bounds = bounds[25]; let beta_entropy_bounds = bounds[26]; let variance_cap_bounds = bounds[27]; // Verify ensemble_size bounds: [3.0, 10.0] assert_eq!(ensemble_size_bounds.0, 3.0, "Ensemble size min should be 3.0"); assert_eq!(ensemble_size_bounds.1, 10.0, "Ensemble size max should be 10.0"); // Verify beta_variance bounds: [0.1, 1.0] assert_eq!(beta_variance_bounds.0, 0.1, "Beta variance min should be 0.1"); assert_eq!(beta_variance_bounds.1, 1.0, "Beta variance max should be 1.0"); // Verify beta_disagreement bounds: [0.1, 1.0] assert_eq!(beta_disagreement_bounds.0, 0.1, "Beta disagreement min should be 0.1"); assert_eq!(beta_disagreement_bounds.1, 1.0, "Beta disagreement max should be 1.0"); // Verify beta_entropy bounds: [0.05, 0.5] assert_eq!(beta_entropy_bounds.0, 0.05, "Beta entropy min should be 0.05"); assert_eq!(beta_entropy_bounds.1, 0.5, "Beta entropy max should be 0.5"); // Verify variance_cap bounds: [0.1, 2.0] assert_eq!(variance_cap_bounds.0, 0.1, "Variance cap min should be 0.1"); assert_eq!(variance_cap_bounds.1, 2.0, "Variance cap max should be 2.0"); } #[test] fn test_from_continuous_validates_ensemble_params() { // WAVE 26 P1.4: Test parameter conversion from continuous space let mut x = vec![0.0; 40]; // Set base parameters to valid values (using defaults) x[0] = (5e-5_f64).ln(); // learning_rate x[1] = 128.0; // batch_size x[2] = 0.99; // gamma x[3] = (75_000_f64).ln(); // buffer_size x[4] = 1.5; // hold_penalty_weight x[5] = 6.0; // max_position_absolute x[6] = (20.0_f64).ln(); // huber_delta x[7] = 0.05; // entropy_coefficient x[8] = 1.0; // transaction_cost_multiplier x[9] = 0.6; // per_alpha x[10] = 0.4; // per_beta_start x[11] = -2.0; // v_min x[12] = 2.0; // v_max x[13] = (0.5_f64).ln(); // noisy_sigma_init x[14] = 256.0; // dueling_hidden_dim x[15] = 3.0; // n_steps x[16] = 51.0; // num_atoms x[17] = 1.5; // minimum_profit_factor x[18] = (1e-4_f64).ln(); // weight_decay x[19] = 0.5; // kelly_fractional x[20] = 0.25; // kelly_max_fraction x[21] = 20.0; // kelly_min_trades x[22] = 20.0; // volatility_window // Set ensemble parameters (indices 23-27) x[23] = 5.0; // ensemble_size x[24] = 0.5; // beta_variance x[25] = 0.5; // beta_disagreement x[26] = 0.2; // beta_entropy x[27] = 1.0; // variance_cap // Set additional parameters (indices 28-39) x[28] = 0.1; // warmup_ratio x[29] = 0.1; // curiosity_weight x[30] = (0.005_f64).ln(); // tau x[31] = 10.0; // td_error_clamp_max x[32] = 50.0; // batch_diversity_cooldown x[33] = 1.0; // lr_decay_type x[34] = 0.1; // sharpe_weight x[35] = 0.95; // gae_lambda x[36] = 0.6; // noisy_sigma_initial x[37] = 0.3; // noisy_sigma_final x[38] = 0.0; // norm_type x[39] = 1.0; // activation_type let params = DQNParams::from_continuous(&x).expect("Should convert valid parameters"); // Verify ensemble parameters are correctly extracted assert_eq!(params.ensemble_size, 5.0, "Ensemble size should be 5.0"); assert_eq!(params.beta_variance, 0.5, "Beta variance should be 0.5"); assert_eq!(params.beta_disagreement, 0.5, "Beta disagreement should be 0.5"); assert_eq!(params.beta_entropy, 0.2, "Beta entropy should be 0.2"); // Note: variance_cap is not in DQNParams, it's only in DQNHyperparameters // This will be tested in the train_with_params integration test } #[test] fn test_from_continuous_clamps_ensemble_params() { // WAVE 26 P1.4: Test that ensemble parameters are clamped to valid ranges let mut x = vec![0.0; 40]; // Set base parameters to valid values (minimal setup) x[0] = (5e-5_f64).ln(); x[1] = 128.0; x[2] = 0.99; x[3] = (75_000_f64).ln(); x[4] = 1.5; x[5] = 6.0; x[6] = (20.0_f64).ln(); x[7] = 0.05; x[8] = 1.0; x[9] = 0.6; x[10] = 0.4; x[11] = -2.0; x[12] = 2.0; x[13] = (0.5_f64).ln(); x[14] = 256.0; x[15] = 3.0; x[16] = 51.0; x[17] = 1.5; x[18] = (1e-4_f64).ln(); x[19] = 0.5; x[20] = 0.25; x[21] = 20.0; x[22] = 20.0; // Set ensemble parameters to out-of-bounds values (indices 23-27) x[23] = 15.0; // ensemble_size (should clamp to 10.0) x[24] = 2.0; // beta_variance (should clamp to 1.0) x[25] = -0.5; // beta_disagreement (should clamp to 0.1) x[26] = 1.0; // beta_entropy (should clamp to 0.5) x[27] = 5.0; // variance_cap (valid, should stay 5.0) // Set additional parameters (indices 28-39) x[28] = 0.1; // warmup_ratio x[29] = 0.1; // curiosity_weight x[30] = (0.005_f64).ln(); // tau x[31] = 10.0; // td_error_clamp_max x[32] = 50.0; // batch_diversity_cooldown x[33] = 1.0; // lr_decay_type x[34] = 0.1; // sharpe_weight x[35] = 0.95; // gae_lambda x[36] = 0.6; // noisy_sigma_initial x[37] = 0.3; // noisy_sigma_final x[38] = 0.0; // norm_type x[39] = 1.0; // activation_type let params = DQNParams::from_continuous(&x).expect("Should convert and clamp parameters"); // Verify clamping of ensemble parameters assert_eq!(params.ensemble_size, 10.0, "Ensemble size should clamp to max 10.0"); assert_eq!(params.beta_variance, 1.0, "Beta variance should clamp to max 1.0"); assert_eq!(params.beta_disagreement, 0.1, "Beta disagreement should clamp to min 0.1"); assert_eq!(params.beta_entropy, 0.5, "Beta entropy should clamp to max 0.5"); } #[test] fn test_to_continuous_includes_ensemble_params() { // WAVE 26 P1.4: Test that to_continuous() includes ensemble parameters let mut params = DQNParams::default(); // Set ensemble parameters to specific values params.ensemble_size = 7.0; params.beta_variance = 0.6; params.beta_disagreement = 0.4; params.beta_entropy = 0.3; let continuous = params.to_continuous(); // Should have 40 values assert_eq!(continuous.len(), 40, "Continuous representation should have 40 values"); // Verify ensemble parameters are at positions 23-26 assert_eq!(continuous[23], 7.0, "Ensemble size should be at position 23"); assert_eq!(continuous[24], 0.6, "Beta variance should be at position 24"); assert_eq!(continuous[25], 0.4, "Beta disagreement should be at position 25"); assert_eq!(continuous[26], 0.3, "Beta entropy should be at position 26"); // Position 27 (variance_cap) is not in DQNParams, won't appear in to_continuous() } #[test] fn test_ensemble_size_rounds_to_integer() { // WAVE 26 P1.4: Ensemble size should be rounded to nearest integer let mut x = vec![0.0; 40]; // Minimal valid setup x[0] = (5e-5_f64).ln(); x[1] = 128.0; x[2] = 0.99; x[3] = (75_000_f64).ln(); x[4] = 1.5; x[5] = 6.0; x[6] = (20.0_f64).ln(); x[7] = 0.05; x[8] = 1.0; x[9] = 0.6; x[10] = 0.4; x[11] = -2.0; x[12] = 2.0; x[13] = (0.5_f64).ln(); x[14] = 256.0; x[15] = 3.0; x[16] = 51.0; x[17] = 1.5; x[18] = (1e-4_f64).ln(); x[19] = 0.5; x[20] = 0.25; x[21] = 20.0; x[22] = 20.0; // Test rounding behavior (ensemble parameters at indices 23-27) x[23] = 5.3; // Should round to 5.0 x[24] = 0.5; x[25] = 0.5; x[26] = 0.2; x[27] = 1.0; // Set additional parameters (indices 28-39) x[28] = 0.1; // warmup_ratio x[29] = 0.1; // curiosity_weight x[30] = (0.005_f64).ln(); // tau x[31] = 10.0; // td_error_clamp_max x[32] = 50.0; // batch_diversity_cooldown x[33] = 1.0; // lr_decay_type x[34] = 0.1; // sharpe_weight x[35] = 0.95; // gae_lambda x[36] = 0.6; // noisy_sigma_initial x[37] = 0.3; // noisy_sigma_final x[38] = 0.0; // norm_type x[39] = 1.0; // activation_type let params = DQNParams::from_continuous(&x).expect("Should convert"); assert_eq!(params.ensemble_size, 5.0, "5.3 should round to 5.0"); x[23] = 7.8; // Should round to 8.0 let params = DQNParams::from_continuous(&x).expect("Should convert"); assert_eq!(params.ensemble_size, 8.0, "7.8 should round to 8.0"); }