Files
foxhunt/ml/tests/ensemble_hyperopt_tests.rs
jgrusewski ac0a83e4f7 refactor(ml): reorganize tests — move integration tests to ml/tests/
Move test files from ml/src/*/tests/ to ml/tests/. Convert
crate-internal imports to public API imports. Rename files to
follow naming conventions (no wave/priority prefixes).

Files moved:
- ml/src/dqn/tests/ -> ml/tests/ (4 files)
- ml/src/trainers/dqn/tests/ -> ml/tests/ (5 files)

Renames:
- target_update_comprehensive_tests.rs -> target_update_tests.rs
- p0_integration_tests.rs -> dqn_trainer_integration_tests.rs
- p1_integration_tests.rs -> dqn_trainer_p1_tests.rs
- ensemble_uncertainty_hyperopt_tests.rs -> ensemble_hyperopt_tests.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 14:07:22 +01:00

296 lines
11 KiB
Rust

//! 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");
}