diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index fc51b58a9..e4763d5c0 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -260,7 +260,8 @@ extern "C" __global__ void experience_state_gather( /* Return over N bars */ float ret = (close_past > 0.0f) ? (close_now - close_past) / close_past : 0.0f; - out[slot + 0] = ret * 100.0f; /* scale to percentage */ + float scaled_ret = ret * 100.0f; + out[slot + 0] = fmaxf(-10.0f, fminf(10.0f, scaled_ret)); /* clamp ±10% */ /* Volatility: scan high/low over window (approx from close changes) */ float max_val = close_now; @@ -278,12 +279,14 @@ extern "C" __global__ void experience_state_gather( } } float range = (close_now > 0.0f) ? (max_val - min_val) / close_now : 0.0f; - out[slot + 1] = range * 100.0f; /* volatility as percentage */ + float scaled_range = range * 100.0f; + out[slot + 1] = fmaxf(0.0f, fminf(10.0f, scaled_range)); /* clamp 0-10% */ /* Volume trend: current vs average */ float avg_vol = (vol_count > 0) ? vol_sum / (float)vol_count : 1.0f; float cur_vol = (market_dim > 4) ? now_row[4] : 1.0f; - out[slot + 2] = (avg_vol > 0.0f) ? cur_vol / avg_vol : 1.0f; + float vol_ratio = (avg_vol > 0.0f) ? cur_vol / avg_vol : 1.0f; + out[slot + 2] = fmaxf(0.0f, fminf(5.0f, vol_ratio)); /* clamp 0-5x */ /* Momentum: position within range [0=bottom, 1=top] */ float range_size = max_val - min_val; @@ -646,6 +649,9 @@ extern "C" __global__ void experience_env_step( : 0.0f; /* ── Enhanced Kelly: weighted blend ── */ + /* NaN guard: if either Kelly produces NaN/Inf, use 0.0 (safe default) */ + if (isnan(continuous_kelly) || isinf(continuous_kelly)) continuous_kelly = 0.0f; + if (isnan(discrete_kelly) || isinf(discrete_kelly)) discrete_kelly = 0.0f; float enhanced_kelly = 0.7f * continuous_kelly + 0.3f * discrete_kelly; /* ── Confidence scaling: penalize small samples ── */ @@ -662,6 +668,12 @@ extern "C" __global__ void experience_env_step( target_position *= kelly_frac / 0.25f; } /* < 20 trades: no Kelly applied (full position until we have statistics) */ + + /* Final NaN/Inf guard on target_position after ALL scaling. + * If any scaling (CVaR, conviction, Kelly) produced NaN, zero the position. */ + if (isnan(target_position) || isinf(target_position)) { + target_position = 0.0f; + } } /* ---- Position adjustment with volatility-scaled transaction cost ---- */ @@ -1056,6 +1068,9 @@ extern "C" __global__ void experience_env_step( * v_range should be dynamically computed from gamma. */ /* ---- Write reward and done flag ---- */ + /* Final NaN guard — if reward is NaN/Inf, write 0.0 instead of poisoning + * the replay buffer. This prevents gradient explosion from propagating. */ + if (isnan(reward) || isinf(reward)) reward = 0.0f; out_rewards[out_off] = reward; out_dones[out_off] = (float)done;