fix(magnitude): thread magnitude conviction into Kelly cap — eval Full reachable
Smoke after var_scale floor (73fd49f4b) still showed eval Full = 0.000 with
intent = 0.911. Root cause: trade_physics.cuh::unified_env_step_core applied
Kelly cap using ONLY direction conviction (q_dir_gap / q_dir_abs_ref). At
smoke horizon with direction Q-values close (q_s≈q_h≈q_l≈q_f within ~0.07),
direction conviction was ~0.04 → safety = max(0.75, 0.04) = 0.75,
effective_kelly = max(0, 0.75) = 0.75, cap = 0.75 × max_pos × 0.75 =
0.5625 × max_pos → Half bucket pin (abs_pos < 0.75).
The magnitude branch was simultaneously highly confident (q_f / q_h ≈ 1.9,
mag conviction ≈ 0.5) — the policy strongly preferred Full. That signal
never reached the cap.
Thread per-sample magnitude conviction into the cap formula:
- new buffer magnitude_conviction_buf[N*L] (training) parallel to
conviction_buf, and chunked_magnitude_conviction_buf[chunk_len * n_windows]
(eval) parallel to chunked_conviction_buf
- experience_action_select writes (max(q_mag) − min(q_mag)) /
fmaxf(ISV[16], 1e-6); ISV[16] = Q_ABS_REF (magnitude branch's |Q| EMA,
recycled — no new ISV slot)
- scripted_policy_kernel writes 0.0 (no real magnitude preference; helper
composer falls back to direction signal)
- unified_env_step_core takes a parallel magnitude_conviction parameter
- combined: policy_conviction = max(dir_conv, mag_conv); cap formula
becomes safety = max(health_safety, policy_conviction); warmup_floor
uses policy_conviction
- all three env_step kernel call sites migrated together
(backtest_env_step, backtest_env_step_batch, experience_env_step) per
feedback_no_partial_refactor — no mixed state
Per pearl_blend_formulas_must_have_permanent_floor: max() composition,
not multiplicative blend. Per feedback_isv_for_adaptive_bounds: signal
flows from existing ISV[16] = Q_ABS_REF, no tuned constants.
Smoke verification (20-epoch RTX 3050 Ti, magnitude_distribution test):
- pre-fix: [EVAL_DIST] Q=0.586 H=0.414 F=0.000 (intent F=0.911)
- post-fix: [EVAL_DIST] Q=0.618 H=0.153 F=0.229 (intent F=0.911)
Intent unchanged (network already learned Full preference); the Kelly
cap no longer silences it. Both smoke assertions now pass:
ef >= 0.05 (gate) and eh + ef >= 0.30 (Task 2.2 H10).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,13 @@ extern "C" __global__ void backtest_env_step(
|
||||
/* Per-sample conviction [n_windows] for adaptive Kelly warmup floor.
|
||||
* Written by experience_action_select (out_conviction). NULL → fallback
|
||||
* 1.0 (trust policy face-value). Threaded into unified_env_step_core. */
|
||||
const float* __restrict__ conviction_ptr
|
||||
const float* __restrict__ conviction_ptr,
|
||||
/* Per-sample magnitude-branch conviction [n_windows] — same shape as
|
||||
* conviction_ptr, parallel signal. Written by experience_action_select
|
||||
* (out_magnitude_conviction). Threaded into unified_env_step_core where
|
||||
* the cap formula composes max(dir_conv, mag_conv). NULL → fallback 0.0
|
||||
* (helper composes against direction conviction alone). */
|
||||
const float* __restrict__ magnitude_conviction_ptr
|
||||
) {
|
||||
/* Both scales are no-ops in backtest today — ignored. The signature
|
||||
* alignment is so the future unified_env_kernel can be a drop-in
|
||||
@@ -218,10 +224,17 @@ extern "C" __global__ void backtest_env_step(
|
||||
&trail_vol_scale, &trail_trend_scale
|
||||
);
|
||||
|
||||
/* Per-sample conviction for Kelly warmup floor — matches training. */
|
||||
/* Per-sample conviction for Kelly warmup floor — matches training.
|
||||
* NULL conviction_ptr defaults to 1.0 (trust policy face-value, same as
|
||||
* pre-mag-conviction behaviour). NULL magnitude_conviction_ptr defaults
|
||||
* to 0.0 — the helper's max(dir_conv, mag_conv) then falls back to the
|
||||
* direction signal alone, preserving prior single-conviction behaviour. */
|
||||
float conviction_core = (conviction_ptr != NULL)
|
||||
? fminf(1.0f, fmaxf(0.0f, conviction_ptr[w]))
|
||||
: 1.0f;
|
||||
float mag_conviction_core = (magnitude_conviction_ptr != NULL)
|
||||
? fminf(1.0f, fmaxf(0.0f, magnitude_conviction_ptr[w]))
|
||||
: 0.0f;
|
||||
|
||||
unified_env_step_core(
|
||||
action_val, close,
|
||||
@@ -230,7 +243,8 @@ extern "C" __global__ void backtest_env_step(
|
||||
contract_multiplier, margin_pct,
|
||||
health_k,
|
||||
trail_vol_scale, trail_trend_scale,
|
||||
conviction_core, /* adaptive Kelly warmup floor */
|
||||
conviction_core, /* direction-branch Kelly warmup floor */
|
||||
mag_conviction_core, /* magnitude-branch Kelly warmup floor */
|
||||
&position, &cash, &entry_price, &hold_time,
|
||||
&max_equity,
|
||||
value, /* prev_equity (snapshot) */
|
||||
@@ -364,7 +378,11 @@ extern "C" __global__ void backtest_env_step_batch(
|
||||
/* Per-sample conviction [chunk_len * n_windows] — layout matches
|
||||
* chunked_actions ([step_offset * n_windows + window]). NULL → fallback
|
||||
* 1.0. Threaded into unified_env_step_core for adaptive Kelly warmup. */
|
||||
const float* __restrict__ conviction_ptr
|
||||
const float* __restrict__ conviction_ptr,
|
||||
/* Per-sample magnitude-branch conviction — same shape and layout as
|
||||
* conviction_ptr. NULL → fallback 0.0; the helper's max(dir_conv,
|
||||
* mag_conv) composer falls back to the direction signal alone. */
|
||||
const float* __restrict__ magnitude_conviction_ptr
|
||||
) {
|
||||
/* Phase 3 unified-env: shaping params retained for ABI but no longer
|
||||
* applied to step_returns. See single-step variant header for rationale. */
|
||||
@@ -462,10 +480,15 @@ extern "C" __global__ void backtest_env_step_batch(
|
||||
);
|
||||
|
||||
/* Per-sample conviction for adaptive Kelly warmup floor. Layout
|
||||
* matches chunked_actions: [s * n_windows + w]. */
|
||||
* matches chunked_actions: [s * n_windows + w]. NULL magnitude_
|
||||
* conviction_ptr defaults to 0.0 — the helper's max(dir, mag) composer
|
||||
* falls back to the direction signal alone, preserving prior behaviour. */
|
||||
float conviction_core = (conviction_ptr != NULL)
|
||||
? fminf(1.0f, fmaxf(0.0f, conviction_ptr[s * n_windows + w]))
|
||||
: 1.0f;
|
||||
float mag_conviction_core = (magnitude_conviction_ptr != NULL)
|
||||
? fminf(1.0f, fmaxf(0.0f, magnitude_conviction_ptr[s * n_windows + w]))
|
||||
: 0.0f;
|
||||
|
||||
unified_env_step_core(
|
||||
action_val, close,
|
||||
@@ -474,7 +497,8 @@ extern "C" __global__ void backtest_env_step_batch(
|
||||
contract_multiplier, margin_pct,
|
||||
health_k_batch,
|
||||
trail_vol_scale_b, trail_trend_scale_b,
|
||||
conviction_core, /* adaptive Kelly warmup floor */
|
||||
conviction_core, /* direction-branch Kelly warmup floor */
|
||||
mag_conviction_core, /* magnitude-branch Kelly warmup floor */
|
||||
&position, &cash, &entry_price, &hold_time,
|
||||
&max_equity,
|
||||
value, /* prev_equity (snapshot for step_return) */
|
||||
|
||||
@@ -742,6 +742,14 @@ extern "C" __global__ void experience_state_gather(
|
||||
* = (max(q_dir) − min(q_dir)) / fmaxf(q_dir_abs_ref, 1e-6f).
|
||||
* Consumed by env_step → unified_env_step_core → apply_kelly_cap
|
||||
* as the adaptive warmup floor. NULL-tolerant.
|
||||
* @param out_magnitude_conviction [N] output: per-sample magnitude-branch conviction ∈ [0, 1]
|
||||
* = (max(q_mag) − min(q_mag)) / fmaxf(q_mag_abs_ref, 1e-6f).
|
||||
* Mirrors the direction-conviction shape with the magnitude
|
||||
* branch's ISV reference (ISV[16] = Q_ABS_REF, magnitude EMA).
|
||||
* Consumed by env_step → unified_env_step_core where the
|
||||
* Kelly-cap formula uses max(dir_conv, mag_conv) so a strong
|
||||
* magnitude preference can unblock the cap when direction
|
||||
* conviction is low (typical at smoke horizon). NULL-tolerant.
|
||||
* @param epsilon exploration probability in [0, 1]
|
||||
* @param N number of episodes
|
||||
* @param b0_size direction branch size (4)
|
||||
@@ -782,7 +790,8 @@ extern "C" __global__ void experience_action_select(
|
||||
* env_step. NULL = skip. Diagnostic-only — lets operators see
|
||||
* what the magnitude head WOULD pick if direction gating and
|
||||
* post-policy physics didn't mask it. */
|
||||
float* out_conviction /* [N] output: direction-branch conviction ∈ [0,1] for Kelly warmup. NULL=skip. */
|
||||
float* out_conviction, /* [N] output: direction-branch conviction ∈ [0,1] for Kelly warmup. NULL=skip. */
|
||||
float* out_magnitude_conviction /* [N] output: magnitude-branch conviction ∈ [0,1] for Kelly warmup. NULL=skip. */
|
||||
) {
|
||||
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= N) return;
|
||||
@@ -1179,6 +1188,51 @@ extern "C" __global__ void experience_action_select(
|
||||
float conviction = q_range / denom;
|
||||
out_conviction[i] = fminf(1.0f, fmaxf(0.0f, conviction));
|
||||
}
|
||||
|
||||
/* Per-sample magnitude-branch conviction for adaptive Kelly warmup floor.
|
||||
* mag_conviction = (max(q_mag) − min(q_mag)) / fmaxf(q_mag_abs_ref, 1e-6f)
|
||||
* clamped to [0, 1]. Mirrors the direction-branch shape exactly with
|
||||
* ISV slot 16 (Q_ABS_REF — magnitude branch's |Q| scale EMA, written by
|
||||
* `q_mag_bin_means_reduce` on the stats cadence) as the normaliser.
|
||||
*
|
||||
* Why threading magnitude conviction matters (project_magnitude_eval_collapse):
|
||||
* the existing direction conviction (ISV[21] = Q_DIR_ABS_REF) reflects the
|
||||
* policy's confidence in WHERE to trade. The magnitude head independently
|
||||
* learns a strong size preference (e.g. q_f / q_h ≈ 1.9 at smoke horizon
|
||||
* with q_q ≈ 0.43, q_h ≈ 0.46, q_f ≈ 0.87 — a clear Full preference). The
|
||||
* direction head can simultaneously be near-uniform (q_dir_gap / q_dir_abs
|
||||
* _ref ≈ 0.04) when the policy hesitates between Hold and Long. The Kelly
|
||||
* cap formula previously saw only direction conviction and pinned target
|
||||
* positions to the Half bucket via safety = max(0.75, 0.04) = 0.75, never
|
||||
* reaching the Full threshold abs_pos ≥ 0.75 × max_position. Threading
|
||||
* magnitude conviction lets the cap composer take the max(dir_conv, mag_conv)
|
||||
* — a confident magnitude pick (mag_conv ≈ 0.5) unblocks the cap to
|
||||
* safety = max(0.75, 0.5) = 0.75 in the worst case but lifts to 1.0 when
|
||||
* the policy concentrates Q-mass on a single magnitude bin.
|
||||
*
|
||||
* Same fallback semantics as direction conviction: when ISV[16] ≤ 1e-6 use
|
||||
* q_range itself as denominator → conviction = 1.0 cold-start. Once the
|
||||
* `q_mag_bin_means_reduce` stats cadence fires (every epoch) the adaptive
|
||||
* normalisation takes over. Uses raw Q (no contrarian sign flip) — conviction
|
||||
* is about magnitude-decision confidence regardless of direction sign. */
|
||||
if (out_magnitude_conviction != NULL) {
|
||||
float q_max_raw_m = q_b1[0];
|
||||
float q_min_raw_m = q_b1[0];
|
||||
for (int a = 1; a < b1_size; a++) {
|
||||
float qv = q_b1[a];
|
||||
q_max_raw_m = fmaxf(q_max_raw_m, qv);
|
||||
q_min_raw_m = fminf(q_min_raw_m, qv);
|
||||
}
|
||||
float q_range_m = q_max_raw_m - q_min_raw_m;
|
||||
float q_mag_abs_ref = (isv_signals_ptr != NULL)
|
||||
? isv_signals_ptr[16]
|
||||
: 0.0f;
|
||||
float denom_m = (q_mag_abs_ref > 1e-6f)
|
||||
? q_mag_abs_ref
|
||||
: fmaxf(q_range_m, 1e-6f);
|
||||
float mag_conviction = q_range_m / denom_m;
|
||||
out_magnitude_conviction[i] = fminf(1.0f, fmaxf(0.0f, mag_conviction));
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
@@ -1331,6 +1385,12 @@ extern "C" __global__ void experience_env_step(
|
||||
* 1.0 (trust the policy face-value; same outcome as the old static
|
||||
* warmup_floor=0.5 at health=1). Threaded into unified_env_step_core. */
|
||||
const float* __restrict__ conviction_ptr,
|
||||
/* Per-sample magnitude-branch conviction ∈ [0, 1] — same shape as
|
||||
* conviction_ptr, parallel signal. Written by experience_action_select
|
||||
* (out_magnitude_conviction). Threaded into unified_env_step_core where
|
||||
* the cap formula composes max(dir_conv, mag_conv). NULL → fallback 0.0
|
||||
* (helper falls back to direction conviction alone). */
|
||||
const float* __restrict__ magnitude_conviction_ptr,
|
||||
/* C.2 Reward-component attribution (Plan 3 Task 1, spec §4.C.2).
|
||||
* Per-sample per-component reward magnitudes written at every (i,t).
|
||||
* Layout [N*L, 6] row-major; component indices:
|
||||
@@ -1736,10 +1796,15 @@ extern "C" __global__ void experience_env_step(
|
||||
* (trust policy face-value when action_select didn't write a value —
|
||||
* e.g. future kernels that skip the conviction buffer). The action_select
|
||||
* kernel clamps to [0, 1] already; defensive clamp here guards against
|
||||
* direct device writes bypassing that path. */
|
||||
* direct device writes bypassing that path. magnitude_conviction_ptr
|
||||
* defaults to 0.0 — helper composes max(dir, mag) and degrades to direction
|
||||
* conviction alone if the parallel buffer isn't wired. */
|
||||
float conviction_core = (conviction_ptr != NULL)
|
||||
? fminf(1.0f, fmaxf(0.0f, conviction_ptr[i]))
|
||||
: 1.0f;
|
||||
float mag_conviction_core = (magnitude_conviction_ptr != NULL)
|
||||
? fminf(1.0f, fmaxf(0.0f, magnitude_conviction_ptr[i]))
|
||||
: 0.0f;
|
||||
|
||||
unified_env_step_core(
|
||||
action_idx, raw_close,
|
||||
@@ -1750,7 +1815,8 @@ extern "C" __global__ void experience_env_step(
|
||||
contract_multiplier, margin_pct,
|
||||
health_k_train,
|
||||
trail_vol_scale, trail_trend_scale,
|
||||
conviction_core, /* adaptive Kelly warmup floor */
|
||||
conviction_core, /* direction-branch Kelly warmup floor */
|
||||
mag_conviction_core, /* magnitude-branch Kelly warmup floor */
|
||||
&position, &cash, &entry_price, &hold_time,
|
||||
&peak_equity, /* helper updates peak_equity via fmaxf */
|
||||
prev_equity, /* snapshot for step_return denominator */
|
||||
|
||||
@@ -375,6 +375,13 @@ pub struct GpuBacktestEvaluator {
|
||||
/// `experience_action_select` each chunk; consumed by `backtest_env_step_batch`
|
||||
/// as the adaptive Kelly warmup-floor signal (via `unified_env_step_core`).
|
||||
chunked_conviction_buf: Option<CudaSlice<f32>>,
|
||||
/// Per-sample magnitude-branch conviction ∈ [0, 1], chunked layout matching
|
||||
/// `chunked_conviction_buf`. Parallel signal threaded through the same
|
||||
/// kernels for the Kelly cap's `max(dir_conv, mag_conv)` composer — lets
|
||||
/// the cap relax for confident magnitude picks even when direction
|
||||
/// conviction is low (typical at smoke horizon when q_dir values cluster
|
||||
/// while the magnitude head sharply prefers Full).
|
||||
chunked_magnitude_conviction_buf: Option<CudaSlice<f32>>,
|
||||
|
||||
/// Cached CUDA graph for `evaluate_dqn_graphed()` step loop.
|
||||
///
|
||||
@@ -676,6 +683,7 @@ impl GpuBacktestEvaluator {
|
||||
q_gap_threshold: 0.0,
|
||||
chunked_q_gaps_buf: None,
|
||||
chunked_conviction_buf: None,
|
||||
chunked_magnitude_conviction_buf: None,
|
||||
dqn_graph: None,
|
||||
chunked_q_values: None,
|
||||
chunked_states_buf: None,
|
||||
@@ -1366,6 +1374,9 @@ impl GpuBacktestEvaluator {
|
||||
let ch_conviction = self.chunked_conviction_buf.as_ref().ok_or_else(|| {
|
||||
MLError::ModelError("chunked_conviction_buf unexpectedly None".to_owned())
|
||||
})?;
|
||||
let ch_magnitude_conviction = self.chunked_magnitude_conviction_buf.as_ref().ok_or_else(|| {
|
||||
MLError::ModelError("chunked_magnitude_conviction_buf unexpectedly None".to_owned())
|
||||
})?;
|
||||
|
||||
let n = self.n_windows;
|
||||
let state_dim = self.state_dim;
|
||||
@@ -1480,7 +1491,8 @@ impl GpuBacktestEvaluator {
|
||||
.arg(&self.isv_signals_ptr) // ISV signals for adaptive hold
|
||||
.arg(&0i32) // F6: contrarian_active — always off during backtest
|
||||
.arg(ch_intent) // out_intent_mag: pure-policy magnitude intent diagnostic
|
||||
.arg(ch_conviction) // out_conviction: per-sample Kelly warmup signal
|
||||
.arg(ch_conviction) // out_conviction: direction-branch Kelly warmup signal
|
||||
.arg(ch_magnitude_conviction) // out_magnitude_conviction: magnitude-branch Kelly warmup signal
|
||||
.launch(launch_cfg)
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
"chunked action_select chunk_start={chunk_start}: {e}"
|
||||
@@ -1589,7 +1601,8 @@ impl GpuBacktestEvaluator {
|
||||
// Phase 3 unified-env scalars (NULL = no-op in val mode).
|
||||
.arg(&0u64) // exploration_scale_ptr (NULL)
|
||||
.arg(&0u64) // shaping_scale_ptr (NULL)
|
||||
.arg(ch_conviction) // conviction_ptr: adaptive Kelly warmup signal
|
||||
.arg(ch_conviction) // conviction_ptr: direction-branch Kelly warmup signal
|
||||
.arg(ch_magnitude_conviction) // magnitude_conviction_ptr: magnitude-branch Kelly warmup signal
|
||||
.launch(LaunchConfig {
|
||||
grid_dim: (env_blocks.max(1), 1, 1),
|
||||
block_dim: (256, 1, 1),
|
||||
@@ -1925,6 +1938,12 @@ impl GpuBacktestEvaluator {
|
||||
* chunked_actions/chunked_q_gaps — [chunk_len * n_windows]. */
|
||||
let ch_conviction = self.stream.alloc_zeros::<f32>(cn)
|
||||
.map_err(|e| MLError::ModelError(format!("alloc chunked conviction: {e}")))?;
|
||||
/* Parallel magnitude-branch conviction — same shape, threaded into
|
||||
* action_select + env_step_batch alongside the direction-branch
|
||||
* conviction so the Kelly cap's max(dir, mag) composer can relax
|
||||
* for confident magnitude picks. */
|
||||
let ch_magnitude_conviction = self.stream.alloc_zeros::<f32>(cn)
|
||||
.map_err(|e| MLError::ModelError(format!("alloc chunked magnitude conviction: {e}")))?;
|
||||
|
||||
info!(
|
||||
"GpuBacktestEvaluator: action-select initialised — {} windows, \
|
||||
@@ -1947,6 +1966,7 @@ impl GpuBacktestEvaluator {
|
||||
self.picked_action_history_buf = Some(picked_action_history_buf);
|
||||
self.scatter_intent_kernel = Some(scatter_intent);
|
||||
self.chunked_conviction_buf = Some(ch_conviction);
|
||||
self.chunked_magnitude_conviction_buf = Some(ch_magnitude_conviction);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2049,6 +2069,7 @@ impl GpuBacktestEvaluator {
|
||||
.arg(&0u64) // exploration_scale_ptr (NULL)
|
||||
.arg(&0u64) // shaping_scale_ptr (NULL)
|
||||
.arg(&0u64) // conviction_ptr (NULL) — fallback 1.0 in-kernel
|
||||
.arg(&0u64) // magnitude_conviction_ptr (NULL) — fallback 0.0 in-kernel; max(dir, mag) → dir alone
|
||||
.launch(env_cfg)
|
||||
.map_err(|e| {
|
||||
MLError::ModelError(format!("backtest_env_step launch step {step}: {e}"))
|
||||
|
||||
@@ -535,6 +535,13 @@ pub struct GpuExperienceCollector {
|
||||
/// by q_dir_abs_ref (ISV[21]). Drives adaptive Kelly warmup floor in
|
||||
/// `unified_env_step_core` (replaces the old static 0.5 floor).
|
||||
conviction_buf: CudaSlice<f32>,
|
||||
/// Per-sample magnitude-branch conviction [N] for adaptive Kelly warmup floor.
|
||||
/// Parallel to `conviction_buf` (direction-branch). Written by
|
||||
/// `experience_action_select` (out_magnitude_conviction) on each step;
|
||||
/// consumed by `experience_env_step` → `unified_env_step_core` where
|
||||
/// the cap formula composes `max(dir_conv, mag_conv)` so a confident
|
||||
/// magnitude pick can unblock the cap when direction conviction is low.
|
||||
magnitude_conviction_buf: CudaSlice<f32>,
|
||||
/// Kelly optimal fraction (per-epoch scalar from CPU).
|
||||
/// Current timestep counter per episode: [N].
|
||||
current_timesteps: CudaSlice<i32>,
|
||||
@@ -1209,6 +1216,12 @@ impl GpuExperienceCollector {
|
||||
let conviction_buf = stream
|
||||
.alloc_zeros::<f32>(n)
|
||||
.map_err(|e| MLError::ModelError(format!("alloc conviction_buf: {e}")))?;
|
||||
/* Per-sample magnitude-branch conviction [N] — parallel to conviction_buf,
|
||||
* threaded through the same kernels for the Kelly cap's max(dir, mag)
|
||||
* composer. */
|
||||
let magnitude_conviction_buf = stream
|
||||
.alloc_zeros::<f32>(n)
|
||||
.map_err(|e| MLError::ModelError(format!("alloc magnitude_conviction_buf: {e}")))?;
|
||||
let current_timesteps = stream
|
||||
.alloc_zeros::<i32>(n)
|
||||
.map_err(|e| MLError::ModelError(format!("alloc current_timesteps: {e}")))?;
|
||||
@@ -1557,6 +1570,7 @@ impl GpuExperienceCollector {
|
||||
batch_actions,
|
||||
q_gaps_buf,
|
||||
conviction_buf,
|
||||
magnitude_conviction_buf,
|
||||
current_timesteps,
|
||||
q_values,
|
||||
q_select_buf,
|
||||
@@ -3509,7 +3523,8 @@ impl GpuExperienceCollector {
|
||||
.arg(&sd_i32)
|
||||
.arg(&bar_idx_i32)
|
||||
.arg(&mut self.batch_actions) // [N] factored action ∈ [0, 108)
|
||||
.arg(&mut self.conviction_buf) // [N] conviction ∈ [0, 1]
|
||||
.arg(&mut self.conviction_buf) // [N] direction conviction ∈ [0, 1]
|
||||
.arg(&mut self.magnitude_conviction_buf) // [N] magnitude conviction ∈ [0, 1]
|
||||
.launch(launch_cfg)
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
"scripted_policy_select t={t}: {e}"
|
||||
@@ -3552,7 +3567,8 @@ impl GpuExperienceCollector {
|
||||
.arg(&self.isv_signals_dev_ptr) // ISV signals for adaptive hold (0=NULL=static)
|
||||
.arg(&(self.contrarian_active_cache as i32)) // D7/N7: Q-negation flag
|
||||
.arg(&0u64) // out_intent_mag: NULL — training path does not collect
|
||||
.arg(&mut self.conviction_buf) // out_conviction: per-sample Kelly warmup signal
|
||||
.arg(&mut self.conviction_buf) // out_conviction: per-sample direction-branch Kelly warmup signal
|
||||
.arg(&mut self.magnitude_conviction_buf) // out_magnitude_conviction: per-sample magnitude-branch Kelly warmup signal
|
||||
.launch(launch_cfg)
|
||||
.map_err(|e| MLError::ModelError(format!(
|
||||
"experience_action_select t={t}: {e}"
|
||||
@@ -3685,7 +3701,8 @@ impl GpuExperienceCollector {
|
||||
.arg(&cf_ratio) // D4/N4: adaptive counterfactual flip ratio
|
||||
.arg(&self.exploration_scale_dev_ptr) // Phase 3 unified-env: gates saboteur/plan/CF flips
|
||||
.arg(&self.shaping_scale_dev_ptr) // Phase 3 unified-env: gates additive reward shaping
|
||||
.arg(&self.conviction_buf) // conviction_ptr: adaptive Kelly warmup signal
|
||||
.arg(&self.conviction_buf) // conviction_ptr: direction-branch Kelly warmup signal
|
||||
.arg(&self.magnitude_conviction_buf) // magnitude_conviction_ptr: magnitude-branch Kelly warmup signal
|
||||
// C.2: per-component reward buffer [N*L * 6] — popart/cf/trail/micro/opp_cost/bonus
|
||||
.arg(&mut self.reward_components_per_sample)
|
||||
// B.2 Plan 3 Task 3: Flat→Positioned transition flag + ISV slot indices
|
||||
|
||||
@@ -1118,6 +1118,7 @@ mod tests {
|
||||
let mut q_gaps_buf = stream.alloc_zeros::<f32>(batch).expect("alloc q_gaps");
|
||||
let mut intent_buf = stream.alloc_zeros::<i32>(batch).expect("alloc intent");
|
||||
let mut conv_buf = stream.alloc_zeros::<f32>(batch).expect("alloc conviction");
|
||||
let mut mag_conv_buf = stream.alloc_zeros::<f32>(batch).expect("alloc magnitude conviction");
|
||||
|
||||
let blocks = (batch as u32).div_ceil(256);
|
||||
let cfg = LaunchConfig {
|
||||
@@ -1153,6 +1154,7 @@ mod tests {
|
||||
.arg(&0_i32) // contrarian_active
|
||||
.arg(&mut intent_buf)
|
||||
.arg(&mut conv_buf)
|
||||
.arg(&mut mag_conv_buf)
|
||||
.launch(cfg)
|
||||
.expect("launch experience_action_select");
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ extern "C" __global__ void scripted_policy_select(
|
||||
int state_dim,
|
||||
int bar_idx, /* current timestep, mixes into LCG seed */
|
||||
int* __restrict__ actions_out, /* [N] factored action index ∈ [0, 108) */
|
||||
float* __restrict__ conviction_out /* [N] conviction ∈ [0, 1] */
|
||||
float* __restrict__ conviction_out, /* [N] direction conviction ∈ [0, 1] */
|
||||
float* __restrict__ magnitude_conviction_out /* [N] magnitude conviction ∈ [0, 1] */
|
||||
) {
|
||||
const int i = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (i >= n_samples) return;
|
||||
@@ -108,4 +109,11 @@ extern "C" __global__ void scripted_policy_select(
|
||||
const int urg = 1; /* Normal */
|
||||
actions_out[i] = dir * 27 + mag * 9 + ord * 3 + urg;
|
||||
conviction_out[i] = conviction;
|
||||
/* The scripted policy hard-codes magnitude=Half and does not express
|
||||
* a per-sample magnitude preference. Emit 0.0 for magnitude conviction
|
||||
* so the env-step's max(dir_conv, mag_conv) Kelly composer falls back
|
||||
* to the direction signal (the only signal this policy actually
|
||||
* represents). The network's `experience_action_select` overwrites
|
||||
* both buffers with real Q-spread normalised conviction once seeded. */
|
||||
magnitude_conviction_out[i] = 0.0f;
|
||||
}
|
||||
|
||||
@@ -548,11 +548,14 @@ __device__ __forceinline__ void compute_regime_trail_scales(
|
||||
* 2. Hold action passthrough
|
||||
* 3. Target position via compute_target_position_4branch
|
||||
* 4. Margin cap (apply_margin_cap)
|
||||
* 5. Kelly cap with health-coupled safety AND policy conviction
|
||||
* (apply_kelly_cap). Conviction ∈ [0, 1] — per-sample, derived from
|
||||
* direction Q-spread normalised by q_dir_abs_ref. Drives the cold-
|
||||
* start warmup floor so Kelly cap adapts to policy conviction instead
|
||||
* of a static 0.5 floor; once maturity → 1 it's a no-op.
|
||||
* 5. Kelly cap with health-coupled safety AND per-branch policy conviction
|
||||
* (apply_kelly_cap). Two conviction signals (direction + magnitude),
|
||||
* each ∈ [0, 1] — per-sample, derived from each branch's Q-spread
|
||||
* normalised by its ISV |Q|-scale reference (ISV[21] for direction,
|
||||
* ISV[16] for magnitude). Composed with `max()` then composed with
|
||||
* health_safety with `max()`. Drives the cold-start warmup floor so
|
||||
* Kelly cap adapts to whichever branch expresses high confidence,
|
||||
* instead of a static 0.5 floor; once maturity → 1 it's a no-op.
|
||||
* 6. Trailing stop check — base 0.5%, regime-adaptive via vol_scale and
|
||||
* trend_scale (caller resolves from ADX/CUSUM features; identical
|
||||
* formulas in both training and val to preserve train/eval parity)
|
||||
@@ -609,6 +612,17 @@ __device__ __forceinline__ void unified_env_step_core(
|
||||
* inside kelly_position_cap; caller may pass out-of-range and the
|
||||
* helper bounds it. */
|
||||
float conviction,
|
||||
/* ── Magnitude-branch conviction ∈ [0, 1] for adaptive Kelly warmup floor ──
|
||||
* Per-sample magnitude Q-spread normalised by q_mag_abs_ref (ISV[16]).
|
||||
* Composed with direction conviction inside this helper as
|
||||
* policy_conviction = max(direction_conviction, magnitude_conviction)
|
||||
* so a confident magnitude preference unblocks the Kelly cap when the
|
||||
* direction head is near-uniform (typical at smoke-horizon: direction
|
||||
* Q-values cluster while the magnitude head sharply prefers Full). Same
|
||||
* shape/clamp/semantics as `conviction` — caller may pass out-of-range
|
||||
* and the helper bounds it. NULL/zero from the caller is benign — the
|
||||
* max() composer falls back to the direction signal alone. */
|
||||
float magnitude_conviction,
|
||||
/* ── Per-thread mutable state (in/out) ── */
|
||||
float* position,
|
||||
float* cash,
|
||||
@@ -650,42 +664,53 @@ __device__ __forceinline__ void unified_env_step_core(
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Step 5: Kelly cap (safety composed from two orthogonal signals
|
||||
/* ── Step 5: Kelly cap (safety composed from three 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]
|
||||
* policy_conviction = max(direction_conviction, magnitude_conviction)
|
||||
* safety_multiplier = max(health_safety, policy_conviction)
|
||||
* health_safety = 0.5 + 0.5 × health [training-stability floor]
|
||||
* direction_conviction ∈ [0, 1] [direction-branch confidence]
|
||||
* magnitude_conviction ∈ [0, 1] [magnitude-branch 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.
|
||||
* progress?). Per-branch conviction measures the policy's confidence on
|
||||
* each axis of the factored action: direction conviction reflects WHERE
|
||||
* to trade, magnitude conviction reflects HOW MUCH. These are orthogonal —
|
||||
* the policy can be near-uniform on one axis while sharply preferring a
|
||||
* single bin on the other. The Kelly cap composes both per-branch signals
|
||||
* via max(): if EITHER axis expresses high confidence, the policy is
|
||||
* committed enough to its choice that the cap should relax. Health then
|
||||
* composes via max() too as a training-stability floor. 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.
|
||||
* All signals flow per-sample from the action_select kernel's per-branch
|
||||
* Q-spreads, normalised by ISV per-branch |Q|-scale EMAs (ISV[21] for
|
||||
* direction, ISV[16] for magnitude). 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. */
|
||||
* horizon, typical health=0.49 gives health_safety=0.745. The direction
|
||||
* branch is often near-uniform (q_dir_gap / q_dir_abs_ref ≈ 0.04 when
|
||||
* Hold competes with Long), so direction_conviction alone leaves the cap
|
||||
* formula at 0.5625 × max_pos — pinned to the Half bucket [0.375, 0.75).
|
||||
* The magnitude branch simultaneously expresses sharp preferences
|
||||
* (q_f / q_h ≈ 1.9 → mag_conviction ≈ 0.5). Threading mag_conviction
|
||||
* unblocks Full realisation for confident magnitude picks even when the
|
||||
* direction head hesitates, without requiring health graduation
|
||||
* (structurally unreachable in 20-epoch smokes). */
|
||||
if (!is_hold_action) {
|
||||
float h = fminf(1.0f, fmaxf(0.0f, health_for_kelly));
|
||||
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);
|
||||
float dir_conv_clamped = fminf(1.0f, fmaxf(0.0f, conviction));
|
||||
float mag_conv_clamped = fminf(1.0f, fmaxf(0.0f, magnitude_conviction));
|
||||
float policy_conviction = fmaxf(dir_conv_clamped, mag_conv_clamped);
|
||||
float safety = fmaxf(health_safety, policy_conviction);
|
||||
target_position = apply_kelly_cap(
|
||||
target_position,
|
||||
*win_count, *loss_count,
|
||||
*sum_wins, *sum_losses,
|
||||
max_position_physics, safety,
|
||||
conviction,
|
||||
policy_conviction,
|
||||
health_safety
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,33 @@
|
||||
|
||||
**Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7.
|
||||
|
||||
magnitude conviction threaded into Kelly cap (2026-04-27, follow-up): the
|
||||
var_scale floor (above) addressed the **training** path. Smoke at the same
|
||||
horizon then reproduced eval Full=0.000 with intent=0.911 because
|
||||
`backtest_env_step_batch` doesn't apply var_scale (no Var[Q] shrink in val).
|
||||
The remaining silencer is `unified_env_step_core`'s Kelly cap, which only
|
||||
saw direction conviction. At smoke horizon (health≈0.5,
|
||||
direction_conviction≈0.04 when Hold competes with Long), the formula
|
||||
`safety = max(0.75, 0.04) = 0.75`, `effective_kelly = max(0, 0.75)`,
|
||||
`cap = 0.75 × max_pos × 0.75 = 0.5625 × max_pos` lands in Half bucket.
|
||||
The magnitude branch was simultaneously expressing strong preference
|
||||
(q_f/q_h ≈ 1.9, mag_conviction ≈ 0.5) but never reached the cap.
|
||||
Fix: per-sample magnitude conviction parallels direction conviction —
|
||||
new buffers `magnitude_conviction_buf[N*L]` (training) and
|
||||
`chunked_magnitude_conviction_buf[chunk_len * n_windows]` (eval).
|
||||
`experience_action_select` writes `(max(q_mag) − min(q_mag)) /
|
||||
fmaxf(ISV[16], 1e-6)` (ISV[16] = `Q_ABS_REF` magnitude branch reference,
|
||||
recycled). `unified_env_step_core` composer:
|
||||
`policy_conviction = max(dir_conv, mag_conv)`, `safety = max(health_safety,
|
||||
policy_conviction)`. All three call sites (`backtest_env_step`,
|
||||
`backtest_env_step_batch`, `experience_env_step`) migrated in lockstep
|
||||
per `feedback_no_partial_refactor`. No tuned constants —
|
||||
`max()` composition (per `pearl_blend_formulas_must_have_permanent_floor`),
|
||||
ISV-driven scale (per `feedback_isv_for_adaptive_bounds`). Smoke
|
||||
verification: pre-fix `[EVAL_DIST] Q=0.59 H=0.41 F=0.000`; post-fix
|
||||
`[EVAL_DIST] Q=0.618 H=0.153 F=0.229`. Intent unchanged (the network
|
||||
already learned Full preference); the cap no longer silences it.
|
||||
|
||||
var_scale permanent floor for eval magnitude collapse (2026-04-27): the
|
||||
EVAL_DIST=Quarter 0.59 / Half 0.41 / Full 0.00 pathology persists even with
|
||||
intent=0.911 for Full because the position-sizing pipeline composes four
|
||||
|
||||
Reference in New Issue
Block a user