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>
188 lines
6.1 KiB
Rust
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]
|
|
);
|
|
}
|
|
}
|