#![allow(unsafe_code)] // Required for f32<->u32 CudaSlice reinterpret casts and safetensors serialization
//! DQN Configuration and Hyperparameters
//!
//! Contains all training configuration including learning rates,
//! batch sizes, exploration parameters, and risk management settings.

use std::sync::Arc;

use cudarc::driver::CudaSlice;
use ml_core::device::MlDevice;
use ml_core::cuda_autograd::GpuTensor;

use crate::dqn::action_space::FactoredAction;
use crate::dqn::dqn::DQN;
use crate::MLError;

/// DQN agent wrapper around `DQN`.
///
/// Provides the unified API surface that the training loop, fused CUDA trainer,
/// and checkpoint code call. Phase 4 of the MoE redesign collapsed the 3-head
/// `RegimeConditionalDQN` wrapper down to a single `DQN` since the regime
/// conditioning infrastructure was vestigial (only `trending_head` ever trained).
/// MoE in the underlying `DQN` provides the regime-conditioned behavior via a
/// learned gate (Phase 3).
pub struct DQNAgentType {
    pub(crate) agent: DQN,
}

impl DQNAgentType {
    pub fn new(agent: DQN) -> Self {
        Self { agent }
    }
}

impl std::fmt::Debug for DQNAgentType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DQNAgentType").finish_non_exhaustive()
    }
}

impl DQNAgentType {
    /// Select action using the DQN agent.
    pub fn select_action(&mut self, state: &[f32]) -> Result<FactoredAction, MLError> {
        self.agent.select_action(state)
    }

    /// Batch greedy action selection — single forward pass.
    ///
    /// Returns `Vec<u32>` action indices (cold path, CPU readback from GPU argmax).
    pub fn batch_greedy_actions(&self, states: &GpuTensor) -> Result<Vec<u32>, MLError> {
        let tensor = self.agent.batch_greedy_actions(states)?;
        let host = tensor.to_host(self.agent.cuda_stream())
            .map_err(|e| MLError::ModelError(format!("batch_greedy_actions DtoH: {e}")))?;
        Ok(host.iter().map(|&v| v as u32).collect())
    }

    /// Batch Q-value computation — returns raw `CudaSlice<f32>` of shape `[batch, num_actions]`.
    ///
    /// Used by `GpuBacktestEvaluator` for GPU-side argmax.
    pub fn batch_q_values(&self, states: &GpuTensor) -> Result<CudaSlice<f32>, MLError> {
        self.agent.q_values_for_batch(states).map(|t| t.into_parts().0)
    }

    /// Per-branch Q-values for branching DQN.
    ///
    /// Returns `Some((exposure, order, urgency))` as raw `CudaSlice<f32>` buffers when
    /// branching is active, `None` otherwise. Used by the GPU action selector's
    /// `select_actions_branching()` for per-branch epsilon-greedy exploration.
    pub fn batch_branching_q_values(
        &self,
        states: &GpuTensor,
    ) -> Result<Option<(CudaSlice<f32>, CudaSlice<f32>, CudaSlice<f32>)>, MLError> {
        self.agent.batch_branching_q_values(states)
    }

    /// Batch softmax (Boltzmann) action selection.
    ///
    /// Returns `GpuTensor` of action indices (GPU-resident, shape `[batch]`).
    pub fn batch_softmax_actions(
        &self,
        states: &GpuTensor,
        temperature: f64,
    ) -> Result<GpuTensor, MLError> {
        self.agent.batch_softmax_actions(states, temperature)
    }

    /// Hierarchical factored softmax — exposure-first, then order/urgency within.
    ///
    /// Returns `GpuTensor` of action indices (GPU-resident, shape `[batch]`).
    pub fn batch_hierarchical_softmax_actions(
        &self,
        states: &GpuTensor,
        temperature: f64,
    ) -> Result<GpuTensor, MLError> {
        self.agent.batch_hierarchical_softmax_actions(states, temperature)
    }

    /// NoisyNets σ mean for a specific action branch
    /// (0 = direction, 1 = magnitude, 2 = order, 3 = urgency).
    /// Returns 0.0 if branching network not initialized. H7 detection signal for HEALTH_DIAG.
    pub fn branch_noisy_sigma_mean(&self, branch_idx: usize) -> f32 {
        self.agent.branch_noisy_sigma_mean(branch_idx)
    }

    /// Set noise sigma scale for annealed noisy exploration.
    pub fn set_noise_sigma_scale(&mut self, scale: f64) {
        self.agent.set_noise_sigma_scale(scale);
    }

    /// Reset count-based exploration bonus (call at epoch boundary).
    pub fn reset_count_bonus(&mut self) {
        self.agent.reset_count_bonus();
    }

    /// Reset DQN state that should not persist across walk-forward folds.
    /// Delegates to `DQN::reset_for_fold` (zeros `gradient_collapse_counter`
    /// and `training_steps`). Pairs with `DQNTrainer::reset_for_fold`.
    pub fn reset_for_fold(&mut self) {
        self.agent.reset_for_fold();
    }

    /// Get count bonuses for all actions (UCB exploration).
    /// Returns a Vec<f32> of length `num_actions` with exploration bonuses.
    pub fn get_count_bonuses(&self) -> Vec<f32> {
        self.agent.get_count_bonuses()
    }

    /// Get per-branch count bonuses for branching DQN (direction [4], order [3], urgency [3]).
    ///
    /// Delegates directly to `DQN::get_count_bonuses_branched`. UCB count bonuses now
    /// reach the GPU action selector — the ghost feature from the Phase 0 deferral is
    /// fixed in this commit.
    pub fn get_count_bonuses_branched(&self) -> ([f32; 4], [f32; 3], [f32; 3]) {
        self.agent.get_count_bonuses_branched()
    }

    /// Number of discrete actions in the action space.
    pub fn num_actions(&self) -> usize {
        self.agent.config.num_actions
    }

    /// Branch sizes for branching DQN: `(direction, magnitude, order_type, urgency)`.
    ///
    /// Returns the actual branch sizes from the DQN config.
    pub fn branch_sizes(&self) -> (usize, usize, usize, usize) {
        let cfg = &self.agent.config;
        (cfg.num_actions, 3, cfg.num_order_types, cfg.num_urgency_levels)
    }

    /// Update epsilon for exploration decay.
    pub fn update_epsilon(&mut self) {
        self.agent.update_epsilon();
    }

    /// Get current epsilon value.
    pub fn get_epsilon(&self) -> f32 {
        self.agent.get_epsilon()
    }

    /// Get the effective epsilon for exploration, respecting noisy_epsilon_floor.
    /// The batch training path must use this instead of get_epsilon() to avoid
    /// zero exploration when noisy nets are enabled.
    pub fn get_effective_epsilon(&self) -> f32 {
        self.agent.get_effective_epsilon()
    }

    /// Set epsilon value for exploration.
    pub fn set_epsilon(&mut self, epsilon: f64) {
        self.agent.set_epsilon(epsilon);
    }

    /// Get regime-specific metrics.
    ///
    /// Always returns `None` — regime metrics were part of the deleted
    /// `RegimeConditionalDQN`. MoE gate utilization is visible via HEALTH_DIAG
    /// `aux_moe` line instead.
    pub fn get_regime_metrics(&self) -> Option<&std::collections::HashMap<crate::dqn::RegimeType, crate::validation::RegimeMetrics>> {
        None
    }

    /// Get replay buffer size.
    pub fn get_replay_buffer_size(&self) -> Result<usize, MLError> {
        self.agent.get_replay_buffer_size()
    }

    /// Forward pass through Q-network (branching path).
    ///
    /// Returns raw `CudaSlice<f32>` of shape `[batch, num_actions]`.
    pub fn forward(&self, state: &GpuTensor) -> Result<CudaSlice<f32>, MLError> {
        self.agent.q_values_for_batch(state).map(|t| t.into_parts().0)
    }

    /// Track action for diversity monitoring.
    pub fn track_action(&mut self, action: FactoredAction) {
        self.agent.track_action(action);
    }

    /// Track a batch of actions for diversity monitoring.
    pub fn track_actions_batch(&mut self, actions: &[FactoredAction]) {
        self.agent.track_actions_batch(actions);
    }

    // Legacy insert_batch_tensors DELETED — use insert_batch directly
    // from the training loop with f32 CudaSlice states (Task #30).

    /// Check if agent can train (has enough replay buffer samples).
    pub fn can_train(&self) -> bool {
        self.agent.can_train()
    }

    /// Get reference to replay buffer memory.
    pub fn memory(&self) -> &crate::dqn::replay_buffer_type::StagedGpuBuffer {
        &self.agent.memory
    }

    /// Get mutable reference to replay buffer memory (for sampling).
    pub fn memory_mut(&mut self) -> &mut crate::dqn::replay_buffer_type::StagedGpuBuffer {
        &mut self.agent.memory
    }

    /// Log diagnostics and check for gradient collapse (early stopping).
    /// Returns Err if gradient collapse detected for consecutive epochs.
    pub fn log_diagnostics(&mut self, grad_norm: f32) -> Result<(), MLError> {
        self.agent.log_diagnostics(grad_norm)
    }

    /// Gradient collapse check only — no dead neuron detection (zero GPU→CPU sync).
    /// Use in CUDA guard hot path; dead neuron detection runs at epoch boundary.
    pub fn check_gradient_collapse(&mut self, grad_norm: f32) -> Result<(), MLError> {
        self.agent.check_gradient_collapse(grad_norm)
    }

    /// Suppress or enable forward() monitoring (GPU→CPU sync avoidance in hot path).
    pub fn set_training_forward_active(&mut self, active: bool) {
        self.agent.set_training_forward_active(active);
    }

    /// Log Q-values and check for Q-value divergence (early stopping).
    /// Returns Err if Q-value divergence detected for consecutive checks.
    pub fn log_q_values(&mut self, states_tensor: &GpuTensor) -> Result<(), MLError> {
        self.agent.log_q_values(states_tensor)
    }

    /// Log Q-values from pre-computed GPU statistics (zero tensor readback).
    /// Returns Err if Q-value divergence detected for consecutive checks.
    pub fn log_q_values_from_stats(
        &mut self,
        q_min: f32,
        q_max: f32,
        q_mean: f32,
        q_variance: f32,
        n_actions: usize,
    ) -> Result<(), MLError> {
        self.agent.log_q_values_from_stats(q_min, q_max, q_mean, q_variance, n_actions)
    }

    /// Get CUDA device.
    pub fn device(&self) -> MlDevice {
        MlDevice::Cuda {
            context: self.agent.cuda_stream().context().clone(),
            stream: Arc::clone(self.agent.cuda_stream()),
        }
    }

    /// Build safetensors checkpoint metadata from the agent's config.
    pub fn checkpoint_metadata(&self) -> std::collections::HashMap<String, String> {
        self.agent.config.checkpoint_metadata()
    }

    /// Get immutable reference to the underlying DQN.
    pub fn primary_dqn(&self) -> &DQN {
        &self.agent
    }

    /// Get mutable reference to the underlying DQN.
    pub fn primary_dqn_mut(&mut self) -> &mut DQN {
        &mut self.agent
    }

    /// Get state dimension from agent configuration.
    ///
    /// Returns the actual state dimension from the canonical STATE_DIM constant.
    pub fn get_state_dim(&self) -> usize {
        self.agent.get_state_dim()
    }

