Files
foxhunt/testing/integration/wave26_hyperopt_lr_range_test.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

188 lines
6.1 KiB
Rust

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