fix(dqn): mag_stats wr_h/wr_f attribution — bin trade closes by pre_mag

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.
This commit is contained in:
jgrusewski
2026-04-27 12:05:49 +02:00
parent a86fba2b1d
commit b8788511ce
2 changed files with 40 additions and 9 deletions

View File

@@ -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;

View File

@@ -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)`