    /// Clear replay buffer.
    pub fn clear_replay_buffer(&mut self) -> Result<(), MLError> {
        self.agent.clear_replay_buffer()
    }

    /// Reset target network to match main Q-network.
    pub fn reset_target_network(&mut self) -> Result<(), MLError> {
        self.agent.reset_target_network()
    }

    /// Update replay buffer priorities from GPU-resident tensors.
    pub fn update_priorities_gpu(
        &mut self,
        indices: &cudarc::driver::CudaSlice<u32>,
        td_errors: &cudarc::driver::CudaSlice<f32>,
        bs: usize,
    ) -> Result<(), MLError> {
        self.primary_dqn_mut().memory.update_priorities_gpu(indices, td_errors, bs)
    }

    /// Step the replay buffer training counter for beta annealing.
    pub fn step_replay_buffer(&mut self) {
        self.primary_dqn_mut().memory.step();
    }

    /// Flush GPU-accumulated max priority to CPU (single scalar readback per epoch).
    pub fn flush_max_priority(&mut self) -> Result<(), crate::MLError> {
        self.primary_dqn_mut().memory.flush_max_priority()
    }

    /// GPU-only post-step bookkeeping: training_steps++ and PER beta annealing.
    pub fn fused_post_step_gpu_bookkeeping(&mut self) -> Result<(), crate::MLError> {
        let dqn = self.primary_dqn_mut();
        dqn.increment_training_steps();
        dqn.memory.step();
        Ok(())
    }

    /// PER alpha/epsilon for GPU-native priority update kernel.
    pub fn per_alpha_epsilon(&self) -> Option<(f32, f32)> {
        Some(self.memory().per_alpha_epsilon())
    }

    /// Update PER priorities from GPU-resident TD errors.
    ///
    /// Routes through the replay buffer's seg_tree_update kernel which
    /// correctly propagates the segment tree from leaf to root.
    pub fn update_priorities_from_td(
        &mut self,
        td_errors: &cudarc::driver::CudaSlice<f32>,
        batch_size: usize,
        stream: &std::sync::Arc<cudarc::driver::CudaStream>,
    ) -> Result<(), crate::MLError> {
        self.primary_dqn_mut().memory.update_priorities_from_td(td_errors, batch_size, stream)
    }

    /// Set max_priority from a CPU scalar (epoch-boundary flush from CUDA kernel).
    pub fn apply_max_priority_scalar(&mut self, max_prio: f32) -> Result<(), crate::MLError> {
        self.primary_dqn_mut().memory.apply_max_priority_scalar(max_prio)
    }

    /// Replay buffer write cursor (for IQL staleness computation).
    pub fn replay_write_cursor(&self) -> usize {
        self.primary_dqn().memory.write_cursor()
    }

    /// Replay buffer capacity.
    pub fn replay_capacity(&self) -> usize {
        self.primary_dqn().memory.capacity()
    }

    /// Update learning rate for the optimizer by applying a decay factor.
    pub fn update_learning_rate(&mut self, decay_factor: f64) -> Result<(), crate::MLError> {
        self.agent.update_learning_rate(decay_factor)
    }

    /// Whether the active agent architecture uses Branching DQN.
    ///
    /// When `true`, weight sets are extracted via `weight_sets_from_branching()`
    /// which provides zero-copy pointer views into the branching network's live weights.
    pub fn is_using_branching(&self) -> bool {
        self.agent.branching_q_network.is_some()
    }

    /// Network dimensions for pure-CUDA forward kernel: `(shared_h1, shared_h2, value_h, adv_h)`.
    ///
    /// These are injected as `#define` constants into the NVRTC-compiled forward kernel.
    pub fn network_dims(&self) -> (usize, usize, usize, usize) {
        let h = &self.agent.config.hidden_dims;
        let shared_h1 = h.first().copied().unwrap_or(256);
        let shared_h2 = h.get(1).copied().unwrap_or(shared_h1);
        let value_h = self.agent.config.dueling_hidden_dim;
        let adv_h = self.agent.config.branch_hidden_dim;
        (shared_h1, shared_h2, value_h, adv_h)
    }
}

/// DQN training hyperparameters from gRPC request
#[derive(Debug, Clone)]
pub struct DQNHyperparameters {
    /// Learning rate (typically 1e-4 to 1e-3).
    ///
    /// Kept as f64 for numerical precision of very small LRs (1e-5..1e-8) and for
    /// hyperopt bound arithmetic. Cast to f32 at the Adam/optimizer boundary only.
    pub learning_rate: f64,
    /// Batch size (dynamically capped by AutoBatchSizer based on GPU VRAM)
    pub batch_size: usize,
    /// Discount factor (typically 0.95-0.99)
    pub gamma: f32,
    /// Initial exploration rate
    pub epsilon_start: f32,
    /// Final exploration rate
    pub epsilon_end: f32,
    /// Exploration decay rate
    pub epsilon_decay: f32,
    /// Per-branch epsilon multiplier: direction (relative to base epsilon). Default 1.0.
    pub epsilon_direction_mult: f32,
    /// Per-branch epsilon multiplier: magnitude (relative to base epsilon). Default 2.0 — needs more exploration.
    pub epsilon_magnitude_mult: f32,
    /// Per-branch epsilon multiplier: order type (relative to base epsilon). Default 1.5.
    pub epsilon_order_mult: f32,
    /// Per-branch epsilon multiplier: urgency (relative to base epsilon). Default 1.5.
    pub epsilon_urgency_mult: f32,
    /// Replay buffer capacity
    pub buffer_size: usize,
    /// Fraction of free VRAM to allocate for replay buffer (0.0 = use static buffer_size).
    /// When > 0.0, overrides buffer_size with VRAM-optimal capacity.
    /// Typical: 0.70 (70% of free VRAM after model load).
    pub replay_buffer_vram_fraction: f32,
    /// Minimum replay buffer size before training starts
    pub min_replay_size: usize,
    /// Number of training epochs
    pub epochs: usize,
    /// Checkpoint save frequency (epochs)
    pub checkpoint_frequency: usize,
    /// Enable early stopping based on convergence criteria
    pub early_stopping_enabled: bool,
    /// Minimum Q-value threshold before stopping (default: 0.5).
    /// f64: early-stopping plateau stat, not a kernel-facing scalar.
    pub q_value_floor: f64,
    /// Minimum loss improvement percentage over window (default: 2.0%).
    /// f64: ratio of long-horizon loss accumulators, keep precision.
    pub min_loss_improvement_pct: f64,
    /// Window size for plateau detection (default: 30 epochs)
    pub plateau_window: usize,
    /// Minimum epochs before early stopping can trigger (default: 50)
    pub min_epochs_before_stopping: usize,
    /// Deprecated: replaced by `w_idle` in composite reward. Kept for hyperopt adapter compat.
    pub hold_penalty: f32,

    // GPU composite reward weights (6-component composite in CUDA kernel)
    /// Normalized PnL weight for composite reward
    pub w_pnl: f32,
    /// Drawdown penalty weight for composite reward
    pub w_dd: f32,
    /// Idle penalty weight for composite reward
    pub w_idle: f32,
    /// Drawdown tolerance before penalty kicks in (fraction, e.g. 0.02 = 2%)
    pub dd_threshold: f32,
    /// Asymmetric loss scaling factor (prospect theory, default 1.5)
    pub loss_aversion: f32,

    /// Reward v7: Counterfactual Exposure Advantage weight.
    /// Blends per-branch advantage signal into the scalar reward.
    /// 0.0 = disabled, 1.0 = full advantage signal.
    pub cea_weight: f32,
    // exposure_aux_weight and exposure_aux_warmup_epochs removed (4-branch refactor)
    /// Position staleness rent per step held
    pub time_decay_rate: f32,
    /// Q-value gap threshold for trade conviction filter (FRACTION of Q-range).
    /// When greedy Q(best_dir) - Q(flat) < threshold × Q_range(direction), default to flat.
    /// Auto-scales with v_min/v_max: 0.05 = "need ≥5% of Q-spread conviction to trade."
    /// 0.0 = disabled. Typical range [0.0, 0.5].
    pub q_gap_threshold: f32,
    /// v8: Dense directional micro-reward scale (R5 reward term).
    /// Parameter, NOT a feature flag. Controls the tanh-bounded per-bar
    /// shaping reward built from OFI momentum, price confirmation, and
    /// retrospective hold quality (see experience_kernels.cu `R5: Dense
    /// micro-reward` branch). Production profile uses 0.1; smoke profile
    /// uses 0.0 to isolate the sparse-reward TD path for reproducibility.
    /// Phase 2 Task 2.3 FIX-documented-disable — per
    /// feedback_no_functionality_removal.md the kernel branch and buffer
    /// infrastructure are preserved even when the scale is 0 in a given profile.
    pub micro_reward_scale: f32,
    /// Dense micro-reward: price confirmation weight (default 0.5).
    pub price_confirm_weight: f32,
    /// Dense micro-reward: book aggression weight (default 0.3).
    pub book_aggression_weight: f32,
    /// Dense micro-reward: retrospective hold quality weight (default 0.2).
    pub hold_quality_weight: f32,
    /// Dense micro-reward: tanh temperature (default 3.0).
    pub micro_reward_temp: f32,
    /// v8: TD(λ) trace parameter. 0=1-step, 1=Monte Carlo.
    pub td_lambda: f32,
    /// v8: Maximum trace length for truncated TD(λ).
    pub max_trace_length: usize,
    /// v8: PopArt reward normalization enabled.
    pub popart_enabled: bool,
    /// v8: Robust PopArt using median/IQR instead of Welford mean/var.
    /// More robust for heavy-tailed bimodal reward distributions.
    pub popart_robust: bool,
    /// v8: Curriculum learning enabled.
    pub curriculum_enabled: bool,
    /// v8: Fraction of training before full dataset (curriculum).
    pub curriculum_warmup_fraction: f32,
    /// v8: Fraction of replay batch relabeled with hindsight optimal exit.
    pub hindsight_fraction: f32,
    /// v8: Lookahead bars for hindsight optimal exit.
    pub hindsight_lookahead: usize,
    /// Huber loss delta threshold (default: 1.0)
    pub huber_delta: f32,
    /// Gradient clipping max norm (None = disabled).
    /// f64: aggregated norm is an f64 accumulator by convention; keep precision at the
    /// norm-computation boundary.
    pub gradient_clip_norm: Option<f64>,
    /// Deprecated: was HOLD action penalty weight, replaced by `w_idle` in GPU composite reward.
    /// Kept for backward compatibility with hyperopt adapters. Not used in reward computation.
    pub hold_penalty_weight: f32,
    /// Price movement threshold for HOLD penalty (as fraction, e.g., 0.02 = 2%)
    pub movement_threshold: f32,
    /// Preprocessing window size (default: 50)
    pub preprocessing_window: i64,
    /// Preprocessing clip sigma (default: 5.0)
    pub preprocessing_clip_sigma: f32,

    // WAVE 16 (Agent 36): Target update configuration
    /// Polyak averaging coefficient for soft target updates (default: 0.001)
    /// Rainbow DQN standard: τ=0.001 gives 693-step convergence half-life
    pub tau: f32,
    /// Target update mode: Soft (Polyak averaging) or Hard (periodic full copy)
    pub target_update_mode: crate::trainers::TargetUpdateMode,
    /// Target network hard update frequency in training steps (default: 10000)
    /// Used when target_update_mode = Hard. Rainbow DQN: 32K frames, Stable Baselines3: 10K steps
    pub target_update_frequency: usize,

