diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index 53419111e..09d9517b6 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -3,7 +3,7 @@ //! This module provides a production-ready adapter for optimizing DQN //! hyperparameters using the generic optimization framework. It implements: //! -//! - 45D continuous parameter space with log-scale handling +//! - 46D continuous parameter space with log-scale handling //! - Training wrapper that integrates with existing DQN pipeline //! - Backtest-based objective using Sharpe ratio (production metric) //! - Prioritized Experience Replay (PER) tuning for Rainbow DQN performance (+25-40% convergence speed) @@ -144,7 +144,7 @@ const TRIAL_VRAM_MB: f64 = 7000.0; /// Corrected: was 0.02 (20KB) — 100x too high const MB_PER_SAMPLE: f64 = 0.0005; -/// DQN hyperparameter space (45D continuous) +/// DQN hyperparameter space (46D continuous) /// /// Fixed params (not in search space): /// - noisy_epsilon_floor: 0.10 (minimum random exploration to prevent action collapse) @@ -406,6 +406,10 @@ pub struct DQNParams { /// CVaR alpha confidence level (0.01-0.20). Lower = more risk-averse. /// 0.05 = optimize for worst 5% outcomes. pub cvar_alpha: f64, + + /// Minimum bars to hold a position before allowing exit or reversal (integer, [2, 20]). + /// Lets PSO discover optimal hold duration for ES futures regime characteristics. + pub min_hold_bars: usize, } impl Default for DQNParams { @@ -498,13 +502,14 @@ impl Default for DQNParams { curiosity_weight_tunable: 0.1, // Default: 10% curiosity-driven intrinsic reward use_cvar_action_selection: 1.0, // Default: enabled (risk-aware IQN) cvar_alpha: 0.05, // Default: worst 5% tail + min_hold_bars: 5, // Default: 5 bars minimum hold } } } impl ParameterSpace for DQNParams { fn continuous_bounds() -> Vec<(f64, f64)> { - // 45D search space (Task 11: +4 dims at indices 41-44) + // 46D search space (Task 11: +4 dims at indices 41-44, Task 4: +1 dim at index 45) // All bounds loaded from config/training/dqn-hyperopt.toml [search_space]. // To change search ranges, edit the TOML — no code changes needed. // Log-scale transforms are applied here; TOML stores linear values. @@ -558,6 +563,7 @@ impl ParameterSpace for DQNParams { let cwt = b("curiosity_weight_tunable", (0.0, 0.2)); let ucvar = b("use_cvar_action_selection", (0.0, 1.0)); let cva = b("cvar_alpha", (0.01, 0.2)); + let mhb = b("min_hold_bars", (2.0, 20.0)); // Two-phase hyperopt: fix architecture OR dynamics params to single-point bounds. // Single-point bounds (v, v) make the optimizer trivially converge on those @@ -692,6 +698,7 @@ impl ParameterSpace for DQNParams { cwt, // 42: curiosity_weight_tunable ucvar, // 43: use_cvar_action_selection cva, // 44: cvar_alpha + mhb, // 45: min_hold_bars [2, 20] ]; // Phase FULL: fix all non-architecture dims to Phase 1 best values. @@ -742,6 +749,8 @@ impl ParameterSpace for DQNParams { let curiosity_weight_tunable = x.get(42).copied().unwrap_or(0.1).clamp(0.0, 0.2); let use_cvar_action_selection = x.get(43).copied().unwrap_or(1.0).clamp(0.0, 1.0); let cvar_alpha = x.get(44).copied().unwrap_or(0.05).clamp(0.01, 0.2); + // min_hold_bars (index 45): integer, rounded from continuous [2, 20] + let min_hold_bars = (x.get(45).copied().unwrap_or(5.0).round() as usize).clamp(2, 20); // === 26 TUNED parameters (from search space) === // C2: Removed curiosity_weight and noisy_epsilon_floor (exploration stacking) @@ -931,13 +940,14 @@ impl ParameterSpace for DQNParams { curiosity_weight_tunable, use_cvar_action_selection, cvar_alpha, + min_hold_bars, // Index 45: min_hold_bars [2, 20] }; Ok(params) } fn to_continuous(&self) -> Vec { - // 45D search space (Task 11: +4 dims at indices 41-44) + // 46D search space (Task 11: +4 dims at indices 41-44, Task 4: +1 dim at index 45) vec![ self.learning_rate.ln(), // 0 self.batch_size as f64, // 1 @@ -986,11 +996,12 @@ impl ParameterSpace for DQNParams { self.curiosity_weight_tunable, // 42 self.use_cvar_action_selection, // 43 self.cvar_alpha, // 44 + self.min_hold_bars as f64, // 45 ] } fn param_names() -> Vec<&'static str> { - // 45 tuned parameters (Task 11: +4 new dims at indices 41-44) + // 46 tuned parameters (Task 11: +4 new dims at indices 41-44, Task 4: +1 dim at index 45) vec![ "learning_rate", // 0 "batch_size", // 1 @@ -1039,6 +1050,7 @@ impl ParameterSpace for DQNParams { "curiosity_weight", // 42 "use_cvar_action_selection", // 43 "cvar_alpha", // 44 + "min_hold_bars", // 45 ] } @@ -2635,7 +2647,7 @@ impl HyperparameterOptimizable for DQNTrainer { // Fix 1: Clamp buffer size to max (4GB GPU constraint) let clamped_buffer_size = params.buffer_size.min(self.buffer_size_max); - info!("Training DQN with parameters (45D search space):"); + info!("Training DQN with parameters (46D search space):"); info!(" Learning rate: {:.6}", params.learning_rate); info!(" Batch size: {}", params.batch_size); info!(" Gamma: {:.3}", params.gamma); @@ -3071,6 +3083,8 @@ impl HyperparameterOptimizable for DQNTrainer { // Task 11: CVaR action selection — wired from hyperopt search space (indices 43-44) use_cvar_action_selection: params.use_cvar_action_selection.round() >= 1.0, cvar_alpha: params.cvar_alpha as f32, + // Task 4: min_hold_bars wired from hyperopt search space (index 45) + min_hold_bars: params.min_hold_bars, ..Default::default() }; @@ -3863,7 +3877,7 @@ mod tests { #[test] fn test_dqn_params_roundtrip() { - // Roundtrip test for the 45D search space (Task 11: +4 new dims) + // Roundtrip test for the 46D search space (Task 11: +4 new dims, Task 4: +1 dim) // Only the tuned parameters roundtrip; fixed params get defaults from from_continuous let params = DQNParams { learning_rate: 3.37e-05, @@ -3935,10 +3949,12 @@ mod tests { curiosity_weight_tunable: 0.15, use_cvar_action_selection: 1.0, cvar_alpha: 0.08, + // Task 4: min_hold_bars in search space + min_hold_bars: 8, }; let continuous = params.to_continuous(); - assert_eq!(continuous.len(), 45, "to_continuous must return 45D vector"); + assert_eq!(continuous.len(), 46, "to_continuous must return 46D vector"); let recovered = DQNParams::from_continuous(&continuous).unwrap(); // Tuned parameters must roundtrip exactly @@ -3994,12 +4010,14 @@ mod tests { assert!((recovered.curiosity_weight_tunable - params.curiosity_weight_tunable).abs() < 1e-6); assert!((recovered.use_cvar_action_selection - params.use_cvar_action_selection).abs() < 1e-6); assert!((recovered.cvar_alpha - params.cvar_alpha).abs() < 1e-6); + // Task 4: min_hold_bars roundtrip (index 45) + assert_eq!(recovered.min_hold_bars, params.min_hold_bars); } #[test] fn test_dqn_params_bounds() { let bounds = DQNParams::continuous_bounds(); - assert_eq!(bounds.len(), 45); // 45D: 39 base + spectral_norm_sigma_max + c51_warmup_epochs + her_ratio + curiosity_weight + use_cvar + cvar_alpha + assert_eq!(bounds.len(), 46); // 46D: 39 base + spectral_norm_sigma_max + c51_warmup_epochs + her_ratio + curiosity_weight + use_cvar + cvar_alpha + min_hold_bars // Check log-scale bounds are reasonable assert!(bounds[0].0 < bounds[0].1); // learning_rate @@ -4059,7 +4077,7 @@ mod tests { #[test] fn test_param_names() { let names = DQNParams::param_names(); - assert_eq!(names.len(), 45); // 45D: 39 base + spectral_norm_sigma_max + c51_warmup_epochs + her_ratio + curiosity_weight + use_cvar + cvar_alpha + assert_eq!(names.len(), 46); // 46D: 39 base + spectral_norm_sigma_max + c51_warmup_epochs + her_ratio + curiosity_weight + use_cvar + cvar_alpha + min_hold_bars assert_eq!(names[0], "learning_rate"); assert_eq!(names[1], "batch_size"); assert_eq!(names[2], "gamma"); @@ -4097,12 +4115,13 @@ mod tests { assert_eq!(names[42], "curiosity_weight"); // 42 assert_eq!(names[43], "use_cvar_action_selection"); // 43 assert_eq!(names[44], "cvar_alpha"); // 44 + assert_eq!(names[45], "min_hold_bars"); // 45 } #[test] fn test_per_params_always_enabled() { // Test that PER is always enabled with tunable alpha/beta parameters - // 45D search space (Task 11: +4 new dims) + // 46D search space (Task 11: +4 new dims, Task 4: +1 min_hold_bars dim) let continuous = vec![ 3e-5_f64.ln(), 92.0, 0.92, 97_273_f64.ln(), 4.0, 24.77_f64.ln(), 0.03, 1.0, 0.6, 0.4, // 8-9: per_alpha, per_beta_start @@ -4499,7 +4518,7 @@ mod tests { fn test_qr_dqn_roundtrip_continuous() { let params = DQNParams::default(); let continuous = params.to_continuous(); - assert_eq!(continuous.len(), 45, "Should have 45 continuous dimensions"); + assert_eq!(continuous.len(), 46, "Should have 46 continuous dimensions"); let roundtrip = DQNParams::from_continuous(&continuous).unwrap(); // Default num_quantiles=32 (IQN default, used alongside C51) assert_eq!(roundtrip.num_quantiles, 32);