diff --git a/crates/ml/src/cuda_pipeline/backtest_plan_kernel.cu b/crates/ml/src/cuda_pipeline/backtest_plan_kernel.cu index d29bfdb78..062b4c49b 100644 --- a/crates/ml/src/cuda_pipeline/backtest_plan_kernel.cu +++ b/crates/ml/src/cuda_pipeline/backtest_plan_kernel.cu @@ -160,13 +160,15 @@ extern "C" __global__ void backtest_plan_state_isv( pisv[PLAN_ISV_PROGRESS] = (plan_tgt_bars > plan_thr) ? fminf(hold_time / plan_tgt_bars, 2.0f) : 0.0f; - /* [1] PnL vs profit target [bounded 2.0] */ + /* [1] PnL vs profit target [symmetric bound -2.0..2.0] + * unrealized can be negative → fminf alone leaves unbounded lower tail. + * Mirror of SP11 symmetric reward cap (35db31089). */ pisv[PLAN_ISV_PNL_VS_TARGET] = (plan_profit > 1e-6f) - ? fminf(unrealized / (plan_profit * equity + 1e-6f), 2.0f) : 0.0f; + ? fmaxf(-2.0f, fminf(unrealized / (plan_profit * equity + 1e-6f), 2.0f)) : 0.0f; - /* [2] PnL vs stop (negative PnL vs stop → loss approaching) */ + /* [2] PnL vs stop (negative PnL vs stop → loss approaching) [symmetric bound -2.0..2.0] */ pisv[PLAN_ISV_PNL_VS_STOP] = (plan_stop > 1e-6f) - ? fminf(-unrealized / (plan_stop * equity + 1e-6f), 2.0f) : 0.0f; + ? fmaxf(-2.0f, fminf(-unrealized / (plan_stop * equity + 1e-6f), 2.0f)) : 0.0f; /* [3] Entry conviction (stable across the trade) */ pisv[PLAN_ISV_ENTRY_CONVICTION] = plan_conv_ent; diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index 65fc38a2e..dcc10f6b1 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -846,11 +846,14 @@ extern "C" __global__ void experience_state_gather( plan_isv[PLAN_ISV_PROGRESS] = (plan_tgt_bars > plan_thr_sg) ? fminf(f_hold_time / plan_tgt_bars, 2.0f) /* plan progress [0, 2] */ : 0.0f; + /* Symmetric clamp [-2, 2]: f_unrealized_pnl can be negative → fminf alone leaves + * an unbounded lower tail (feature jitter at policy input). Mirror of SP11 + * symmetric reward cap (35db31089). See pearl_symmetric_clamp_audit. */ plan_isv[PLAN_ISV_PNL_VS_TARGET] = (plan_profit > 1e-6f) - ? fminf(f_unrealized_pnl / (plan_profit * f_equity + 1e-6f), 2.0f) /* pnl vs target */ + ? fmaxf(-2.0f, fminf(f_unrealized_pnl / (plan_profit * f_equity + 1e-6f), 2.0f)) /* pnl vs target */ : 0.0f; plan_isv[PLAN_ISV_PNL_VS_STOP] = (plan_stop > 1e-6f) - ? fminf(-f_unrealized_pnl / (plan_stop * f_equity + 1e-6f), 2.0f) /* pnl vs stop */ + ? fmaxf(-2.0f, fminf(-f_unrealized_pnl / (plan_stop * f_equity + 1e-6f), 2.0f)) /* pnl vs stop */ : 0.0f; plan_isv[PLAN_ISV_ENTRY_CONVICTION] = plan_conv; /* conviction at entry [0, 1] */ diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index a1107a5d1..bdca78332 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -5687,3 +5687,31 @@ new HEALTH_DIAG line, the follow-up commit will: Per `feedback_no_partial_refactor`: instrumentation lands as one atomic commit; removal lands as one atomic commit (with the fix). + +## SP11 plan_isv symmetric clamp — 4 mirror sites (2026-05-04) + +The implementer of `35db31089` (symmetric reward cap) flagged 4 +additional asymmetric-clamp sites in `plan_isv` policy-input +features, mirroring the same bug class but on the feature side +rather than reward side: + +| Site | Slot | Bug | +|---------------------------------------|-----------------------|----------------------------------------| +| `experience_kernels.cu:850` | `PLAN_ISV_PNL_VS_TARGET` | `fminf(.., 2.0)` capped above only | +| `experience_kernels.cu:853` | `PLAN_ISV_PNL_VS_STOP` | `fminf(.., 2.0)` capped above only | +| `backtest_plan_kernel.cu:164` | `PLAN_ISV_PNL_VS_TARGET` | mirror of 850 | +| `backtest_plan_kernel.cu:168` | `PLAN_ISV_PNL_VS_STOP` | mirror of 853 | + +`unrealized` P&L can be negative (and `-unrealized` becomes negative +when the position is profitable). With only `fminf(x, 2.0)`, the +lower tail is unbounded, producing feature jitter at the policy +input under losing-trade conditions. + +Fix: bilateral clamp via `fmaxf(-2.0, fminf(x, 2.0))`. These slots +feed the network state vector at line 956 of `experience_kernels.cu` +(consumer site) and at the `pisv` write-out in `backtest_plan_kernel`. + +This is the policy-input mirror of the reward-side bug fixed in +`35db31089`. Both share the same architectural lesson — see +`pearl_symmetric_clamp_audit` (added in the SP11 close-out commit) +and `pearl_bounded_modifier_outputs_require_structural_activation`.