feat: min_hold_bars in hyperopt search space [2, 20]
Integer parameter (rounded from continuous). Lets PSO discover optimal hold duration for ES futures regime characteristics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<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)
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user