    // Rainbow DQN warmup period
    /// Warmup steps for random exploration (Rainbow DQN standard: 80K for 50M+ steps)
    /// For short training (<200K steps), warmup=0 is recommended.
    /// Adaptive CLI defaults: 0 (<200K), 5% (200K-500K), 8% (500K-1M), 80K (>1M)
    pub warmup_steps: usize,

    // P2-A Enhancement: Initial capital for portfolio
    /// Initial capital for portfolio trading (default: $100,000)
    /// Minimum: $1,000 (validated at CLI layer)
    pub initial_capital: f32,

    /// Bars per trading day at current data resolution.
    /// 390 = 1-minute (6.5h × 60min), 78 = 5-minute, 1 = daily.
    /// Used for Sharpe/Sortino annualization and return scaling.
    /// Derived from data pipeline's BarSize when available.
    pub bars_per_day: f32,

    /// Holding cost rate per bar per unit position. Continuous inventory penalty.
    /// Default: 0.00000052 (~0.52 bps / 100 bars)
    pub holding_cost_rate: f32,
    /// Bars threshold for churn penalty. Trades within this window get graduated penalty.
    pub churn_threshold_bars: usize,
    /// Scale factor for churn penalty.
    pub churn_penalty_scale: f32,

    /// Auxiliary ops (IQL, IQN, attention, CQL) run every Nth training step.
    /// Default: 4 (aux ops every 4th step, ~4× faster epochs).
    /// 1 = every step (full quality), higher = faster but less frequent aux updates.
    pub aux_frequency: usize,

    // P2-B Enhancement: Cash reserve requirement
    /// Cash reserve requirement as percentage of portfolio value (0-100)
    /// Default: 0.0 (no reserve, backward compatible)
    pub cash_reserve_percent: f32,

    // WAVE 16S: Adaptive Risk Management Features
    /// Kelly fractional multiplier (0.5 = half-Kelly, conservative)
    pub kelly_fractional: f32,
    /// Maximum Kelly fraction (cap at 0.25 = 25% of portfolio)
    pub kelly_max_fraction: f32,
    /// Minimum trades required for Kelly calculation
    pub kelly_min_trades: usize,
    /// Volatility rolling window size
    pub volatility_window: usize,

    // WAVE 35: Advanced Features - Regime-Conditional Q-Networks + Compliance Engine
    // (regime Q-network, compliance, drawdown monitoring, position limits,
    //  circuit breaker, and domain randomization are always active)

    /// Periodic shrink-and-perturb interval (0=disabled, 20=every 20 epochs)
    pub shrink_perturb_interval: usize,
    /// Shrink-and-perturb alpha (0.85 = keep 85%, reinit 15%)
    pub shrink_perturb_alpha: f32,
    /// Shrink-and-perturb noise scale (Xavier)
    pub shrink_perturb_sigma: f32,

    /// Adversarial regime injection: MaxDD threshold below which we suspect overfitting.
    /// If MaxDD stays below this for `adversarial_epochs_trigger` consecutive epochs,
    /// inject harsh market conditions to stress-test the policy.
    pub adversarial_dd_threshold: f32,
    /// Consecutive low-drawdown epochs before adversarial injection activates
    pub adversarial_epochs_trigger: usize,

    /// Anti-correlation penalty: how much to penalize market-beta in rewards.
    /// At epoch boundary, Pearson correlation between model returns and market returns
    /// is computed. If |corr| > 0.3, next epoch reward_scale *= (1 - penalty * |corr|).
    /// Forces alpha generation over beta riding. 0.0 = disabled. Typical: 0.3-0.5.
    pub beta_penalty_strength: f32,

    // ── Gems & Pearls: generalization techniques ────────────────────

    /// #24 Anti-intuitive LR: when epoch Sharpe > threshold, INCREASE LR to
    /// kick the model out of overfit minima. When Sharpe < -threshold, decrease
    /// to stabilize. Opposite of standard practice. Always active.
    /// LR multiplier when epoch Sharpe is "good" (> threshold)
    pub anti_lr_good_mult: f32,
    /// LR multiplier when epoch Sharpe is "bad" (< -threshold)
    pub anti_lr_bad_mult: f32,
    /// Sharpe threshold for anti-intuitive LR switching
    pub anti_lr_sharpe_threshold: f32,

    /// #27 Ensemble disagreement Q-penalty weight. When ensemble heads disagree
    /// (high std across Q-values), subtract weighted std from Q-target.
    /// Makes model conservative on novel/uncertain states. 0.0 = disabled.
    pub ensemble_disagreement_penalty: f32,

    /// #34 Causal intervention: compute per-feature causal sensitivity by running
    /// intervened forward passes. Always active.
    /// #34 Weight for causal regularization loss. 0.0 = disabled.
    pub causal_weight: f32,
    /// #34 How often to run causal intervention (every N training steps).
    /// Higher = less overhead. 10 = every 10th step.
    pub causal_intervention_interval: usize,

    /// #33 Adversarial self-play: always active.
    /// #33 Warmup epochs before self-play begins (let trader learn basics first)
    pub adversarial_warmup_epochs: usize,
    /// #33 How often to update the saboteur's reference checkpoint (epochs)
    pub adversarial_checkpoint_interval: usize,

    /// #23 Causal feature masking: each epoch, randomly zero out this fraction
    /// of features. Forces redundant representations (random forest principle).
    /// 0.0 = disabled, 0.4 = mask 40% of features each epoch.
    pub feature_mask_fraction: f32,

    /// #22 Feature-wise noise injection: add Gaussian noise with sigma =
    /// noise_scale * std(feature). Strictly better than dropout for continuous
    /// features. 0.0 = disabled.
    pub feature_noise_scale: f32,

    // #13 Volatility regime normalization: always active.
    // #10 Mirror universe: always active.

    /// #11 Time-reversal modulus: episodes where index % mod == 0 are played backwards.
    /// 5 = 20% reversed, 0 = disabled.
    pub time_reversal_mod: usize,

    /// #17 Counterfactual regret blend: 0.0 = pure PnL reward, 1.0 = pure regret.
    /// Regret = reward(taken) - max(reward(all_exposures)). Normalizes across regimes.
    pub regret_blend: f32,

    /// #25 Trade clustering penalty: penalize temporally clustered trades.
    /// CV(inter-trade intervals) * this weight is subtracted from reward.
    /// 0.0 = disabled.
    pub trade_clustering_penalty: f32,

    /// #20 Epoch at which to compute lottery ticket pruning mask.
    pub pruning_epoch: usize,
    /// #20 Fraction of weights to prune (0.7 = remove 70% smallest).
    pub pruning_fraction: f32,

    // #32 Gradient vaccine: always active.

    /// #31 Temporal causal bottleneck dimension. 0 = disabled, 2 = maximum compression.
    /// All market features are compressed through [market_dim → bottleneck_dim] linear + tanh
    /// before reaching the Q-network. Makes memorization structurally impossible.
    pub bottleneck_dim: usize,

    /// #21 Stochastic depth: drop probability per hidden layer during training.
    /// Each layer is independently dropped with this probability. Surviving layers
    /// are scaled by 1/(1-p) for expected-value correction. 0.0 = disabled.
    pub stochastic_depth_prob: f32,

    /// Manifold Mixup alpha for Beta distribution.
    /// Mixes pairs of C51 target distributions during training.
    /// 0.0 = disabled. Range: [0.0, 2.0]. Default: 0.2.
    pub mixup_alpha: f32,

    // ── Core hyperopt families (1.0 = defaults, range [0.0, 2.0]) ──

    /// Learning family: scales learning_rate, tau, weight_decay.
    pub learning_intensity: f64,
    /// Exploration family: scales entropy_coefficient, noisy_sigma_init.
    pub exploration_intensity: f64,
    /// Replay family: scales buffer_size, per_alpha, per_beta_start, n_steps.
    pub replay_intensity: f64,
    /// Architecture family: scales hidden_dim_base, dueling_hidden_dim, num_atoms.
    pub architecture_intensity: f64,
    /// Risk family: scales max_position, huber_delta, min_profit_factor, w_dd, dd_threshold.
    pub risk_intensity: f64,

    // ── Generalization family intensity scalars for hyperopt (1.0 = defaults, range [0.0, 2.0]) ──

    /// Adversarial family intensity: scales saboteur perturbation, warmup, interval.
    pub adversarial_intensity: f64,
    /// Regularization family intensity: scales dropout, spectral norm, stochastic depth, spectral decoupling.
    pub regularization_intensity: f64,
    /// Data augmentation family intensity: scales feature masking, feature noise.
    pub augmentation_intensity: f64,
    /// Loss shaping family intensity: scales asymmetric DD, regret blend, trade clustering, position entropy.
    pub loss_shaping_intensity: f64,
    /// Ensemble family intensity: scales ensemble disagreement penalty.
    pub ensemble_intensity: f64,
    /// Causal family intensity: scales causal weight, intervention interval.
    pub causal_intensity: f64,

    /// #18 Asymmetric drawdown loss: extra penalty on Q-overestimation when
    /// portfolio is in drawdown. Weight for L_dd = max(0, Q_pred-Q_target) * dd^2.
    /// 0.0 = disabled.
    pub asymmetric_dd_weight: f32,

    // Wave 16 Portfolio Features (action masking, entropy regularization, stress testing always active)
    /// Maximum absolute position size for action masking (contracts).
    /// DERIVED: computed from `max_leverage`, `initial_capital`, `contract_multiplier`,
    /// and a reference price. Do not set directly in config — use `max_leverage` instead.
    /// Fallback default: 2.0 (matches legacy production behavior).
    pub max_position_absolute: f32,
    /// Maximum leverage ratio relative to initial capital.
    /// Position cap = floor(initial_capital * max_leverage / (reference_price * contract_multiplier)).
    /// Default: 5.0 (5x leverage). Scaled by `risk_intensity` during hyperopt.
    pub max_leverage: f32,

    // WAVE 17: Hyperopt-tuned parameters
    /// SAC-style entropy coefficient for Q-value softmax regularization (default: 0.001).
    /// f64: tested at 1e-9 tolerance; reward-scale ratio sensitive to sub-1e-6 offsets.
    pub entropy_coefficient: f64,
    /// Minimum epsilon when noisy nets are enabled (default: 0.05)
    pub noisy_epsilon_floor: Option<f32>,
    /// Count-based exploration bonus coefficient (default: 0.1, range: [0.01, 0.5])
    pub count_bonus_coefficient: Option<f32>,
    /// Transaction cost multiplier for reward calculation
    pub transaction_cost_multiplier: f32,

    // Wave 3 (Phase 2): Triple Barrier Method (always active)
    pub triple_barrier_profit_target_bps: u32,
    pub triple_barrier_stop_loss_bps: u32,
    pub triple_barrier_max_holding_seconds: u64,

    // P0: Prioritized Experience Replay (always enabled)
    pub per_alpha: f32,
    pub per_beta_start: f32,

    /// Regime-aware replay decay factor. PER priorities for cross-regime
    /// transitions are multiplied by decay^|regime_distance|.
    /// 1.0 = no bias. 0.0 = only current regime. Default: 0.3.
    pub regime_replay_decay: f32,

