diff --git a/crates/ml/src/cuda_pipeline/trade_physics.cuh b/crates/ml/src/cuda_pipeline/trade_physics.cuh index 5fe68bb90..f72d98785 100644 --- a/crates/ml/src/cuda_pipeline/trade_physics.cuh +++ b/crates/ml/src/cuda_pipeline/trade_physics.cuh @@ -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,