diag(crt-2): per-horizon alpha-input EMA test — hypothesis investigation

Driven by ffr59 (commit b44a97ff9) findings: all 5 horizons flip
direction every 2.5 events; win rate flat 24% across conviction; mean
PnL anti-correlated with conviction. Hypothesis: per-event alpha output
is high-frequency noise on top of a slower signal. If true, smoothing
input alpha BEFORE the conviction formula should reduce direction flips
and recover signal.

Adds Wiener-α adaptive EMA on raw alpha_probs[h] for each horizon,
applied BEFORE the multi-horizon conviction formula. Floor at 0.1
(stronger than the 0.4 floor on the output-side conviction EMA — this
tests whether INPUT smoothing has different impact than OUTPUT smoothing).

Three new device slots:
  - alpha_ema_per_b_per_h (per-horizon EMA state)
  - alpha_diff_var_per_b_per_h (variance of changes)
  - alpha_sample_var_per_b_per_h (variance of value)

The CRT.diag Group A direction-flip counter still reads RAW alpha_probs
so we have a head-to-head comparison: raw flip rate vs smoothed flip rate.
Group E adds the smoothed-direction counter + mean run length.

End-of-run log adds one line per horizon:
  crt_diag h<X> smoothed: flips=Y mean_run_len=Z events (vs raw F / M)

If smoothed mean_run_len >> raw mean_run_len: hypothesis is RIGHT, the
input had signal under the noise. Next step would be to make this an
operational EMA in the controller.

If smoothed and raw are similar: hypothesis is WRONG, per-event output
is genuinely noisy. Next step would be to investigate model training
(horizon collapse) OR the AUC=0.66 measurement definition.

Either way, definitive result from one smoke run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-20 22:34:35 +02:00
parent b44a97ff94
commit 0e17fd4f2d
3 changed files with 329 additions and 14 deletions

View File