    // Wave 2.1: Dueling Networks (always enabled)
    /// Hidden dimension for dueling value/advantage streams
    pub dueling_hidden_dim: usize,

    // Wave 2.2: Multi-Step Returns (N-step TD)
    /// Number of steps for n-step returns (1-10, default: 3 for Rainbow DQN)
    /// Recommended: 3-5 for balance between bias and variance
    pub n_steps: usize,

    // Wave 2.3: Distributional RL (C51) — always enabled
    /// Number of atoms for value distribution (Rainbow DQN standard: 51)
    pub num_atoms: usize,
    /// Minimum value for distribution support (must be -v_max for symmetric support)
    pub v_min: f32,
    /// Maximum value for distribution support (v_min = -v_max enforced by hyperopt)
    pub v_max: f32,

    /// Reward scale factor for v_range computation.
    /// Q* ~ reward_scale / (1 - gamma). With 20% headroom and [20, 300] clamp:
    /// v_range = (reward_scale / (1 - gamma) * 1.2).clamp(20.0, 300.0)
    /// Default: 10.0 (reward v6 REWARD_SCALE).
    pub reward_scale: f32,

    // Wave 2.4: Noisy Networks for Exploration (always enabled)
    /// Initial noise std dev (Rainbow DQN standard: 0.5, scaled by 1/√in_features)
    pub noisy_sigma_init: f32,

    // Two-Phase Feature Normalization Configuration
    /// Ratio of total epochs to use for feature statistics collection (default: 0.3 = 30%)
    /// Phase 1 collects statistics for this percentage of training
    /// Example: 100 epochs * 0.3 = 30 epochs for stats collection
    pub feature_stats_collection_ratio: f32,
    /// Maximum number of epochs for stats collection (default: Some(10))
    /// Acts as a cap: min(epochs * ratio, max_epochs)
    /// Example: 100 epochs → min(30, 10) = 10; 200 epochs → min(60, 10) = 10
    /// Set to None for no cap (pure percentage-based)
    pub max_feature_stats_epochs: Option<usize>,

    // WAVE 23 P0: Early Stopping for Gradient Collapse
    /// Adaptive gradient collapse threshold multiplier (default: 100.0)
    /// Threshold = learning_rate × gradient_collapse_multiplier
    /// WAVE 23: Replaces hardcoded 0.1 threshold with learning-rate aware detection
    pub gradient_collapse_multiplier: f32,
    /// Consecutive epoch patience before early stopping (default: 5)
    /// Prevents false positives from single-epoch anomalies
    pub gradient_collapse_patience: usize,

    // WAVE 26 P0.6: Learning Rate Scheduling with Warmup
    // NOTE: warmup_steps is defined above (line 331) - using same field for both purposes
    /// Learning rate decay type after warmup
    pub lr_decay_type: super::lr_scheduler::LRDecayType,
    /// Minimum learning rate for Cosine decay.
    /// f64: paired with `learning_rate` (f64); precision matters for the 1e-6..1e-5 floor.
    pub min_learning_rate: f64,
    /// Minimum learning rate floor (absolute minimum, default: 1e-6).
    /// f64: precision matters for the 1e-6 floor.
    pub lr_min: f64,
    /// Steps for learning rate decay (default: 1000)
    pub lr_decay_steps: usize,
    /// Learning rate decay rate (default: 0.99)
    pub lr_decay_rate: f32,

    // WAVE 26 P1.4: Ensemble Uncertainty for Exploration (6D hyperopt expansion)
    /// Enable ensemble uncertainty-based exploration (replaces noisy networks)
    pub use_ensemble_uncertainty: bool,
    /// Number of Q-network heads in ensemble (3-10, default: 5)
    pub ensemble_size: usize,
    /// Variance penalty weight for uncertainty estimation (0.1-1.0)
    pub beta_variance: f32,
    /// Disagreement penalty weight for epistemic uncertainty (0.1-1.0)
    pub beta_disagreement: f32,
    /// Entropy bonus weight for exploration diversity (0.05-0.5)
    pub beta_entropy: f32,
    /// Variance cap to prevent over-penalization (0.1-2.0)
    pub variance_cap: f32,

    // WAVE 26 P1.8: Curiosity-Driven Exploration
    /// Curiosity weight for intrinsic reward (0.0 = disabled, typical range: 0.0-0.5)
    /// Balances extrinsic reward from trading vs intrinsic reward from novelty
    pub curiosity_weight: f32,
    /// Curiosity Q-penalty lambda: scales Bellman gamma by 1/(1 + lambda * prediction_error).
    /// When the curiosity forward model can't predict next state well (high error),
    /// Q-values are discounted toward immediate reward — preventing overconfident
    /// extrapolation into novel/OOS states. 0.0 = disabled. Typical: 1.0-5.0.
    pub curiosity_q_penalty_lambda: f32,

    // WAVE 26 P2.2: Gradient Accumulation
    /// Number of mini-batches to accumulate gradients over before optimizer step
    /// Default: 1 (no accumulation, standard training)
    /// Effective batch size = batch_size × gradient_accumulation_steps
    /// Example: batch_size=64, accumulation_steps=4 → effective_batch=256
    /// Benefits: Larger effective batch size without GPU memory constraints
    pub gradient_accumulation_steps: usize,

    // WAVE 26 P1: Advanced DQN Features Integration
    // P1.1: Spectral Normalization (in network config - see DQNConfig)
    // P1.2: Self-Attention (in network config - see DQNConfig)

    // P1.3: Sharpe Ratio Reward Component
    /// Enable Sharpe ratio as reward component (0.0 = disabled, typical: 0.2-0.5)
    pub sharpe_weight: f32,
    /// Rolling window size for Sharpe calculation (default: 20)
    pub sharpe_window: usize,

    // P1.6: Adaptive Dropout Scheduling (always active)
    /// Initial dropout rate (default: 0.5)
    pub dropout_initial: f32,
    /// Final dropout rate (default: 0.1)
    pub dropout_final: f32,
    /// Steps for dropout annealing (default: 10000)
    pub dropout_anneal_steps: usize,

    // P1.7: Hindsight Experience Replay (HER)
    /// Enable HER for improved data efficiency (0.0 = disabled, typical: 0.3-0.8)
    pub her_ratio: f32,
    /// HER strategy: "final" or "future"
    pub her_strategy: String,

    // P1.8: Curiosity-Driven Exploration (already has curiosity_weight above)
    // Uses existing curiosity_weight parameter

    // P1.9: Generalized Advantage Estimation (GAE) — always active
    /// GAE lambda parameter (default: 0.95)
    pub gae_lambda: f32,

    // P1.11: Noisy Network Sigma Scheduling — always active
    /// Initial sigma for noisy networks (default: 0.6)
    pub noisy_sigma_initial: f32,
    /// Final sigma for noisy networks (default: 0.4)
    pub noisy_sigma_final: f32,
    /// Steps for sigma annealing (default: 10000)
    pub noisy_sigma_anneal_steps: usize,

    // SP4 Layer B (2026-05-01): `weight_decay` field removed from
    // hyperparams. Per-group AdamW weight_decay rates are now sourced
    // from the SP4 ISV bus at `ISV[WD_RATE_BASE..+SP4_PARAM_GROUP_COUNT)`
    // (slot indices 160..168) — see
    // `crates/ml/src/cuda_pipeline/sp4_isv_slots.rs::wd_rate(group)`.
    // Per-step rate adapts via the `param_group_oracle` Pearl B kernel
    // (Layer A Task A7) and Pearls A+D smoothing (Task A12).

    /// Adam epsilon — BF16 requires ≥1e-4 (1e-8 rounds to 0 in f32 → div-by-zero).
    /// f64: tiny values (1e-8..1e-4) preserve denorm behaviour.
    pub adam_epsilon: f64,

    // Conservative Q-Learning (CQL) — always enabled, alpha controls strength
    /// CQL regularization strength (default: 0.1, range 0.0-1.0).
    /// f64: flows into DQNConfig.cql_alpha (ml-dqn crate, f64); convert in ml-dqn or at boundary.
    pub cql_alpha: f64,

    // QR-DQN (Quantile Regression DQN) -- replaces disabled C51
    /// Enable QR-DQN (IQN) for distributional RL
    /// Number of quantiles for QR-DQN (32-200)
    pub num_quantiles: usize,
    /// Kappa for quantile Huber loss (0.5-2.0)
    pub qr_kappa: f32,
    /// Lambda weight for IQN quantile loss relative to C51 loss in the dual-head ensemble.
    /// L_total = L_c51 + iqn_lambda * L_iqn
    /// Range [0.0, 2.0]: 0.0 = C51 only, 0.5 = balanced, 1.0 = equal weight
    pub iqn_lambda: f32,
    /// Mixture-of-Experts load-balance λ controller — permanent floor weight.
    ///
    /// `λ_eff = floor + max_extra × deficit`, where `deficit ∈ [0, 1]` is
    /// the normalised entropy shortfall against `target_frac × ln(K)`. The
    /// GPU kernel `moe_lambda_eff_update` writes ISV[128] each step;
    /// consumer kernel `moe_load_balance_loss` reads it at runtime.
    ///
    /// Default 0.01 (matches the legacy static `moe_lambda` default — the
    /// old "anti-collapse only" baseline becomes the permanent minimum).
    /// Hyperopt search range: [0.001, 0.1].
    pub moe_lambda_floor: Option<f32>,
    /// MoE adaptive λ controller — maximum additional λ added under full
    /// gate-entropy collapse. Peak λ_eff is `floor + max_extra`.
    /// Default 0.09 (peak λ = 0.10, 10× boost over floor).
    /// Hyperopt search range: [0.0, 0.5].
    pub moe_lambda_max_extra: Option<f32>,
    /// MoE adaptive λ controller — fraction of `ln(K)` treated as "diverse
    /// enough"; entropy at or above `target_frac × ln(K)` produces zero
    /// deficit (λ_eff = floor). Default 0.7 (target = 70% of ln(K)).
    /// Hyperopt search range: [0.3, 0.95].
    pub moe_entropy_target_frac: Option<f32>,
    /// Spectral norm σ_max — constrains ||W||_σ ≤ σ_max.
    /// Range [1.0, 10.0]. Default 3.0 (permits Xavier scaling, prevents Q-explosion).
    pub spectral_norm_sigma_max: f32,
    /// Spectral decoupling: L2 penalty on Q-value logit magnitudes.
    /// Prevents overconfident Q-values. Pezeshki et al. 2021.
    /// Range: [0.0, 0.1]. Default: 0.01.
    pub spectral_decoupling_lambda: f32,
    /// When true, IQN's bounded Huber loss is used for PER priorities instead of
    /// C51's unbounded cross-entropy. Prevents the PER feedback explosion.

    // Phase 3: GPU experience collection kernel configuration
    /// Enable the NVRTC-compiled GPU experience collection kernel.
    /// When false, experience collection uses the batched CPU path while
    /// the training step (forward/backward/optimizer) still runs on CUDA.
    /// Timesteps per episode in GPU kernel (default: 500, max: 1000)
    /// Higher values collect more experience per launch at the cost of VRAM
    pub gpu_timesteps_per_episode: usize,
    /// Number of parallel episodes for GPU experience collection.
    /// Controls VRAM usage: total = gpu_n_episodes × gpu_timesteps_per_episode × 2 (counterfactual) × state_dim × 4 bytes.
    pub gpu_n_episodes: usize,
    /// Average bid-ask spread for GPU portfolio simulation (default: 0.0001 = 1bp)
    pub avg_spread: f32,

