diff --git a/crates/ml/src/hyperopt/adapters/dqn.rs b/crates/ml/src/hyperopt/adapters/dqn.rs index d993b4590..7d938fb68 100644 --- a/crates/ml/src/hyperopt/adapters/dqn.rs +++ b/crates/ml/src/hyperopt/adapters/dqn.rs @@ -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 { - 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 { + // 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(¶ms).unwrap();