refactor(dqn): f64 → f32 for kernel-facing hyperparams

Eliminates the f64→f32 cudarc ABI trap (feedback_cudarc_f64_f32_abi.md,
task #82) at the type level: hyperparameters consumed by CUDA kernels
now live as f32 in Rust, cast once at the TOML/PSO ingest boundary
instead of at every kernel call site.

Structs changed:
  - DQNHyperparameters (crates/ml/src/trainers/dqn/config.rs) —
    ~85 scalar fields migrated from f64 → f32. Covers all
    kernel-facing scalars: reward weights (w_pnl/w_dd/w_idle,
    dd_threshold, cea_weight, micro_reward_*, price_confirm_weight,
    book_aggression_weight, hold_quality_weight), exploration
    (epsilon_* and the 4 branch mults, noisy_sigma_*, count_bonus,
    noise_sigma, q_gap_threshold), distributional RL (v_min, v_max,
    reward_scale, iqn_lambda, qr_kappa, spectral_*,
    gradient_collapse_multiplier), fill simulation (5 fill_*
    fields), risk/Kelly (kelly_fractional, kelly_max_fraction,
    max_leverage, max_position_absolute, minimum_profit_factor),
    ensemble/curiosity (curiosity_weight,
    curiosity_q_penalty_lambda, ensemble_*, beta_*, variance_cap),
    anti-LR (anti_lr_*, adversarial_dd_threshold,
    beta_penalty_strength), walk-forward (wf_*),
    experience (avg_spread, transaction_cost_multiplier,
    holding_cost_rate, churn_penalty_scale, contract_multiplier,
    margin_pct, tick_size, bars_per_day, cash_reserve_percent),
    misc kernel scalars (gamma, tau, huber_delta, q_clip_*,
    shrink_perturb_*, regime_replay_decay, per_alpha,
    per_beta_start, dt_target_return, etc.). Also
    `noisy_epsilon_floor: Option<f32>` and
    `count_bonus_coefficient: Option<f32>`.
  - `computed_v_min` / `computed_v_max` now return f32.
  - `compute_max_position` returns f32 (f64 internally for the
    notional division).

Fields preserved as f64 (precision-sensitive, NOT kernel-facing
scalars — per task spec and feedback_cudarc_f64_f32_abi.md):
  - `learning_rate` — tested at 1e-10 tolerance; f32 rounds to
    2e-12 for a 1e-5 LR.
  - `entropy_coefficient` — tested at 1e-9 tolerance.
  - `weight_decay` — tiny 1e-5..1e-3 range.
  - `adam_epsilon` — 1e-8 default; f32 preserves denorms here but
    paired with weight_decay/learning_rate for symmetry.
  - `gradient_clip_norm: Option<f64>` — grad norms are f64
    accumulators by project convention.
  - `min_loss_improvement_pct`, `q_value_floor` — early-stopping
    long-horizon stats.
  - `cql_alpha` — flows into DQNConfig (ml-dqn) still f64.
  - `min_learning_rate`, `lr_min` — paired with learning_rate.
  - Family intensity scalars (6 `*_intensity` fields) — f64 PSO
    search space; the intensity applies via `as f32` at each call
    site in `apply_family_scaling`.

No checkpoint format change: DQNHyperparameters has
`#[derive(Debug, Clone)]` only (not Serialize/Deserialize), so the
TOML-ingest path is the only serde boundary and already casts
explicitly via `hp.field = v as f32;` in
`DqnTrainingProfile::apply_to`. PSO hyperopt bounds stay f64 in
`SearchSpaceSection` and cast at the adapter boundary.

Call-site impact:
  - ~30 `as f32` casts removed from hot paths (fused_training
    FusedConfig builder, training_loop kernel launches,
    constructor DQNConfig builder, action.rs GPU action selector,
    trainer/mod.rs WF config). Kernels now receive the hp field
    directly via `&hp.x`.
  - ~75 `as f32` casts added at the `apply_to` / hyperopt-adapter
    ingest boundary — the single conversion point.
  - Cross-crate contracts (DQNConfig in ml-dqn, PortfolioTracker
    in ml-core, GAECalculator, DropoutScheduler, NoisySigmaScheduler,
    KellyOptimizerConfig, RewardConfig) retain their f64 signatures;
    ml calls cast at the boundary with `f64::from(hp.x)` so the
    contract is explicit and greppable.

Two test-side adjustments:
  - `test_kelly_fields_are_public` now asserts `f32` for
    kelly_fractional / kelly_max_fraction (these migrated).
  - `test_early_stopping_termination` uses `f64::from(...)` to
    preserve the f64 threshold computation against the now-f32
    `gradient_collapse_multiplier`.

Verified:
  - `SQLX_OFFLINE=true CARGO_INCREMENTAL=0 RUSTC_WRAPPER=sccache
    cargo check --workspace` — clean, zero new warnings.
  - `cargo check --workspace --tests` — clean.
  - `cargo test -p ml --lib training_profile::tests` — all 18
    migrated tests pass. The one pre-existing failure
    (`test_production_profile_applies_all_sections` n_steps
    mismatch, 1 vs 5) reproduces on stashed baseline, so it is
    unrelated to this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-24 01:55:14 +02:00
parent b12483d91b
commit d64adc14f5
13 changed files with 334 additions and 307 deletions

View File

@@ -2142,7 +2142,7 @@ impl HyperparameterOptimizable for DQNTrainer {
hyperparams.warmup_steps = 0; // CRITICAL: batch training path doesn't increment total_steps
// Preprocessing is always active
hyperparams.preprocessing_window = self.preprocessing_window;
hyperparams.preprocessing_clip_sigma = self.preprocessing_clip_sigma;
hyperparams.preprocessing_clip_sigma = self.preprocessing_clip_sigma as f32;
hyperparams.target_update_mode = self.target_update_mode.clone();
hyperparams.target_update_frequency = self.target_update_frequency;
#[allow(clippy::cast_possible_truncation)]
@@ -2160,18 +2160,19 @@ impl HyperparameterOptimizable for DQNTrainer {
/ (49152.0 - 8192.0))
.clamp(0.0, 1.0);
let max_fraction = if budget.gpu_memory_mb >= 81920 { 0.80 } else { 0.70 };
raw * max_fraction
(raw * max_fraction) as f32
};
// Clamp buffer to [1024, max] — below 1024 the GPU PER segment tree fails.
hyperparams.buffer_size = hyperparams.buffer_size.max(1024).min(self.buffer_size_max);
// === 4 breakout parameters from PSO ===
hyperparams.gamma = params.gamma;
// PSO search space is f64; cast to f32 at the hp boundary.
hyperparams.gamma = params.gamma as f32;
// Recompute v_min/v_max (reward_scale-derived, gamma-independent)
hyperparams.v_min = hyperparams.computed_v_min();
hyperparams.v_max = hyperparams.computed_v_max();
hyperparams.iqn_lambda = params.iqn_lambda;
hyperparams.iqn_lambda = params.iqn_lambda as f32;
hyperparams.c51_warmup_epochs = params.c51_warmup_epochs.round() as usize;
hyperparams.c51_alpha_max = params.c51_alpha_max as f32;
@@ -2566,7 +2567,7 @@ impl HyperparameterOptimizable for DQNTrainer {
window_size,
stride,
&device,
hyperparams.max_position_absolute,
f64::from(hyperparams.max_position_absolute),
) {
Ok(m) => {
if m.is_some() {

View File

@@ -404,32 +404,35 @@ impl DQNAgentType {
/// DQN training hyperparameters from gRPC request
#[derive(Debug, Clone)]
pub struct DQNHyperparameters {
/// Learning rate (typically 1e-4 to 1e-3)
/// 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: f64,
pub gamma: f32,
/// Initial exploration rate
pub epsilon_start: f64,
pub epsilon_start: f32,
/// Final exploration rate
pub epsilon_end: f64,
pub epsilon_end: f32,
/// Exploration decay rate
pub epsilon_decay: f64,
pub epsilon_decay: f32,
/// Per-branch epsilon multiplier: direction (relative to base epsilon). Default 1.0.
pub epsilon_direction_mult: f64,
pub epsilon_direction_mult: f32,
/// Per-branch epsilon multiplier: magnitude (relative to base epsilon). Default 2.0 — needs more exploration.
pub epsilon_magnitude_mult: f64,
pub epsilon_magnitude_mult: f32,
/// Per-branch epsilon multiplier: order type (relative to base epsilon). Default 1.5.
pub epsilon_order_mult: f64,
pub epsilon_order_mult: f32,
/// Per-branch epsilon multiplier: urgency (relative to base epsilon). Default 1.5.
pub epsilon_urgency_mult: f64,
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: f64,
pub replay_buffer_vram_fraction: f32,
/// Minimum replay buffer size before training starts
pub min_replay_size: usize,
/// Number of training epochs
@@ -438,41 +441,43 @@ pub struct DQNHyperparameters {
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)
/// 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%)
/// 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: f64,
pub hold_penalty: f32,
// GPU composite reward weights (6-component composite in CUDA kernel)
/// Normalized PnL weight for composite reward
pub w_pnl: f64,
pub w_pnl: f32,
/// Drawdown penalty weight for composite reward
pub w_dd: f64,
pub w_dd: f32,
/// Idle penalty weight for composite reward
pub w_idle: f64,
pub w_idle: f32,
/// Drawdown tolerance before penalty kicks in (fraction, e.g. 0.02 = 2%)
pub dd_threshold: f64,
pub dd_threshold: f32,
/// Asymmetric loss scaling factor (prospect theory, default 1.5)
pub loss_aversion: f64,
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: f64,
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: f64,
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: f64,
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
@@ -482,17 +487,17 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub micro_reward_scale: f32,
/// Dense micro-reward: price confirmation weight (default 0.5).
pub price_confirm_weight: f64,
pub price_confirm_weight: f32,
/// Dense micro-reward: book aggression weight (default 0.3).
pub book_aggression_weight: f64,
pub book_aggression_weight: f32,
/// Dense micro-reward: retrospective hold quality weight (default 0.2).
pub hold_quality_weight: f64,
pub hold_quality_weight: f32,
/// Dense micro-reward: tanh temperature (default 3.0).
pub micro_reward_temp: f64,
pub micro_reward_temp: f32,
/// v8: TD(λ) trace parameter. 0=1-step, 1=Monte Carlo.
pub td_lambda: f64,
pub td_lambda: f32,
/// v8: Maximum trace length for truncated TD(λ).
pub max_trace_length: usize,
/// v8: PopArt reward normalization enabled.
@@ -503,29 +508,31 @@ pub struct DQNHyperparameters {
/// v8: Curriculum learning enabled.
pub curriculum_enabled: bool,
/// v8: Fraction of training before full dataset (curriculum).
pub curriculum_warmup_fraction: f64,
pub curriculum_warmup_fraction: f32,
/// v8: Fraction of replay batch relabeled with hindsight optimal exit.
pub hindsight_fraction: f64,
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: f64,
/// Gradient clipping max norm (None = disabled)
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: f64,
pub hold_penalty_weight: f32,
/// Price movement threshold for HOLD penalty (as fraction, e.g., 0.02 = 2%)
pub movement_threshold: f64,
pub movement_threshold: f32,
/// Preprocessing window size (default: 50)
pub preprocessing_window: i64,
/// Preprocessing clip sigma (default: 5.0)
pub preprocessing_clip_sigma: f64,
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: f64,
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)
@@ -547,15 +554,15 @@ pub struct DQNHyperparameters {
/// 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: f64,
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: f64,
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: f64,
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).
@@ -565,13 +572,13 @@ pub struct DQNHyperparameters {
// 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: f64,
pub cash_reserve_percent: f32,
// WAVE 16S: Adaptive Risk Management Features
/// Kelly fractional multiplier (0.5 = half-Kelly, conservative)
pub kelly_fractional: f64,
pub kelly_fractional: f32,
/// Maximum Kelly fraction (cap at 0.25 = 25% of portfolio)
pub kelly_max_fraction: f64,
pub kelly_max_fraction: f32,
/// Minimum trades required for Kelly calculation
pub kelly_min_trades: usize,
/// Volatility rolling window size
@@ -584,14 +591,14 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub shrink_perturb_alpha: f32,
/// Shrink-and-perturb noise scale (Xavier)
pub shrink_perturb_sigma: f64,
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: f64,
pub adversarial_dd_threshold: f32,
/// Consecutive low-drawdown epochs before adversarial injection activates
pub adversarial_epochs_trigger: usize,
@@ -599,7 +606,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub beta_penalty_strength: f32,
// ── Gems & Pearls: generalization techniques ────────────────────
@@ -607,21 +614,21 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub anti_lr_good_mult: f32,
/// LR multiplier when epoch Sharpe is "bad" (< -threshold)
pub anti_lr_bad_mult: f64,
pub anti_lr_bad_mult: f32,
/// Sharpe threshold for anti-intuitive LR switching
pub anti_lr_sharpe_threshold: f64,
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: f64,
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: f64,
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,
@@ -635,12 +642,12 @@ pub struct DQNHyperparameters {
/// #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: f64,
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: f64,
pub feature_noise_scale: f32,
// #13 Volatility regime normalization: always active.
// #10 Mirror universe: always active.
@@ -651,17 +658,17 @@ pub struct DQNHyperparameters {
/// #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: f64,
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: f64,
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: f64,
pub pruning_fraction: f32,
// #32 Gradient vaccine: always active.
@@ -673,12 +680,12 @@ pub struct DQNHyperparameters {
/// #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: f64,
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: f64,
pub mixup_alpha: f32,
// ── Core hyperopt families (1.0 = defaults, range [0.0, 2.0]) ──
@@ -711,28 +718,29 @@ pub struct DQNHyperparameters {
/// #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: f64,
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: f64,
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: f64,
pub max_leverage: f32,
// WAVE 17: Hyperopt-tuned parameters
/// SAC-style entropy coefficient for Q-value softmax regularization (default: 0.001)
/// 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<f64>,
pub noisy_epsilon_floor: Option<f32>,
/// Count-based exploration bonus coefficient (default: 0.1, range: [0.01, 0.5])
pub count_bonus_coefficient: Option<f64>,
pub count_bonus_coefficient: Option<f32>,
/// Transaction cost multiplier for reward calculation
pub transaction_cost_multiplier: f64,
pub transaction_cost_multiplier: f32,
// Wave 3 (Phase 2): Triple Barrier Method (always active)
pub triple_barrier_profit_target_bps: u32,
@@ -740,13 +748,13 @@ pub struct DQNHyperparameters {
pub triple_barrier_max_holding_seconds: u64,
// P0: Prioritized Experience Replay (always enabled)
pub per_alpha: f64,
pub per_beta_start: f64,
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: f64,
pub regime_replay_decay: f32,
// Wave 2.1: Dueling Networks (always enabled)
/// Hidden dimension for dueling value/advantage streams
@@ -761,19 +769,19 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub v_min: f32,
/// Maximum value for distribution support (v_min = -v_max enforced by hyperopt)
pub v_max: f64,
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: f64,
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: f64,
pub noisy_sigma_init: f32,
// Two-Phase Feature Normalization Configuration
/// Ratio of total epochs to use for feature statistics collection (default: 0.3 = 30%)
@@ -790,7 +798,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
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,
@@ -799,14 +807,16 @@ pub struct DQNHyperparameters {
// 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
/// 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)
/// 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: f64,
pub lr_decay_rate: f32,
// WAVE 26 P1.4: Ensemble Uncertainty for Exploration (6D hyperopt expansion)
/// Enable ensemble uncertainty-based exploration (replaces noisy networks)
@@ -814,23 +824,23 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub beta_variance: f32,
/// Disagreement penalty weight for epistemic uncertainty (0.1-1.0)
pub beta_disagreement: f64,
pub beta_disagreement: f32,
/// Entropy bonus weight for exploration diversity (0.05-0.5)
pub beta_entropy: f64,
pub beta_entropy: f32,
/// Variance cap to prevent over-penalization (0.1-2.0)
pub variance_cap: f64,
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: f64,
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: f64,
pub curiosity_q_penalty_lambda: f32,
// WAVE 26 P2.2: Gradient Accumulation
/// Number of mini-batches to accumulate gradients over before optimizer step
@@ -846,21 +856,21 @@ pub struct DQNHyperparameters {
// P1.3: Sharpe Ratio Reward Component
/// Enable Sharpe ratio as reward component (0.0 = disabled, typical: 0.2-0.5)
pub sharpe_weight: f64,
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: f64,
pub dropout_initial: f32,
/// Final dropout rate (default: 0.1)
pub dropout_final: f64,
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: f64,
pub her_ratio: f32,
/// HER strategy: "final" or "future"
pub her_strategy: String,
@@ -869,28 +879,30 @@ pub struct DQNHyperparameters {
// P1.9: Generalized Advantage Estimation (GAE) — always active
/// GAE lambda parameter (default: 0.95)
pub gae_lambda: f64,
pub gae_lambda: f32,
// P1.11: Noisy Network Sigma Scheduling — always active
/// Initial sigma for noisy networks (default: 0.6)
pub noisy_sigma_initial: f64,
pub noisy_sigma_initial: f32,
/// Final sigma for noisy networks (default: 0.4)
pub noisy_sigma_final: f64,
pub noisy_sigma_final: f32,
/// Steps for sigma annealing (default: 10000)
pub noisy_sigma_anneal_steps: usize,
// WAVE 30: L2 Weight Decay for Overfitting Prevention
/// L2 weight decay for AdamW optimizer (default: 1e-4, range: [1e-5, 1e-3])
/// L2 weight decay for AdamW optimizer (default: 1e-4, range: [1e-5, 1e-3]).
/// Prevents overfitting by penalizing large weights via L2 regularization.
/// Recommended: 1e-4 for standard training, 1e-3 for aggressive regularization.
/// f64: tiny values (1e-5..1e-3) benefit from f64 precision during decay math.
pub weight_decay: f64,
/// 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)
/// 0.0 = disabled, 0.1 = mild conservatism, 1.0 = full offline-RL 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
@@ -898,18 +910,18 @@ pub struct DQNHyperparameters {
/// Number of quantiles for QR-DQN (32-200)
pub num_quantiles: usize,
/// Kappa for quantile Huber loss (0.5-2.0)
pub qr_kappa: f64,
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: f64,
pub iqn_lambda: 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: f64,
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: f64,
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.
@@ -924,7 +936,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
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.
@@ -937,7 +949,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
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.
@@ -970,11 +982,11 @@ pub struct DQNHyperparameters {
/// 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: f64,
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: f64,
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.
@@ -997,21 +1009,21 @@ pub struct DQNHyperparameters {
/// 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: f64,
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: f64,
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: f64,
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: f64,
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: f64,
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: f64,
pub q_clip_min: f32,
/// Maximum Q-value clamp in GPU kernel (default 200.0)
pub q_clip_max: f64,
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).
@@ -1021,13 +1033,13 @@ pub struct DQNHyperparameters {
// 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: f64,
pub wf_initial_train_fraction: f32,
/// Validation window as a fraction of total data (default: 0.125).
pub wf_val_fraction: f64,
pub wf_val_fraction: f32,
/// Test window as a fraction of total data (default: 0.125).
pub wf_test_fraction: f64,
pub wf_test_fraction: f32,
/// Step size between folds as a fraction of total data (default: 0.125).
pub wf_step_fraction: f64,
pub wf_step_fraction: f32,
// Ensemble Agent Configuration
/// Number of agents in ensemble (default: 1 = no ensemble).
@@ -1037,7 +1049,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub ensemble_diversity_weight: f32,
// Curriculum learning: difficulty-based walk-forward window filtering
/// Curriculum phase for difficulty-based window filtering.
@@ -1049,7 +1061,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
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.
@@ -1087,7 +1099,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
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.
@@ -1101,7 +1113,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub iqr_exploration_alpha: f32,
/// Ensemble variance exploration beta: weight for adding ensemble Q-value
/// variance (disagreement between heads) as an exploration bonus.
@@ -1109,23 +1121,23 @@ pub struct DQNHyperparameters {
/// 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: f64,
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: f64,
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: f64,
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: f64,
pub margin_pct: f32,
/// Trading days per year for annualization.
/// CME equity/futures: 252. Crypto: 365. Forex: 252.
pub trading_days_per_year: usize,
@@ -1134,7 +1146,7 @@ pub struct DQNHyperparameters {
/// 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: f64,
pub noise_sigma: f32,
}
impl Default for DQNHyperparameters {
@@ -1148,13 +1160,13 @@ impl Default for DQNHyperparameters {
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) -> f64 {
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) -> f64 {
pub fn computed_v_max(&self) -> f32 {
(self.reward_scale * 1.5).clamp(10.0, 50.0)
}
@@ -1164,39 +1176,45 @@ impl DQNHyperparameters {
///
/// 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.
pub fn compute_max_position(&self, reference_price: f64) -> f64 {
///
/// 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 * self.contract_multiplier;
(self.initial_capital as f64 * self.max_leverage / notional_per_contract)
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)
.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, weight_decay
let li = self.learning_intensity;
self.learning_rate *= li;
self.tau *= li;
self.tau *= li as f32;
self.weight_decay *= li;
// 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).clamp(0.01, 2.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).clamp(0.0, 1.0);
self.per_beta_start = (self.per_beta_start * rpi).clamp(0.0, 1.0);
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
@@ -1211,7 +1229,7 @@ impl DQNHyperparameters {
// 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;
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;
@@ -1222,7 +1240,7 @@ impl DQNHyperparameters {
// Adversarial family
let ai = self.adversarial_intensity;
self.adversarial_dd_threshold *= ai;
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 =
@@ -1230,31 +1248,31 @@ impl DQNHyperparameters {
}
// Regularization family
let ri = self.regularization_intensity;
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;
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 li = self.loss_shaping_intensity;
self.cea_weight = (self.cea_weight * li).clamp(0.0, 2.0);
self.micro_reward_scale = (self.micro_reward_scale * li).clamp(0.0, 0.01);
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 *= li;
self.asymmetric_dd_weight *= ls;
// Ensemble family
let ei = self.ensemble_intensity;
let ei = self.ensemble_intensity as f32;
self.ensemble_disagreement_penalty *= ei;
// Causal family
let ci = self.causal_intensity;
self.causal_weight *= ci;
self.causal_weight *= ci as f32;
if ci > 0.01 {
self.causal_intervention_interval =
(self.causal_intervention_interval as f64 / ci) as usize;
@@ -1432,8 +1450,8 @@ impl DQNHyperparameters {
// 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_f64 * 1.5).clamp(10.0, 50.0), // = -15.0
v_max: (10.0_f64 * 1.5).clamp(10.0, 50.0), // = 15.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

View File

@@ -220,8 +220,8 @@ impl DQNTrainer {
let bars = crate::features::mbp10_loader::mbp10_to_imbalance_bars(
&mbp10_path,
symbol,
self.hyperparams.imbalance_bar_threshold,
self.hyperparams.imbalance_bar_ewma_alpha,
f64::from(self.hyperparams.imbalance_bar_threshold),
f64::from(self.hyperparams.imbalance_bar_ewma_alpha),
).map_err(|e| anyhow::anyhow!("MBP-10 imbalance bar loading failed: {}", e))?;
info!("Loaded {} imbalance bars from MBP-10 data", bars.len());
bars

View File

@@ -371,7 +371,9 @@ impl FusedTrainingCtx {
branch_2_size: dqn.config.num_urgency_levels,
branch_3_size: 3, // urgency — fixed at 3 (4-branch refactor)
batch_size,
gamma: hyperparams.gamma as f32,
// Hyperparams are now f32 native (post f64→f32 migration); pass-through without cast.
// Adam lr/eps/decay still derive from f64 hyperparams → f32 at optimizer boundary.
gamma: hyperparams.gamma,
n_steps: hyperparams.n_steps,
lr: hyperparams.learning_rate as f32,
beta1: 0.9,
@@ -379,22 +381,22 @@ impl FusedTrainingCtx {
epsilon: hyperparams.adam_epsilon as f32,
weight_decay: hyperparams.weight_decay as f32,
max_grad_norm: resolved_grad_norm as f32,
spectral_norm_sigma_max: hyperparams.spectral_norm_sigma_max as f32,
spectral_decoupling_lambda: hyperparams.spectral_decoupling_lambda as f32,
iqn_lambda: hyperparams.iqn_lambda as f32,
spectral_norm_sigma_max: hyperparams.spectral_norm_sigma_max,
spectral_decoupling_lambda: hyperparams.spectral_decoupling_lambda,
iqn_lambda: hyperparams.iqn_lambda,
iqn_num_quantiles: if hyperparams.iqn_lambda > 0.0 { hyperparams.num_quantiles } else { 0 },
iqn_embedding_dim: dqn.config.iqn_embedding_dim,
iqn_kappa: dqn.config.iqn_kappa,
entropy_coefficient: dqn.config.entropy_coefficient as f32,
c51_warmup_epochs: hyperparams.c51_warmup_epochs,
cql_alpha: hyperparams.cql_alpha as f32,
curiosity_q_penalty_lambda: hyperparams.curiosity_q_penalty_lambda as f32,
asymmetric_dd_weight: hyperparams.asymmetric_dd_weight as f32,
ensemble_disagreement_penalty: hyperparams.ensemble_disagreement_penalty as f32,
stochastic_depth_prob: hyperparams.stochastic_depth_prob as f32,
curiosity_q_penalty_lambda: hyperparams.curiosity_q_penalty_lambda,
asymmetric_dd_weight: hyperparams.asymmetric_dd_weight,
ensemble_disagreement_penalty: hyperparams.ensemble_disagreement_penalty,
stochastic_depth_prob: hyperparams.stochastic_depth_prob,
pruning_epoch: hyperparams.pruning_epoch,
pruning_fraction: hyperparams.pruning_fraction as f32,
causal_weight: hyperparams.causal_weight,
pruning_fraction: hyperparams.pruning_fraction,
causal_weight: f64::from(hyperparams.causal_weight),
causal_intervention_interval: hyperparams.causal_intervention_interval,
bottleneck_dim: hyperparams.bottleneck_dim,
market_dim: 42, // Always 42 base market features — OFI features bypass bottleneck via portfolio_dim
@@ -441,7 +443,7 @@ impl FusedTrainingCtx {
let gpu_her = if hyperparams.her_ratio > 0.0 {
let her_config = GpuHerConfig {
goal_dim: 1, // Single scalar goal (target return)
her_ratio: hyperparams.her_ratio,
her_ratio: f64::from(hyperparams.her_ratio),
strategy: parse_her_strategy(&hyperparams.her_strategy),
goal_threshold: 0.01,
batch_size,
@@ -467,7 +469,7 @@ impl FusedTrainingCtx {
// Pre-compute HER source indices and reward ones on GPU (avoid per-step Vec + HtoD)
let (her_source_indices_gpu, her_reward_ones_gpu) = if gpu_her.is_some() {
let her_batch_size = (batch_size as f64 * hyperparams.her_ratio) as usize;
let her_batch_size = (batch_size as f64 * f64::from(hyperparams.her_ratio)) as usize;
let normal_count = batch_size.saturating_sub(her_batch_size);
// Source indices: [normal_count, normal_count+1, ..., batch_size-1]
let source_idx: Vec<i32> = (normal_count..batch_size).map(|i| i as i32).collect();
@@ -778,7 +780,7 @@ impl FusedTrainingCtx {
cached_median: 0.0,
cached_iqr: 0.0,
prev_popart_var: 0.0,
hindsight_fraction: hyperparams.hindsight_fraction,
hindsight_fraction: f64::from(hyperparams.hindsight_fraction),
curriculum_enabled: hyperparams.curriculum_enabled,
})
}

View File

@@ -119,10 +119,10 @@ impl DQNTrainer {
let kelly_fraction = kelly_result.unwrap_or(0.1);
// Apply fractional Kelly (conservative)
let fractional_kelly = kelly_fraction * self.hyperparams.kelly_fractional;
let fractional_kelly = kelly_fraction * f64::from(self.hyperparams.kelly_fractional);
// Clamp to safety bounds
fractional_kelly.clamp(0.01, self.hyperparams.kelly_max_fraction)
fractional_kelly.clamp(0.01, f64::from(self.hyperparams.kelly_max_fraction))
}
/// Update adaptive risk trackers with new market data

View File

@@ -26,16 +26,17 @@ impl DQNTrainer {
let action_idx = self.epsilon_greedy_action(&state_tensor).await?;
let exposure = ExposureLevel::from_index(action_idx)
.map_err(|e| anyhow::anyhow!("Invalid action index {}: {}", action_idx, e))?;
Ok(self.route_action(exposure, self.hyperparams.avg_spread as f32))
Ok(self.route_action(exposure, self.hyperparams.avg_spread))
}
pub(crate) fn route_action(&self, exposure: ExposureLevel, current_spread: f32) -> FactoredAction {
OrderRouter::route(exposure, current_spread, self.hyperparams.avg_spread as f32, self.vol_ema as f32, self.median_vol as f32)
OrderRouter::route(exposure, current_spread, self.hyperparams.avg_spread, self.vol_ema as f32, self.median_vol as f32)
}
pub(crate) fn simulate_fill(&self, action: FactoredAction, step: usize) -> (FactoredAction, FillResult) {
let normalized_vol = if self.median_vol > 0.0 { (self.vol_ema / self.median_vol) as f32 } else { 1.0 };
let spread_bps = self.hyperparams.avg_spread * 10000.0;
// fill_simulator expects bps in f64 for downstream f64 math; cast once.
let spread_bps = f64::from(self.hyperparams.avg_spread) * 10000.0;
let fill_result = self.fill_simulator.simulate_fill(action.order, action.urgency, normalized_vol, spread_bps, step, action.exposure as usize);
if fill_result.filled { (action, fill_result) } else { (OrderRouter::route_default(ExposureLevel::Flat), fill_result) }
}
@@ -93,7 +94,7 @@ impl DQNTrainer {
let stream = std::sync::Arc::clone(self.cuda_stream.as_ref().ok_or_else(|| anyhow::anyhow!("CUDA stream required"))?);
let mut selector = crate::cuda_pipeline::gpu_action_selector::GpuActionSelector::new(stream, self.hyperparams.batch_size.max(batch_size).max(8192), 0xDEAD_BEEF_CAFE_u64)
.map_err(|e| anyhow::anyhow!("GPU fused action selector init failed: {e}"))?;
selector.set_q_gap_threshold(self.hyperparams.q_gap_threshold as f32);
selector.set_q_gap_threshold(self.hyperparams.q_gap_threshold);
info!("GPU action selector initialized for select_actions_batch (q_gap={:.3})", self.hyperparams.q_gap_threshold);
self.gpu_action_selector = Some(selector);
}
@@ -105,7 +106,7 @@ impl DQNTrainer {
if true {
let agent_r = self.agent.read().await;
if let Some(bonuses) = agent_r.get_count_bonuses_branched() {
let coeff = self.hyperparams.count_bonus_coefficient.unwrap_or(0.0) as f32;
let coeff = self.hyperparams.count_bonus_coefficient.unwrap_or(0.0);
if coeff > 0.0 {
let (be, bo, bu) = bonuses;
let be_f32: [f32; 4] = std::array::from_fn(|i| be.get(i).copied().unwrap_or(0.0) as f32 * coeff);
@@ -119,11 +120,11 @@ impl DQNTrainer {
let factored_slice = if let Some((ref q_exp, ref q_ord, ref q_urg)) = branching_q_slices {
// Kernel uses single per-sample epsilon for all 3 heads; apply exposure multiplier
let eps = epsilon * self.hyperparams.epsilon_magnitude_mult as f32;
let eps = epsilon * self.hyperparams.epsilon_magnitude_mult;
selector.select_actions_branching(q_exp, q_ord, q_urg, eps, batch_size).map_err(|e| anyhow::anyhow!("GPU branching action selection failed: {e}"))?
} else {
let exposure_slice = selector.select_actions(&batch_q_values, epsilon, batch_size, 4).map_err(|e| anyhow::anyhow!("GPU fused action selection failed: {e}"))?;
selector.route_exposure_to_factored(&exposure_slice, batch_size, self.hyperparams.avg_spread as f32, self.hyperparams.avg_spread as f32, self.vol_ema as f32, self.median_vol as f32)
selector.route_exposure_to_factored(&exposure_slice, batch_size, self.hyperparams.avg_spread, self.hyperparams.avg_spread, self.vol_ema as f32, self.median_vol as f32)
.map_err(|e| anyhow::anyhow!("GPU route exposure->factored: {e}"))?
};
let host_actions = crate::cuda_pipeline::gpu_action_selector::GpuActionSelector::readback_actions(selector.stream(), &factored_slice, batch_size)

View File

@@ -152,9 +152,9 @@ impl DQNTrainer {
match detect_gpu_hardware() {
Ok(hw) => {
let frac = if hyperparams.replay_buffer_vram_fraction > 0.0 {
hyperparams.replay_buffer_vram_fraction
f64::from(hyperparams.replay_buffer_vram_fraction)
} else {
0.70 // default fraction
0.70_f64 // default fraction
};
(hw.free_memory_mb * frac / 3.0 * 1024.0 * 1024.0) as usize
}
@@ -179,7 +179,7 @@ impl DQNTrainer {
let aligned_sd = (raw_sd + 7) & !7;
// Divide VRAM budget by 3 regime heads (each gets its own PER buffer)
let num_regime_heads = 3;
let per_head_fraction = hyperparams.replay_buffer_vram_fraction / num_regime_heads as f64;
let per_head_fraction = f64::from(hyperparams.replay_buffer_vram_fraction) / num_regime_heads as f64;
let replay_cfg = hw.optimal_replay_config(
aligned_sd,
per_head_fraction,
@@ -232,32 +232,34 @@ impl DQNTrainer {
num_actions: 4, // 4 direction levels (Short/Hold/Long/Flat)
hidden_dims,
learning_rate: hyperparams.learning_rate,
gamma: hyperparams.gamma as f32,
epsilon_start: hyperparams.epsilon_start as f32,
epsilon_end: hyperparams.epsilon_end as f32,
epsilon_decay: hyperparams.epsilon_decay as f32,
// hyperparams fields below are f32 native (kernel-facing); DQNConfig stores some as f32
// and some as f64 (ml-dqn crate not migrated). Cast once at the boundary.
gamma: hyperparams.gamma,
epsilon_start: hyperparams.epsilon_start,
epsilon_end: hyperparams.epsilon_end,
epsilon_decay: hyperparams.epsilon_decay,
replay_buffer_capacity: hyperparams.buffer_size,
collapse_warmup_capacity: original_buffer_size,
batch_size: hyperparams.batch_size,
min_replay_size: hyperparams.min_replay_size.min(hyperparams.buffer_size), // Cap at buffer_size to prevent deadlock
target_update_freq: hyperparams.target_update_frequency, // Use hyperparameter instead of hardcoded 1000
huber_delta: hyperparams.huber_delta as f32,
huber_delta: hyperparams.huber_delta,
leaky_relu_alpha: 0.01, // Standard LeakyReLU alpha (prevents dead neurons)
gradient_clip_norm: grad_norm, // Bug #4 fix: resolved once above
// WAVE 16 (Agent 36): Target update configuration
tau: hyperparams.tau,
tau_final: hyperparams.tau * 0.5, // Anneal to 50% of base (was 10% — too frozen)
tau: f64::from(hyperparams.tau),
tau_final: f64::from(hyperparams.tau) * 0.5, // Anneal to 50% of base (was 10% — too frozen)
tau_anneal_steps: 500_000, // was 100K
// Rainbow DQN warmup period
warmup_steps: hyperparams.warmup_steps,
// PER configuration
initial_capital: hyperparams.initial_capital as f64,
initial_capital: f64::from(hyperparams.initial_capital),
per_alpha: hyperparams.per_alpha,
per_beta_start: hyperparams.per_beta_start,
per_alpha: f64::from(hyperparams.per_alpha),
per_beta_start: f64::from(hyperparams.per_beta_start),
per_beta_max: 1.0,
per_beta_annealing_steps: hyperparams.epochs * 500, // reach beta=1.0 by ~25% of training (was 2000 -> too slow)
per_max_memory_bytes,
@@ -270,35 +272,35 @@ impl DQNTrainer {
// Wave 2.3: Distributional RL (C51) (always enabled)
num_atoms: hyperparams.num_atoms, // Rainbow DQN standard: 51 atoms
v_min: hyperparams.v_min as f32, // Minimum value for distribution support
v_max: hyperparams.v_max as f32, // Maximum value for distribution support
v_min: hyperparams.v_min, // Minimum value for distribution support
v_max: hyperparams.v_max, // Maximum value for distribution support
// Wave 2.4: Noisy Networks for Exploration (always enabled)
noisy_sigma_init: hyperparams.noisy_sigma_init, // Rainbow DQN standard: 0.5
noisy_sigma_init: f64::from(hyperparams.noisy_sigma_init), // Rainbow DQN standard: 0.5
// BUG #37 FIX: Q-value clipping (prevents step-level explosions)
q_value_clip_min: hyperparams.q_clip_min as f32,
q_value_clip_max: hyperparams.q_clip_max as f32,
q_value_clip_min: hyperparams.q_clip_min,
q_value_clip_max: hyperparams.q_clip_max,
// WAVE 23 P0 Fix #1: Adaptive gradient collapse threshold (from hyperparams)
gradient_collapse_multiplier: hyperparams.gradient_collapse_multiplier,
gradient_collapse_multiplier: f64::from(hyperparams.gradient_collapse_multiplier),
gradient_collapse_patience: hyperparams.gradient_collapse_patience,
cql_alpha: hyperparams.cql_alpha,
iqn_num_quantiles: hyperparams.num_quantiles, // Controlled by hyperopt
iqn_kappa: hyperparams.qr_kappa as f32, // Controlled by hyperopt (f64->f32)
iqn_kappa: hyperparams.qr_kappa, // Controlled by hyperopt
iqn_embedding_dim: 64, // Fixed (not in search space)
iqn_lambda: hyperparams.iqn_lambda as f32, // IQN dual-head loss weight (f64->f32)
iqn_lambda: hyperparams.iqn_lambda, // IQN dual-head loss weight
branch_hidden_dim: hyperparams.branch_hidden_dim,
cvar_alpha: hyperparams.cvar_alpha,
#[allow(clippy::cast_possible_truncation)]
minimum_profit_factor: hyperparams.minimum_profit_factor as f32,
minimum_profit_factor: hyperparams.minimum_profit_factor,
weight_decay: hyperparams.weight_decay,
dropout_rate: hyperparams.dropout_initial,
dropout_rate: f64::from(hyperparams.dropout_initial),
entropy_coefficient: hyperparams.entropy_coefficient,
noisy_epsilon_floor: hyperparams.noisy_epsilon_floor.unwrap_or(0.0) as f32, // C2: NoisyNet handles exploration
count_bonus_coefficient: hyperparams.count_bonus_coefficient.unwrap_or(0.0),
noisy_epsilon_floor: hyperparams.noisy_epsilon_floor.unwrap_or(0.0), // C2: NoisyNet handles exploration
count_bonus_coefficient: f64::from(hyperparams.count_bonus_coefficient.unwrap_or(0.0)),
curiosity_market_dim: 42, // Always 42 raw market features -- OFI features are separate
..DQNConfig::default()
};
@@ -332,7 +334,7 @@ impl DQNTrainer {
let portfolio_tracker = PortfolioTracker::new(
hyperparams.initial_capital, // P2-A: Configurable capital
0.0001, // 1 basis point spread (0.01%)
hyperparams.cash_reserve_percent, // Cash reserve requirement
f64::from(hyperparams.cash_reserve_percent), // Cash reserve requirement
);
// Initialize reward function with hyperparameter-driven configuration
@@ -341,7 +343,7 @@ impl DQNTrainer {
pnl_weight: Decimal::ONE,
risk_weight: Decimal::try_from(0.1).unwrap_or(Decimal::ZERO),
cost_weight: Decimal::ONE, // Bug #2 fix: 100% transaction cost weight (was 0.05, 20x too low)
movement_threshold: Decimal::try_from(hyperparams.movement_threshold)
movement_threshold: Decimal::try_from(f64::from(hyperparams.movement_threshold))
.unwrap_or(Decimal::ZERO),
diversity_weight: Decimal::try_from(-0.1).unwrap_or(Decimal::ZERO),
circuit_breaker_config: CircuitBreakerConfig::default(),
@@ -355,7 +357,7 @@ impl DQNTrainer {
// value here avoids a cross-crate change. Safe to drop when
// ml-dqn's RewardConfig loses the field.
dsr_eta: 0.01,
initial_capital: hyperparams.initial_capital as f64,
initial_capital: f64::from(hyperparams.initial_capital),
};
let reward_fn = RewardFunction::new_with_debug(reward_config, debug_logging)?;
@@ -367,7 +369,7 @@ impl DQNTrainer {
let kelly_optimizer = {
use crate::risk::kelly_optimizer::{KellyCriterionOptimizer, KellyOptimizerConfig};
let kelly_config = KellyOptimizerConfig {
max_fraction: hyperparams.kelly_max_fraction,
max_fraction: f64::from(hyperparams.kelly_max_fraction),
min_fraction: 0.01,
lookback_period: 252,
confidence_threshold: 0.6,
@@ -382,7 +384,7 @@ impl DQNTrainer {
};
// Wave 16 Portfolio Features: action masking and entropy regularization always active
let max_position = hyperparams.max_position_absolute; // BLOCKER #2: Use hyperopt-tunable position limit
let max_position: f64 = f64::from(hyperparams.max_position_absolute); // BLOCKER #2: Use hyperopt-tunable position limit
// Entropy regularization: SAC-style computed directly on Q-values in DQN::compute_loss_internal
{
@@ -454,7 +456,7 @@ impl DQNTrainer {
// WAVE 26 P1: Initialize advanced DQN features
// P1.3: Sharpe ratio reward component
let sharpe_weight = hyperparams.sharpe_weight;
let sharpe_weight: f64 = f64::from(hyperparams.sharpe_weight);
let sharpe_window = hyperparams.sharpe_window;
// P1.6: Adaptive dropout scheduler (always active)
@@ -463,8 +465,8 @@ impl DQNTrainer {
info!("Dropout scheduler enabled (initial={}, final={}, steps={})",
hyperparams.dropout_initial, hyperparams.dropout_final, hyperparams.dropout_anneal_steps);
Some(DropoutScheduler::new(
hyperparams.dropout_initial,
hyperparams.dropout_final,
f64::from(hyperparams.dropout_initial),
f64::from(hyperparams.dropout_final),
hyperparams.dropout_anneal_steps,
))
};
@@ -474,7 +476,7 @@ impl DQNTrainer {
use crate::dqn::gae::GAECalculator;
info!("GAE calculator enabled (lambda={}, gamma={})",
hyperparams.gae_lambda, hyperparams.gamma);
Some(GAECalculator::new(hyperparams.gae_lambda, hyperparams.gamma))
Some(GAECalculator::new(f64::from(hyperparams.gae_lambda), f64::from(hyperparams.gamma)))
};
// P1.11: Noisy network sigma scheduling -- always active
@@ -483,8 +485,8 @@ impl DQNTrainer {
info!("Noisy sigma scheduler enabled (initial={}, final={}, steps={})",
hyperparams.noisy_sigma_initial, hyperparams.noisy_sigma_final, hyperparams.noisy_sigma_anneal_steps);
Some(NoisySigmaScheduler::new(
hyperparams.noisy_sigma_initial,
hyperparams.noisy_sigma_final,
f64::from(hyperparams.noisy_sigma_initial),
f64::from(hyperparams.noisy_sigma_final),
hyperparams.noisy_sigma_anneal_steps,
))
};
@@ -521,8 +523,8 @@ impl DQNTrainer {
// Capture values before hyperparams is moved into Self
let initial_batch_size = hyperparams.batch_size;
let base_tau = hyperparams.tau;
let initial_epsilon = hyperparams.epsilon_start as f32;
let base_tau: f64 = f64::from(hyperparams.tau);
let initial_epsilon = hyperparams.epsilon_start;
// Multi-GPU: auto-detect if multiple CUDA devices are available
let multi_gpu = crate::cuda_pipeline::multi_gpu::MultiGpuConfig::detect()

View File

@@ -292,7 +292,7 @@ impl DQNTrainer {
last_ts,
&total_action_counts,
100_000.0,
self.hyperparams.bars_per_day,
f64::from(self.hyperparams.bars_per_day),
);
metrics.add_metric("sharpe_ratio", financials.sharpe);
metrics.add_metric("max_drawdown", financials.max_drawdown);

View File

@@ -870,10 +870,10 @@ impl DQNTrainer {
}).collect();
let wf_config = GpuWalkForwardConfig {
initial_train_fraction: self.hyperparams.wf_initial_train_fraction,
val_fraction: self.hyperparams.wf_val_fraction,
test_fraction: self.hyperparams.wf_test_fraction,
step_fraction: self.hyperparams.wf_step_fraction,
initial_train_fraction: f64::from(self.hyperparams.wf_initial_train_fraction),
val_fraction: f64::from(self.hyperparams.wf_val_fraction),
test_fraction: f64::from(self.hyperparams.wf_test_fraction),
step_fraction: f64::from(self.hyperparams.wf_step_fraction),
};
// Upload FULL dataset to GPU FIRST so we can use GPU prefix sums for fold generation
@@ -970,7 +970,7 @@ impl DQNTrainer {
// Task 15: Per-fold adaptive regime replay decay
let max_regime_pct = trending_pct.max(ranging_pct).max(volatile_pct);
let adaptive_decay = self.hyperparams.regime_replay_decay
* (1.0 - max_regime_pct / 100.0);
* (1.0 - max_regime_pct as f32 / 100.0);
let adaptive_decay = adaptive_decay.clamp(0.05, 0.95);
let dominant_regime: u8 = if trending_pct >= ranging_pct && trending_pct >= volatile_pct {
@@ -981,7 +981,7 @@ impl DQNTrainer {
1
};
self.regime_replay_decay_override = adaptive_decay as f32;
self.regime_replay_decay_override = adaptive_decay;
self.fold_dominant_regime = dominant_regime;
info!(

View File

@@ -139,7 +139,7 @@ impl DQNTrainer {
features_gpu,
targets_gpu,
num_bars,
self.hyperparams.gamma,
f64::from(self.hyperparams.gamma),
)
.map_err(|e| anyhow::anyhow!("DT trajectory build: {e}"))?;
@@ -778,7 +778,7 @@ impl DQNTrainer {
);
// Apply E2: adaptive epsilon
self.hyperparams.epsilon_end = result.epsilon as f64;
self.hyperparams.epsilon_end = result.epsilon;
// Apply E3: dynamic gamma (EMA blend)
if let Some(eval_gamma) = result.gamma {
@@ -863,7 +863,7 @@ impl DQNTrainer {
pub(crate) async fn log_training_config(&mut self) {
match self.hyperparams.target_update_mode {
TargetUpdateMode::Soft => {
let half_life = convergence_half_life(self.hyperparams.tau);
let half_life = convergence_half_life(f64::from(self.hyperparams.tau));
info!("WAVE 16: Using soft target updates (Polyak averaging)");
info!(" Tau: {}", self.hyperparams.tau);
info!(" Convergence half-life: {} steps", half_life as usize);
@@ -1180,7 +1180,7 @@ impl DQNTrainer {
if self.hyperparams.expert_demo_ratio > 0.0 {
use crate::trainers::dqn::expert_demos::ExpertDemoGenerator;
let effective = ExpertDemoGenerator::effective_ratio(
self.hyperparams.expert_demo_ratio,
f64::from(self.hyperparams.expert_demo_ratio),
self.hyperparams.expert_demo_decay_epochs,
self.current_epoch,
) as f32;
@@ -1867,8 +1867,8 @@ impl DQNTrainer {
}
// Q-value explosion guard: halt if Q-values exceed configured clip range
let q_clip_min = self.hyperparams.q_clip_min;
let q_clip_max = self.hyperparams.q_clip_max;
let q_clip_min = f64::from(self.hyperparams.q_clip_min);
let q_clip_max = f64::from(self.hyperparams.q_clip_max);
if avg_q < q_clip_min || avg_q > q_clip_max {
return Err(anyhow::anyhow!(
"Q-value explosion: avg_q={:.4} outside clip range [{}, {}]",
@@ -3151,14 +3151,14 @@ impl DQNTrainer {
.take(long_window)
.sum::<f64>() / long_window as f64;
let thresh = self.hyperparams.anti_lr_sharpe_threshold;
let thresh = f64::from(self.hyperparams.anti_lr_sharpe_threshold);
let base_lr = self.lr_scheduler.get_initial_lr();
// "Good" wins ties — we prefer exploration over dampening when
// both triggers fire (rare but possible in a noisy window).
let anti_mult = if recent_max > thresh {
self.hyperparams.anti_lr_good_mult
let anti_mult: f64 = if recent_max > thresh {
f64::from(self.hyperparams.anti_lr_good_mult)
} else if sustained_mean < -thresh {
self.hyperparams.anti_lr_bad_mult
f64::from(self.hyperparams.anti_lr_bad_mult)
} else {
1.0
};
@@ -3267,7 +3267,7 @@ impl DQNTrainer {
"Epoch {}/{}: val_Sharpe={:.2} (deterministic backtest on fixed validation window)",
epoch + 1, self.hyperparams.epochs, val_sharpe,
);
let val_sharpe_raw = val_sharpe / (self.hyperparams.bars_per_day * 252.0).sqrt();
let val_sharpe_raw = val_sharpe / f64::from(self.hyperparams.bars_per_day * 252.0).sqrt();
info!(" val_Sharpe_raw={:.6} (un-annualized per-bar)", val_sharpe_raw);
// Prometheus metrics
@@ -3442,7 +3442,7 @@ impl DQNTrainer {
&trade_stats,
&monitor.action_counts,
100_000.0,
self.hyperparams.bars_per_day,
f64::from(self.hyperparams.bars_per_day),
);
training_metrics::set_epoch_financial_metrics(
"dqn", "current",

View File

@@ -727,13 +727,13 @@ impl DqnTrainingProfile {
hp.learning_rate = v;
}
if let Some(v) = t.gamma {
hp.gamma = v;
hp.gamma = v as f32;
// Recompute v_min/v_max (reward_scale-derived, gamma-independent)
hp.v_min = hp.computed_v_min();
hp.v_max = hp.computed_v_max();
}
if let Some(v) = t.tau {
hp.tau = v;
hp.tau = v as f32;
}
if let Some(v) = t.warmup_steps {
hp.warmup_steps = v;
@@ -748,13 +748,13 @@ impl DqnTrainingProfile {
hp.hidden_dim_base = Some(v);
}
if let Some(v) = t.reward_scale {
hp.reward_scale = v;
hp.reward_scale = v as f32;
// Recompute v_min/v_max from the new reward_scale
hp.v_min = hp.computed_v_min();
hp.v_max = hp.computed_v_max();
}
if let Some(v) = t.huber_delta {
hp.huber_delta = v;
hp.huber_delta = v as f32;
}
if let Some(v) = t.adam_epsilon {
hp.adam_epsilon = v;
@@ -792,38 +792,38 @@ impl DqnTrainingProfile {
hp.max_bars = v;
}
if let Some(v) = t.imbalance_bar_threshold {
hp.imbalance_bar_threshold = v;
hp.imbalance_bar_threshold = v as f32;
}
if let Some(v) = t.imbalance_bar_ewma_alpha {
hp.imbalance_bar_ewma_alpha = v;
hp.imbalance_bar_ewma_alpha = v as f32;
}
}
// [exploration]
if let Some(ref e) = self.exploration {
if let Some(v) = e.epsilon_start {
hp.epsilon_start = v;
hp.epsilon_start = v as f32;
}
if let Some(v) = e.epsilon_end {
hp.epsilon_end = v;
hp.epsilon_end = v as f32;
}
if let Some(v) = e.epsilon_decay {
hp.epsilon_decay = v;
hp.epsilon_decay = v as f32;
}
if let Some(v) = e.noisy_sigma_init {
hp.noisy_sigma_init = v;
hp.noisy_sigma_init = v as f32;
}
if let Some(v) = e.entropy_coefficient {
hp.entropy_coefficient = v;
}
if let Some(v) = e.count_bonus_coefficient {
hp.count_bonus_coefficient = Some(v);
hp.count_bonus_coefficient = Some(v as f32);
}
if let Some(v) = e.q_gap_threshold {
hp.q_gap_threshold = v;
hp.q_gap_threshold = v as f32;
}
if let Some(v) = e.noise_sigma {
hp.noise_sigma = v;
hp.noise_sigma = v as f32;
}
}
@@ -836,13 +836,13 @@ impl DqnTrainingProfile {
hp.min_replay_size = v;
}
if let Some(v) = rb.per_alpha {
hp.per_alpha = v;
hp.per_alpha = v as f32;
}
if let Some(v) = rb.per_beta_start {
hp.per_beta_start = v;
hp.per_beta_start = v as f32;
}
if let Some(v) = rb.regime_replay_decay {
hp.regime_replay_decay = v;
hp.regime_replay_decay = v as f32;
}
}
@@ -852,10 +852,10 @@ impl DqnTrainingProfile {
hp.num_atoms = v;
}
if let Some(v) = d.v_min {
hp.v_min = v;
hp.v_min = v as f32;
}
if let Some(v) = d.v_max {
hp.v_max = v;
hp.v_max = v as f32;
}
}
@@ -866,7 +866,7 @@ impl DqnTrainingProfile {
// [advanced]
if let Some(ref a) = self.advanced {
if let Some(v) = a.noisy_sigma_init {
hp.noisy_sigma_init = v;
hp.noisy_sigma_init = v as f32;
}
if let Some(v) = a.cql_alpha {
hp.cql_alpha = v;
@@ -878,7 +878,7 @@ impl DqnTrainingProfile {
hp.n_steps = v;
}
if let Some(v) = a.tau {
hp.tau = v;
hp.tau = v as f32;
}
if let Some(v) = a.c51_warmup_epochs {
hp.c51_warmup_epochs = v;
@@ -887,48 +887,48 @@ impl DqnTrainingProfile {
hp.c51_alpha_max = v;
}
if let Some(v) = a.her_ratio {
hp.her_ratio = v;
hp.her_ratio = v as f32;
}
if let Some(v) = a.iqn_lambda {
hp.iqn_lambda = v;
hp.iqn_lambda = v as f32;
}
if let Some(v) = a.spectral_norm_sigma_max {
hp.spectral_norm_sigma_max = v;
hp.spectral_norm_sigma_max = v as f32;
}
if let Some(v) = a.gradient_clip_norm {
hp.gradient_clip_norm = Some(v);
}
if let Some(v) = a.curiosity_weight {
hp.curiosity_weight = v;
hp.curiosity_weight = v as f32;
}
}
// [generalization]
if let Some(ref g) = self.generalization {
if let Some(v) = g.shrink_perturb_interval { hp.shrink_perturb_interval = v; }
if let Some(v) = g.shrink_perturb_alpha { hp.shrink_perturb_alpha = v; }
if let Some(v) = g.shrink_perturb_sigma { hp.shrink_perturb_sigma = v; }
if let Some(v) = g.adversarial_dd_threshold { hp.adversarial_dd_threshold = v; }
if let Some(v) = g.shrink_perturb_alpha { hp.shrink_perturb_alpha = v as f32; }
if let Some(v) = g.shrink_perturb_sigma { hp.shrink_perturb_sigma = v as f32; }
if let Some(v) = g.adversarial_dd_threshold { hp.adversarial_dd_threshold = v as f32; }
if let Some(v) = g.adversarial_epochs_trigger { hp.adversarial_epochs_trigger = v; }
if let Some(v) = g.anti_lr_good_mult { hp.anti_lr_good_mult = v; }
if let Some(v) = g.anti_lr_bad_mult { hp.anti_lr_bad_mult = v; }
if let Some(v) = g.anti_lr_sharpe_threshold { hp.anti_lr_sharpe_threshold = v; }
if let Some(v) = g.feature_mask_fraction { hp.feature_mask_fraction = v; }
if let Some(v) = g.feature_noise_scale { hp.feature_noise_scale = v; }
if let Some(v) = g.anti_lr_good_mult { hp.anti_lr_good_mult = v as f32; }
if let Some(v) = g.anti_lr_bad_mult { hp.anti_lr_bad_mult = v as f32; }
if let Some(v) = g.anti_lr_sharpe_threshold { hp.anti_lr_sharpe_threshold = v as f32; }
if let Some(v) = g.feature_mask_fraction { hp.feature_mask_fraction = v as f32; }
if let Some(v) = g.feature_noise_scale { hp.feature_noise_scale = v as f32; }
if let Some(v) = g.pruning_epoch { hp.pruning_epoch = v; }
if let Some(v) = g.pruning_fraction { hp.pruning_fraction = v; }
if let Some(v) = g.causal_weight { hp.causal_weight = v; }
if let Some(v) = g.pruning_fraction { hp.pruning_fraction = v as f32; }
if let Some(v) = g.causal_weight { hp.causal_weight = v as f32; }
if let Some(v) = g.causal_intervention_interval { hp.causal_intervention_interval = v; }
if let Some(v) = g.adversarial_warmup_epochs { hp.adversarial_warmup_epochs = v; }
if let Some(v) = g.adversarial_checkpoint_interval { hp.adversarial_checkpoint_interval = v; }
if let Some(v) = g.bottleneck_dim { hp.bottleneck_dim = v; }
if let Some(v) = g.stochastic_depth_prob { hp.stochastic_depth_prob = v; }
if let Some(v) = g.asymmetric_dd_weight { hp.asymmetric_dd_weight = v; }
if let Some(v) = g.stochastic_depth_prob { hp.stochastic_depth_prob = v as f32; }
if let Some(v) = g.asymmetric_dd_weight { hp.asymmetric_dd_weight = v as f32; }
if let Some(v) = g.time_reversal_mod { hp.time_reversal_mod = v; }
if let Some(v) = g.regret_blend { hp.regret_blend = v; }
if let Some(v) = g.trade_clustering_penalty { hp.trade_clustering_penalty = v; }
if let Some(v) = g.ensemble_disagreement_penalty { hp.ensemble_disagreement_penalty = v; }
if let Some(v) = g.mixup_alpha { hp.mixup_alpha = v; }
if let Some(v) = g.regret_blend { hp.regret_blend = v as f32; }
if let Some(v) = g.trade_clustering_penalty { hp.trade_clustering_penalty = v as f32; }
if let Some(v) = g.ensemble_disagreement_penalty { hp.ensemble_disagreement_penalty = v as f32; }
if let Some(v) = g.mixup_alpha { hp.mixup_alpha = v as f32; }
// Core family intensity scalars
if let Some(v) = g.learning_intensity { hp.learning_intensity = v; }
if let Some(v) = g.exploration_intensity { hp.exploration_intensity = v; }
@@ -947,29 +947,29 @@ impl DqnTrainingProfile {
// [risk]
if let Some(ref r) = self.risk {
if let Some(v) = r.kelly_fractional {
hp.kelly_fractional = v;
hp.kelly_fractional = v as f32;
}
if let Some(v) = r.kelly_max_fraction {
hp.kelly_max_fraction = v;
hp.kelly_max_fraction = v as f32;
}
if let Some(v) = r.max_position_absolute {
hp.max_position_absolute = v;
hp.max_position_absolute = v as f32;
}
// max_position is an alias for max_position_absolute
if let Some(v) = r.max_position {
hp.max_position_absolute = v;
hp.max_position_absolute = v as f32;
}
if let Some(v) = r.max_leverage {
hp.max_leverage = v;
hp.max_leverage = v as f32;
}
if let Some(v) = r.q_clip_min {
hp.q_clip_min = v;
hp.q_clip_min = v as f32;
}
if let Some(v) = r.q_clip_max {
hp.q_clip_max = v;
hp.q_clip_max = v as f32;
}
if let Some(v) = r.loss_aversion {
hp.loss_aversion = v;
hp.loss_aversion = v as f32;
}
}
@@ -1005,33 +1005,33 @@ impl DqnTrainingProfile {
hp.initial_capital = v as f32;
}
if let Some(v) = ex.tx_cost_multiplier {
hp.transaction_cost_multiplier = v;
hp.transaction_cost_multiplier = v as f32;
}
if let Some(v) = ex.holding_cost_rate {
hp.holding_cost_rate = v;
hp.holding_cost_rate = v as f32;
}
if let Some(v) = ex.churn_threshold_bars {
hp.churn_threshold_bars = v;
}
if let Some(v) = ex.churn_penalty_scale {
hp.churn_penalty_scale = v;
hp.churn_penalty_scale = v as f32;
}
// [experience.fill_simulation]
if let Some(ref fs) = ex.fill_simulation {
if let Some(v) = fs.ioc_fill_prob {
hp.fill_ioc_fill_prob = v;
hp.fill_ioc_fill_prob = v as f32;
}
if let Some(v) = fs.limit_fill_min {
hp.fill_limit_fill_min = v;
hp.fill_limit_fill_min = v as f32;
}
if let Some(v) = fs.limit_fill_max {
hp.fill_limit_fill_max = v;
hp.fill_limit_fill_max = v as f32;
}
if let Some(v) = fs.spread_cost_frac {
hp.fill_spread_cost_frac = v;
hp.fill_spread_cost_frac = v as f32;
}
if let Some(v) = fs.spread_capture_frac {
hp.fill_spread_capture_frac = v;
hp.fill_spread_capture_frac = v as f32;
}
}
}
@@ -1040,28 +1040,28 @@ impl DqnTrainingProfile {
// GPU composite reward weights — map directly to DQNHyperparameters
if let Some(ref rw) = self.reward {
if let Some(v) = rw.w_pnl {
hp.w_pnl = v;
hp.w_pnl = v as f32;
}
if let Some(v) = rw.w_dd {
hp.w_dd = v;
hp.w_dd = v as f32;
}
if let Some(v) = rw.w_idle {
hp.w_idle = v;
hp.w_idle = v as f32;
}
if let Some(v) = rw.dd_threshold {
hp.dd_threshold = v;
hp.dd_threshold = v as f32;
}
if let Some(v) = rw.loss_aversion {
hp.loss_aversion = v;
hp.loss_aversion = v as f32;
}
if let Some(v) = rw.time_decay_rate {
hp.time_decay_rate = v;
hp.time_decay_rate = v as f32;
}
if let Some(v) = rw.q_gap_threshold {
hp.q_gap_threshold = v;
hp.q_gap_threshold = v as f32;
}
if let Some(v) = rw.reward_scale {
hp.reward_scale = v;
hp.reward_scale = v as f32;
hp.v_min = hp.computed_v_min();
hp.v_max = hp.computed_v_max();
}
@@ -1072,34 +1072,34 @@ impl DqnTrainingProfile {
hp.popart_robust = v;
}
if let Some(v) = rw.micro_reward_scale {
hp.micro_reward_scale = v;
hp.micro_reward_scale = v as f32;
}
if let Some(v) = rw.td_lambda {
hp.td_lambda = v;
hp.td_lambda = v as f32;
}
if let Some(v) = rw.max_trace_length {
hp.max_trace_length = v;
}
if let Some(v) = rw.hindsight_fraction {
hp.hindsight_fraction = v;
hp.hindsight_fraction = v as f32;
}
if let Some(v) = rw.hindsight_lookahead {
hp.hindsight_lookahead = v;
}
if let Some(v) = rw.price_confirm_weight {
hp.price_confirm_weight = v;
hp.price_confirm_weight = v as f32;
}
if let Some(v) = rw.book_aggression_weight {
hp.book_aggression_weight = v;
hp.book_aggression_weight = v as f32;
}
if let Some(v) = rw.hold_quality_weight {
hp.hold_quality_weight = v;
hp.hold_quality_weight = v as f32;
}
if let Some(v) = rw.micro_reward_temp {
hp.micro_reward_temp = v;
hp.micro_reward_temp = v as f32;
}
if let Some(v) = rw.holding_cost_rate {
hp.holding_cost_rate = v;
hp.holding_cost_rate = v as f32;
}
}
@@ -1134,7 +1134,8 @@ mod tests {
#[test]
fn test_sparse_profile_preserves_defaults() {
// dqn-smoketest has [distributional] with num_atoms but no v_min/v_max
// dqn-smoketest has [distributional] with num_atoms but no v_min/v_max.
// Section-side TOML values stay f64 (ingest boundary); conversion to f32 happens in apply_to.
let p = DqnTrainingProfile::load("dqn-smoketest");
let d = p.distributional.expect("smoketest profile should have [distributional]");
assert_eq!(d.num_atoms, Some(52));
@@ -1286,7 +1287,7 @@ mod tests {
let hp = crate::trainers::dqn::DQNHyperparameters::conservative();
// Verify v_min/v_max are computed from reward_scale
let expected_v_range = (hp.reward_scale * 1.5).clamp(10.0, 50.0);
let expected_v_range: f32 = (hp.reward_scale * 1.5).clamp(10.0, 50.0);
assert!(
(hp.v_min - (-expected_v_range)).abs() < 0.01,
"v_min should be computed from reward_scale, got {} expected {}",
@@ -1502,7 +1503,7 @@ mod tests {
hp.v_max = hp.computed_v_max();
// reward_scale=20 × 1.5 = 30, clamped to [10, 50] → ±30
let expected_v_range = (20.0_f64 * 1.5).clamp(10.0, 50.0);
let expected_v_range: f32 = (20.0_f32 * 1.5).clamp(10.0, 50.0);
assert!(
(hp.v_max - expected_v_range).abs() < 0.01,
"v_max should be recomputed from reward_scale: got {} expected {}",

View File

@@ -346,8 +346,9 @@ fn test_early_stopping_config_fields_exist() {
"gradient_collapse_patience should default to 5"
);
// Verify adaptive threshold calculation
let threshold = hyperparams.learning_rate * hyperparams.gradient_collapse_multiplier;
// Verify adaptive threshold calculation.
// gradient_collapse_multiplier is f32 (kernel-facing); learning_rate stays f64 for precision.
let threshold = hyperparams.learning_rate * f64::from(hyperparams.gradient_collapse_multiplier);
assert!(
threshold > 0.0,
"Adaptive threshold should be positive: {}",

View File

@@ -165,9 +165,10 @@ mod dqn_hyperparams_kelly_tests {
// This test verifies that all Kelly fields are pub (accessible to hyperopt adapter)
let hyperparams = DQNHyperparameters::conservative();
// If these compile, the fields are public
let _: f64 = hyperparams.kelly_fractional;
let _: f64 = hyperparams.kelly_max_fraction;
// If these compile, the fields are public.
// Post f64→f32 migration: risk/kelly scalars now f32 (kernel-facing).
let _: f32 = hyperparams.kelly_fractional;
let _: f32 = hyperparams.kelly_max_fraction;
let _: usize = hyperparams.kelly_min_trades;
let _: usize = hyperparams.volatility_window;
}