    /// Maximum training steps per epoch (0 = unlimited, uses full dataset).
    // max_training_steps_per_epoch removed — always train on full dataset.
    // Epoch duration self-balances: bigger GPU → bigger batch → fewer steps.

    /// Hidden dimension base for GPU-dynamic network sizing.
    /// None = use default [256, 128, 64]. Some(base) = [base, base/2, base/4].
    pub hidden_dim_base: Option<usize>,

    /// Minimum profit factor for trade execution (BUG #7 fix)
    /// Trades must have profit > cost × this factor to be executed.
    /// Range [1.1, 2.0]: 1.1 = 10% margin above costs, 2.0 = 100% margin.
    pub minimum_profit_factor: f32,

    // Differential Sharpe Ratio (DSR) — always enabled in GPU composite reward kernel.
    /// Deprecated: DSR is always enabled. Kept for backward compatibility with hyperopt adapters.
    pub use_dsr: bool,

    // OFI (Order Flow Imbalance) features from MBP-10 data — ALWAYS LOADED.
    /// Directory containing MBP-10 .dbn.zst files for OFI feature extraction.
    /// 20 microstructure features (8 OFI + 12 tick-level) always appended to state.
    /// Default: "test_data/futures-baseline-mbp10" (local), "/data/futures-baseline-mbp10" (H100).
    pub mbp10_data_dir: String,
    /// Directory containing trade .dbn.zst files for real VPIN/Kyle's Lambda.
    /// Trades always loaded and fed to `OFICalculator::feed_trade()`.
    /// Default: "test_data/futures-baseline-trades" (local), "/data/futures-baseline-trades" (H100).
    pub trades_data_dir: String,

    /// Data source for training bars: "ohlcv" (1-minute candles) or "mbp10"
    /// (imbalance bars from MBP-10 order book). No fallback -- fails if the
    /// chosen source's data doesn't exist. Default: "mbp10".
    pub data_source: String,

    /// Trading symbol subdirectory filter (e.g. "ES.FUT", "NQ.FUT").
    /// When non-empty, load_training_data appends this to dbn_data_dir so only
    /// one instrument's DBN files are loaded. Default: "ES.FUT".
    pub symbol: String,

    /// Maximum bars to load for training. 0 = unlimited.
    /// Truncates dataset after loading, before walk-forward split.
    pub max_bars: usize,

    /// Imbalance bar threshold for MBP-10 to bar conversion.
    /// Higher = fewer bars (less noise), lower = more bars (more signal).
    /// Only used when `data_source = "mbp10"`. Default: 100.0.
    pub imbalance_bar_threshold: f32,

    /// EWMA alpha for adaptive imbalance threshold. Default: 0.1.
    /// Only used when `data_source = "mbp10"`.
    pub imbalance_bar_ewma_alpha: f32,

    // Offline RL mode (CQL/IQL on fixed datasets)
    /// Offline RL mode: skip online experience collection, train from a fixed dataset.
    /// Requires `dataset_path` to point to a pre-collected experience dataset.
    pub offline_mode: bool,
    /// Path to a pre-collected experience dataset (bincode format).
    /// Used with `offline_mode` to load a fixed dataset into the replay buffer.
    pub dataset_path: Option<String>,

    // IQL (Implicit Q-Learning) -- Kostrikov et al. 2021
    /// IQL expectile parameter tau (0.5-0.95, higher = more optimistic value estimates).
    /// Controls the asymmetry of the value function loss: tau > 0.5 penalizes
    /// underestimation more, effectively extracting the value of the best
    /// in-distribution action.
    pub iql_expectile_tau: f32,
    /// IQL advantage temperature beta (1.0-10.0, higher = more greedy policy).
    /// Controls how sharply the extracted policy concentrates on high-advantage
    /// actions during advantage-weighted regression.
    pub iql_advantage_temperature: f32,
    /// Enable IQL mode (Implicit Q-Learning for offline RL).
    // Fill simulation parameters for GPU experience collector
    /// IoC base fill probability (default 0.85, range [0.5, 1.0])
    pub fill_ioc_fill_prob: f32,
    /// Minimum limit fill probability at wide spread (default 0.30, range [0.1, 0.5])
    pub fill_limit_fill_min: f32,
    /// Maximum limit fill probability at tight spread (default 0.80, range [0.5, 0.95])
    pub fill_limit_fill_max: f32,
    /// Fraction of spread paid by market orders (default 0.50, range [0.2, 0.8])
    pub fill_spread_cost_frac: f32,
    /// Fraction of spread captured by limit orders (default 0.50, range [0.2, 0.8])
    pub fill_spread_capture_frac: f32,

    // Q-value clipping for GPU experience collector
    /// Minimum Q-value clamp in GPU kernel (default -200.0)
    pub q_clip_min: f32,
    /// Maximum Q-value clamp in GPU kernel (default 200.0)
    pub q_clip_max: f32,

    // Phase C+: Branching DQN (Tavakoli et al., 2018) — always enabled
    /// Hidden dimension for each branching advantage head (64-256, step=64).
    /// Controls capacity of the per-dimension advantage streams in Branching DQN.
    /// Controls capacity of the per-dimension advantage streams in Branching DQN (always active).
    pub branch_hidden_dim: usize,

    // GPU Walk-Forward: expanding-window cross-validation entirely on GPU (always active)
    /// Initial training window as a fraction of total data (default: 0.5).
    pub wf_initial_train_fraction: f32,
    /// Validation window as a fraction of total data (default: 0.125).
    pub wf_val_fraction: f32,
    /// Test window as a fraction of total data (default: 0.125).
    pub wf_test_fraction: f32,
    /// Step size between folds as a fraction of total data (default: 0.125).
    pub wf_step_fraction: f32,

    // SP15 Phase 1.6: Q1-Q7 train, Q8 dev val, Q9 sealed final-test split.
    /// Number of trailing quarters held out as sealed final test set.
    /// Default 1 (Q9 sealed). Walk-forward folds are generated on
    /// `Q1..Q(total - holdout_quarters - dev_quarters)` only; the sealed
    /// holdout is never reachable from any training path and is consumed
    /// exclusively by the Phase 4.3 `argo-eval-final.sh` workflow.
    pub holdout_quarters: usize,
    /// Number of trailing quarters used as final dev validation slice
    /// (after the sealed holdout). Default 1 (Q8 dev). Walk-forward folds
    /// are generated on `Q1..Q(total - holdout_quarters - dev_quarters)`
    /// only; the dev slice is consumed by end-of-training evaluation
    /// (deferred to a follow-up commit per Phase 1 precedent).
    pub dev_quarters: usize,

    // Ensemble Agent Configuration
    /// Number of agents in ensemble (default: 1 = no ensemble).
    /// When > 1, trains multiple independent DQN agents and combines their
    /// Q-values for more robust action selection with uncertainty estimation.
    pub ensemble_count: usize,
    /// KL-divergence diversity loss weight for ensemble training (default: 0.01).
    /// Encourages each agent in the ensemble to learn different strategies
    /// by penalizing Q-value distribution similarity between ensemble members.
    pub ensemble_diversity_weight: f32,

    // Curriculum learning: difficulty-based walk-forward window filtering
    /// Curriculum phase for difficulty-based window filtering.
    /// 0 = Easy (ADX>30 windows only), 1 = Mixed (trending weighted 2x), 2 = Full (equal weight).
    /// Default: 2 (Full -- no filtering, backward compatible).
    pub curriculum_phase: usize,

    // Expert demonstration generation for imitation learning warmup
    /// Fraction of each training batch sampled from expert demonstrations (0.0-1.0).
    /// 0.0 = no demonstrations (pure RL), 0.5 = half batch from expert.
    /// Default: 0.0 (disabled, backward compatible).
    pub expert_demo_ratio: f32,
    /// Number of epochs over which expert_demo_ratio decays linearly to 0.
    /// After this many epochs, training is pure RL.
    /// Default: 20.
    pub expert_demo_decay_epochs: usize,

    // GPU Multi-Head Feature Attention
    /// Enable GPU multi-head feature attention over h_s2 (post-graph, 1-step lag).
    /// When true, 4-head self-attention is applied to the shared trunk output
    /// after each CUDA Graph replay, modifying save_h_s2 for the next step.
    /// Default: false.
    pub use_attention: bool,

    // C51 distributional loss warmup
    /// Number of epochs to use MSE loss on expected Q-values before switching to C51 cross-entropy.
    /// MSE provides stronger gradient signal during early training when C51 cross-entropy between
    /// nearly-identical distributions produces near-zero gradient for the value mean.
    /// Default: 5. Set to 0 to use C51 from the start.
    pub c51_warmup_epochs: usize,
    /// Maximum C51 blend factor (0.0 = pure MSE, 1.0 = pure C51).
    /// Caps the alpha ramp so MSE always contributes (1 - alpha_max) of the
    /// primary gradient. Prevents C51 premature convergence from killing
    /// the gradient signal on large batches.
    /// Default: 0.5. Hyperopt range: [0.3, 0.9].
    pub c51_alpha_max: f32,

    // Decision Transformer pre-training
    /// Decision Transformer pre-training epochs (0 = disabled).
    /// When > 0, runs DT pre-training on offline trajectories before the main DQN loop.
    /// The DT kernels and pretrain_step() are implemented in decision_transformer.rs.
    pub dt_pretrain_epochs: usize,
    /// DT context length (number of past timesteps per sequence).
    pub dt_context_len: usize,
    /// DT embedding dimension for states, actions, and returns-to-go.
    pub dt_embed_dim: usize,
    /// DT number of transformer layers.
    pub dt_num_layers: usize,
    /// DT target return-to-go for conditioning (in units of cumulative reward).
    pub dt_target_return: f32,

    // CVaR (Conditional Value at Risk) action selection for IQN
    /// CVaR confidence level alpha (0.0-1.0). Lower alpha = more risk-averse.
    /// 0.05 = optimize for worst 5% outcomes (extreme caution).
    /// 0.25 = optimize for worst 25% outcomes (moderate risk aversion).
    /// Default: 0.05.
    pub cvar_alpha: f32,

    /// IQR exploration bonus alpha: weight for adding IQN interquartile range
    /// to Q-values before Boltzmann action selection. High-IQR actions (uncertain
    /// outcomes, typically large-magnitude positions) get an exploration boost,
    /// countering C51's distributional bias toward low-variance (Small) actions.
    /// 0.0 = disabled. Typical range [0.1, 0.5]. Default: 0.3.
    pub iqr_exploration_alpha: f32,

    /// Ensemble variance exploration beta: weight for adding ensemble Q-value
    /// variance (disagreement between heads) as an exploration bonus.
    /// High ensemble disagreement = high uncertainty = explore more.
    /// Applied uniformly across all actions: Q_select += beta * sqrt(mean_var).
    /// Only active when ensemble_count > 1. No-op when ensemble_count <= 1.
    /// 0.0 = disabled. Typical range [0.05, 0.3]. Default: 0.2.
    pub ensemble_exploration_beta: f32,

