feat(ml): WAVE 29 DQN Codebase Cleanup & Refactoring Campaign
BREAKING CHANGES: - Removed orphaned dqn.rs monolithic trainer (4,975 lines) - Removed orphaned dqn_ensemble.rs module (816 lines) - Removed orphaned tft.rs and tft_complete_int8_integration_test.rs - TFT trainer split into modular directory structure DQN Module Refactoring: - Split trainers/dqn.rs into modular structure (config.rs, statistics.rs, trainer.rs) - Fixed hyperopt 39D search space (continuous params only) - Boolean flags (use_dueling, use_double_dqn, use_per, use_noisy_nets) are now FIXED architectural decisions - use_distributional defaults to false (Candle BUG #36 - scatter_add gradient issues) Clean Module Structure: - ml/src/trainers/dqn/ directory with proper mod.rs exports - ml/src/trainers/tft/ directory with config.rs, types.rs, model.rs, trainer.rs, tests.rs - All P0 features validated: TD-error clamping, batch diversity, LR scheduler, priority staleness Documentation: - Added comprehensive docs in docs/codebase-cleanup/ - ADR-001 for DQN refactoring decisions - Rainbow DQN component matrix and quick reference guides Build Status: Compiles with zero errors 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
187
tests/wave26_hyperopt_lr_range_test.rs
Normal file
187
tests/wave26_hyperopt_lr_range_test.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
/// 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]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user