From b8788511ce561014ce918a900ce67ac2f208398d Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Mon, 27 Apr 2026 12:05:49 +0200 Subject: [PATCH] =?UTF-8?q?fix(dqn):=20mag=5Fstats=20wr=5Fh/wr=5Ff=20attri?= =?UTF-8?q?bution=20=E2=80=94=20bin=20trade=20closes=20by=20pre=5Fmag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit experience_kernels.cu line 1916 binned action_mag_per_sample by actual_mag_core at every step, including trade-close events. But unified_env_step_core forces `actual_mag = 0 (Quarter)` whenever actual_dir is Hold/Flat (trade_physics.cuh:772) and trade closes always land in Hold/Flat state — so every Half/Full close was attributed to the Quarter bin. close_counts[Half] and close_counts[Full] were structurally pinned to 0, giving wr_h = wr_f = 0 across all training runs. Fix: introduce `seg_mag_bin = is_close ? pre_mag_bin : actual_mag_core`. At close events bin by pre_mag_bin (the magnitude of the position being closed); at non-close events keep actual_mag_core (current realized magnitude). pre_mag_bin is always 0/1/2 at close events since exiting/reversing requires prev_sign != 0 → pre_trade_position != 0 → pre_frac > 0.001 → pre_mag_bin in {0,1,2}. Smoke verification (5-epoch local): wr_h/wr_f remain 0 because var_scale (1/(1+sqrt(var_q))) shrinks effective_max_pos to 10-19% of broker max at smoke maturity → even Long Full target lands at abs_pos ≈ 0.15 < 0.375 → all positions decode as Quarter; no Half/Full positions exist for the fix to attribute. This is the expected structural consequence of the var_scale design (uncertain Q → smaller position, conservative). The fix is latent correctness: in mature L40S 30+ epoch runs where var_q drops and var_scale grows above 0.375, Half/Full positions become reachable and wr_h/wr_f will reflect their real realized win rates. Without the fix, even mature training would show wr_h = wr_f = 0 because of the Hold/Flat → Quarter close convention masking real per-magnitude win rates. Audit entry updated. --- .../src/cuda_pipeline/experience_kernels.cu | 31 +++++++++++++------ docs/dqn-wire-up-audit.md | 18 +++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index 0c4fdde30..b42d4f73e 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -1904,16 +1904,29 @@ extern "C" __global__ void experience_env_step( /* Task 2.X prerequisite — per-magnitude win-rate + variance instrumentation. * Same race-free per-sample-plus-host-reduce pattern as Task 0.5 / 0.8. - * Bin by actual_mag_core (post-enforcement magnitude from - * unified_env_step_core, 0=Quarter / 1=Half / 2=Full). Overwrites the - * -1 sentinel written at the default block above. Profitability - * predicate conditions on trade_close so we only count events where a - * segment actually closed this step — entering-only samples don't - * count toward win rate (they have no realized outcome yet), but DO - * count toward the variance sum (step_ret_core is the raw per-step - * return and is the unit the host variance reducer consumes). */ + * + * Bin by the SEGMENT magnitude — the magnitude associated with the + * trade segment whose P&L is being recorded this bar: + * - At close events (exiting_trade || reversing_trade): the segment + * just closed; bin by `pre_mag_bin` (the magnitude of the position + * being closed). `actual_mag_core` is forced to 0 (Quarter) at + * close bars by `unified_env_step_core` because actual_dir becomes + * Hold/Flat — using it would attribute every Half/Full close to + * the Quarter bucket, structurally pinning wr_h = wr_f = 0. + * - Otherwise (holding / entering): bin by `actual_mag_core` (the + * current realized magnitude). `pre_mag_bin` is unsuitable here + * because it reflects the prior bar's position, not the segment + * P&L is being earned on this bar. + * + * Profitability predicate conditions on trade_close so we only count + * events where a segment actually closed this step — entering-only + * samples don't count toward win rate (they have no realized outcome + * yet), but DO count toward the variance sum (step_ret_core is the + * raw per-step return and is the unit the host variance reducer + * consumes). */ int is_close = (exiting_trade || reversing_trade) ? 1 : 0; - action_mag_per_sample[out_off] = actual_mag_core; + int seg_mag_bin = is_close ? pre_mag_bin : actual_mag_core; + action_mag_per_sample[out_off] = seg_mag_bin; step_ret_per_sample[out_off] = step_ret_core; trade_close_per_sample[out_off] = is_close; trade_profitable_per_sample[out_off] = (is_close && step_ret_core > 0.0f) ? 1 : 0; diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index 8080acd30..8a368becc 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -2,6 +2,24 @@ **Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7. +mag_stats wr_h/wr_f attribution fix (2026-04-27): `experience_kernels.cu` +line 1916 binned `action_mag_per_sample` by `actual_mag_core` at every step, +including trade-close events. `unified_env_step_core` forces +`actual_mag = 0 (Quarter)` whenever `actual_dir` is Hold/Flat (line 772 of +trade_physics.cuh), and trade closes always land in Hold/Flat state — so +every Half/Full close was attributed to the Quarter bin. close_counts[Half] +and close_counts[Full] structurally pinned to 0; wr_h=wr_f=0 across all +training runs. Fix: at close events bin by `pre_mag_bin` (the magnitude of +the position being closed); at non-close events keep `actual_mag_core` +(current realized magnitude). `pre_mag_bin` is always 0/1/2 at close events +since exiting/reversing requires `prev_sign != 0` → `pre_trade_position != 0`. +Smoke verification: wr_h/wr_f remain 0 in 5-epoch smoke because var_scale +(~0.10-0.19 at smoke maturity, from `1/(1+sqrt(var_q))` shrinkage) makes +even Long Full target → abs_pos ≈ 0.15 < 0.375 → all positions decode as +Quarter; no Half/Full positions exist for the fix to attribute. Latent +correctness for mature training (var_q drops as Q-distribution converges → +var_scale grows → Half/Full positions become reachable). + actions_history_buf measurement artifact fix (2026-04-27): two-part fix. (1) `gpu_backtest_evaluator.rs::reset_evaluation_state` now initialises `actions_history_buf` to `-1` sentinel via `cuMemsetD32Async(0xFFFFFFFFu32)`