    // Contract specification — symbol-dependent constants
    /// Futures tick size (minimum price increment).
    /// ES = 0.25, NQ = 0.25, 6E = 0.00005, ZN = 1/64 ≈ 0.015625.
    /// Used with `contract_multiplier` to compute spread cost.
    /// Default: 0.25 (ES/NQ).
    pub tick_size: f32,
    /// Contract multiplier (dollar value per point).
    /// ES = 50, NQ = 20, 6E = 125_000, ZN = 1000.
    /// Used for margin computation and spread cost.
    /// Default: 50.0 (ES).
    pub contract_multiplier: f32,
    /// Initial margin as fraction of notional value.
    /// CME initial margin ≈ 6% of notional for equity index futures.
    /// Default: 0.06.
    pub margin_pct: f32,
    /// Trading days per year for annualization.
    /// CME equity/futures: 252. Crypto: 365. Forex: 252.
    pub trading_days_per_year: usize,

    /// Advantage noise std for dueling symmetry breaking.
    /// Per-action Gaussian noise replaces dead NoisyLinear (cuBLAS bypasses Candle).
    /// Breaks the dueling symmetry trap where advantage heads produce zero Q-gap.
    /// 0.0 = disabled. Default: 0.1.
    pub noise_sigma: f32,

    /// Plan 3 Task 8 B.3: number of experience-collection samples drawn from
    /// the GPU scripted-policy mixture (40% uniform / 20% momentum / 20%
    /// mean-reversion / 20% vwap-deviation) before action selection switches
    /// back to the network. Default 100_000. Set to 0 to disable. Smoke configs
    /// override to a smaller value (e.g. 10_000) so the transition is
    /// observable inside the smoke run. Cast to `u32` at the
    /// `GpuDqnTrainConfig` boundary.
    pub replay_seed_steps: u32,
}

impl Default for DQNHyperparameters {
    /// Default implementation uses conservative hyperparameters.
    /// WARNING: These are NOT optimized for production. Use hyperopt results instead.
    fn default() -> Self {
        Self::conservative()
    }
}

impl DQNHyperparameters {
    /// Initial v_min for C51 atom support (used at construction only —
    /// per-sample IQL support replaces batch-level v_range during training).
    pub fn computed_v_min(&self) -> f32 {
        let r = (self.reward_scale * 1.5).clamp(10.0, 50.0);
        -r
    }

    /// Initial v_max for C51 atom support.
    pub fn computed_v_max(&self) -> f32 {
        (self.reward_scale * 1.5).clamp(10.0, 50.0)
    }

    /// Compute max position (contracts) from leverage, capital, and reference price.
    ///
    /// `max_position = floor(initial_capital * max_leverage / (reference_price * contract_multiplier))`
    ///
    /// Always returns at least 1.0 so the agent can take at least one contract.
    /// `reference_price` is typically the median close price from training data.
    ///
    /// Computed in f64 internally for the leverage × capital division, then returned as f32
    /// to match the f32-native position/max_leverage/contract_multiplier kernel contract.
    pub fn compute_max_position(&self, reference_price: f64) -> f32 {
        if reference_price <= 0.0 || self.contract_multiplier <= 0.0 {
            return 1.0;
        }
        let notional_per_contract = reference_price * f64::from(self.contract_multiplier);
        (f64::from(self.initial_capital) * f64::from(self.max_leverage) / notional_per_contract)
            .floor()
            .max(1.0) as f32
    }

    /// Apply family intensity scalars to their respective parameters.
    /// Call after constructing from hyperopt PSO vector.
    /// Intensity 1.0 = no change. 0.5 = halved. 2.0 = doubled.
    /// For interval/epoch params: higher intensity = more frequent (divide by intensity).
    ///
    /// Intensity scalars stay f64 (PSO search space is f64); each multiplicand is cast to
    /// match its target field's storage type at the call site.
    pub fn apply_family_scaling(&mut self) {
        // ── Core families ──

        // Learning family: scales learning_rate, tau.
        // (SP4 Layer B: `weight_decay` family-scaling removed — per-group
        // weight_decay rates are now ISV-driven; PSO learning-intensity no
        // longer perturbs the static config field that no longer exists.)
        let li = self.learning_intensity;
        self.learning_rate *= li;
        self.tau *= li as f32;

        // Exploration family: scales entropy_coefficient, noisy_sigma_init
        let exi = self.exploration_intensity;
        self.entropy_coefficient = (self.entropy_coefficient * exi).clamp(0.001, 1.0);
        self.noisy_sigma_init = (self.noisy_sigma_init * exi as f32).clamp(0.01, 2.0);

        // Replay family: scales buffer_size, per_alpha, per_beta_start, n_steps
        let rpi = self.replay_intensity;
        self.buffer_size = (self.buffer_size as f64 * rpi).max(1000.0) as usize;
        self.per_alpha = (self.per_alpha * rpi as f32).clamp(0.0, 1.0);
        self.per_beta_start = (self.per_beta_start * rpi as f32).clamp(0.0, 1.0);
        self.n_steps = (self.n_steps as f64 * rpi).clamp(1.0, 20.0) as usize;

        // Architecture family: scales hidden_dim_base, dueling_hidden_dim, num_atoms
        let ari = self.architecture_intensity;
        if let Some(base) = self.hidden_dim_base {
            self.hidden_dim_base = Some(((base as f64 * ari) as usize).max(64));
        }
        self.dueling_hidden_dim = ((self.dueling_hidden_dim as f64 * ari) as usize).max(32);
        let scaled_atoms = (self.num_atoms as f64 * ari) as usize;
        self.num_atoms = (scaled_atoms | 1).max(11); // ensure odd for C51 symmetry

        // Risk family: scales max_leverage (not max_position_absolute directly),
        // huber_delta, minimum_profit_factor, w_dd, dd_threshold.
        // max_position_absolute is derived later via compute_max_position(reference_price).
        let rki = self.risk_intensity as f32;
        self.max_leverage *= rki;
        self.huber_delta *= rki;
        self.minimum_profit_factor = 1.0 + (self.minimum_profit_factor - 1.0) * rki;
        self.w_dd *= rki;
        self.dd_threshold *= rki;

        // ── Generalization families ──

        // Adversarial family
        let ai = self.adversarial_intensity;
        self.adversarial_dd_threshold *= ai as f32;
        if ai > 0.01 {
            self.adversarial_warmup_epochs = (self.adversarial_warmup_epochs as f64 / ai) as usize;
            self.adversarial_checkpoint_interval =
                (self.adversarial_checkpoint_interval as f64 / ai) as usize;
        }

        // Regularization family
        let ri = self.regularization_intensity as f32;
        self.dropout_initial = (self.dropout_initial * ri).clamp(0.0, 0.5);
        self.spectral_norm_sigma_max *= ri;
        self.stochastic_depth_prob = (self.stochastic_depth_prob * ri).clamp(0.0, 0.5);
        self.spectral_decoupling_lambda *= ri;

        // Data augmentation family
        let di = self.augmentation_intensity as f32;
        self.feature_mask_fraction = (self.feature_mask_fraction * di).clamp(0.0, 0.8);
        self.feature_noise_scale *= di;

        // Loss shaping family
        let ls = self.loss_shaping_intensity as f32;
        self.cea_weight = (self.cea_weight * ls).clamp(0.0, 2.0);
        self.micro_reward_scale = (self.micro_reward_scale * ls).clamp(0.0, 0.01);
        // v6 legacy (zeroed defaults, no-op)
        self.asymmetric_dd_weight *= ls;

        // Ensemble family
        let ei = self.ensemble_intensity as f32;
        self.ensemble_disagreement_penalty *= ei;

        // Causal family
        let ci = self.causal_intensity;
        self.causal_weight *= ci as f32;
        if ci > 0.01 {
            self.causal_intervention_interval =
                (self.causal_intervention_interval as f64 / ci) as usize;
        }
    }