@@ -394,6 +394,127 @@ __device__ static float compute_multi_horizon_conviction(
: 0.0f;
}
// CRT.diag.2: per-horizon Wiener-α adaptive EMA on RAW alpha probabilities.
// Smooths alpha_probs[h] BEFORE compute_multi_horizon_conviction consumes
// it — testing whether per-event output noise is hiding a slower signal
// (ffr59 found all 5 horizons flip every 2.5 events, win rate flat ~24%,
// mean PnL anti-correlated with conviction; either smoothing recovers
// a signal, or the per-event output is genuinely noisy and the hypothesis
// is wrong).
//
// Pattern mirrors update_conviction_ema (which works on the multi-horizon
// scalar). Per pearl_wiener_optimal_adaptive_alpha + pearl_first_observation_bootstrap.
// Floor at 0.1 — stronger smoothing than the 0.4 conviction-level floor,
// testing whether INPUT-side smoothing differs from OUTPUT-side.
//
// Writes alpha_smoothed[h] for h in 0..N_HORIZONS; updates the three per-
// horizon EMA-state slots in place. Single-thread-per-backtest invariant:
// caller must already have gated on threadIdx.x == 0.
__device__ static void compute_alpha_ema_per_horizon(
int b,
const float* __restrict__ alpha_probs,
float* __restrict__ alpha_ema_per_b_per_h,
float* __restrict__ alpha_diff_var_per_b_per_h,
float* __restrict__ alpha_sample_var_per_b_per_h,
float (&alpha_smoothed)[N_HORIZONS]
) {
#pragma unroll
for (int h = 0; h < N_HORIZONS; ++h) {
const float p_raw = alpha_probs[h];
const float prev_ema = alpha_ema_per_b_per_h[b * N_HORIZONS + h];
const float diff = p_raw - prev_ema;
const float diff_var_prev = alpha_diff_var_per_b_per_h[b * N_HORIZONS + h];
const float new_diff_var = (diff_var_prev == 0.0f)
? (diff * diff) // first-observation bootstrap
: (0.9f * diff_var_prev + 0.1f * diff * diff);
alpha_diff_var_per_b_per_h[b * N_HORIZONS + h] = new_diff_var;
// sample_var approximates the variance of p around its natural
// no-edge mean (0.5). Centering on 0.5 (instead of the running
// mean) gives a Wiener-α that reads "how far from the no-edge
// anchor the signal sits" rather than "how dispersed it is".
const float sample_var_prev = alpha_sample_var_per_b_per_h[b * N_HORIZONS + h];
const float p_centered = p_raw - 0.5f;
const float new_sample_var = (sample_var_prev == 0.0f)
? (p_centered * p_centered) // first-observation bootstrap
: (0.9f * sample_var_prev + 0.1f * p_centered * p_centered);
alpha_sample_var_per_b_per_h[b * N_HORIZONS + h] = new_sample_var;
// Wiener α with FLOOR 0.1 (stronger smoothing than the 0.4 floor
// on the output-side conviction EMA — testing input-side smoothing
// differs from output-side).
const float eps_v = 1e-6f;
const float alpha_raw = new_diff_var / (new_diff_var + new_sample_var + eps_v);
const float alpha_active = (alpha_raw > 0.1f) ? alpha_raw : 0.1f;
const float new_ema = (prev_ema == 0.0f)
? p_raw // first-observation bootstrap
: (alpha_active * p_raw + (1.0f - alpha_active) * prev_ema);
alpha_ema_per_b_per_h[b * N_HORIZONS + h] = new_ema;
alpha_smoothed[h] = new_ema;
}
}
// CRT.diag.2: variant of compute_multi_horizon_conviction that reads from
// a thread-local alpha_smoothed[h] array instead of the broadcast
// alpha_probs[h] pointer. The §4.4 weight math is identical — only the
// per-horizon p_h source differs.
__device__ static float compute_multi_horizon_conviction_smoothed(
const float (&alpha_smoothed)[N_HORIZONS],
const IsvKellyState* __restrict__ isv,
float cost
) {
const float eps_edge = cost * 0.01f;
float weighted_sum_signed = 0.0f;
float total_abs_weight = 0.0f;
#pragma unroll
for (int h = 0; h < N_HORIZONS; ++h) {
const float p_h = alpha_smoothed[h];
const float direction_h = (p_h > 0.5f) ? 1.0f : -1.0f;
const float magnitude_h = fabsf(p_h - 0.5f) * 2.0f;
const float net_edge_h = fmaxf(isv[h].pnl_ema_win - isv[h].pnl_ema_loss, eps_edge);
const float weight_h = net_edge_h / (isv[h].realised_return_var + cost * cost);
weighted_sum_signed += magnitude_h * weight_h * direction_h;
total_abs_weight += fabsf(weight_h);
}
return (total_abs_weight > 1e-9f)
? (weighted_sum_signed / total_abs_weight)
: 0.0f;
}
// CRT.diag.2 Group E: per-horizon SMOOTHED direction-flip + mean-run-length
// tracking. Mirrors Group A (which reads RAW alpha_probs[h]) but reads
// alpha_smoothed[h]. Single-writer-per-block; caller must already have
// gated on threadIdx.x == 0. Skips the 5-bucket histogram (Group A keeps
// the histogram; Group E only needs flip count + mean run length for the
// head-to-head comparison).
__device__ static void update_smoothed_flip_counters(
int b,
const float (&alpha_smoothed)[N_HORIZONS],
unsigned int* __restrict__ diag_smoothed_flip_count,
unsigned int* __restrict__ diag_smoothed_current_run_length,
unsigned long long* __restrict__ diag_smoothed_sum_run_length,
signed char* __restrict__ diag_smoothed_prev_dir
) {
#pragma unroll
for (int h = 0; h < N_HORIZONS; ++h) {
const float p = alpha_smoothed[h];
const signed char new_dir = (p > 0.5f) ? (signed char)1
: ((p < 0.5f) ? (signed char)-1 : (signed char)0);
const signed char prev_dir = diag_smoothed_prev_dir[b * N_HORIZONS + h];
const unsigned int prev_run = diag_smoothed_current_run_length[b * N_HORIZONS + h];
if (prev_dir != 0 && new_dir != 0 && prev_dir != new_dir) {
diag_smoothed_flip_count[b * N_HORIZONS + h] += 1u;
diag_smoothed_sum_run_length[b * N_HORIZONS + h] += (unsigned long long)prev_run;
diag_smoothed_current_run_length[b * N_HORIZONS + h] = 1u;
} else {
diag_smoothed_current_run_length[b * N_HORIZONS + h] = prev_run + 1u;
}
diag_smoothed_prev_dir[b * N_HORIZONS + h] = new_dir;
}
}
extern "C" __global__ void decision_policy_default(
const float* __restrict__ alpha_probs, // [N_HORIZONS] broadcast
const unsigned char* __restrict__ isv_kelly_base, // [n_backtests * N_HORIZONS * sizeof(IsvKellyState)]
@@ -435,6 +556,18 @@ extern "C" __global__ void decision_policy_default(
signed char* __restrict__ diag_prev_dir_signed, // [n_backtests * N_HORIZONS]
// CRT.diag Group B: conviction-EMA histogram.
unsigned int* __restrict__ diag_conv_hist, // [n_backtests * 10]
// CRT.diag.2: per-horizon alpha-input EMA state. Smooths raw
// alpha_probs[h] BEFORE the §4.4 conviction formula consumes it.
// Three slots mirror the conviction-level EMA pattern.
float* __restrict__ alpha_ema_per_b_per_h, // [n_backtests * N_HORIZONS]
float* __restrict__ alpha_diff_var_per_b_per_h, // [n_backtests * N_HORIZONS]
float* __restrict__ alpha_sample_var_per_b_per_h, // [n_backtests * N_HORIZONS]
// CRT.diag.2 Group E: smoothed-direction-flip counters (mirror Group A
// but reading alpha_smoothed[h] instead of alpha_probs[h]). No histogram.
unsigned int* __restrict__ diag_smoothed_flip_count, // [n_backtests * N_HORIZONS]
unsigned int* __restrict__ diag_smoothed_current_run_length, // [n_backtests * N_HORIZONS]
unsigned long long* __restrict__ diag_smoothed_sum_run_length, // [n_backtests * N_HORIZONS]
signed char* __restrict__ diag_smoothed_prev_dir, // [n_backtests * N_HORIZONS]
int n_backtests
) {
int b = blockIdx.x;
@@ -450,18 +583,36 @@ extern "C" __global__ void decision_policy_default(
return;
}
// CRT.diag.2: smooth RAW alpha_probs[h] per-horizon BEFORE the §4.4
// conviction formula consumes it. Wiener-α EMA with floor 0.1 (stronger
// than the 0.4 output-side floor). The alpha_smoothed[] array stays in
// registers — fed into both compute_multi_horizon_conviction_smoothed
// and the Group E flip counter. RAW alpha_probs[] is still passed
// through to Group A so the head-to-head comparison is apples-to-apples.
float alpha_smoothed[N_HORIZONS];
compute_alpha_ema_per_horizon(
b, alpha_probs,
alpha_ema_per_b_per_h,
alpha_diff_var_per_b_per_h,
alpha_sample_var_per_b_per_h,
alpha_smoothed);
// CRT.1 C1.2: multi-horizon ISV-weighted conviction (spec §4.4).
// Replaces v2 A2 (1d889d2de) + A2.1 (fe2498769) scalar approach that
// failed Gate 1 catastrophically on smokes vjmwc + lkrdf (155k trades,
// 9100% drawdown, Sharpe -15.7). Direction now comes from the
// per-horizon SIGNED sum so disagreeing horizons cancel — the
// structural fix the scalar EMA could not provide.
//
// CRT.diag.2: the §4.4 formula now reads alpha_smoothed[] (per-horizon
// EMA) instead of alpha_probs[]. Hypothesis test: does input smoothing
// recover signal hidden under per-event noise?
const IsvKellyState* isv = reinterpret_cast<const IsvKellyState*>(
isv_kelly_base + (size_t)b * N_HORIZONS * sizeof(IsvKellyState)
);
const float cost = cost_per_lot_per_side_per_b[b];
const float conviction_signed = compute_multi_horizon_conviction(
alpha_probs, isv, cost);
const float conviction_signed = compute_multi_horizon_conviction_smoothed(
alpha_smoothed, isv, cost);
const float conviction_abs = fabsf(conviction_signed);
const float conviction_dir = (conviction_signed >= 0.0f) ? 1.0f : -1.0f;
@@ -477,6 +628,8 @@ extern "C" __global__ void decision_policy_default(
// CRT.diag Group A: per-horizon direction-flip + run-length tracking.
// Observe-only. Single-writer-per-block (already gated on thread 0).
// Reads RAW alpha_probs[h] — head-to-head comparison with Group E
// (which reads alpha_smoothed[h]). Apples-to-apples vs ffr59 baseline.
#pragma unroll
for (int h = 0; h < N_HORIZONS; ++h) {
const float p = alpha_probs[h];
@@ -502,6 +655,15 @@ extern "C" __global__ void decision_policy_default(
diag_prev_dir_signed[b * N_HORIZONS + h] = new_dir;
}
// CRT.diag.2 Group E: per-horizon SMOOTHED direction-flip + mean-run-length.
// Mirrors Group A reading alpha_smoothed[] instead of alpha_probs[].
update_smoothed_flip_counters(
b, alpha_smoothed,
diag_smoothed_flip_count,
diag_smoothed_current_run_length,
diag_smoothed_sum_run_length,
diag_smoothed_prev_dir);
// CRT.diag Group B: smoothed-conviction histogram (10 buckets over [0, 1)).
{
float c = conv_ema;
@@ -515,6 +677,9 @@ extern "C" __global__ void decision_policy_default(
// CRT.1 C1.4: refresh intra-trade trajectory state (offsets 24, 44, 48,
// 56) for the composite exit_signal. Skipped internally when flat.
// NOTE: keeps RAW alpha_probs for the short-vs-long disagreement
// counter (offset 56) — composite exit's disagreement signal is meant
// to react fast (32-event window), opposite of the smoothing rationale.
update_open_trade_trajectory(b, positions, books_per_b, alpha_probs,
conv_ema, open_trade_state_per_b);
@@ -539,11 +704,13 @@ extern "C" __global__ void decision_policy_default(
// Build per-horizon attribution mask: every horizon whose signed
// contribution agrees with the multi-horizon direction gets credit.
// This replaces the v2 sharpe-weight attribution because the v3
// weights come from the §4.4 formula directly.
// weights come from the §4.4 formula directly. CRT.diag.2: reads
// alpha_smoothed[] so attribution matches the smoothed conviction the
// size formula used.
unsigned int attribution_mask = 0u;
#pragma unroll
for (int h = 0; h < N_HORIZONS; ++h) {
const float dir_h = (alpha_probs[h] > 0.5f) ? 1.0f : -1.0f;
const float dir_h = (alpha_smoothed[h] > 0.5f) ? 1.0f : -1.0f;
if (dir_h == conviction_dir) attribution_mask |= (1u << h);
}
@@ -652,6 +819,15 @@ extern "C" __global__ void decision_policy_program(
signed char* __restrict__ diag_prev_dir_signed, // [n_backtests * N_HORIZONS]
// CRT.diag Group B: conviction-EMA histogram.
unsigned int* __restrict__ diag_conv_hist, // [n_backtests * 10]
// CRT.diag.2: per-horizon alpha-input EMA state.
float* __restrict__ alpha_ema_per_b_per_h, // [n_backtests * N_HORIZONS]
float* __restrict__ alpha_diff_var_per_b_per_h, // [n_backtests * N_HORIZONS]
float* __restrict__ alpha_sample_var_per_b_per_h, // [n_backtests * N_HORIZONS]
// CRT.diag.2 Group E: smoothed-direction-flip counters.
unsigned int* __restrict__ diag_smoothed_flip_count, // [n_backtests * N_HORIZONS]
unsigned int* __restrict__ diag_smoothed_current_run_length, // [n_backtests * N_HORIZONS]
unsigned long long* __restrict__ diag_smoothed_sum_run_length, // [n_backtests * N_HORIZONS]
signed char* __restrict__ diag_smoothed_prev_dir, // [n_backtests * N_HORIZONS]
int n_backtests
) {
int b = blockIdx.x;
@@ -672,6 +848,16 @@ extern "C" __global__ void decision_policy_program(
return;
}
// CRT.diag.2: smooth RAW alpha_probs[h] per-horizon BEFORE the §4.4
// conviction formula consumes it. Mirrors decision_policy_default.
float alpha_smoothed[N_HORIZONS];
compute_alpha_ema_per_horizon(
b, alpha_probs,
alpha_ema_per_b_per_h,
alpha_diff_var_per_b_per_h,
alpha_sample_var_per_b_per_h,
alpha_smoothed);
// CRT.1 C1.2: multi-horizon ISV-weighted conviction (spec §4.4).
// Mirrors decision_policy_default — see that kernel for the full
// rationale. The VM's per-horizon emit + aggregate opcodes still
@@ -679,6 +865,9 @@ extern "C" __global__ void decision_policy_program(
// signed `final_size` and uses the §4.4 conviction-driven target
// instead. The attribution mask from the stack IS still consumed
// at OP_WRITE_ORDER so the VM continues to credit horizons on entry.
//
// CRT.diag.2: feeds the §4.4 formula alpha_smoothed[] instead of
// alpha_probs[].
const Instruction* prog = programs + (size_t)b * max_instructions;
const IsvKellyState* isv = reinterpret_cast<const IsvKellyState*>(
isv_kelly_base + (size_t)b * N_HORIZONS * sizeof(IsvKellyState)
@@ -686,8 +875,8 @@ extern "C" __global__ void decision_policy_program(
const unsigned int my_regime = regimes[b];
const float cost = cost_per_lot_per_side_per_b[b];
const float conviction_signed = compute_multi_horizon_conviction(
alpha_probs, isv, cost);
const float conviction_signed = compute_multi_horizon_conviction_smoothed(
alpha_smoothed, isv, cost);
const float conviction_abs = fabsf(conviction_signed);
const float conviction_dir = (conviction_signed >= 0.0f) ? 1.0f : -1.0f;
@@ -701,6 +890,7 @@ extern "C" __global__ void decision_policy_program(
// CRT.diag Group A: per-horizon direction-flip + run-length tracking.
// Mirrors decision_policy_default. Observe-only; single-writer-per-block.
// Reads RAW alpha_probs[h] — head-to-head comparison with Group E.
#pragma unroll
for (int h = 0; h < N_HORIZONS; ++h) {
const float p = alpha_probs[h];
@@ -725,6 +915,14 @@ extern "C" __global__ void decision_policy_program(
diag_prev_dir_signed[b * N_HORIZONS + h] = new_dir;
}
// CRT.diag.2 Group E: smoothed-direction-flip + mean-run-length.
update_smoothed_flip_counters(
b, alpha_smoothed,
diag_smoothed_flip_count,
diag_smoothed_current_run_length,
diag_smoothed_sum_run_length,
diag_smoothed_prev_dir);
// CRT.diag Group B: smoothed-conviction histogram.
{
float c = conv_ema;
@@ -737,6 +935,7 @@ extern "C" __global__ void decision_policy_program(
}
// CRT.1 C1.4: refresh intra-trade trajectory state for composite exit.
// RAW alpha_probs kept here — disagreement signal is meant to be fast.
update_open_trade_trajectory(b, positions, books_per_b, alpha_probs,
conv_ema, open_trade_state_per_b);

View File

@@ -392,6 +392,27 @@ impl BacktestHarness {
hist_str.push_str(&format!(" {}:{}", bucket_labels[bk], count));
}
eprintln!("crt_diag h{} run_length_hist:{}", horizons[h], hist_str);
// CRT.diag.2 Group E: SMOOTHED-direction flip rate head-to-head
// vs raw. If smoothed mean_run_len ≫ raw mean_run_len, the
// hypothesis (per-event output noise masks a slower signal)
// is supported. If similar, hypothesis is falsified.
let smoothed_flips: u64 = (0..n_b)
.map(|b| diag.smoothed_flip_count[b * n_horizons + h] as u64)
.sum();
let smoothed_sum_run: u64 = (0..n_b)
.map(|b| diag.smoothed_sum_run_length[b * n_horizons + h])
.sum();
let smoothed_mean_run = if smoothed_flips > 0 {
smoothed_sum_run as f64 / smoothed_flips as f64
} else {
f64::NAN
};
eprintln!(
"crt_diag h{} smoothed: flips={} mean_run_len={:.1} events (vs raw {} / {:.1})",
horizons[h], smoothed_flips, smoothed_mean_run,
flips, mean_run,
);
}
// Group B — smoothed-conviction histogram.

View File

@@ -124,6 +124,14 @@ pub struct CrtDiagnostics {
/// Win count per entry-conviction bucket (segment_realized > 0).
/// Layout: `[backtest * 10 + bucket]`.
pub outcome_n_wins: Vec<u32>,
/// CRT.diag.2 Group E: per-horizon SMOOTHED-direction flip count.
/// Layout: `[backtest * N_HORIZONS + h]`. Compare head-to-head with
/// `flip_count` (which counts RAW alpha_probs direction flips).
pub smoothed_flip_count: Vec<u32>,
/// CRT.diag.2 Group E: per-horizon cumulative SMOOTHED run length over
/// completed runs (u64 to avoid overflow at multi-million-event smoke).
/// Layout: `[backtest * N_HORIZONS + h]`.
pub smoothed_sum_run_length: Vec<u64>,
}
/// S2.1/S2.2: max_hold enforcement diagnostic counters.
@@ -313,6 +321,28 @@ pub struct LobSimCuda {
pub(crate) diag_outcome_n_d: CudaSlice<u32>, // [n_backtests * 10]
pub(crate) diag_outcome_sum_pnl_d: CudaSlice<f32>, // [n_backtests * 10]
pub(crate) diag_outcome_n_wins_d: CudaSlice<u32>, // [n_backtests * 10]
// CRT.diag.2: per-horizon Wiener-α EMA on raw alpha_probs. Smoothed
// alpha enters the multi-horizon §4.4 conviction formula instead of
// the raw broadcast values — hypothesis test for whether per-event
// output noise is hiding a slower signal. All zero-init: sentinel "no
// observation yet" so the first decision-kernel call bootstraps per
// pearl_first_observation_bootstrap. Floor on Wiener α = 0.1 (stronger
// smoothing than the 0.4 output-side floor; testing input vs output
// smoothing differ). Touched by both decision_policy_default and
// decision_policy_program every event; no host roundtrip.
pub(crate) alpha_ema_per_b_per_h_d: CudaSlice<f32>, // [n_backtests * N_HORIZONS]
pub(crate) alpha_diff_var_per_b_per_h_d: CudaSlice<f32>, // [n_backtests * N_HORIZONS]
pub(crate) alpha_sample_var_per_b_per_h_d: CudaSlice<f32>, // [n_backtests * N_HORIZONS]
// CRT.diag.2 Group E: per-horizon SMOOTHED direction-flip + mean-run-length.
// Mirrors Group A but reading alpha_smoothed[h] instead of alpha_probs[h].
// Skipping the 5-bucket histogram (Group A keeps it). Head-to-head
// comparison: raw flip rate vs smoothed flip rate, per horizon.
pub(crate) diag_smoothed_flip_count_d: CudaSlice<u32>, // [n_backtests * N_HORIZONS]
pub(crate) diag_smoothed_current_run_length_d: CudaSlice<u32>, // [n_backtests * N_HORIZONS]
pub(crate) diag_smoothed_sum_run_length_d: CudaSlice<u64>, // [n_backtests * N_HORIZONS]
pub(crate) diag_smoothed_prev_dir_d: CudaSlice<i8>, // [n_backtests * N_HORIZONS]
}
impl LobSimCuda {
@@ -611,6 +641,35 @@ impl LobSimCuda {
.alloc_zeros::<u32>(n_backtests * 10)
.context("alloc diag_outcome_n_wins_d")?;
// CRT.diag.2: per-horizon alpha-input EMA state. Zero-init = "no
// observation yet" — first decision-kernel call bootstraps per
// pearl_first_observation_bootstrap. Same pattern as the
// conviction-level Wiener-α EMA above (zero-init = sentinel).
let alpha_ema_per_b_per_h_d = stream
.alloc_zeros::<f32>(n_backtests * N_HORIZONS)
.context("alloc alpha_ema_per_b_per_h_d")?;
let alpha_diff_var_per_b_per_h_d = stream
.alloc_zeros::<f32>(n_backtests * N_HORIZONS)
.context("alloc alpha_diff_var_per_b_per_h_d")?;
let alpha_sample_var_per_b_per_h_d = stream
.alloc_zeros::<f32>(n_backtests * N_HORIZONS)
.context("alloc alpha_sample_var_per_b_per_h_d")?;
// CRT.diag.2 Group E: per-horizon SMOOTHED direction-flip counters.
// Same shape as Group A's flip/run-length slots (minus the histogram).
let diag_smoothed_flip_count_d = stream
.alloc_zeros::<u32>(n_backtests * N_HORIZONS)
.context("alloc diag_smoothed_flip_count_d")?;
let diag_smoothed_current_run_length_d = stream
.alloc_zeros::<u32>(n_backtests * N_HORIZONS)
.context("alloc diag_smoothed_current_run_length_d")?;
let diag_smoothed_sum_run_length_d = stream
.alloc_zeros::<u64>(n_backtests * N_HORIZONS)
.context("alloc diag_smoothed_sum_run_length_d")?;
let diag_smoothed_prev_dir_d = stream
.alloc_zeros::<i8>(n_backtests * N_HORIZONS)
.context("alloc diag_smoothed_prev_dir_d")?;
// CRT Phase A0.5 corrective: on-device conviction history buffer.
// Capacity = 5M decisions × 4B/f32 = 20MB. Matches the prior
// host-side `Vec::with_capacity(3_000_000)` plus headroom for
@@ -715,6 +774,13 @@ impl LobSimCuda {
diag_outcome_n_d,
diag_outcome_sum_pnl_d,
diag_outcome_n_wins_d,
alpha_ema_per_b_per_h_d,
alpha_diff_var_per_b_per_h_d,
alpha_sample_var_per_b_per_h_d,
diag_smoothed_flip_count_d,
diag_smoothed_current_run_length_d,
diag_smoothed_sum_run_length_d,
diag_smoothed_prev_dir_d,
})
}
@@ -953,14 +1019,17 @@ impl LobSimCuda {
/// touches none of these.
pub fn read_diagnostics(&self) -> Result<CrtDiagnostics> {
let n_b = self.n_backtests;
let mut flip_count = vec![0u32; n_b * N_HORIZONS];
let mut sum_run_length = vec![0u64; n_b * N_HORIZONS];
let mut run_length_hist = vec![0u32; n_b * N_HORIZONS * 5];
let mut conv_hist = vec![0u32; n_b * 10];
let mut hold_hist = vec![0u32; n_b * 6];
let mut outcome_n = vec![0u32; n_b * 10];
let mut outcome_sum_pnl = vec![0f32; n_b * 10];
let mut outcome_n_wins = vec![0u32; n_b * 10];
let mut flip_count = vec![0u32; n_b * N_HORIZONS];
let mut sum_run_length = vec![0u64; n_b * N_HORIZONS];
let mut run_length_hist = vec![0u32; n_b * N_HORIZONS * 5];
let mut conv_hist = vec![0u32; n_b * 10];
let mut hold_hist = vec![0u32; n_b * 6];
let mut outcome_n = vec![0u32; n_b * 10];
let mut outcome_sum_pnl = vec![0f32; n_b * 10];
let mut outcome_n_wins = vec![0u32; n_b * 10];
// CRT.diag.2 Group E: smoothed-direction flip/run-length.
let mut smoothed_flip_count = vec![0u32; n_b * N_HORIZONS];
let mut smoothed_sum_run_length = vec![0u64; n_b * N_HORIZONS];
self.stream.memcpy_dtoh(&self.diag_flip_count_d, flip_count.as_mut_slice())
.context("diag_flip_count DtoH")?;
self.stream.memcpy_dtoh(&self.diag_sum_run_length_d, sum_run_length.as_mut_slice())
@@ -977,6 +1046,12 @@ impl LobSimCuda {
.context("diag_outcome_sum_pnl DtoH")?;
self.stream.memcpy_dtoh(&self.diag_outcome_n_wins_d, outcome_n_wins.as_mut_slice())
.context("diag_outcome_n_wins DtoH")?;
self.stream.memcpy_dtoh(&self.diag_smoothed_flip_count_d,
smoothed_flip_count.as_mut_slice())
.context("diag_smoothed_flip_count DtoH")?;
self.stream.memcpy_dtoh(&self.diag_smoothed_sum_run_length_d,
smoothed_sum_run_length.as_mut_slice())
.context("diag_smoothed_sum_run_length DtoH")?;
Ok(CrtDiagnostics {
flip_count,
sum_run_length,
@@ -986,6 +1061,8 @@ impl LobSimCuda {
outcome_n,
outcome_sum_pnl,
outcome_n_wins,
smoothed_flip_count,
smoothed_sum_run_length,
})
}
@@ -1392,6 +1469,15 @@ impl LobSimCuda {
.arg(&mut self.diag_run_length_hist_d)
.arg(&mut self.diag_prev_dir_signed_d)
.arg(&mut self.diag_conv_hist_d)
// CRT.diag.2: per-horizon alpha-input EMA state.
.arg(&mut self.alpha_ema_per_b_per_h_d)
.arg(&mut self.alpha_diff_var_per_b_per_h_d)
.arg(&mut self.alpha_sample_var_per_b_per_h_d)
// CRT.diag.2 Group E: smoothed-direction flip counters.
.arg(&mut self.diag_smoothed_flip_count_d)
.arg(&mut self.diag_smoothed_current_run_length_d)
.arg(&mut self.diag_smoothed_sum_run_length_d)
.arg(&mut self.diag_smoothed_prev_dir_d)
.arg(&n)
.launch(cfg)?;
}
@@ -1432,6 +1518,15 @@ impl LobSimCuda {
.arg(&mut self.diag_run_length_hist_d)
.arg(&mut self.diag_prev_dir_signed_d)
.arg(&mut self.diag_conv_hist_d)
// CRT.diag.2: per-horizon alpha-input EMA state.
.arg(&mut self.alpha_ema_per_b_per_h_d)
.arg(&mut self.alpha_diff_var_per_b_per_h_d)
.arg(&mut self.alpha_sample_var_per_b_per_h_d)
// CRT.diag.2 Group E: smoothed-direction flip counters.
.arg(&mut self.diag_smoothed_flip_count_d)
.arg(&mut self.diag_smoothed_current_run_length_d)
.arg(&mut self.diag_smoothed_sum_run_length_d)
.arg(&mut self.diag_smoothed_prev_dir_d)
.arg(&n)
.launch(cfg)?;
}