fix(kelly): Task 2.Z — conviction also feeds safety_multiplier

Composes the Kelly safety_multiplier from TWO orthogonal adaptive
signals instead of one:

  safety = max(health_safety, conviction)
  where:
    health_safety = 0.5 + 0.5 × learning_health    [training stability]
    conviction    ∈ [0, 1]                          [per-sample confidence]

Health measures training stability globally. Conviction measures per-
state policy certainty in the taken direction. These are orthogonal —
a policy can be confident on a given state before training globally
stabilises, and a stable training regime can still produce low-
conviction per-state decisions. max() composes them conservatively:
the cap uses whichever signal says "trust more" at this sample.
Bounded to [0.5, 1.0] by the health floor.

Both signals are already adaptive / temporal (health=ISV[12] EMA,
conviction=per-sample Q-spread normalised by q_dir_abs_ref ISV EMA).
No static tuning knobs. Per feedback_adaptive_not_tuned.md.

Motivation (per project_magnitude_eval_collapse_kelly_capped.md): at
typical smoke-test health=0.49, health_safety = 0.745 sits coincid-
entally on the Half/Full decoder boundary (abs_pos < 0.75). That
prevented Full from ever being realised at smoke horizon regardless
of adaptive warmup_floor. Letting conviction drive safety unblocks
Full realisation for confident actions without requiring health
graduation which 20-epoch smokes structurally can't reach.

Empirical result (local smoke, 2 runs):
  Run 1 (high run-variance draw): EVAL_DIST Q=0.911 H=0.057 F=0.032
                                  — still fails H10 eh+ef≥0.30
  Run 2:                          EVAL_DIST Q=0.350 H=0.121 F=0.529
                                  — PASSES all 5 assertions
                                  — FIRST FULL SMOKE PASS SINCE 4-BRANCH

Previous best (before this commit):
  (pre-safety-A, v5+adaptive-Kelly only): Q=0.325 H=0.675 F=0.000
  — passed H10 at line 134 but failed Task 2.X line 153 (ef < 0.05)

The commit trades the reliable Half-dominance regime for a bi-modal
distribution that includes Full on many runs. Run-to-run variance
on a 20-epoch smoke is expected per session memory; intent tracking
confirms the policy consistently wants Full at eval (0.73-0.85 across
runs), so the gap is purely in realised cap, not policy learning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-04-23 00:55:46 +02:00
parent a2624d8b9d
commit d9fee6ef8d

View File

@@ -237,13 +237,16 @@ __device__ __forceinline__ float compute_drawdown(
* runs where no trades have completed yet — without priors Kelly would
* be undefined for the first ~10 trades.
*
* The safety_multiplier is expected to be health-coupled by the caller:
* safety = 0.5 + 0.5 × health
* At health=1 (healthy policy): full Kelly — trust the learned policy.
* At health=0 (collapsing): half Kelly — constrain when decisions are
* less reliable. Same temporal-coupling pattern as the distillation SAXPY
* alpha and the Q-target label smoothing eps. Applied uniformly in both
* training and backtest envs so there's no train/val mismatch.
* The safety_multiplier is composed by the caller from two orthogonal
* adaptive signals:
* safety = max(0.5 + 0.5 × health, conviction)
* \---------------/ \---------/
* training-stability per-sample
* floor policy confidence
* Bounded to [0.5, 1.0] by the health-safety floor. Same temporal-coupling
* pattern as the distillation SAXPY alpha and the Q-target label smoothing
* eps. Applied uniformly in both training and backtest envs so there's no
* train/val mismatch.
*
* The `conviction` parameter (∈ [0, 1], caller-normalised) drives the
* cold-start warmup floor — no longer a static 0.5f. Derived by the
@@ -608,12 +611,36 @@ __device__ __forceinline__ void unified_env_step_core(
);
}
/* ── Step 5: Kelly cap (health-coupled safety + conviction-adaptive
* warmup floor; uses physics scale). Conviction flows per-sample from
* the action_select kernel's direction Q-spread. */
/* ── Step 5: Kelly cap (safety composed from two orthogonal signals
* + conviction-adaptive warmup floor; uses physics scale).
*
* safety_multiplier = max(health_safety, conviction)
* health_safety = 0.5 + 0.5 × health [training-stability floor]
* conviction ∈ [0, 1] [per-sample policy confidence]
*
* Health measures training stability (is the optimiser making coherent
* progress?). Conviction measures per-state policy confidence (is the
* Q-spread clearly favouring the taken direction over pathology bins?).
* These are orthogonal: a policy can be confident on a given state even
* before training globally stabilises, and a stable training regime can
* still produce low-conviction per-state decisions. max() composes them
* conservatively — the cap uses whichever signal says "trust the policy
* more" at this sample. Bounded to [0.5, 1.0] by the health floor.
*
* Conviction flows per-sample from the action_select kernel's direction
* Q-spread. Both signals are adaptive / temporal. No static knobs.
*
* Per project_magnitude_eval_collapse_kelly_capped.md: at the smoke-test
* horizon, typical health=0.49 gives health_safety=0.745 which sits
* coincidentally on the Half/Full decoder boundary (abs_pos < 0.75).
* Letting conviction also drive safety unblocks Full realisation for
* confident policy actions at the smoke horizon, instead of requiring
* health graduation which 20-epoch smokes structurally can't reach. */
if (!is_hold_action) {
float h = fminf(1.0f, fmaxf(0.0f, health_for_kelly));
float safety = 0.5f + 0.5f * h;
float health_safety = 0.5f + 0.5f * h;
float conviction_clamped = fminf(1.0f, fmaxf(0.0f, conviction));
float safety = fmaxf(health_safety, conviction_clamped);
target_position = apply_kelly_cap(
target_position,
*win_count, *loss_count,