    /// Create conservative hyperparameters suitable for testing and development.
    /// WARNING: These are NOT optimized for production. Use hyperopt results instead.
    /// After DQN hyperopt completes, update ml/hyperparams/dqn_best.toml with optimal values.
    pub fn conservative() -> Self {
        Self {
            learning_rate: 0.00003,
            batch_size: 1024,  // H100 80GB target; AutoBatchSizer caps for smaller GPUs
            gamma: 0.95,
            epsilon_start: 0.3,
            epsilon_end: 0.02,
            epsilon_decay: 0.995,
            epsilon_direction_mult: 1.0,
            epsilon_magnitude_mult: 2.0,
            epsilon_order_mult: 1.5,
            epsilon_urgency_mult: 1.5,
            buffer_size: 500000,  // WAVE 24: Increased from 100K for better sample diversity (anti-overfitting)
            replay_buffer_vram_fraction: 0.70,
            min_replay_size: 1000,
            epochs: 100,
            checkpoint_frequency: 10,
            early_stopping_enabled: true,
            q_value_floor: -5.0,  // Wave 3 fix: allow normal negative Q-values, catch explosions only
            min_loss_improvement_pct: 2.0,
            plateau_window: 30,
            min_epochs_before_stopping: 50,
            hold_penalty: -0.001, // deprecated: kept for hyperopt adapter compat
            w_pnl: 0.3,
            w_dd: 2.0,   // was 1.0 — stronger penalty now that threshold actually fires
            w_idle: 0.01,
            dd_threshold: 0.005,   // 0.5% — active for bar-level trades (was 2%, effectively dead)
            loss_aversion: 1.0,
            time_decay_rate: 0.0005,
            q_gap_threshold: 0.05, // Tier 2 default: mild conviction gating (hyperopt searches [0.0, 0.5])
            huber_delta: 100.0,   // BUG #12 FIX: Scale delta 100x for gradient explosion fix (was 1.0)
            gradient_clip_norm: Some(1.0), // Safety net — mean-reduced gradients have norm ≈ 1.0
            hold_penalty_weight: 0.01,      // Default: 1% penalty weight
            movement_threshold: 0.02,       // Default: 2% price movement threshold
            preprocessing_window: 50,       // Default: 50-bar rolling window
            preprocessing_clip_sigma: 5.0,  // Default: clip at ±5σ

            // WAVE 16 (Agent 36): Target update defaults (SOFT UPDATES for gradient stability)
            tau: 0.005, // Polyak averaging with 0.5% blend per step. Old 0.001 was too slow for
                        // reward v4's tight v_range=±0.5 — target couldn't track online, Q saturated v_max.
            target_update_mode: crate::trainers::TargetUpdateMode::Soft, // Soft updates (Rainbow DQN standard)
            target_update_frequency: 500, // BUG #9 FIX: Hard update frequency: 500 steps (optimal Rainbow DQN, was 10K)

            // Rainbow DQN warmup
            warmup_steps: 0, // Adaptive in CLI (0 for <200K, scaled 200K-1M, 80K for >1M)

            // P2-A Enhancement
            initial_capital: 100_000.0, // $100K default
            bars_per_day: 390.0, // 1-minute bars (6.5h × 60min)
            holding_cost_rate: 0.00000052,  // ~0.52 bps / 100 bars inventory penalty
            churn_threshold_bars: 10,      // graduated penalty for trades within 10 bars
            churn_penalty_scale: 0.001,    // churn penalty scale factor
            aux_frequency: 4,  // aux ops every 4th step (~4× faster epochs)

            // P2-B Enhancement
            cash_reserve_percent: 0.0, // Default: no reserve (backward compatible)

            // WAVE 16S: Adaptive Risk Management
            kelly_fractional: 0.5,               // Default: half-Kelly (conservative)
            kelly_max_fraction: 0.25,            // Default: max 25% of portfolio
            kelly_min_trades: 20,                // Default: 20 trades minimum for statistics
            volatility_window: 20,               // Default: 20-period rolling window

            // WAVE 35: Advanced Features (always active)
            // WAVE 16: Core Risk Management (always active)

            // Generalization: periodic shrink-and-perturb (kills memorized weights)
            shrink_perturb_interval: 20,         // Every 20 epochs (0=disabled)
            shrink_perturb_alpha: 0.85,          // Keep 85% of weights, reinit 15%
            shrink_perturb_sigma: 0.01,          // Xavier noise scale
            // Generalization: adversarial regime injection (stress-test low-DD runs)
            adversarial_dd_threshold: 0.005,     // 0.5% MaxDD threshold — suspiciously low
            adversarial_epochs_trigger: 5,       // 5 consecutive low-DD epochs triggers injection
            beta_penalty_strength: 0.3,          // 30% reward reduction at full correlation

            // Gems & Pearls: generalization techniques
            // Anti-LR multipliers retained at original 3.0/0.3 — the noise
            // amplification problem (grad_norm spikes to 1.16M when Sharpe
            // oscillates epoch-to-epoch, train-br8cb Fold 0) is now fixed by
            // feeding the controller a temporally smoothed Sharpe instead of
            // the raw last-epoch value. See training_loop.rs anti-LR block.
            anti_lr_good_mult: 3.0,               // 3x LR when Sharpe is good (destabilize overfit)
            anti_lr_bad_mult: 0.3,                // 0.3x LR when Sharpe is bad (stabilize)
            anti_lr_sharpe_threshold: 0.3,        // Sharpe threshold for switching
            ensemble_disagreement_penalty: 0.3,   // #27: penalize Q-targets by ensemble std
            causal_weight: 0.1,                    // #34: mild causal regularization
            causal_intervention_interval: 100,     // #34: every 100th training step (was 10 — 14 cuBLAS fwd per invocation)
            adversarial_warmup_epochs: 50,         // #33: 50 epochs warmup before self-play
            adversarial_checkpoint_interval: 50,   // #33: update saboteur reference every 50 epochs
            feature_mask_fraction: 0.3,           // #23: mask 30% of features each epoch
            feature_noise_scale: 0.1,             // #22: add N(0, 0.1*std) noise to features
            time_reversal_mod: 5,                  // #11: 20% of episodes played backwards
            regret_blend: 0.0,                     // #17: zeroed in v7 (no-op, kept for TOML compat)
            trade_clustering_penalty: 0.0,        // #25: zeroed in v7 (no-op, kept for TOML compat)
            // Reward — Counterfactual Branch Attribution
            cea_weight: 0.3,
            // v8: Dense micro-reward, TD(λ), PopArt, curriculum, hindsight
            // R5 micro-reward: per-bar OFI-momentum + price-confirmation signal.
            // Phase 2 Task 2.3 FIX-documented-disable: production uses 0.1,
            // smoke uses 0.0 (intentional — see kernel comment at
            // experience_kernels.cu `R5: Dense micro-reward` branch).
            // DO NOT remove — feedback_no_functionality_removal.md applies.
            micro_reward_scale: 0.01,
            price_confirm_weight: 0.5,
            book_aggression_weight: 0.3,
            hold_quality_weight: 0.2,
            micro_reward_temp: 3.0,
            td_lambda: 0.9,
            max_trace_length: 7,
            popart_enabled: true,
            popart_robust: true,
            curriculum_enabled: false,  // disabled by default until wired
            curriculum_warmup_fraction: 0.6,
            hindsight_fraction: 0.1,
            hindsight_lookahead: 10,
            pruning_epoch: 50,                     // #20: compute mask at epoch 50
            pruning_fraction: 0.7,                 // #20: prune 70% of smallest weights
            bottleneck_dim: 16,                   // #31: 16D bottleneck — 8× more market feature info (was 2, threw away 97.6%)
            stochastic_depth_prob: 0.2,            // #21: 20% drop probability per hidden layer
            mixup_alpha: 0.2,                      // Manifold Mixup: Beta(0.2,0.2) target distribution mixing
            // Core family intensity scalars: 1.0 = no change to defaults (neutral)
            learning_intensity: 1.0,
            exploration_intensity: 1.0,
            replay_intensity: 1.0,
            architecture_intensity: 1.0,
            risk_intensity: 1.0,
            // Generalization family intensity scalars: 1.0 = no change to defaults (neutral)
            adversarial_intensity: 1.0,
            regularization_intensity: 1.0,
            augmentation_intensity: 1.0,
            loss_shaping_intensity: 1.0,
            ensemble_intensity: 1.0,
            causal_intensity: 1.0,
            asymmetric_dd_weight: 0.5,            // #18: extra loss on Q-overestimation in drawdown

            // Wave 16 Portfolio Features
            max_position_absolute: 2.0,          // Default: ±2.0 position limit (legacy fallback)
            max_leverage: 5.0,                   // Default: 5x leverage cap

            // WAVE 17: Hyperopt-tuned parameters
            entropy_coefficient: 0.001, // Must be ≤ reward magnitude (~0.001). Old 0.01 was 10x reward → entropy dominated learning.
            noisy_epsilon_floor: Some(0.02), // NoisyNet handles exploration; low floor avoids interference
            count_bonus_coefficient: Some(0.1),
            transaction_cost_multiplier: 1.0,

            // Triple Barrier defaults
            triple_barrier_profit_target_bps: 100,
            triple_barrier_stop_loss_bps: 50,
            triple_barrier_max_holding_seconds: 3600,

            // P0: Prioritized Experience Replay (WAVE 6.4: ENABLED BY DEFAULT)

            per_alpha: 0.6,
            per_beta_start: 0.6,  // Higher start → faster IS weight correction (was 0.4)
            regime_replay_decay: 0.3,  // Regime-aware replay: bias toward current regime

            // Wave 2.1: Dueling Networks (WAVE 6.4: ENABLED BY DEFAULT)
            dueling_hidden_dim: 128,  // Default: 128 hidden units

            // Wave 2.2: Multi-Step Returns (WAVE 6.4: ENABLED BY DEFAULT)
            n_steps: 5,  // v8: Default 5 (was 3; better bias-variance for longer horizons)

            // Wave 2.3: Distributional RL (BUG #36 FIXED — scatter_add gradient flow verified)
            num_atoms: 51,  // Rainbow DQN standard: 51 atoms
            // v_min/v_max = ±(reward_scale × 1.5) = ±15.
            // Tight support: delta_z = 30/50 = 0.6 (was 9.6 at ±240).
            // 16× better atom resolution in the active Q-value range.
            reward_scale: 10.0,
            v_min: -(10.0_f32 * 1.5).clamp(10.0, 50.0), // = -15.0
            v_max: (10.0_f32 * 1.5).clamp(10.0, 50.0),  // = 15.0

            // Wave 2.4: Noisy Networks (WAVE 6.4: ENABLED BY DEFAULT)
            noisy_sigma_init: 0.5,  // Rainbow DQN standard: 0.5

            // Two-Phase Feature Normalization Configuration
            feature_stats_collection_ratio: 0.3,  // Default: 30% of epochs for stats collection
            max_feature_stats_epochs: Some(10),   // Default: cap at 10 epochs

            // WAVE 23 P0: Early Stopping for Gradient Collapse
            gradient_collapse_multiplier: 100.0,  // Adaptive threshold (LR × 100)
            gradient_collapse_patience: 5,        // 5 consecutive epochs before early stop

            // WAVE 26 P0.6: Learning Rate Scheduling
            // NOTE: warmup_steps already set above (line 500)
            lr_decay_type: super::lr_scheduler::LRDecayType::Constant, // Default: constant LR
            min_learning_rate: 1e-6,              // Default: min LR for Cosine decay
            lr_min: 1e-6,                         // Default: absolute minimum LR
            lr_decay_steps: 1000,                 // Default: decay every 1000 steps
            lr_decay_rate: 0.99,                  // Default: 1% decay per step

            // WAVE 26 P1.4: Ensemble Uncertainty (default: DISABLED for backward compatibility)
            use_ensemble_uncertainty: true,      // Default: disabled (use noisy networks instead)
            ensemble_size: 3,                     // 3 heads for epistemic uncertainty
            beta_variance: 0.5,                   // Default: 50% variance penalty
            beta_disagreement: 0.5,               // Default: 50% disagreement penalty
            beta_entropy: 0.1,                    // Default: 10% entropy bonus
            variance_cap: 1.0,                    // Default: cap at 1.0 to prevent over-penalization

            // WAVE 26 P1.8: Curiosity-Driven Exploration
            curiosity_weight: 0.1,                // Default: enabled (ungates GPU experience collector; hyperopt tunes 0.01-0.5)
            curiosity_q_penalty_lambda: 1.0,      // Default: moderate penalty on Q-values in novel states

            // WAVE 26 P2.2: Gradient Accumulation
            gradient_accumulation_steps: 1,       // Default: no accumulation (standard training)

            // WAVE 26 P1: Advanced DQN Features Integration
            // P1.3: Sharpe Ratio Reward
            sharpe_weight: 0.0,                   // Default: disabled (hyperopt will tune 0.0-0.5)
            sharpe_window: 20,                    // Default: 20-step rolling window

            // P1.6: Adaptive Dropout
            dropout_initial: 0.5,                 // Default: 50% initial dropout
            dropout_final: 0.1,                   // Default: 10% final dropout
            dropout_anneal_steps: 10000,          // Default: 10K steps for annealing

            // P1.7: Hindsight Experience Replay
            her_ratio: 0.2,                       // Tier 2 default: 20% HER relabeling (hyperopt tunes 0.0-0.8)
            her_strategy: "future".to_owned(),   // Default: future strategy (better than final)

            // P1.9: Generalized Advantage Estimation
            gae_lambda: 0.95,                     // Default: 0.95 (standard PPO/A2C value)

            // P1.11: Noisy Sigma Scheduling
            noisy_sigma_initial: 0.8,             // Higher initial noise for 81-action exploration
            noisy_sigma_final: 0.4,               // Default: 40% final noise
            noisy_sigma_anneal_steps: 10000,      // Default: 10K steps for annealing

            // SP4 Layer B: `weight_decay` field removed; per-group rates
            // are sourced from ISV[WD_RATE[group]] at runtime.
            adam_epsilon: 1e-8, // f32 Adam (master weights are f32)

            // Conservative Q-Learning (CQL)
            cql_alpha: 1.0,                       // Strong conservatism — penalizes OOS-destructive Q-values

            // QR-DQN (complementary to C51 — IQN for quantile estimation).
            // Plan 4 Task 3 (E.3): IQN is pinned to FIXED_TAUS.len() (= 5)
            // at the GpuIqnConfig construction site. This hyperparam slot
            // stays for compat (search-space definitions / older checkpoint
            // metadata may reference it) but the IQN forward no longer
            // honours it — overriding here would not change runtime behaviour.
            num_quantiles: 5,                     // = FIXED_TAUS.len(); was 32.
            qr_kappa: 1.0,                        // Default: 1.0 (standard quantile Huber loss)
            iqn_lambda: 0.25,                     // Default: mild IQN regularization alongside C51
            spectral_norm_sigma_max: 3.0,        // Default: permits Xavier scaling [1.0, 10.0]
            spectral_decoupling_lambda: 0.01,    // Default: mild logit magnitude penalty (Pezeshki 2021)

            // Phase 3: GPU experience collection
            gpu_timesteps_per_episode: 500,       // Default: 500 timesteps per episode
            gpu_n_episodes: 256,                  // Default: 256 parallel episodes
            avg_spread: 0.0001,                   // Default: 1bp (ES/NQ futures)

            // GPU-dynamic network sizing — None means auto-detect from hardware.
            // H100: optimal_n_episodes fills 132 SMs; hidden_dim_base expanded by hyperopt bounds.
            hidden_dim_base: None,                // Default: None (use [256, 128, 64])

            // BUG #7: Minimum profit factor for trade execution
            minimum_profit_factor: 1.5,   // Default: 50% margin above breakeven

            // DSR: legacy flag (kept for hyperopt adapter compat; no reward effect)
            use_dsr: true,

            mbp10_data_dir: "test_data/futures-baseline-mbp10".to_string(),
            trades_data_dir: "test_data/futures-baseline-trades".to_string(),

            // Data source: "ohlcv" (1-min candles) or "mbp10" (imbalance bars from MBP-10 order book)
            data_source: "mbp10".to_string(),
            symbol: "ES.FUT".to_string(),
            max_bars: 0, // 0 = unlimited
            imbalance_bar_threshold: 100.0,
            imbalance_bar_ewma_alpha: 0.1,

            // Offline RL: disabled by default (online training with experience collection)
            offline_mode: false,
            dataset_path: None,

            // IQL: disabled by default (use CQL for standard offline RL)
            iql_expectile_tau: 0.7,
            iql_advantage_temperature: 3.0,
            // Fill simulation: match ExperienceCollectorConfig::default()
            fill_ioc_fill_prob: 0.85,
            fill_limit_fill_min: 0.30,
            fill_limit_fill_max: 0.80,
            fill_spread_cost_frac: 0.50,
            fill_spread_capture_frac: 0.50,

            // Q-value clipping: match ExperienceCollectorConfig::default()
            q_clip_min: -200.0,
            q_clip_max: 200.0,

            branch_hidden_dim: 128,

            // GPU walk-forward (always active)
            wf_initial_train_fraction: 0.5,
            wf_val_fraction: 0.125,
            wf_test_fraction: 0.125,
            wf_step_fraction: 0.125,

            // SP15 Phase 1.6: Q9 sealed, Q8 dev val by default
            holdout_quarters: 1,
            dev_quarters: 1,

            // Ensemble Agent: disabled by default (single agent training)
            ensemble_count: 3,                    // 3 ensemble heads for epistemic uncertainty
            ensemble_diversity_weight: 0.01,      // Default: 1% KL-divergence diversity loss weight

            // Curriculum learning: disabled by default (Full = no filtering)
            curriculum_phase: 2,                  // Default: 2 = Full (all windows, equal weight)

            // Expert demonstrations: disabled by default (pure RL)
            expert_demo_ratio: 0.0,               // Default: 0.0 = no expert demos
            expert_demo_decay_epochs: 20,          // Default: 20 epochs to decay to pure RL

            // GPU attention: always on (unconditional — no use_attention flag)
            use_attention: true,  // kept for backward compat with deserialized configs

            // C51 warmup: disabled — c51_alpha starts at c51_alpha_max from epoch 0
            c51_warmup_epochs: 0,
            c51_alpha_max: 0.5, // MSE always contributes 50%. Hyperopt searches [0.3, 0.9].

            // Decision Transformer pre-training: disabled by default
            dt_pretrain_epochs: 0,    // 0 = disabled
            dt_context_len: 20,
            dt_embed_dim: 128,
            dt_num_layers: 3,
            dt_target_return: 2.0,

            cvar_alpha: 0.05,         // Optimize for worst 5% quantile tail
            iqr_exploration_alpha: 0.3, // IQR anti-collapse exploration bonus (0.0 = disabled)
            ensemble_exploration_beta: 0.2, // Ensemble variance exploration bonus (0.0 = disabled)

            // Contract specification: ES futures defaults
            tick_size: 0.25,              // ES/NQ tick size ($0.25)
            contract_multiplier: 50.0,    // ES: $50 per point (NQ=20, 6E=125_000, ZN=1000)
            margin_pct: 0.06,             // CME initial margin ~6% of notional
            trading_days_per_year: 252,    // CME equity/futures (crypto=365, forex=252)

            // Advantage noise for dueling symmetry breaking (replaces dead NoisyLinear)
            noise_sigma: 0.1,

            // Plan 3 Task 8 B.3: GPU-only seeded warm-start.
            // 100k samples ≈ first 5 production-scale collection epochs.
            replay_seed_steps: 100_000,

            // MoE adaptive load-balance λ controller (2026-04-27).
            // Floor is the permanent minimum (= old static 0.01 default);
            // the controller raises λ_eff up to (floor + max_extra) under
            // entropy collapse. Target_frac sets the entropy threshold at
            // which λ_eff = floor (no extra push when gate is diverse).
            moe_lambda_floor: Some(0.01),
            moe_lambda_max_extra: Some(0.09),
            moe_entropy_target_frac: Some(0.7),
        }
    }
}


