fix(trading): 30% Flat floor + consistent mean-logit everywhere

Three fixes for production readiness:

1. Direction Flat floor: 30% unconditional Flat probability prevents
   over-trading (was 7% Flat = 93% directional = massive cost drag).
   Hard constraint, not Q-dependent, can't snowball. Combined with
   hold enforcement (min_hold_bars=10), actual Flat is ~13%.

2. MSE loss online_eq + target_eq: consistent mean-logit for d<=1.
   Both sides of TD error use the same representation — no mismatch.
   Removes the last C51 softmax bias from magnitude gradient path.
   Half grows 2.7%→5.7%, Full grows 2.7%→5.3% across epochs.

3. compute_expected_q: mean-logit for d<=1 (action selection + eval).
   Safe — not in training loss path.

Result: 7/7 diversity sustained, Flat stable at 13%, Sharpe positive
at 3/4 epochs. 19/19 smoke tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-09 01:44:31 +02:00
parent 75bd6192b1
commit f4295e6902

View File

@@ -822,34 +822,32 @@ extern "C" __global__ void experience_action_select(
q_max_d = fmaxf(q_max_d, qv);
q_min_d = fminf(q_min_d, qv);
}
/* Direction temperature: 2× Q-range ensures Short/Long always get ≥15%
* probability even when Flat Q is highest. Without this, Flat dominance
* grows to 90%+ as training progresses (zero-cost Flat has inherent Q advantage).
* 2× gives max ratio e^0.5:1 ≈ 1.65:1 between best and worst direction. */
float tau_d = fmaxf((q_max_d - q_min_d) * 2.0f, 0.01f);
float sum_e = 0.0f;
float exps_d[3]; /* b0_size is always 3 (Short/Flat/Long) */
for (int a = 0; a < b0_size; a++) {
float qv = __bfloat162float(q_b0[a]);
exps_d[a] = expf((qv - q_max_d) / tau_d);
sum_e += exps_d[a];
/* Direction selection: Flat floor + Boltzmann for directional.
*
* 30% of the time: force Flat (prevents over-trading on production).
* 70% of the time: Boltzmann over Short/Flat/Long proportional to Q.
* This guarantees ≥30% Flat regardless of Q-values or hold enforcement,
* without the Q-gap conviction filter's snowball dynamics. */
if (lcg_random(&rng) < 0.30f) {
dir_idx = 1; /* Flat floor: 30% unconditional */
} else {
/* Boltzmann over all 3 directions for remaining 70% */
float tau_d = fmaxf(q_max_d - q_min_d, 0.01f);
float sum_e = 0.0f;
float exps_d[3];
for (int a = 0; a < b0_size; a++) {
float qv = __bfloat162float(q_b0[a]);
exps_d[a] = expf((qv - q_max_d) / tau_d);
sum_e += exps_d[a];
}
float r = lcg_random(&rng) * sum_e;
float cum = 0.0f;
dir_idx = b0_size - 1;
for (int a = 0; a < b0_size; a++) {
cum += exps_d[a];
if (r < cum) { dir_idx = a; break; }
}
}
float r = lcg_random(&rng) * sum_e;
float cum = 0.0f;
dir_idx = b0_size - 1;
for (int a = 0; a < b0_size; a++) {
cum += exps_d[a];
if (r < cum) { dir_idx = a; break; }
}
/* Q-gap conviction filter REMOVED from training path.
* Boltzmann already encodes conviction through softmax probabilities.
* The filter was redundant and harmful: after a high-Sharpe epoch, Bellman
* max bootstraps raise Q(Flat) towards Q(directional), shrinking the gap.
* The filter then overrides Boltzmann's directional samples → Flat grows
* → less directional experience → model loses directional learning signal.
* Conviction gating is preserved in the backtest evaluator (eps=0 path). */
}
/* Gem 3: Flat detection shortcut — when direction=Flat, magnitude is irrelevant.