fix: NaN guards + multi-timeframe feature clamping

Gradient explosion from unbounded multi-timeframe features (240-bar
return could be ±50%). All multi-timeframe outputs now clamped:
- Return: ±10%
- Volatility: 0-10%
- Volume ratio: 0-5x
- Momentum: [0, 1] (already bounded)

NaN guards added:
- Kelly: NaN check on continuous/discrete Kelly before blend
- target_position: final NaN→0 after all scaling
- reward: final NaN→0 before writing to replay buffer

Local test shows Q-gap=18.7 (epoch 5) — 18x better than start-of-session 0.000.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-23 14:07:21 +01:00
parent b7ddf812cb
commit 1936c160b5

View File

@@ -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;