diff --git a/crates/ml-core/src/state_layout.rs b/crates/ml-core/src/state_layout.rs index af9471591..7e7f624f5 100644 --- a/crates/ml-core/src/state_layout.rs +++ b/crates/ml-core/src/state_layout.rs @@ -96,10 +96,12 @@ pub const PS_PLAN_CONVICTION: usize = 27; // plan: conviction at entry [0, pub const PS_PLAN_ASYMMETRY: usize = 28; // plan: profit/stop asymmetry pub const PS_PLAN_ENTRY_REGIME: usize = 29; // plan: regime_stability at entry pub const PS_OFI_PREV_BASE: usize = 30; // first OFI prev-bar slot (stride 8: [30..37]) -pub const PS_PEAK_PNL_BAR: usize = 38; // Plan 3 C.4: hold_time at which MAX_PNL was hit (temporal timing bonus) -pub const PS_INTRA_TRADE_MIN_PNL: usize = 39; // Plan 3 D.4a: worst unrealised PnL during trade (symmetric to MAX_PNL at 21) -pub const PS_REGIME_SHIFT_BAR: usize = 40; // Plan 3 D.4b: hold_time at first regime-shift detection (0 if no shift) -pub const PS_STRIDE: usize = 41; // total portfolio state stride (== PORTFOLIO_STRIDE), grown 40→41 by Plan 3 D.4b +pub const PS_PEAK_PNL_BAR: usize = 38; // Plan 3 C.4: hold_time at which MAX_PNL was hit (temporal timing bonus) +pub const PS_INTRA_TRADE_MIN_PNL: usize = 39; // Plan 3 D.4a: worst unrealised PnL during trade (symmetric to MAX_PNL at 21) +pub const PS_REGIME_SHIFT_BAR: usize = 40; // Plan 3 D.4b: hold_time at first regime-shift detection (0 if no shift) +pub const PS_PRE_ENTRY_CONVICTION_EMA: usize = 41; // Plan 3 D.4c: EMA of conviction_core during Flat bars +pub const PS_PRE_ENTRY_CONVICTION_VAR_EMA: usize = 42; // Plan 3 D.4c: EMA of (conviction_core − mean)² during Flat bars +pub const PS_STRIDE: usize = 43; // total portfolio state stride (== PORTFOLIO_STRIDE), grown 41→43 by Plan 3 D.4c conviction consistency // ──────────────────────────────────────────────────────────────────────────── // Mirrors of PLAN_ISV_* constants (plan_isv[0..PLAN_ISV_DIM=7)). diff --git a/crates/ml/src/cuda_pipeline/experience_kernels.cu b/crates/ml/src/cuda_pipeline/experience_kernels.cu index 6df28a4e3..01223532f 100644 --- a/crates/ml/src/cuda_pipeline/experience_kernels.cu +++ b/crates/ml/src/cuda_pipeline/experience_kernels.cu @@ -59,10 +59,10 @@ */ /* ------------------------------------------------------------------ */ -/* Portfolio stride for experience kernels (41 floats per episode). */ +/* Portfolio stride for experience kernels (43 floats per episode). */ /* portfolio_sim_kernel uses its own stride of 8 — do NOT change it. */ /* ------------------------------------------------------------------ */ -#define PORTFOLIO_STRIDE 41 /* was 40. Slots 30-37: prev-bar OFI; slot 38: PS_PEAK_PNL_BAR (Plan 3 C.4); slot 39: PS_INTRA_TRADE_MIN_PNL (Plan 3 D.4a); slot 40: PS_REGIME_SHIFT_BAR (Plan 3 D.4b) */ +#define PORTFOLIO_STRIDE 43 /* was 41. Slots 30-37: prev-bar OFI; slot 38: PS_PEAK_PNL_BAR (Plan 3 C.4); slot 39: PS_INTRA_TRADE_MIN_PNL (Plan 3 D.4a); slot 40: PS_REGIME_SHIFT_BAR (Plan 3 D.4b); slots 41-42: PS_PRE_ENTRY_CONVICTION_{EMA,VAR_EMA} (Plan 3 D.4c) */ #define DSR_A_SLOT 3 #define DSR_B_SLOT 4 #define DSR_TRADE_COUNT_SLOT 5 @@ -1852,10 +1852,12 @@ extern "C" __global__ void experience_env_step( ps[PS_PLAN_ASYMMETRY] = pp[PLAN_PARAM_ASYMMETRY]; /* asymmetry */ /* Store entry regime_stability for drift detection (P12). */ ps[PS_PLAN_ENTRY_REGIME] = (isv_signals_ptr != NULL) ? isv_signals_ptr[11] : 1.0f; - ps[PS_INTRA_TRADE_MAX_PNL] = 0.0f; /* reset max P&L for new trade */ - ps[PS_PEAK_PNL_BAR] = 0.0f; /* Plan 3 C.4: reset peak-bar alongside MAX_PNL */ - ps[PS_INTRA_TRADE_MIN_PNL] = 0.0f; /* Plan 3 D.4a: reset MIN_PNL alongside MAX_PNL */ - ps[PS_REGIME_SHIFT_BAR] = 0.0f; /* Plan 3 D.4b: reset at trade lifecycle boundary */ + ps[PS_INTRA_TRADE_MAX_PNL] = 0.0f; /* reset max P&L for new trade */ + ps[PS_PEAK_PNL_BAR] = 0.0f; /* Plan 3 C.4: reset peak-bar alongside MAX_PNL */ + ps[PS_INTRA_TRADE_MIN_PNL] = 0.0f; /* Plan 3 D.4a: reset MIN_PNL alongside MAX_PNL */ + ps[PS_REGIME_SHIFT_BAR] = 0.0f; /* Plan 3 D.4b: reset at trade lifecycle boundary */ + ps[PS_PRE_ENTRY_CONVICTION_EMA] = 0.0f; /* Plan 3 D.4c: next Flat epoch starts fresh */ + ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA] = 0.0f; /* Plan 3 D.4c: next Flat epoch starts fresh */ /* Apply conviction to position size — scale by readiness for smooth ramp */ position *= fmaxf(ps[PS_PLAN_CONVICTION] * readiness, 0.1f); } @@ -2012,17 +2014,21 @@ extern "C" __global__ void experience_env_step( float intra_trade_max_dd = (ps[PS_INTRA_TRADE_MAX_DD]); if (entering_trade) { intra_trade_max_dd = 0.0f; - ps[PS_INTRA_TRADE_MAX_PNL] = 0.0f; /* reset max P&L for new trade */ - ps[PS_PEAK_PNL_BAR] = 0.0f; /* Plan 3 C.4: reset peak-bar alongside MAX_PNL */ - ps[PS_INTRA_TRADE_MIN_PNL] = 0.0f; /* Plan 3 D.4a: reset MIN_PNL alongside MAX_PNL */ - ps[PS_REGIME_SHIFT_BAR] = 0.0f; /* Plan 3 D.4b: reset at trade lifecycle boundary */ + ps[PS_INTRA_TRADE_MAX_PNL] = 0.0f; /* reset max P&L for new trade */ + ps[PS_PEAK_PNL_BAR] = 0.0f; /* Plan 3 C.4: reset peak-bar alongside MAX_PNL */ + ps[PS_INTRA_TRADE_MIN_PNL] = 0.0f; /* Plan 3 D.4a: reset MIN_PNL alongside MAX_PNL */ + ps[PS_REGIME_SHIFT_BAR] = 0.0f; /* Plan 3 D.4b: reset at trade lifecycle boundary */ + ps[PS_PRE_ENTRY_CONVICTION_EMA] = 0.0f; /* Plan 3 D.4c: next Flat epoch starts fresh */ + ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA] = 0.0f; /* Plan 3 D.4c: next Flat epoch starts fresh */ } if (reversing_trade) { intra_trade_max_dd = 0.0f; - ps[PS_INTRA_TRADE_MAX_PNL] = 0.0f; /* reset max P&L for reversed trade */ - ps[PS_PEAK_PNL_BAR] = 0.0f; /* Plan 3 C.4: reset peak-bar alongside MAX_PNL */ - ps[PS_INTRA_TRADE_MIN_PNL] = 0.0f; /* Plan 3 D.4a: reset MIN_PNL alongside MAX_PNL */ - ps[PS_REGIME_SHIFT_BAR] = 0.0f; /* Plan 3 D.4b: reset at trade lifecycle boundary */ + ps[PS_INTRA_TRADE_MAX_PNL] = 0.0f; /* reset max P&L for reversed trade */ + ps[PS_PEAK_PNL_BAR] = 0.0f; /* Plan 3 C.4: reset peak-bar alongside MAX_PNL */ + ps[PS_INTRA_TRADE_MIN_PNL] = 0.0f; /* Plan 3 D.4a: reset MIN_PNL alongside MAX_PNL */ + ps[PS_REGIME_SHIFT_BAR] = 0.0f; /* Plan 3 D.4b: reset at trade lifecycle boundary */ + ps[PS_PRE_ENTRY_CONVICTION_EMA] = 0.0f; /* Plan 3 D.4c: next Flat epoch starts fresh */ + ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA] = 0.0f; /* Plan 3 D.4c: next Flat epoch starts fresh */ } if (fabsf(position) > 0.001f && entry_price > 0.0f) { float unrealized = (position > 0.0f) @@ -2395,6 +2401,31 @@ extern "C" __global__ void experience_env_step( /* Fallback: original holding cost when micro_reward_scale=0 or no OFI */ reward = -shaping_scale * holding_cost_rate * fabsf(position); } else if (!segment_complete) { + /* Plan 3 D.4c: Welford-style EMA of conviction_core during Flat bars. + * Updates running mean and squared-deviation EMAs that the consumer + * (entry-bonus block below) uses to detect stable pre-entry conviction. + * + * α = 0.05 matches the Plan 3 Task 1 reward-component EMA convention — + * not a tuned constant per task; same α_base used for all Plan 3 EMAs. + * + * Per-bar update: + * delta = conviction_core − prev_mean + * new_mean = prev_mean + α × delta + * new_var = (1 − α) × (prev_var + α × delta²) // EMA-style, not exact Welford + * + * Runs only when conviction_ptr is available; otherwise skip cleanly + * (cold-start or test-fixture safety). Reset to 0 at trade entry, + * trade reversal, and fold/episode reset (see lifecycle blocks). */ + if (conviction_ptr != NULL) { + const float alpha_c = 0.05f; + const float prev_m = ps[PS_PRE_ENTRY_CONVICTION_EMA]; + const float prev_v = ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA]; + const float delta = conviction_core - prev_m; + const float new_m = prev_m + alpha_c * delta; + const float new_v = (1.0f - alpha_c) * (prev_v + alpha_c * delta * delta); + ps[PS_PRE_ENTRY_CONVICTION_EMA] = new_m; + ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA] = new_v; + } /* ── Flat opportunity cost — ISV-driven self-adapting via conviction ── * (val-Flat-collapse fix #3 revision, 2026-04-24) * @@ -2505,6 +2536,62 @@ extern "C" __global__ void experience_env_step( } } + /* ── D.4c (Plan 3 Task 6c): Conviction consistency bonus ── + * + * Fires on Flat→Positioned entry when the network's pre-entry conviction + * has been STABLE across the Flat run (stddev/mean < 0.2). Rewards + * deliberated entries vs noise-spike entries. + * + * Mirrors B.2 novelty-bonus structure exactly: + * bonus = shaping × vol_proxy × stability × conviction_core + * + * All multiplicands are in [0,1] except vol_proxy (bounded ≤ 0.01). + * Max bonus ≈ 0.01 — same order as B.2. Per + * pearl_one_unbounded_signal_per_reward.md: exactly ONE technically + * unbounded multiplicand (vol_proxy); all others bounded. + * + * stability mapping: + * ratio = stddev / mean (sentinel 2.0 when mean ≈ 0 → disables bonus) + * ratio < 0.2 ⇒ stable ⇒ stability = clamp(0, 1, 1 − ratio/0.2) + * ratio ≥ 0.2 ⇒ skip + * + * The 0.2 threshold is the single tuned constant retained for this first + * cut. Reviewer note: a follow-up could derive it from + * ISV[Q_DIR_ABS_REF] for full ISV-driven shaping; deferred to keep this + * task surgical. The `ratio/0.2` normalisation inside `1 − ratio/0.2` + * makes stability=1 mean "perfectly stable" (ratio=0) and stability→0 + * as ratio→0.2 (margin), so credit is proportional to how stable the + * pre-entry epoch was rather than a flat threshold pass/fail. + * + * Attribution: rc[5] += bonus (co-exists with B.2/C.4/D.4a/D.4b at + * different (i,t) slots; the same ISV[68] REWARD_BONUS_EMA tracks the + * net rc[5] signal). */ + if (entering_trade && isv_signals_ptr != NULL + && features != NULL && bar_idx < total_bars && market_dim > 9) { + const float mean_c = ps[PS_PRE_ENTRY_CONVICTION_EMA]; + const float var_c = ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA]; + const float stddev_c = sqrtf(fmaxf(0.0f, var_c)); + const float ratio = (mean_c > 1e-6f) ? (stddev_c / mean_c) : 2.0f; + if (ratio < 0.2f) { + const float stability = fmaxf(0.0f, fminf(1.0f, 1.0f - ratio / 0.2f)); + const float atr_norm_c = features[(long long)bar_idx * market_dim + 9]; + const float log_atr_c = atr_norm_c * 16.0f - 7.0f; + const float atr_pct_c = expf(log_atr_c) / fmaxf(raw_close, 1.0f); + float vol_proxy_c = fmaxf(atr_pct_c, 0.0001f); + if (vol_proxy_c > 0.01f) vol_proxy_c = 0.01f; + const float bonus = shaping_scale * vol_proxy_c * stability * conviction_core; + reward += bonus; + reward_components_per_sample[out_off * 6 + 5] += bonus; + } + } + /* Defensive reset of D.4c EMA slots at entry — redundant with the + * lifecycle resets at the entering_trade handlers above (lines ~1858 and + * ~2018), but cheap and harmless. The next Flat epoch starts fresh. */ + if (entering_trade) { + ps[PS_PRE_ENTRY_CONVICTION_EMA] = 0.0f; + ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA] = 0.0f; + } + /* ---- Drawdown penalty (from trade_physics.cuh) ────────────────────── * Phase 3: gated by shaping_scale. The drawdown penalty is *behavioral* * (steers the policy away from large equity drops); the *physics* of @@ -2848,6 +2935,8 @@ extern "C" __global__ void experience_env_step( ps[PS_PEAK_PNL_BAR] = 0.0f; /* Plan 3 C.4: peak-bar alongside MAX_PNL */ ps[PS_INTRA_TRADE_MIN_PNL] = 0.0f; /* Plan 3 D.4a: MIN_PNL alongside MAX_PNL */ ps[PS_REGIME_SHIFT_BAR] = 0.0f; /* Plan 3 D.4b: reset at trade lifecycle boundary */ + ps[PS_PRE_ENTRY_CONVICTION_EMA] = 0.0f; /* Plan 3 D.4c: hard-reset Flat-epoch EMA */ + ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA] = 0.0f; /* Plan 3 D.4c: hard-reset Flat-epoch EMA */ ps[PS_PLAN_TARGET_BARS] = 0.0f; ps[PS_PLAN_PROFIT_TARGET] = 0.0f; ps[PS_PLAN_STOP_LOSS] = 0.0f; ps[PS_PLAN_SCALE_AGGRESSION] = 0.0f; ps[PS_PLAN_CONVICTION] = 0.0f; ps[PS_PLAN_ASYMMETRY] = 0.0f; ps[PS_PLAN_ENTRY_REGIME] = 0.0f; /* plan slots zeroed */ @@ -2865,6 +2954,8 @@ extern "C" __global__ void experience_env_step( ps[PS_PEAK_PNL_BAR] = 0.0f; /* Plan 3 C.4: peak-bar reset for next trade */ ps[PS_INTRA_TRADE_MIN_PNL] = 0.0f; /* Plan 3 D.4a: MIN_PNL reset for next trade */ ps[PS_REGIME_SHIFT_BAR] = 0.0f; /* Plan 3 D.4b: reset at trade lifecycle boundary */ + ps[PS_PRE_ENTRY_CONVICTION_EMA] = 0.0f; /* Plan 3 D.4c: soft-reset Flat-epoch EMA */ + ps[PS_PRE_ENTRY_CONVICTION_VAR_EMA] = 0.0f; /* Plan 3 D.4c: soft-reset Flat-epoch EMA */ ps[PS_PLAN_TARGET_BARS] = 0.0f; ps[PS_PLAN_PROFIT_TARGET] = 0.0f; ps[PS_PLAN_STOP_LOSS] = 0.0f; ps[PS_PLAN_SCALE_AGGRESSION] = 0.0f; ps[PS_PLAN_CONVICTION] = 0.0f; ps[PS_PLAN_ASYMMETRY] = 0.0f; ps[PS_PLAN_ENTRY_REGIME] = 0.0f; /* plan slots zeroed on trade complete */ diff --git a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs index 962aa262d..0104a8bc1 100644 --- a/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs +++ b/crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs @@ -10656,7 +10656,7 @@ impl GpuDqnTrainer { ) -> Result<(), MLError> { let isv_ptr = self.isv_signals_dev_ptr; let isv_out = isv_ptr; // kernel writes to the same ISV bus (offset 44) - let ps_stride: i32 = 41; // PS_STRIDE from state_layout.cuh (grown 40→41 by Plan 3 D.4b) + let ps_stride: i32 = 43; // PS_STRIDE from state_layout.cuh (grown 41→43 by Plan 3 D.4c) unsafe { self.stream .launch_builder(&self.kelly_cap_update_kernel) diff --git a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs index 89a447a39..a3b31def7 100644 --- a/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs +++ b/crates/ml/src/cuda_pipeline/gpu_experience_collector.rs @@ -41,10 +41,10 @@ const MAX_EPISODES_LIMIT: usize = 0x8000; /// Absolute upper bound for validation — reject configs above this. const MAX_TIMESTEPS_LIMIT: usize = 1000; const PORTFOLIO_STATE_SIZE: usize = 8; -/// Portfolio stride for DQN experience kernels (41 floats per episode). +/// Portfolio stride for DQN experience kernels (43 floats per episode). /// Matches PORTFOLIO_STRIDE in experience_kernels.cu and trade_stats_kernel.cu. /// Do NOT change PORTFOLIO_STATE_SIZE above — it's for the PPO/legacy path. -const PORTFOLIO_STRIDE: usize = 41; // was 40. Slots 30-37: prev-bar OFI; slot 38: PS_PEAK_PNL_BAR (Plan 3 C.4); slot 39: PS_INTRA_TRADE_MIN_PNL (Plan 3 D.4a); slot 40: PS_REGIME_SHIFT_BAR (Plan 3 D.4b) +const PORTFOLIO_STRIDE: usize = 43; // was 41. Slots 30-37: prev-bar OFI; slot 38: PS_PEAK_PNL_BAR (Plan 3 C.4); slot 39: PS_INTRA_TRADE_MIN_PNL (Plan 3 D.4a); slot 40: PS_REGIME_SHIFT_BAR (Plan 3 D.4b); slots 41-42: PS_PRE_ENTRY_CONVICTION_{EMA,VAR_EMA} (Plan 3 D.4c) /// Number of floats in the trade_stats_reduce output buffer. /// Layout: [win_count, loss_count, sum_wins, sum_losses, sum_returns, sum_sq_returns] @@ -568,7 +568,7 @@ pub struct GpuExperienceCollector { trade_stats_buf: CudaSlice, // Per-episode state buffers [alloc_episodes, ...] - portfolio_states: CudaSlice, // [alloc_episodes * PORTFOLIO_STRIDE] (41 floats per episode) + portfolio_states: CudaSlice, // [alloc_episodes * PORTFOLIO_STRIDE] (43 floats per episode) episode_starts_buf: CudaSlice,// [alloc_episodes] // Output buffers [alloc_episodes * alloc_timesteps, ...] diff --git a/crates/ml/src/cuda_pipeline/state_layout.cuh b/crates/ml/src/cuda_pipeline/state_layout.cuh index 5b4bcd49f..4dcc12776 100644 --- a/crates/ml/src/cuda_pipeline/state_layout.cuh +++ b/crates/ml/src/cuda_pipeline/state_layout.cuh @@ -71,10 +71,12 @@ #define PS_PLAN_ENTRY_REGIME 29 // plan: regime_stability at entry // Slots 30-37: prev-bar OFI scratch for delta computation (see experience_kernels.cu) #define PS_OFI_PREV_BASE 30 // first OFI prev-bar slot (stride 8: [30..37]) -#define PS_PEAK_PNL_BAR 38 // Plan 3 C.4: hold_time at which MAX_PNL was hit (temporal timing bonus) -#define PS_INTRA_TRADE_MIN_PNL 39 // Plan 3 D.4a: worst unrealised PnL during trade (symmetric to MAX_PNL at 21) -#define PS_REGIME_SHIFT_BAR 40 // Plan 3 D.4b: hold_time at first regime-shift detection (0 if no shift) -#define PS_STRIDE 41 // total portfolio state stride (== PORTFOLIO_STRIDE), grown 40→41 by Plan 3 D.4b +#define PS_PEAK_PNL_BAR 38 // Plan 3 C.4: hold_time at which MAX_PNL was hit (temporal timing bonus) +#define PS_INTRA_TRADE_MIN_PNL 39 // Plan 3 D.4a: worst unrealised PnL during trade (symmetric to MAX_PNL at 21) +#define PS_REGIME_SHIFT_BAR 40 // Plan 3 D.4b: hold_time at first regime-shift detection (0 if no shift) +#define PS_PRE_ENTRY_CONVICTION_EMA 41 // Plan 3 D.4c: EMA of conviction_core during Flat bars +#define PS_PRE_ENTRY_CONVICTION_VAR_EMA 42 // Plan 3 D.4c: EMA of (conviction_core − mean)² during Flat bars +#define PS_STRIDE 43 // total portfolio state stride (== PORTFOLIO_STRIDE), grown 41→43 by Plan 3 D.4c conviction consistency // ──────────────────────────────────────────────────────────────────────────── // Plan-ISV slot indices (plan_isv[0..PLAN_ISV_DIM=7)). diff --git a/crates/ml/src/cuda_pipeline/trade_stats_kernel.cu b/crates/ml/src/cuda_pipeline/trade_stats_kernel.cu index a138380f9..26d99ea2a 100644 --- a/crates/ml/src/cuda_pipeline/trade_stats_kernel.cu +++ b/crates/ml/src/cuda_pipeline/trade_stats_kernel.cu @@ -1,7 +1,7 @@ /** * Trade stats reduction kernel. * - * Reads portfolio_states[N, PORTFOLIO_STRIDE=41] and sums fields [14:19] + * Reads portfolio_states[N, PORTFOLIO_STRIDE=43] and sums fields [14:19] * (win_count, loss_count, sum_wins, sum_losses, sum_returns, sum_sq_returns) * across all N episodes into a 6-float output buffer. * Uses shared-memory warp reduction (same pattern as monitoring_reduce). @@ -9,7 +9,7 @@ * Launch config: grid=(1, 1, 1), block=(256, 1, 1). */ -#define PORTFOLIO_STRIDE 41 // was 40; grown by Plan 3 D.4b PS_REGIME_SHIFT_BAR append. Kelly slots [14..19] unchanged. +#define PORTFOLIO_STRIDE 43 // was 41; grown by Plan 3 D.4c PS_PRE_ENTRY_CONVICTION_{EMA,VAR_EMA} append. Kelly slots [14..19] unchanged. #include "state_layout.cuh" extern "C" __global__ void trade_stats_reduce( diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index 5c10e4104..442eaa3df 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -64,6 +64,7 @@ | C.4 Temporal timing bonus on trade exit (`experience_kernels.cu` segment_complete block) | `experience_env_step` kernel (inside `segment_complete && segment_hold_time > 0` block, AFTER Layer 2–9 credits land in `reward`); accumulates `+=` into `rc[5]` and adds `timing_bonus` to `reward`; consumed by `reward_component_ema` → ISV[68] (shares the bonus slot with B.2 — different (i,t) slots, so `+=` is idempotent) | Wired | Plan 3 Task 5 C.4 — bars_early = max(0, segment_hold_time − PS_PEAK_PNL_BAR); timing_bonus = shaping_scale × (bars_early / segment_hold_time) × \|final_pnl\| × conviction_core. Peak bar tracked in new portfolio-state slot PS_PEAK_PNL_BAR=38 (PS_STRIDE 38→39), snapshot alongside every MAX_PNL update, reset at every MAX_PNL reset site (entry/reverse/fold-reset). No tuned multiplier. | — | | D.4a Persistence credit on profitable drawdown recovery (`experience_kernels.cu` segment_complete block) | `experience_env_step` kernel (inside `segment_complete && segment_hold_time > 0` block, IMMEDIATELY AFTER C.4 timing bonus, gated on `reward > 0` AND `drawdown_depth > 1e-6`); accumulates `+=` into `rc[5]` and adds `persist_bonus` to `reward`; consumed by `reward_component_ema` → ISV[68] (shares the bonus slot with B.2 entry + C.4 exit — different (i,t) slots per trade, so `+=` is idempotent across all three) | Wired | Plan 3 Task 6a D.4a — drawdown_depth = max(0, −PS_INTRA_TRADE_MIN_PNL); persist_bonus = shaping_scale × conviction_core × drawdown_depth × tanh(reward / max(1e-4, drawdown_depth)). MIN_PNL tracked in new portfolio-state slot PS_INTRA_TRADE_MIN_PNL=39 (PS_STRIDE 39→40), updated per-bar in the same block as MAX_PNL (`fminf` against `pnl_pct`), reset at every MAX_PNL reset site (entry/reverse/fold hard-reset/trade-complete soft-reset). Self-scaling tanh: saturates +1 when reward ≫ drawdown, ≈0 when reward ≈ drawdown. No tuned multiplier. | — | | D.4b Regime-shift penalty for trades held past a regime flip (`experience_kernels.cu` segment_complete block) | `experience_env_step` kernel: detector inside the active-trade block (after MIN/MAX_PNL update) writes `PS_REGIME_SHIFT_BAR` on the FIRST bar where `\|isv[11] − PS_PLAN_ENTRY_REGIME\| > clamp(0.25 × \|clamp(sharpe, ±2)\|, 0.05, 0.5)`; consumer inside `segment_complete` block IMMEDIATELY AFTER D.4a accumulates `−=` into `rc[5]` and subtracts `penalty` from `reward`; consumed by `reward_component_ema` → ISV[68] (cancellation within rc[5] vs B.2/C.4/D.4a is intentional — that's what ISV[68] REWARD_BONUS_EMA tracks). Consumer also resets `PS_REGIME_SHIFT_BAR = 0` after use. | Wired | Plan 3 Task 6b D.4b — bars_late = max(0, saved_hold_time − PS_REGIME_SHIFT_BAR); penalty = shaping_scale × conviction_core × (bars_late / saved_hold_time) × ISV[Q_DIR_ABS_REF_INDEX=21] × \|reward\|. Shift-bar tracked in new portfolio-state slot PS_REGIME_SHIFT_BAR=40 (PS_STRIDE 40→41), reset at every MAX_PNL reset site (entry/reverse/fold hard-reset/trade-complete soft-reset) PLUS post-consumer (defensive). Self-scaling Q_DIR_ABS_REF coefficient (same B.1 pattern). Adaptive threshold tightens when \|sharpe\| is high. First-shift-only. No tuned multiplier. | — | +| D.4c Conviction consistency bonus on Flat→Positioned entry (`experience_kernels.cu` Flat branch + entering_trade block) | `experience_env_step` kernel: producer inside the Flat branch (`!segment_complete && position≈0`, before opp-cost) updates two PS slots `PS_PRE_ENTRY_CONVICTION_EMA` (mean) and `PS_PRE_ENTRY_CONVICTION_VAR_EMA` (variance) via Welford-style EMA at α=0.05 on `conviction_core`. Consumer inside `entering_trade` block IMMEDIATELY AFTER B.2 novelty bonus computes `ratio = stddev/mean`; when `ratio < 0.2` accumulates `+=` into `rc[5]` and adds `bonus` to `reward`; consumed by `reward_component_ema` → ISV[68] (shares the bonus slot with B.2/C.4/D.4a/D.4b at different (i,t) slots; `+=` is idempotent). Both EMA slots reset at every trade-lifecycle reset site (entry/reverse/fold hard-reset/trade-complete soft-reset) PLUS post-consumer (defensive). | Wired | Plan 3 Task 6c D.4c — `bonus = shaping_scale × vol_proxy × stability × conviction_core` where `vol_proxy ∈ [0.0001, 0.01]`, `stability = clamp(0, 1, 1 − ratio/0.2)`, `conviction_core ∈ [0,1]`. Mirrors B.2 novelty-bonus structure exactly — ONE technically unbounded multiplicand (`vol_proxy`, bounded ≤ 0.01), all others bounded — per pearl_one_unbounded_signal_per_reward.md. Max bonus ≈ 0.01, same order as B.2. New portfolio-state slots `PS_PRE_ENTRY_CONVICTION_EMA=41` and `PS_PRE_ENTRY_CONVICTION_VAR_EMA=42`; `PS_STRIDE` 41→43; `PORTFOLIO_STRIDE` 41→43 in all consumers (`experience_kernels.cu`, `trade_stats_kernel.cu`, `gpu_experience_collector.rs`, `gpu_dqn_trainer.rs::launch_kelly_cap_update`, `ml-core::state_layout.rs`). The 0.2 stability threshold is the single tuned constant retained for this first cut (commented; reviewer may demote to ISV-driven follow-up). α=0.05 matches Plan 3 Task 1 reward-EMA convention. | — | ## CUDA Pipeline — Rust Wrappers