// ============================================================================
// 2025-Optimized DQN Configuration Presets
// ============================================================================
//
// Production-ready defaults based on 2025 research and empirical results.
// Incorporates lessons from hyperopt trials, ablation studies, and real-world trading performance.
//
// Key Features:
// - Ensemble uncertainty estimation for better risk-aware exploration
// - Noisy networks with scheduled sigma decay for exploration
// - Soft target updates (Polyak averaging) for training stability
// - Prioritized experience replay with balanced alpha/beta
// - Moderate network capacity (512-256-128) for generalization
// - Conservative hyperparameters validated through production use

use crate::dqn::dqn::DQNConfig;

/// Production-ready 2025 DQN configuration
///
/// This configuration represents the best-known defaults as of 2025, incorporating:
/// - Results from 100+ hyperopt trials
/// - Ablation studies on Rainbow components
/// - Production trading performance data
/// - Modern RL research best practices
///
/// # Design Principles
///
/// 1. **Stability over speed**: Conservative learning rates and update frequencies
/// 2. **Ensemble-based exploration**: Use model uncertainty for exploration bonuses
/// 3. **Robust replay**: Prioritized replay with balanced diversity
/// 4. **Adaptive exploration**: Noisy networks + epsilon decay for multi-phase exploration
/// 5. **Risk-aware**: Integrated Sharpe ratio and curiosity-driven rewards
///
/// # Recommended Use Cases
///
/// - Production trading systems (live or paper)
/// - Baseline for hyperparameter tuning
/// - Reference for ablation studies
/// - Starting point for custom variants
///
/// # Not Recommended For
///
/// - HFT (use dqn_hft_config with attention layers enabled)
/// - Very small datasets (use dqn_conservative_config with reduced capacity)
/// - Offline RL (disable exploration)
pub(crate) fn dqn_default_config() -> DQNConfig {
    DQNConfig {
        // Network Architecture (state_dim is now the STATE_DIM constant)
        num_actions: 4,  // 4 direction levels (Short/Hold/Long/Flat) — 4-branch refactor
        hidden_dims: vec![512, 256, 128],  // Sufficient capacity without overfitting (Trial 19)
        leaky_relu_alpha: 0.01,

        // Training Hyperparameters
        learning_rate: 1e-4,
        warmup_steps: 5000,
        batch_size: 1024,  // H100 80GB easily fits 1024; AutoBatchSizer caps for smaller GPUs
        gamma: 0.95,
        gradient_clip_norm: 100.0,
        huber_delta: 10.0,

        // Replay Buffer
        replay_buffer_capacity: 500_000,
        min_replay_size: 10_000,
        per_alpha: 0.6,
        per_beta_start: 0.4,
        per_beta_max: 1.0,
        per_beta_annealing_steps: 100_000,
        per_max_memory_bytes: 0, // Constructor overrides via VRAM probe on CUDA

        // Exploration Strategy
        epsilon_start: 0.3,
        epsilon_end: 0.02,
        epsilon_decay: 0.9999,  // Reaches ~0.01 after 100k steps
        noisy_sigma_init: 0.5,

        // Target Network Updates
        target_update_freq: 1,
        tau: 0.001,
        tau_final: 0.0005,         // was 0.0001 — too frozen, target diverges
        tau_anneal_steps: 500_000,  // was 100K — reached asymptote by epoch 2

        // Rainbow DQN Components
        dueling_hidden_dim: 256,
        num_atoms: 51,
        // v_min/v_max from conservative defaults: reward_scale=10 × 1.5 → ±15
        v_min: -15.0,
        v_max: 15.0,
        n_steps: 5,

        // Stability & Safety
        q_value_clip_min: -5.0,   // Reward v4: Q-values in [-0.5, +0.5], 10x headroom
        q_value_clip_max: 5.0,
        gradient_collapse_multiplier: 100.0,
        gradient_collapse_patience: 5,

        // Portfolio Tracking
        initial_capital: 100_000.0,

        // CQL + IQN (2026 modernization). IQN runs unconditionally inside
        // the cuda_pipeline dual-head with a fixed 5-quantile schedule;
        // `iqn_lambda` (set elsewhere) controls the dual-head loss weight.
        cql_alpha: 1.0,
        cvar_alpha: 0.05,

        minimum_profit_factor: 1.5,
        // SP4 Layer B: `weight_decay` field removed; per-group rates are
        // sourced from ISV[WD_RATE[group]] at runtime.
        adam_epsilon: 1e-8, // f32 Adam (master weights are f32)
        ..Default::default()
    }
}

/// HFT-optimized variant with attention layers and faster updates
///
/// Differences from default config:
/// - Attention layers enabled for temporal pattern recognition (requires network config)
/// - Faster target updates (tau = 0.005)
/// - Smaller warmup (1000 steps)
/// - Larger batch size (512) for throughput
pub(crate) fn dqn_hft_config() -> DQNConfig {
    let mut config = dqn_default_config();

    // HFT-specific overrides
    config.warmup_steps = 1000;
    config.batch_size = 1024;
    config.tau = 0.005;

    config
}

/// Conservative variant for small datasets or cautious exploration
///
/// Differences from default config:
/// - Smaller network (256-128-64)
/// - Lower learning rate (5e-5)
/// - More aggressive exploration decay
/// - Smaller batch size (256)
pub(crate) fn dqn_conservative_config() -> DQNConfig {
    let mut config = dqn_default_config();

    // Conservative overrides
    config.hidden_dims = vec![256, 128, 64];
    config.learning_rate = 5e-5;
    config.batch_size = 256;
    config.epsilon_decay = 0.999;  // Faster decay to conservative policy

    config
}

/// Aggressive variant for maximum exploration and learning
///
/// Differences from default config:
/// - Larger network (768-512-256)
/// - Higher learning rate (3e-4)
/// - Slower exploration decay
/// - Larger batch size (2048)
pub(crate) fn dqn_aggressive_config() -> DQNConfig {
    let mut config = dqn_default_config();

    // Aggressive overrides
    config.hidden_dims = vec![768, 512, 256];
    config.learning_rate = 3e-4;
    config.batch_size = 2048;
    config.epsilon_decay = 0.99995;  // Slower decay, more exploration

    config
}
