feat(hyperopt): reduce DQN search space from 45D to 25D

Fix 20 exotic parameters to validated defaults, keeping only the 25
that genuinely affect trading performance. This makes both PSO and
TPE dramatically more effective at exploring the parameter space.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-03 18:42:37 +01:00
parent 903e690e9a
commit c29fafbbb6

View File

@@ -455,184 +455,126 @@ impl Default for DQNParams {
impl ParameterSpace for DQNParams {
fn continuous_bounds() -> Vec<(f64, f64)> {
// 42D continuous space (40D base + 2D QR-DQN: num_quantiles, qr_kappa)
// Base parameters (11D from Wave 1-2): LR, batch, gamma, buffer, hold_penalty, max_pos, huber, entropy, tx_cost, per_alpha, per_beta
// Rainbow extensions (6D): v_min, v_max, noisy_sigma_init, dueling_hidden_dim, n_steps, num_atoms
// v_min/v_max/num_atoms tuned by hyperopt (C51 re-enabled, BUG #36 fixed)
// Bug #7 addition (1D): minimum_profit_factor
// Weight decay addition (1D): weight_decay (L2 regularization)
// Rainbow booleans: use_dueling=TRUE, use_distributional=FALSE (BUG #36), use_noisy_nets=TRUE
// 25D reduced search space (down from 45D)
// 20 exotic parameters fixed to validated defaults in from_continuous()
// This makes PSO (20 particles) and TPE dramatically more effective
//
// WAVE 26 P1.5 FIX: Learning rate range EXPANDED to include production default 1e-4
// CRITICAL: Previous range [2e-5, 8e-5] excluded production default 1e-4!
// New range: [1e-5, 3e-4] (30x range, includes 1e-4)
// Kept: the 25 params that genuinely affect trading performance
// Fixed: ensemble/architecture/scheduling params that rarely deviate from defaults
vec![
// Base parameters (11D) - WAVE 26 P1.5: EXPANDED learning rate range
(1e-5_f64.ln(), 3e-4_f64.ln()), // 0: learning_rate (log scale) - EXPANDED from [2e-5, 8e-5] to include production default 1e-4
// Base parameters (11D)
(1e-5_f64.ln(), 3e-4_f64.ln()), // 0: learning_rate (log scale) - includes production default 1e-4
(64.0, 4096.0), // 1: batch_size (linear, upper bound adjusted by HardwareBudget)
(0.95, 0.99), // 2: gamma (linear) - KEPT (already optimal)
(50_000_f64.ln(), 100_000_f64.ln()), // 3: buffer_size (log scale) - KEPT (already optimal)
(1.0, 2.0), // 4: hold_penalty_weight (linear) - NARROWED from [0.5, 5.0]
(1.0, 4.0), // 5: max_position_absolute (linear) - Tightened from [4,8] to prevent catastrophic leverage
(10.0_f64.ln(), 40.0_f64.ln()), // 6: huber_delta (log scale: 10.0-40.0) - Starting from conservative 10.0 default
(0.0, 0.1), // 7: entropy_coefficient (linear) - KEPT
(0.5, 2.0), // 8: transaction_cost_multiplier (linear) - KEPT
(0.4, 0.8), // 9: per_alpha (linear) - KEPT
(0.2, 0.6), // 10: per_beta_start (linear) - KEPT
(0.95, 0.99), // 2: gamma (linear)
(50_000_f64.ln(), 100_000_f64.ln()), // 3: buffer_size (log scale)
(1.0, 2.0), // 4: hold_penalty_weight (linear)
(1.0, 4.0), // 5: max_position_absolute (linear)
(10.0_f64.ln(), 40.0_f64.ln()), // 6: huber_delta (log scale: 10.0-40.0)
(0.0, 0.1), // 7: entropy_coefficient (linear)
(0.5, 2.0), // 8: transaction_cost_multiplier (linear)
(0.4, 0.8), // 9: per_alpha (linear)
(0.2, 0.6), // 10: per_beta_start (linear)
// Rainbow DQN extensions (6D continuous)
// BUG #5 FIX: Search space centered around validated defaults (-2.0/+2.0)
// After commit b2967277: rewards are ±1.0 (normalized), 10-step return is ±6.0, 100-step is ±50
// Validated defaults: v_min=-2.0, v_max=+2.0 (from DQN training validation)
(-3.0, -1.0), // 11: v_min (linear) - Bug #5 fix: Search space [-3, -1], center: -2.0
(1.0, 3.0), // 12: v_max (linear) - Bug #5 fix: Search space [1, 3], center: +2.0
(0.1_f64.ln(), 1.0_f64.ln()), // 13: noisy_sigma_init (log scale) - NoisyNet exploration
(128.0, 512.0), // 14: dueling_hidden_dim (linear, step=128) - Dueling architecture capacity
(1.0, 5.0), // 15: n_steps (linear, int) - N-step return horizon
(51.0, 201.0), // 16: num_atoms (linear, step=50) - Distributional atoms count
// Rainbow DQN extensions (6D)
(-3.0, -1.0), // 11: v_min (linear) - Bug #5 fix: center -2.0
(1.0, 3.0), // 12: v_max (linear) - Bug #5 fix: center +2.0
(0.1_f64.ln(), 1.0_f64.ln()), // 13: noisy_sigma_init (log scale)
(128.0, 512.0), // 14: dueling_hidden_dim (linear, step=128)
(1.0, 5.0), // 15: n_steps (linear, int)
(51.0, 201.0), // 16: num_atoms (linear, step=50)
// BUG #7: Minimum profit threshold (1D)
(1.1, 2.0), // 17: minimum_profit_factor (linear) - Profit margin requirement
// Weight decay (1D)
(1e-5_f64.ln(), 1e-3_f64.ln()), // 17: weight_decay (log scale)
// Weight decay (L2 regularization) (1D)
(1e-5_f64.ln(), 1e-3_f64.ln()), // 18: weight_decay (log scale) - L2 regularization strength
// Kelly risk parameters (2D — kelly_min_trades fixed to 20)
(0.25, 1.0), // 18: kelly_fractional
(0.1, 0.5), // 19: kelly_max_fraction
// WAVE 19: Kelly risk parameters (19D → 23D)
(0.25, 1.0), // 19: kelly_fractional
(0.1, 0.5), // 20: kelly_max_fraction
(10.0, 50.0), // 21: kelly_min_trades
(10.0, 30.0), // 22: volatility_window
// Volatility window (1D)
(10.0, 30.0), // 20: volatility_window
// WAVE 26 P1.4: Ensemble Uncertainty (23D → 28D)
(3.0, 10.0), // 23: ensemble_size (will be rounded to int)
(0.1, 1.0), // 24: beta_variance
(0.1, 1.0), // 25: beta_disagreement
(0.05, 0.5), // 26: beta_entropy
(0.1, 2.0), // 27: variance_cap (fixed in DQNHyperparameters, not tuned per-trial)
// Curiosity + soft update (2D)
(0.01, 0.5), // 21: curiosity_weight (intrinsic reward scaling)
(0.0001_f64.ln(), 0.01_f64.ln()), // 22: tau (log scale, Polyak soft update)
// WAVE 26 P1.5: Learning rate warmup ratio (28D → 29D)
(0.0, 0.2), // 28: warmup_ratio (0-20% warmup)
// GPU-dynamic network sizing (1D)
(256.0, 4096.0), // 23: hidden_dim_base (linear, step=256)
// WAVE 26 P1.8: Curiosity-driven exploration (29D → 30D)
(0.01, 0.5), // 29: curiosity_weight (intrinsic reward scaling, >0 required for GPU experience collector)
// WAVE 26 P1.12: Polyak soft update coefficient (30D → 31D)
(0.0001_f64.ln(), 0.01_f64.ln()), // 30: tau (log scale, 0.0001-0.01, Rainbow default: 0.001)
// WAVE 26 P0: TD Error and Batch Diversity (31D → 33D)
(1.0, 100.0), // 31: td_error_clamp_max (linear, prevents extreme TD errors)
(10.0, 100.0), // 32: batch_diversity_cooldown (linear, diversity sampling frequency)
// WAVE 26 P1: Advanced Training Parameters (33D → 38D)
(0.0, 2.0), // 33: lr_decay_type (0=constant, 1=linear, 2=cosine)
(0.0, 0.5), // 34: sharpe_weight (risk-adjusted return weight)
(0.9, 0.99), // 35: gae_lambda (GAE bias-variance tradeoff)
(0.4, 0.8), // 36: noisy_sigma_initial (initial exploration noise)
(0.2, 0.5), // 37: noisy_sigma_final (final exploration noise)
// WAVE 26 P1: Network Architecture (38D → 40D)
// Note: Booleans (use_spectral_norm, use_attention, use_residual) NOT in search space
// They default to false and can be enabled via CLI or config
(0.0, 2.0), // 38: norm_type (0=LayerNorm, 1=RMSNorm, 2=None)
(0.0, 3.0), // 39: activation_type (0=ReLU, 1=LeakyReLU, 2=GELU, 3=Mish)
// QR-DQN parameters (40D -> 42D)
(32.0, 200.0), // 40: num_quantiles (linear, integer)
(0.5_f64.ln(), 2.0_f64.ln()), // 41: qr_kappa (log scale)
// GPU-dynamic network sizing (42D → 43D)
(256.0, 4096.0), // 42: hidden_dim_base (linear, step=256)
// Exploration anti-collapse parameters (43D → 45D)
(0.02, 0.10), // 43: noisy_epsilon_floor (linear, minimum random exploration)
(0.01, 0.5), // 44: count_bonus_coefficient (linear, UCB exploration bonus)
// Exploration anti-collapse (1D)
(0.02, 0.10), // 24: noisy_epsilon_floor (linear, minimum random exploration)
]
}
fn from_continuous(x: &[f64]) -> Result<Self, MLError> {
if x.len() != 45 {
if x.len() != 25 {
return Err(MLError::ConfigError {
reason: format!("Expected 45 continuous parameters (added noisy_epsilon_floor, count_bonus_coefficient), got {}", x.len()),
reason: format!("Expected 25 continuous parameters (reduced from 45D), got {}", x.len()),
});
}
// === 25 TUNED parameters (from search space) ===
let learning_rate = x[0].exp();
let mut batch_size = x[1].round().max(64.0) as usize; // Only enforce floor. PSO + HardwareBudget control the upper bound.
let buffer_size = x[3].exp().round().max(50_000.0) as usize; // OPTIMIZED: from 10_000
let hold_penalty_weight = x[4].clamp(1.0, 2.0); // OPTIMIZED: from [0.5, 5.0]
let max_position_absolute = x[5].clamp(1.0, 4.0); // Tightened from [4,8] to prevent catastrophic leverage
let huber_delta = x[6].exp(); // OPTIMIZED: log scale now [15.0, 40.0] (was [10.0, 200.0])
let buffer_size = x[3].exp().round().max(50_000.0) as usize;
let hold_penalty_weight = x[4].clamp(1.0, 2.0);
let max_position_absolute = x[5].clamp(1.0, 4.0);
let huber_delta = x[6].exp();
let entropy_coefficient = x[7];
let transaction_cost_multiplier = x[8];
let per_alpha = x[9].clamp(0.4, 0.8);
let per_beta_start = x[10].clamp(0.2, 0.6);
// Rainbow DQN extensions (Wave 6.4: 14D → 17D)
// BUG #5 FIX: Search space centered around validated defaults (-2.0/+2.0)
let v_min = x[11].clamp(-3.0, -1.0); // Bug #5 fix: Search space [-3, -1], center: -2.0
let v_max = x[12].clamp(1.0, 3.0); // Bug #5 fix: Search space [1, 3], center: +2.0
// Rainbow DQN extensions
let v_min = x[11].clamp(-3.0, -1.0);
let v_max = x[12].clamp(1.0, 3.0);
let noisy_sigma_init = x[13].exp().clamp(0.1, 1.0);
// Wave 6.4: Dueling, N-step, Distributional atoms
let dueling_hidden_dim = (x[14].round() / 128.0).round() * 128.0; // Round to nearest 128
let dueling_hidden_dim = (x[14].round() / 128.0).round() * 128.0;
let dueling_hidden_dim = dueling_hidden_dim.clamp(128.0, 512.0) as usize;
let n_steps = x[15].round().clamp(1.0, 5.0) as usize;
let num_atoms = (x[16].round() / 50.0).round() * 50.0; // Round to nearest 50
let num_atoms = (x[16].round() / 50.0).round() * 50.0;
let num_atoms = num_atoms.clamp(51.0, 201.0) as usize;
// BUG #7: Minimum profit factor (18th parameter)
let minimum_profit_factor = x[17].clamp(1.1, 2.0);
// Weight decay
let weight_decay = x[17].exp().clamp(1e-5, 1e-3);
// CRITICAL GAP FIX: Weight decay (L2 regularization) (19th parameter)
let weight_decay = x[18].exp().clamp(1e-5, 1e-3);
// Kelly risk parameters (kelly_min_trades fixed to 20)
let kelly_fractional = x[18].clamp(0.25, 1.0);
let kelly_max_fraction = x[19].clamp(0.1, 0.5);
// WAVE 19: Extract Kelly parameters (shifted by +1 due to weight_decay)
let kelly_fractional = x[19].clamp(0.25, 1.0);
let kelly_max_fraction = x[20].clamp(0.1, 0.5);
let kelly_min_trades = x[21].round().clamp(10.0, 50.0) as usize;
let volatility_window = x[22].round().clamp(10.0, 30.0) as usize;
// Volatility window
let volatility_window = x[20].round().clamp(10.0, 30.0) as usize;
// WAVE 26 P1.4: Extract ensemble uncertainty parameters (shifted by +1 due to weight_decay)
let ensemble_size = x[23].round().clamp(3.0, 10.0);
let beta_variance = x[24].clamp(0.1, 1.0);
let beta_disagreement = x[25].clamp(0.1, 1.0);
let beta_entropy = x[26].clamp(0.05, 0.5);
// Note: x[27] is variance_cap, but it's NOT in DQNParams (fixed in DQNHyperparameters)
// WAVE 26 P1.5: Extract warmup ratio
let warmup_ratio = x[28].clamp(0.0, 0.2);
// WAVE 26 P1.8: Extract curiosity weight
let curiosity_weight = x[29].clamp(0.01, 0.5);
// WAVE 26 P1.12: Extract tau (Polyak soft update coefficient)
let tau = x[30].exp().clamp(0.0001, 0.01); // Log scale: 0.0001-0.01, default: 0.001
// WAVE 26 P0: Extract TD error and batch diversity parameters
let td_error_clamp_max = x[31].clamp(1.0, 100.0);
let batch_diversity_cooldown = x[32].clamp(10.0, 100.0);
// WAVE 26 P1: Extract advanced training parameters
let lr_decay_type = x[33].round().clamp(0.0, 2.0); // 0=constant, 1=linear, 2=cosine
let sharpe_weight = x[34].clamp(0.0, 0.5);
let gae_lambda = x[35].clamp(0.9, 0.99);
let noisy_sigma_initial = x[36].clamp(0.4, 0.8);
let noisy_sigma_final = x[37].clamp(0.2, 0.5);
// WAVE 26 P1: Extract network architecture parameters
let norm_type = x[38].round().clamp(0.0, 2.0); // 0=LayerNorm, 1=RMSNorm, 2=None
let activation_type = x[39].round().clamp(0.0, 3.0); // 0=ReLU, 1=LeakyReLU, 2=GELU, 3=Mish
// QR-DQN parameters
let num_quantiles = x[40].round().clamp(32.0, 200.0) as usize;
let qr_kappa = x[41].exp().clamp(0.5, 2.0);
// Curiosity + soft update
let curiosity_weight = x[21].clamp(0.01, 0.5);
let tau = x[22].exp().clamp(0.0001, 0.01);
// GPU-dynamic hidden dims
let hidden_dim_base = ((x[42].round() / 256.0).round() * 256.0).clamp(256.0, 4096.0) as usize;
let hidden_dim_base = ((x[23].round() / 256.0).round() * 256.0).clamp(256.0, 4096.0) as usize;
// Exploration anti-collapse parameters
let noisy_epsilon_floor = x[43].clamp(0.02, 0.10);
let count_bonus_coefficient = x[44].clamp(0.01, 0.5);
// Exploration anti-collapse
let noisy_epsilon_floor = x[24].clamp(0.02, 0.10);
// WAVE 11: Rainbow DQN boolean parameters are ALWAYS TRUE (removed from search space)
// User requirement: "I want them enabled!" - no point in tuning boolean flags
// === 20 FIXED parameters (validated defaults, removed from search) ===
let minimum_profit_factor = 1.5;
let kelly_min_trades: usize = 20;
let ensemble_size = 5.0;
let beta_variance = 0.5;
let beta_disagreement = 0.5;
let beta_entropy = 0.2;
let warmup_ratio = 0.0;
let td_error_clamp_max = 10.0;
let batch_diversity_cooldown = 50.0;
let lr_decay_type = 0.0; // constant
let sharpe_weight = 0.0;
let gae_lambda = 0.95;
let noisy_sigma_initial = 0.5;
let noisy_sigma_final = 0.3;
let norm_type = 1.0; // RMSNorm
let activation_type = 1.0; // LeakyReLU
let num_quantiles: usize = 64;
let qr_kappa = 1.0;
let count_bonus_coefficient = 0.1;
// variance_cap is not in DQNParams (fixed in DQNHyperparameters)
// WAVE 6 FIX #2: Batch size floor for high learning rates
// High LR + small batch = Q-collapse. Enforce minimum batch size for LR > 2e-4
@@ -664,52 +606,45 @@ impl ParameterSpace for DQNParams {
huber_delta,
entropy_coefficient,
transaction_cost_multiplier,
use_per: true, // P0: Always enabled for Rainbow DQN performance (25-40% improvement)
use_per: true,
per_alpha,
per_beta_start,
use_dueling: true, // WAVE 11: Always enabled for full Rainbow DQN (6/6 components)
dueling_hidden_dim, // Wave 6.4: TUNABLE (128-512, step=128)
n_steps, // Wave 6.4: TUNABLE (1-5 steps)
// BUG #36 FIXED: scatter_add gradient flow verified — C51 re-enabled
use_distributional: true, // ENABLED: full Rainbow DQN C51 (BUG #36 fixed)
num_atoms, // Wave 6.4: TUNABLE (51-201, step=50)
use_dueling: true,
dueling_hidden_dim,
n_steps,
use_distributional: true,
num_atoms,
v_min,
v_max,
use_noisy_nets: true, // WAVE 11: Always enabled for full Rainbow DQN
use_noisy_nets: true,
noisy_sigma_init,
minimum_profit_factor, // BUG #7: Configurable profit margin (1.1-2.0)
weight_decay, // CRITICAL GAP FIX: Now tunable in hyperopt search space (1e-5 to 1e-3)
// WAVE 19: Kelly risk parameters
minimum_profit_factor,
weight_decay,
kelly_fractional,
kelly_max_fraction,
kelly_min_trades,
volatility_window,
// WAVE 26 P1.4: Ensemble uncertainty parameters
use_ensemble_uncertainty: false, // WAVE 26 P1.4: Boolean not in search space, hardcoded disabled (use noisy nets instead)
use_ensemble_uncertainty: false,
ensemble_size,
beta_variance,
beta_disagreement,
beta_entropy,
warmup_ratio,
curiosity_weight,
tau, // WAVE 26 P1.12: Polyak soft update coefficient (now in search space)
// WAVE 26 P0: TD error and batch diversity
tau,
td_error_clamp_max,
batch_diversity_cooldown,
// WAVE 26 P1: Advanced training parameters
lr_decay_type,
sharpe_weight,
gae_lambda,
noisy_sigma_initial,
noisy_sigma_final,
// WAVE 26 P1: Network architecture (booleans hardcoded to false)
use_spectral_norm: false,
use_attention: false,
use_residual: false,
norm_type,
activation_type,
// QR-DQN (replaces disabled C51)
use_qr_dqn: true, // Always enabled
use_qr_dqn: true,
num_quantiles,
qr_kappa,
hidden_dim_base,
@@ -717,129 +652,69 @@ impl ParameterSpace for DQNParams {
count_bonus_coefficient,
};
// Note: HFT constraint validation moved to evaluate_objective (train_with_params)
// to allow pruning instead of crashing the entire hyperopt run
Ok(params)
}
fn to_continuous(&self) -> Vec<f64> {
// 25D reduced space — only the tuned parameters
// Fixed parameters are NOT emitted (they get their defaults in from_continuous)
vec![
self.learning_rate.ln(),
self.batch_size as f64,
self.gamma,
(self.buffer_size as f64).ln(),
self.hold_penalty_weight,
self.max_position_absolute,
self.huber_delta.ln(),
self.entropy_coefficient,
self.transaction_cost_multiplier,
self.per_alpha,
self.per_beta_start,
// Rainbow DQN extensions (Wave 6.4: 14D → 17D)
self.v_min,
self.v_max,
self.noisy_sigma_init.ln(),
self.dueling_hidden_dim as f64, // Dueling hidden dimension
self.n_steps as f64, // N-step return horizon
self.num_atoms as f64, // Distributional atoms count
// BUG #7: Minimum profit factor (18D)
self.minimum_profit_factor,
// CRITICAL GAP FIX: Weight decay (19D)
self.weight_decay.ln(),
// WAVE 19: Kelly risk parameters (23D)
self.kelly_fractional,
self.kelly_max_fraction,
self.kelly_min_trades as f64,
self.volatility_window as f64,
// WAVE 26 P1.4: Ensemble uncertainty parameters (27D)
self.ensemble_size,
self.beta_variance,
self.beta_disagreement,
self.beta_entropy,
1.0, // variance_cap placeholder (not in DQNParams, fixed in DQNHyperparameters)
self.warmup_ratio,
self.curiosity_weight,
self.tau.ln(), // WAVE 26 P1.12: Polyak soft update coefficient (log scale)
// WAVE 26 P0: TD error and batch diversity
self.td_error_clamp_max,
self.batch_diversity_cooldown,
// WAVE 26 P1: Advanced training parameters
self.lr_decay_type,
self.sharpe_weight,
self.gae_lambda,
self.noisy_sigma_initial,
self.noisy_sigma_final,
// WAVE 26 P1: Network architecture
self.norm_type,
self.activation_type,
// QR-DQN parameters (42D)
self.num_quantiles as f64,
self.qr_kappa.ln(),
self.hidden_dim_base as f64,
// Exploration anti-collapse
self.noisy_epsilon_floor,
self.count_bonus_coefficient,
self.learning_rate.ln(), // 0
self.batch_size as f64, // 1
self.gamma, // 2
(self.buffer_size as f64).ln(), // 3
self.hold_penalty_weight, // 4
self.max_position_absolute, // 5
self.huber_delta.ln(), // 6
self.entropy_coefficient, // 7
self.transaction_cost_multiplier, // 8
self.per_alpha, // 9
self.per_beta_start, // 10
self.v_min, // 11
self.v_max, // 12
self.noisy_sigma_init.ln(), // 13
self.dueling_hidden_dim as f64, // 14
self.n_steps as f64, // 15
self.num_atoms as f64, // 16
self.weight_decay.ln(), // 17
self.kelly_fractional, // 18
self.kelly_max_fraction, // 19
self.volatility_window as f64, // 20
self.curiosity_weight, // 21
self.tau.ln(), // 22
self.hidden_dim_base as f64, // 23
self.noisy_epsilon_floor, // 24
]
}
fn param_names() -> Vec<&'static str> {
// 25 tuned parameters (matches continuous_bounds / from_continuous / to_continuous)
vec![
"learning_rate",
"batch_size",
"gamma",
"buffer_size",
"hold_penalty_weight",
"max_position_absolute",
"huber_delta",
"entropy_coefficient",
"transaction_cost_multiplier",
"per_alpha",
"per_beta_start",
// Rainbow DQN extensions (Wave 6.4: 14D → 17D)
"v_min",
"v_max",
"noisy_sigma_init",
"dueling_hidden_dim", // Dueling hidden dimension
"n_steps", // N-step return horizon
"num_atoms", // Distributional atoms count
// BUG #7: Minimum profit factor (18D)
"minimum_profit_factor",
// CRITICAL GAP FIX: Weight decay (19D)
"weight_decay",
// WAVE 19: Kelly risk parameters (23D)
"kelly_fractional",
"kelly_max_fraction",
"kelly_min_trades",
"volatility_window",
// WAVE 26 P1.4: Ensemble uncertainty parameters (27D)
"ensemble_size",
"beta_variance",
"beta_disagreement",
"beta_entropy",
"variance_cap",
"warmup_ratio",
"curiosity_weight",
"tau", // WAVE 26 P1.12: Polyak soft update coefficient
// WAVE 26 P0: TD error and batch diversity
"td_error_clamp_max",
"batch_diversity_cooldown",
// WAVE 26 P1: Advanced training parameters
"lr_decay_type",
"sharpe_weight",
"gae_lambda",
"noisy_sigma_initial",
"noisy_sigma_final",
// WAVE 26 P1: Network architecture
"norm_type",
"activation_type",
// QR-DQN parameters
"num_quantiles",
"qr_kappa",
"hidden_dim_base",
// Exploration anti-collapse
"noisy_epsilon_floor",
"count_bonus_coefficient",
"learning_rate", // 0
"batch_size", // 1
"gamma", // 2
"buffer_size", // 3
"hold_penalty_weight", // 4
"max_position_absolute", // 5
"huber_delta", // 6
"entropy_coefficient", // 7
"transaction_cost_multiplier", // 8
"per_alpha", // 9
"per_beta_start", // 10
"v_min", // 11
"v_max", // 12
"noisy_sigma_init", // 13
"dueling_hidden_dim", // 14
"n_steps", // 15
"num_atoms", // 16
"weight_decay", // 17
"kelly_fractional", // 18
"kelly_max_fraction", // 19
"volatility_window", // 20
"curiosity_weight", // 21
"tau", // 22
"hidden_dim_base", // 23
"noisy_epsilon_floor", // 24
]
}
@@ -851,9 +726,9 @@ impl ParameterSpace for DQNParams {
batch_bound.1 = max_batch;
}
}
// Cap hidden_dim_base by VRAM (index 42)
// Cap hidden_dim_base by VRAM (index 23 in 25D space)
let max_base = budget.max_hidden_dim_base(4, 256, 54, 45);
if let Some(dim_bound) = bounds.get_mut(42) {
if let Some(dim_bound) = bounds.get_mut(23) {
dim_bound.1 = max_base as f64;
}
bounds
@@ -3353,59 +3228,57 @@ mod tests {
#[test]
fn test_dqn_params_roundtrip() {
// Roundtrip test for the 25D reduced search space
// Only the 25 tuned parameters roundtrip; the 20 fixed params get defaults from from_continuous
let params = DQNParams {
learning_rate: 3.37e-05, // OPTIMIZED: Trial 3 best value (within [2e-5, 8e-5])
batch_size: 92, // OPTIMIZED: Trial 3 best value (within [64, 160])
gamma: 0.9588, // OPTIMIZED: Trial 3 best value
learning_rate: 3.37e-05,
batch_size: 92,
gamma: 0.9588,
buffer_size: 97_273,
hold_penalty_weight: 1.404, // OPTIMIZED: Trial 3 best value (within [1.0, 2.0])
max_position_absolute: 2.5, // Tightened range [1.0, 4.0] to prevent catastrophic leverage
huber_delta: 24.77, // OPTIMIZED: Trial 3 best value (within [15.0, 40.0])
hold_penalty_weight: 1.404,
max_position_absolute: 2.5,
huber_delta: 24.77,
entropy_coefficient: 0.01,
transaction_cost_multiplier: 1.0,
use_per: true, // P0: Default enabled
per_alpha: 0.6, // P0: Rainbow DQN standard
per_beta_start: 0.4, // P0: Rainbow DQN standard
use_dueling: false, // Wave 2.1: Standard architecture
use_per: true,
per_alpha: 0.6,
per_beta_start: 0.4,
use_dueling: true,
dueling_hidden_dim: 128,
n_steps: 1,
tau: 0.001,
use_distributional: false,
use_distributional: true,
num_atoms: 51,
v_min: -1000.0,
v_max: 1000.0,
use_noisy_nets: false,
v_min: -2.0,
v_max: 2.0,
use_noisy_nets: true,
noisy_sigma_init: 0.5,
minimum_profit_factor: 1.5, // Bug #7 fix
weight_decay: 1e-4, // P0.1 FIX: L2 regularization (was missing, causing test compilation failure)
// Fixed params — these will be overwritten by from_continuous defaults
minimum_profit_factor: 1.5,
weight_decay: 1e-4,
kelly_fractional: 0.5,
kelly_max_fraction: 0.25,
kelly_min_trades: 20,
volatility_window: 20,
// WAVE 26 P1.4: Ensemble uncertainty
use_ensemble_uncertainty: false,
ensemble_size: 5.0,
beta_variance: 0.5, beta_disagreement: 0.5, beta_entropy: 0.1,
warmup_ratio: 0.1, // WAVE 26 P1.5
curiosity_weight: 0.1, // WAVE 26 P1.8: enabled (GPU experience collector)
// WAVE 26 P0: TD Error and Batch Diversity
beta_variance: 0.5, beta_disagreement: 0.5, beta_entropy: 0.2,
warmup_ratio: 0.0,
curiosity_weight: 0.1,
td_error_clamp_max: 10.0,
batch_diversity_cooldown: 50.0,
// WAVE 26 P1: Advanced Training Parameters
lr_decay_type: 0.0,
sharpe_weight: 0.3,
sharpe_weight: 0.0,
gae_lambda: 0.95,
noisy_sigma_initial: 0.6,
noisy_sigma_final: 0.4,
// WAVE 26 P1: Network Architecture
noisy_sigma_initial: 0.5,
noisy_sigma_final: 0.3,
use_spectral_norm: false,
use_attention: false,
use_residual: false,
norm_type: 0.0,
activation_type: 0.0,
// QR-DQN
norm_type: 1.0,
activation_type: 1.0,
use_qr_dqn: true,
num_quantiles: 32,
num_quantiles: 64,
qr_kappa: 1.0,
hidden_dim_base: 512,
noisy_epsilon_floor: 0.05,
@@ -3413,222 +3286,185 @@ mod tests {
};
let continuous = params.to_continuous();
assert_eq!(continuous.len(), 25, "to_continuous must return 25D vector");
let recovered = DQNParams::from_continuous(&continuous).unwrap();
// Tuned parameters must roundtrip exactly
assert!((recovered.learning_rate - params.learning_rate).abs() < 1e-6);
assert_eq!(recovered.batch_size, params.batch_size);
assert!((recovered.gamma - params.gamma).abs() < 1e-6);
assert_eq!(recovered.buffer_size, params.buffer_size);
assert!((recovered.hold_penalty_weight - params.hold_penalty_weight).abs() < 1e-6);
assert!((recovered.max_position_absolute - params.max_position_absolute).abs() < 1e-6); // BLOCKER #2: Test roundtrip
assert!((recovered.per_alpha - params.per_alpha).abs() < 1e-6); // P0: Test PER roundtrip
assert!((recovered.per_beta_start - params.per_beta_start).abs() < 1e-6); // P0: Test PER roundtrip
// Note: use_per is discrete parameter, tested separately in from_mixed
// Note: tau and epsilon_decay are not part of DQNParams (fixed at default values)
assert!((recovered.max_position_absolute - params.max_position_absolute).abs() < 1e-6);
assert!((recovered.per_alpha - params.per_alpha).abs() < 1e-6);
assert!((recovered.per_beta_start - params.per_beta_start).abs() < 1e-6);
assert!((recovered.weight_decay - params.weight_decay).abs() < 1e-8);
assert!((recovered.kelly_fractional - params.kelly_fractional).abs() < 1e-6);
assert!((recovered.kelly_max_fraction - params.kelly_max_fraction).abs() < 1e-6);
assert_eq!(recovered.volatility_window, params.volatility_window);
assert!((recovered.curiosity_weight - params.curiosity_weight).abs() < 1e-6);
assert!((recovered.tau - params.tau).abs() < 1e-6);
assert_eq!(recovered.hidden_dim_base, params.hidden_dim_base);
assert!((recovered.noisy_epsilon_floor - params.noisy_epsilon_floor).abs() < 1e-6);
// Fixed parameters must get their validated defaults
assert!((recovered.minimum_profit_factor - 1.5).abs() < 1e-6);
assert_eq!(recovered.kelly_min_trades, 20);
assert!((recovered.ensemble_size - 5.0).abs() < 1e-6);
assert!((recovered.warmup_ratio - 0.0).abs() < 1e-6);
assert!((recovered.td_error_clamp_max - 10.0).abs() < 1e-6);
assert!((recovered.batch_diversity_cooldown - 50.0).abs() < 1e-6);
assert!((recovered.lr_decay_type - 0.0).abs() < 1e-6);
assert!((recovered.norm_type - 1.0).abs() < 1e-6); // RMSNorm
assert!((recovered.activation_type - 1.0).abs() < 1e-6); // LeakyReLU
assert_eq!(recovered.num_quantiles, 64);
assert!((recovered.qr_kappa - 1.0).abs() < 1e-6);
assert!((recovered.count_bonus_coefficient - 0.1).abs() < 1e-6);
}
#[test]
fn test_dqn_params_bounds() {
let bounds = DQNParams::continuous_bounds();
assert_eq!(bounds.len(), 45); // 45 continuous parameters (added noisy_epsilon_floor, count_bonus_coefficient)
assert_eq!(bounds.len(), 25); // 25D reduced search space (down from 45)
// Check log-scale bounds are reasonable
assert!(bounds[0].0 < bounds[0].1); // learning_rate
assert!(bounds[3].0 < bounds[3].1); // buffer_size
assert!(bounds[6].0 < bounds[6].1); // huber_delta
assert!(bounds[13].0 < bounds[13].1); // noisy_sigma_init
assert!(bounds[17].0 < bounds[17].1); // weight_decay (log scale)
assert!(bounds[22].0 < bounds[22].1); // tau (log scale)
// WAVE 26 P1.5: Check expanded learning rate range includes production default 1e-4
// Check learning rate range includes production default 1e-4
let lr_min = bounds[0].0.exp();
let lr_max = bounds[0].1.exp();
assert!(lr_min <= 1e-4 && 1e-4 <= lr_max, "Learning rate range [{}, {}] must include production default 1e-4", lr_min, lr_max);
assert!((lr_min - 1e-5).abs() < 1e-7, "Learning rate lower bound should be 1e-5, got {}", lr_min);
assert!((lr_max - 3e-4).abs() < 1e-6, "Learning rate upper bound should be 3e-4, got {}", lr_max);
// Check linear bounds - OPTIMIZED (2025-11-19): Narrowed based on Trial 3 empirical evidence
assert_eq!(bounds[1], (64.0, 4096.0)); // batch_size (upper bound adjusted by HardwareBudget)
assert_eq!(bounds[2], (0.95, 0.99)); // gamma (HFT temporal discounting - KEPT)
assert_eq!(bounds[4], (1.0, 2.0)); // hold_penalty_weight (OPTIMIZED from [0.5, 5.0])
assert_eq!(bounds[5], (1.0, 4.0)); // max_position_absolute (tightened from [4,8] to prevent catastrophic leverage)
assert_eq!(bounds[7], (0.0, 0.1)); // entropy_coefficient
assert_eq!(bounds[8], (0.5, 2.0)); // transaction_cost_multiplier
assert_eq!(bounds[9], (0.4, 0.8)); // P0: per_alpha (prioritization exponent)
assert_eq!(bounds[10], (0.2, 0.6)); // P0: per_beta_start (IS correction)
assert_eq!(bounds[11], (-3.0, -1.0)); // Bug #5: v_min (search space centered around -2.0)
assert_eq!(bounds[12], (1.0, 3.0)); // Bug #5: v_max (search space centered around +2.0)
assert_eq!(bounds[14], (128.0, 512.0)); // Wave 6.4: dueling_hidden_dim (Dueling architecture)
assert_eq!(bounds[15], (1.0, 5.0)); // Wave 6.4: n_steps (N-step returns)
assert_eq!(bounds[16], (51.0, 201.0)); // Wave 6.4: num_atoms (Distributional atoms)
assert_eq!(bounds[17], (1.1, 2.0)); // Bug #7: minimum_profit_factor (profit margin requirement)
// WAVE 11: Rainbow boolean parameters removed (always TRUE)
// Check linear bounds
assert_eq!(bounds[1], (64.0, 4096.0)); // batch_size
assert_eq!(bounds[2], (0.95, 0.99)); // gamma
assert_eq!(bounds[4], (1.0, 2.0)); // hold_penalty_weight
assert_eq!(bounds[5], (1.0, 4.0)); // max_position_absolute
assert_eq!(bounds[7], (0.0, 0.1)); // entropy_coefficient
assert_eq!(bounds[8], (0.5, 2.0)); // transaction_cost_multiplier
assert_eq!(bounds[9], (0.4, 0.8)); // per_alpha
assert_eq!(bounds[10], (0.2, 0.6)); // per_beta_start
assert_eq!(bounds[11], (-3.0, -1.0)); // v_min
assert_eq!(bounds[12], (1.0, 3.0)); // v_max
assert_eq!(bounds[14], (128.0, 512.0)); // dueling_hidden_dim
assert_eq!(bounds[15], (1.0, 5.0)); // n_steps
assert_eq!(bounds[16], (51.0, 201.0)); // num_atoms
// Weight decay (L2 regularization)
assert!(bounds[18].0 < bounds[18].1); // weight_decay (log scale)
// Kelly risk parameters
assert_eq!(bounds[18], (0.25, 1.0)); // kelly_fractional
assert_eq!(bounds[19], (0.1, 0.5)); // kelly_max_fraction
assert_eq!(bounds[20], (10.0, 30.0)); // volatility_window
// WAVE 19: Kelly risk parameter bounds
assert_eq!(bounds[19], (0.25, 1.0)); // kelly_fractional
assert_eq!(bounds[20], (0.1, 0.5)); // kelly_max_fraction
assert_eq!(bounds[21], (10.0, 50.0)); // kelly_min_trades
assert_eq!(bounds[22], (10.0, 30.0)); // volatility_window
// WAVE 26 P1.5: Warmup ratio bounds
assert_eq!(bounds[28], (0.0, 0.2)); // warmup_ratio (0-20% warmup)
// WAVE 26 P1.12: Tau bounds
assert!(bounds[30].0 < bounds[30].1); // tau (log scale)
// Curiosity + exploration
assert_eq!(bounds[21], (0.01, 0.5)); // curiosity_weight
assert_eq!(bounds[23], (256.0, 4096.0)); // hidden_dim_base
assert_eq!(bounds[24], (0.02, 0.10)); // noisy_epsilon_floor
}
#[test]
fn test_param_names() {
let names = DQNParams::param_names();
assert_eq!(names.len(), 45); // 45 tunable hyperparameters (added noisy_epsilon_floor, count_bonus_coefficient)
assert_eq!(names.len(), 25); // 25D reduced search space
assert_eq!(names[0], "learning_rate");
assert_eq!(names[1], "batch_size");
assert_eq!(names[2], "gamma");
assert_eq!(names[3], "buffer_size");
assert_eq!(names[4], "hold_penalty_weight");
assert_eq!(names[5], "max_position_absolute"); // BLOCKER #2: Action masking limits
assert_eq!(names[5], "max_position_absolute");
assert_eq!(names[6], "huber_delta");
assert_eq!(names[7], "entropy_coefficient");
assert_eq!(names[8], "transaction_cost_multiplier");
assert_eq!(names[9], "per_alpha"); // P0: PER prioritization
assert_eq!(names[10], "per_beta_start"); // P0: PER IS correction
assert_eq!(names[11], "v_min"); // Wave 6.4: Distributional RL
assert_eq!(names[12], "v_max"); // Wave 6.4: Distributional RL
assert_eq!(names[13], "noisy_sigma_init"); // Wave 6.4: NoisyNet exploration
assert_eq!(names[14], "dueling_hidden_dim"); // Wave 6.4: Dueling architecture
assert_eq!(names[15], "n_steps"); // Wave 6.4: N-step returns
assert_eq!(names[16], "num_atoms"); // Wave 6.4: Distributional atoms
// WAVE 11: use_dueling, use_distributional, use_noisy_nets hardcoded to TRUE (not tunable)
// Note: use_per is always true (fixed), epsilon_decay and tau are not tunable (fixed at defaults for stability)
// WAVE 19: Kelly risk parameters
assert_eq!(names[17], "minimum_profit_factor"); // Bug #7
assert_eq!(names[18], "weight_decay"); // CRITICAL GAP FIX: L2 regularization
assert_eq!(names[19], "kelly_fractional");
assert_eq!(names[20], "kelly_max_fraction");
assert_eq!(names[21], "kelly_min_trades");
assert_eq!(names[22], "volatility_window");
assert_eq!(names[28], "warmup_ratio");
assert_eq!(names[9], "per_alpha");
assert_eq!(names[10], "per_beta_start");
assert_eq!(names[11], "v_min");
assert_eq!(names[12], "v_max");
assert_eq!(names[13], "noisy_sigma_init");
assert_eq!(names[14], "dueling_hidden_dim");
assert_eq!(names[15], "n_steps");
assert_eq!(names[16], "num_atoms");
assert_eq!(names[17], "weight_decay");
assert_eq!(names[18], "kelly_fractional");
assert_eq!(names[19], "kelly_max_fraction");
assert_eq!(names[20], "volatility_window");
assert_eq!(names[21], "curiosity_weight");
assert_eq!(names[22], "tau");
assert_eq!(names[23], "hidden_dim_base");
assert_eq!(names[24], "noisy_epsilon_floor");
}
#[test]
fn test_per_params_always_enabled() {
// Test that PER is always enabled with tunable alpha/beta parameters
// 25D reduced search space
let continuous = vec![
3e-5_f64.ln(), 92.0, 0.9588, 97_273_f64.ln(), 1.404, 5.563, 24.77_f64.ln(), 0.01, 1.0,
0.6, 0.4, // per_alpha, per_beta_start
-2.0, 2.0, 0.5_f64.ln(), // v_min, v_max, noisy_sigma_init (OPTIMIZED - BUG #5 fix)
256.0, 3.0, 101.0, // dueling_hidden_dim, n_steps, num_atoms (Wave 6.4)
1.5, // minimum_profit_factor (mid-point of 1.1-2.0 range, BUG #7)
1e-4_f64.ln(), // weight_decay (L2 regularization, about -9.21)
0.5, 0.25, 20.0, 20.0, // WAVE 19: Kelly risk parameters (kelly_fractional, kelly_max_fraction, kelly_min_trades, volatility_window)
// WAVE 26 P1.4: Ensemble uncertainty parameters
5.0, 0.5, 0.5, 0.1, 1.0, // ensemble_size, beta_variance, beta_disagreement, beta_entropy, variance_cap
// WAVE 26 P1.5: Warmup ratio
0.1, // warmup_ratio (10% warmup)
// WAVE 26 P1.8: Curiosity weight
0.1, // curiosity_weight (>0 required for GPU experience collector)
// WAVE 26 P1.12: Tau (Polyak soft update)
0.005_f64.ln(), // tau (about -5.3)
// WAVE 26 P0: TD Error and Batch Diversity
10.0, 50.0, // td_error_clamp_max, batch_diversity_cooldown
// WAVE 26 P1: Advanced Training Parameters
1.0, 0.1, 0.95, 0.6, 0.3, // lr_decay_type, sharpe_weight, gae_lambda, noisy_sigma_initial, noisy_sigma_final
// WAVE 26 P1: Network Architecture
0.0, 1.0, // norm_type (LayerNorm), activation_type (LeakyReLU)
// QR-DQN parameters
64.0, 1.0_f64.ln(), // num_quantiles=64, qr_kappa=1.0 (ln(1.0)=0.0)
// GPU-dynamic network sizing
512.0, // hidden_dim_base
// Exploration anti-collapse
0.05, // noisy_epsilon_floor
0.1, // count_bonus_coefficient
3e-5_f64.ln(), 92.0, 0.9588, 97_273_f64.ln(), 1.404, 4.0, 24.77_f64.ln(), 0.01, 1.0,
0.6, 0.4, // 9-10: per_alpha, per_beta_start
-2.0, 2.0, 0.5_f64.ln(), // 11-13: v_min, v_max, noisy_sigma_init
256.0, 3.0, 101.0, // 14-16: dueling_hidden_dim, n_steps, num_atoms
1e-4_f64.ln(), // 17: weight_decay
0.5, 0.25, // 18-19: kelly_fractional, kelly_max_fraction
20.0, // 20: volatility_window
0.1, // 21: curiosity_weight
0.005_f64.ln(), // 22: tau
512.0, // 23: hidden_dim_base
0.05, // 24: noisy_epsilon_floor
];
let params = DQNParams::from_continuous(&continuous).unwrap();
assert!(params.use_per); // P0: Always enabled for Rainbow DQN performance
assert!(params.use_per);
assert!((params.per_alpha - 0.6).abs() < 1e-6);
assert!((params.per_beta_start - 0.4).abs() < 1e-6);
// WAVE 11: Check Rainbow booleans are hardcoded
assert!(params.use_dueling);
assert!(params.use_distributional); // BUG #36 FIXED: C51 re-enabled
assert!(params.use_distributional);
assert!(params.use_noisy_nets);
// Test PER parameter bounds (min values)
// Test PER parameter bounds (min values) — 25D
let continuous_min = vec![
2e-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, // per_alpha min, per_beta_start min
-3.0, 1.0, 0.1_f64.ln(), // v_min, v_max, noisy_sigma_init (OPTIMIZED)
128.0, 1.0, 51.0, // dueling_hidden_dim min, n_steps min, num_atoms min (Wave 6.4)
1.1, // minimum_profit_factor min (BUG #7)
1e-5_f64.ln(), // weight_decay min (about -11.51)
0.25, 0.1, 10.0, 10.0, // WAVE 19: Kelly min values
// WAVE 26 P1.4: Ensemble min values
3.0, 0.1, 0.1, 0.05, 0.1, // ensemble min
// WAVE 26 P1.5: Warmup ratio min
0.0, // warmup_ratio min
// WAVE 26 P1.8: Curiosity weight min
0.01, // curiosity_weight min (>0 required for GPU experience collector)
// WAVE 26 P1.12: Tau min
0.0001_f64.ln(), // tau min (about -9.21)
// WAVE 26 P0: TD Error and Batch Diversity min
1.0, 10.0, // td_error_clamp_max min, batch_diversity_cooldown min
// WAVE 26 P1: Advanced Training Parameters min
0.0, 0.0, 0.9, 0.4, 0.2, // lr_decay_type min, sharpe_weight min, gae_lambda min, noisy_sigma_initial min, noisy_sigma_final min
// WAVE 26 P1: Network Architecture min
0.0, 0.0, // norm_type min (LayerNorm), activation_type min (ReLU)
// QR-DQN parameters min
32.0, 0.5_f64.ln(), // num_quantiles min=32, qr_kappa min=0.5
// GPU-dynamic network sizing min
256.0, // hidden_dim_base min
// Exploration anti-collapse min
0.02, // noisy_epsilon_floor min
0.01, // count_bonus_coefficient min
2e-5_f64.ln(), 64.0, 0.95, 50_000_f64.ln(), 1.0, 1.0, 10.0_f64.ln(), 0.0, 0.5,
0.4, 0.2, // per_alpha min, per_beta_start min
-3.0, 1.0, 0.1_f64.ln(), // v_min, v_max, noisy_sigma_init
128.0, 1.0, 51.0, // dueling_hidden_dim, n_steps, num_atoms
1e-5_f64.ln(), // weight_decay min
0.25, 0.1, // kelly_fractional, kelly_max_fraction
10.0, // volatility_window
0.01, // curiosity_weight min
0.0001_f64.ln(), // tau min
256.0, // hidden_dim_base min
0.02, // noisy_epsilon_floor min
];
let params_min = DQNParams::from_continuous(&continuous_min).unwrap();
assert!((params_min.per_alpha - 0.4).abs() < 1e-6);
assert!((params_min.per_beta_start - 0.2).abs() < 1e-6);
assert!(params_min.use_dueling);
assert!(params_min.use_distributional); // BUG #36 FIXED
assert!(params_min.use_distributional);
assert!(params_min.use_noisy_nets);
// Test PER parameter bounds (max values) — 25D
let continuous_max = vec![
8e-5_f64.ln(), 4096.0, 0.99, 100_000_f64.ln(), 2.0, 8.0, 40.0_f64.ln(), 0.1, 2.0,
0.8, 0.6, // per_alpha max, per_beta_start max
-1.0, 3.0, 1.0_f64.ln(), // v_min, v_max, noisy_sigma_init (OPTIMIZED)
512.0, 5.0, 201.0, // dueling_hidden_dim max, n_steps max, num_atoms max (Wave 6.4)
2.0, // minimum_profit_factor max (BUG #7)
1e-3_f64.ln(), // weight_decay max (about -6.91)
1.0, 0.5, 50.0, 30.0, // WAVE 19: Kelly max values
// WAVE 26 P1.4: Ensemble max values
10.0, 1.0, 1.0, 0.5, 2.0, // ensemble max
// WAVE 26 P1.5: Warmup ratio max
0.2, // warmup_ratio max (20% warmup)
// WAVE 26 P1.8: Curiosity weight max
0.5, // curiosity_weight max
// WAVE 26 P1.12: Tau max
0.01_f64.ln(), // tau max (about -4.61)
// WAVE 26 P0: TD Error and Batch Diversity max
100.0, 100.0, // td_error_clamp_max max, batch_diversity_cooldown max
// WAVE 26 P1: Advanced Training Parameters max
2.0, 0.5, 0.99, 0.8, 0.5, // lr_decay_type max, sharpe_weight max, gae_lambda max, noisy_sigma_initial max, noisy_sigma_final max
// WAVE 26 P1: Network Architecture max
2.0, 3.0, // norm_type max (None), activation_type max (Mish)
// QR-DQN parameters max
200.0, 2.0_f64.ln(), // num_quantiles max=200, qr_kappa max=2.0
// GPU-dynamic network sizing max
4096.0, // hidden_dim_base max
// Exploration anti-collapse max
0.10, // noisy_epsilon_floor max
0.5, // count_bonus_coefficient max
8e-5_f64.ln(), 4096.0, 0.99, 100_000_f64.ln(), 2.0, 4.0, 40.0_f64.ln(), 0.1, 2.0,
0.8, 0.6, // per_alpha max, per_beta_start max
-1.0, 3.0, 1.0_f64.ln(), // v_min, v_max, noisy_sigma_init
512.0, 5.0, 201.0, // dueling_hidden_dim, n_steps, num_atoms
1e-3_f64.ln(), // weight_decay max
1.0, 0.5, // kelly_fractional, kelly_max_fraction
30.0, // volatility_window
0.5, // curiosity_weight max
0.01_f64.ln(), // tau max
4096.0, // hidden_dim_base max
0.10, // noisy_epsilon_floor max
];
let params_max = DQNParams::from_continuous(&continuous_max).unwrap();
assert!((params_max.per_alpha - 0.8).abs() < 1e-6);
assert!((params_max.per_beta_start - 0.6).abs() < 1e-6);
// WAVE 11: Check Rainbow booleans are hardcoded
assert!(params_max.use_dueling);
assert!(params_max.use_distributional); // BUG #36 FIXED: C51 re-enabled
assert!(params_max.use_distributional);
assert!(params_max.use_noisy_nets);
}
@@ -3994,10 +3830,11 @@ 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 (added noisy_epsilon_floor, count_bonus_coefficient)");
assert_eq!(continuous.len(), 25, "Should have 25 continuous dimensions (reduced from 45D)");
let roundtrip = DQNParams::from_continuous(&continuous).unwrap();
assert_eq!(roundtrip.num_quantiles, params.num_quantiles);
assert!((roundtrip.qr_kappa - params.qr_kappa).abs() < 0.01);
// num_quantiles and qr_kappa are now fixed defaults (not in search space)
assert_eq!(roundtrip.num_quantiles, 64); // Fixed default
assert!((roundtrip.qr_kappa - 1.0).abs() < 0.01); // Fixed default
}
#[test]
@@ -4017,22 +3854,22 @@ mod tests {
#[test]
fn test_batch_size_respects_wide_bounds() {
// Simulate PSO choosing batch_size=2048 (within VRAM-aware bounds)
let mut params = vec![0.0_f64; 45];
let mut params = vec![0.0_f64; 25];
params[1] = 2048.0; // batch_size (index 1)
// Fill other required params with valid defaults
params[0] = (1e-4_f64).ln(); // learning_rate
params[2] = 0.99; // gamma
params[3] = (100_000.0_f64).ln(); // buffer_size
params[4] = 1.5; // hold_penalty_weight
params[5] = 6.0; // max_position_absolute
params[5] = 3.0; // max_position_absolute
params[6] = (25.0_f64).ln(); // huber_delta
params[7] = 0.01; // entropy_coefficient
params[8] = 0.5; // transaction_cost_multiplier
params[9] = 0.6; // per_alpha
params[10] = 0.4; // per_beta_start
// Rainbow extensions (indices 11-44) -- use midpoint of bounds
// Remaining params (indices 11-24) -- use midpoint of bounds
let bounds = DQNParams::continuous_bounds();
for i in 11..45 {
for i in 11..25 {
params[i] = (bounds[i].0 + bounds[i].1) / 2.0;
}
let result = DQNParams::from_continuous(&params).unwrap();