diag(crt): empirical measurement battery for signal × controller × cost analysis

Pure-instrumentation commit. Zero behavior change. The Gate CRT.1 smoke
p9cnk (commit 1e656948b) produced 222k trades and 29688% drawdown,
worse than the failed CRT.A. The structural fixes (multi-horizon
conviction, no-trade band, composite exit) all work as designed — but
the trade cadence is fundamentally too fast for the signal/cost ratio.
Before another round of threshold tuning, measure the structural truth.

Adds device-side counters dumped at smoke close (single end-of-run
memcpy_dtoh batch in read_diagnostics(), NOT per-event):

Group A — per-horizon signal persistence:
  - flip_count[h]:        total direction-sign flips
  - sum_run_length[h]:    cumulative events between flips (u64)
  - run_length_hist[h]:   5-bucket log-scale histogram (1-9, 10-99,
                          100-999, 1k-9.9k, 10k+)
  Output: mean_run_len per horizon = signal half-life proxy

Group B — conviction smoothed-EMA distribution:
  - conv_hist[10]:        10-bucket histogram over [0, 1)
  Output: shape of conviction distribution at decision time

Group C — hold-time distribution:
  - hold_hist[6]:         <1s, 1-10s, 10-60s, 1-10m, 10-60m, >1h
  Output: empirical distribution of trade hold times

Group D — outcome by entry-conviction:
  - outcome_n[10]:        trades per conviction bucket
  - outcome_sum_pnl[10]:  cumulative realized PnL per bucket (price-units)
  - outcome_n_wins[10]:   wins per bucket
  Output: validates "high conviction = better trades" hypothesis OR
          surfaces signal miscalibration

Per-backtest memory: ~330 bytes. Negligible vs existing state.

All counters single-writer-per-block (threadIdx.x == 0 convention; no
atomicAdd per feedback_no_atomicadd). All updates kernel-internal (no
host roundtrip per event). The end-of-run dump uses memcpy_dtoh in
read_diagnostics() — called once at harness termination from the
post-stop_ctrl_counters block, never per-event. Per
feedback_no_htod_htoh_only_mapped_pinned, the hot path is untouched.

N_HORIZONS = 5 (heads.rs canonical {30, 100, 300, 1000, 6000}); the
plan text says 4 but the workspace constant is 5 — used the code value.

Output at end of cluster smoke as a `crt_diag` log block:
  crt_diag h30: flips=X mean_run_len=Y events
  crt_diag h30 run_length_hist: 1-9:N 10-99:N 100-999:N ...
  ...
  crt_diag conv_ema_hist: [0.0-0.1]:N [0.1-0.2]:N ...
  crt_diag hold_time_hist: <1s:N 1-10s:N ...
  crt_diag outcome_by_entry_conv[0.0-0.1]: n=N win_rate=X.X% mean_pnl_pu=Y.YYY
  ...

The output drives the next round of CRT.1 threshold design (delta_floor,
composite exit factors, min-hold cooldown) from data instead of guesses.

Per pearl_adaptive_not_tuned and pearl_controller_anchors_isv_driven:
thresholds will become ISV-derived in CRT.2; CRT.1 must first establish
defensible defaults from the empirical signal characteristics.

Verified: stop_controller (22/22 pass), decision_floor_coldstart (3/3
pass), threshold_and_cost (3/3 pass), parallel_sim_correctness (1/1
pass), lob_sim_integrated_fuzz (3/3 pass). Two lob_sim_fixtures
failures (fix_decision_alpha_buy_close, fix_decision_program_h4_only)
were pre-existing at HEAD 1e656948b — not caused by this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-20 21:56:07 +02:00
parent 1e656948b3
commit b44a97ff94
4 changed files with 395 additions and 1 deletions

View File

@@ -425,6 +425,16 @@ extern "C" __global__ void decision_policy_default(
// this kernel updates the intra-trade fields each event so the composite
// exit check has fresh inputs on the next pass.
unsigned char* __restrict__ open_trade_state_per_b, // [n_backtests * 64]
// CRT.diag Group A: per-horizon signal persistence counters.
// Observe-only; zero behavior impact. Single-writer-per-block
// (caller already gated on threadIdx.x == 0). End-of-run dump only.
unsigned int* __restrict__ diag_flip_count, // [n_backtests * N_HORIZONS]
unsigned int* __restrict__ diag_current_run_length, // [n_backtests * N_HORIZONS]
unsigned long long* __restrict__ diag_sum_run_length, // [n_backtests * N_HORIZONS]
unsigned int* __restrict__ diag_run_length_hist, // [n_backtests * N_HORIZONS * 5]
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]
int n_backtests
) {
int b = blockIdx.x;
@@ -465,6 +475,44 @@ extern "C" __global__ void decision_policy_default(
conviction_diff_var_ema_per_b,
conviction_sample_var_ema_per_b);
// CRT.diag Group A: per-horizon direction-flip + run-length tracking.
// Observe-only. Single-writer-per-block (already gated on thread 0).
#pragma unroll
for (int h = 0; h < N_HORIZONS; ++h) {
const float p = alpha_probs[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_prev_dir_signed[b * N_HORIZONS + h];
const unsigned int prev_run = diag_current_run_length[b * N_HORIZONS + h];
if (prev_dir != 0 && new_dir != 0 && prev_dir != new_dir) {
// Direction flipped — record the prev run length.
diag_flip_count[b * N_HORIZONS + h] += 1u;
diag_sum_run_length[b * N_HORIZONS + h] += (unsigned long long)prev_run;
int bk;
if (prev_run < 10u) bk = 0;
else if (prev_run < 100u) bk = 1;
else if (prev_run < 1000u) bk = 2;
else if (prev_run < 10000u) bk = 3;
else bk = 4;
diag_run_length_hist[(b * N_HORIZONS + h) * 5 + bk] += 1u;
diag_current_run_length[b * N_HORIZONS + h] = 1u; // start new run
} else {
diag_current_run_length[b * N_HORIZONS + h] = prev_run + 1u;
}
diag_prev_dir_signed[b * N_HORIZONS + h] = new_dir;
}
// CRT.diag Group B: smoothed-conviction histogram (10 buckets over [0, 1)).
{
float c = conv_ema;
if (c < 0.0f) c = 0.0f;
if (c >= 1.0f) c = 0.999999f;
int bk = (int)(c * 10.0f);
if (bk < 0) bk = 0;
if (bk > 9) bk = 9;
diag_conv_hist[b * 10 + bk] += 1u;
}
// CRT.1 C1.4: refresh intra-trade trajectory state (offsets 24, 44, 48,
// 56) for the composite exit_signal. Skipped internally when flat.
update_open_trade_trajectory(b, positions, books_per_b, alpha_probs,
@@ -596,6 +644,14 @@ extern "C" __global__ void decision_policy_program(
// CRT.1 C1.4: trajectory state + composite exit_signal circuit-breaker.
// Mirrors decision_policy_default — see that kernel for the contract.
unsigned char* __restrict__ open_trade_state_per_b, // [n_backtests * 64]
// CRT.diag Group A: per-horizon signal persistence counters (observe-only).
unsigned int* __restrict__ diag_flip_count, // [n_backtests * N_HORIZONS]
unsigned int* __restrict__ diag_current_run_length, // [n_backtests * N_HORIZONS]
unsigned long long* __restrict__ diag_sum_run_length, // [n_backtests * N_HORIZONS]
unsigned int* __restrict__ diag_run_length_hist, // [n_backtests * N_HORIZONS * 5]
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]
int n_backtests
) {
int b = blockIdx.x;
@@ -643,6 +699,43 @@ extern "C" __global__ void decision_policy_program(
conviction_diff_var_ema_per_b,
conviction_sample_var_ema_per_b);
// CRT.diag Group A: per-horizon direction-flip + run-length tracking.
// Mirrors decision_policy_default. Observe-only; single-writer-per-block.
#pragma unroll
for (int h = 0; h < N_HORIZONS; ++h) {
const float p = alpha_probs[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_prev_dir_signed[b * N_HORIZONS + h];
const unsigned int prev_run = diag_current_run_length[b * N_HORIZONS + h];
if (prev_dir != 0 && new_dir != 0 && prev_dir != new_dir) {
diag_flip_count[b * N_HORIZONS + h] += 1u;
diag_sum_run_length[b * N_HORIZONS + h] += (unsigned long long)prev_run;
int bk;
if (prev_run < 10u) bk = 0;
else if (prev_run < 100u) bk = 1;
else if (prev_run < 1000u) bk = 2;
else if (prev_run < 10000u) bk = 3;
else bk = 4;
diag_run_length_hist[(b * N_HORIZONS + h) * 5 + bk] += 1u;
diag_current_run_length[b * N_HORIZONS + h] = 1u;
} else {
diag_current_run_length[b * N_HORIZONS + h] = prev_run + 1u;
}
diag_prev_dir_signed[b * N_HORIZONS + h] = new_dir;
}
// CRT.diag Group B: smoothed-conviction histogram.
{
float c = conv_ema;
if (c < 0.0f) c = 0.0f;
if (c >= 1.0f) c = 0.999999f;
int bk = (int)(c * 10.0f);
if (bk < 0) bk = 0;
if (bk > 9) bk = 9;
diag_conv_hist[b * 10 + bk] += 1u;
}
// CRT.1 C1.4: refresh intra-trade trajectory state for composite exit.
update_open_trade_trajectory(b, positions, books_per_b, alpha_probs,
conv_ema, open_trade_state_per_b);

View File

@@ -68,7 +68,13 @@ extern "C" __global__ void pnl_track_step(
// decision_policy kernels). Read on open transition to snapshot
// conviction_at_entry + initialize conviction_ema mirror in
// open_trade_state.
const float* __restrict__ conviction_ema_per_b // [n_backtests]
const float* __restrict__ conviction_ema_per_b, // [n_backtests]
// CRT.diag Group C: hold-time histogram at trade close (6 log buckets).
unsigned int* __restrict__ diag_hold_hist, // [n_backtests * 6]
// CRT.diag Group D: outcome by entry-conviction (10 conviction buckets).
unsigned int* __restrict__ diag_outcome_n, // [n_backtests * 10]
float* __restrict__ diag_outcome_sum_pnl, // [n_backtests * 10] — segment_realized (price-units × lots)
unsigned int* __restrict__ diag_outcome_n_wins // [n_backtests * 10]
) {
int b = blockIdx.x;
if (b >= n_backtests || threadIdx.x != 0) return;
@@ -120,6 +126,40 @@ extern "C" __global__ void pnl_track_step(
// Realized P&L delta = pos.realized_pnl_now pos.realized_pnl_at_open.
// In price-units × lots.
const float segment_realized = pos->realized_pnl - realized_at_open;
// CRT.diag Group C: hold-time histogram. Observe-only. The buckets
// (< 1s, 1-10s, 10-60s, 1-10m, 10-60m, > 1h) capture log-scale
// distribution of how long trades stay open.
{
const unsigned long long hold_ns = current_ts_ns - entry_ts;
const double hold_s = (double)hold_ns / 1.0e9;
int bk;
if (hold_s < 1.0) bk = 0;
else if (hold_s < 10.0) bk = 1;
else if (hold_s < 60.0) bk = 2;
else if (hold_s < 600.0) bk = 3;
else if (hold_s < 3600.0) bk = 4;
else bk = 5;
diag_hold_hist[b * 6 + bk] += 1u;
}
// CRT.diag Group D: outcome by entry-conviction (10 conviction
// buckets over [0, 1)). Reads conviction_at_entry from open_trade_state
// (offset 20, populated by the open branch above on a prior call).
{
const float conv_at_entry = *reinterpret_cast<const float*>(st + 20);
float c = conv_at_entry;
if (c < 0.0f) c = 0.0f;
if (c >= 1.0f) c = 0.999999f;
int bk = (int)(c * 10.0f);
if (bk < 0) bk = 0;
if (bk > 9) bk = 9;
diag_outcome_n[b * 10 + bk] += 1u;
diag_outcome_sum_pnl[b * 10 + bk] += segment_realized;
if (segment_realized > 0.0f) {
diag_outcome_n_wins[b * 10 + bk] += 1u;
}
}
// Convert to USD ×100 fixed-point: × $50/index-point × 100 = ×5000.
const int realised_usd_fp = (int)(segment_realized * 5000.0f + (segment_realized >= 0.0f ? 0.5f : -0.5f));
// Exit price reconstructed from realized delta: realized = (exit entry) × dir × |size|.

View File

@@ -354,6 +354,101 @@ impl BacktestHarness {
Err(e) => eprintln!("stop_ctrl_counters read failed: {e}"),
}
// CRT.diag: empirical measurement battery (Groups A-D). Drives the
// next round of CRT.1 threshold design — measure structural truth
// BEFORE another tuning pass. Single end-of-run dump, observe-only.
match self.sim.read_diagnostics() {
Ok(diag) => {
let n_b = self.cfg.n_parallel;
// Horizons match crates/ml-alpha/src/heads.rs::HORIZONS.
let horizons: [&str; 5] = ["30", "100", "300", "1000", "6000"];
let n_horizons = horizons.len();
// Group A — per-horizon signal persistence.
for h in 0..n_horizons {
let flips: u64 = (0..n_b)
.map(|b| diag.flip_count[b * n_horizons + h] as u64)
.sum();
let sum_run: u64 = (0..n_b)
.map(|b| diag.sum_run_length[b * n_horizons + h])
.sum();
let mean_run = if flips > 0 {
sum_run as f64 / flips as f64
} else {
f64::NAN
};
eprintln!(
"crt_diag h{}: flips={} mean_run_len={:.1} events",
horizons[h], flips, mean_run,
);
let bucket_labels = ["1-9", "10-99", "100-999", "1k-9.9k", "10k+"];
let mut hist_str = String::new();
for bk in 0..5 {
let count: u64 = (0..n_b)
.map(|b| {
diag.run_length_hist[(b * n_horizons + h) * 5 + bk] as u64
})
.sum();
hist_str.push_str(&format!(" {}:{}", bucket_labels[bk], count));
}
eprintln!("crt_diag h{} run_length_hist:{}", horizons[h], hist_str);
}
// Group B — smoothed-conviction histogram.
let mut conv_str = String::new();
for bk in 0..10 {
let count: u64 = (0..n_b)
.map(|b| diag.conv_hist[b * 10 + bk] as u64)
.sum();
let lo = bk as f32 / 10.0;
let hi = (bk + 1) as f32 / 10.0;
conv_str.push_str(&format!(" [{:.1}-{:.1}]:{}", lo, hi, count));
}
eprintln!("crt_diag conv_ema_hist:{}", conv_str);
// Group C — hold-time histogram.
let hold_labels = ["<1s", "1-10s", "10-60s", "1-10m", "10-60m", ">1h"];
let mut hold_str = String::new();
for bk in 0..6 {
let count: u64 = (0..n_b)
.map(|b| diag.hold_hist[b * 6 + bk] as u64)
.sum();
hold_str.push_str(&format!(" {}:{}", hold_labels[bk], count));
}
eprintln!("crt_diag hold_time_hist:{}", hold_str);
// Group D — outcome by entry-conviction.
for bk in 0..10 {
let n_total: u64 = (0..n_b)
.map(|b| diag.outcome_n[b * 10 + bk] as u64)
.sum();
let n_wins: u64 = (0..n_b)
.map(|b| diag.outcome_n_wins[b * 10 + bk] as u64)
.sum();
let sum_pnl: f64 = (0..n_b)
.map(|b| diag.outcome_sum_pnl[b * 10 + bk] as f64)
.sum();
let mean_pnl = if n_total > 0 {
sum_pnl / n_total as f64
} else {
f64::NAN
};
let win_rate = if n_total > 0 {
n_wins as f64 / n_total as f64
} else {
f64::NAN
};
let lo = bk as f32 / 10.0;
let hi = (bk + 1) as f32 / 10.0;
eprintln!(
"crt_diag outcome_by_entry_conv[{:.1}-{:.1}]: n={} win_rate={:.2}% mean_pnl_pu={:.3}",
lo, hi, n_total, win_rate * 100.0, mean_pnl,
);
}
}
Err(e) => eprintln!("crt_diag read failed: {e}"),
}
Ok(RunStats {
events_processed: self.event_count,
decisions_taken: total_decisions,

View File

@@ -94,6 +94,38 @@ pub struct NanCountersV2 {
pub last_bad_path: Vec<u32>,
}
/// CRT.diag: end-of-run empirical measurement battery dump.
///
/// All vectors are flat per-backtest layouts; the harness aggregates
/// across backtests for the log summary. See `LobSimCuda::read_diagnostics`
/// for the read-back path (single batched DtoV at smoke termination).
pub struct CrtDiagnostics {
/// Per-horizon flip count. Layout: `[backtest * N_HORIZONS + h]`.
pub flip_count: Vec<u32>,
/// Per-horizon cumulative run length over completed runs (u64 to avoid
/// overflow at multi-million-event smoke). Layout: `[backtest * N_HORIZONS + h]`.
pub sum_run_length: Vec<u64>,
/// Per-horizon 5-bucket log-scale run-length histogram.
/// Layout: `[(backtest * N_HORIZONS + h) * 5 + bucket]`. Buckets:
/// 0=1-9, 1=10-99, 2=100-999, 3=1k-9.9k, 4=10k+.
pub run_length_hist: Vec<u32>,
/// 10-bucket histogram of smoothed conviction at decision time.
/// Layout: `[backtest * 10 + bucket]`. Bucket k = [k/10, (k+1)/10).
pub conv_hist: Vec<u32>,
/// 6-bucket log-scale hold-time histogram at trade close.
/// Layout: `[backtest * 6 + bucket]`. Buckets:
/// 0=<1s, 1=1-10s, 2=10-60s, 3=1-10m, 4=10-60m, 5=>1h.
pub hold_hist: Vec<u32>,
/// Trade count per entry-conviction bucket. Layout: `[backtest * 10 + bucket]`.
pub outcome_n: Vec<u32>,
/// Cumulative realized PnL per entry-conviction bucket (price-units × lots,
/// pre USD-fp conversion). Layout: `[backtest * 10 + bucket]`.
pub outcome_sum_pnl: Vec<f32>,
/// Win count per entry-conviction bucket (segment_realized > 0).
/// Layout: `[backtest * 10 + bucket]`.
pub outcome_n_wins: Vec<u32>,
}
/// S2.1/S2.2: max_hold enforcement diagnostic counters.
///
/// After S2.2, max_hold enforcement lives in `resting_orders_step` (event-rate).
@@ -261,6 +293,26 @@ pub struct LobSimCuda {
// block). Read by the kernel on every event; zero device-to-host traffic
// on the per-event hot path.
pub(crate) delta_floor_d: CudaSlice<f32>, // [n_backtests] — no-trade band in lots
// CRT.diag: empirical measurement battery (observe-only, end-of-run dump).
// All counters allocated via alloc_zeros and incremented kernel-side; no
// host roundtrip per event. `read_diagnostics()` materialises them at
// smoke termination via a single batch of memcpy_dtov calls.
//
// Group A — per-horizon signal persistence:
pub(crate) diag_flip_count_d: CudaSlice<u32>, // [n_backtests * N_HORIZONS]
pub(crate) diag_current_run_length_d: CudaSlice<u32>, // [n_backtests * N_HORIZONS]
pub(crate) diag_sum_run_length_d: CudaSlice<u64>, // [n_backtests * N_HORIZONS]
pub(crate) diag_run_length_hist_d: CudaSlice<u32>, // [n_backtests * N_HORIZONS * 5]
pub(crate) diag_prev_dir_signed_d: CudaSlice<i8>, // [n_backtests * N_HORIZONS]
// Group B — conviction-EMA histogram:
pub(crate) diag_conv_hist_d: CudaSlice<u32>, // [n_backtests * 10]
// Group C — hold-time histogram at trade close:
pub(crate) diag_hold_hist_d: CudaSlice<u32>, // [n_backtests * 6]
// Group D — outcome by entry-conviction:
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]
}
impl LobSimCuda {
@@ -525,6 +577,40 @@ impl LobSimCuda {
.alloc_zeros::<f32>(n_backtests)
.context("alloc delta_floor_d")?;
// CRT.diag: empirical measurement battery (observe-only). Per-backtest
// memory footprint ≈ 4*(4+4+8+5*4)+10*4+6*4+10*(4+4+4) = 144+40+24+120
// = 328 bytes. Negligible vs other state. All counters zero-init.
let diag_flip_count_d = stream
.alloc_zeros::<u32>(n_backtests * N_HORIZONS)
.context("alloc diag_flip_count_d")?;
let diag_current_run_length_d = stream
.alloc_zeros::<u32>(n_backtests * N_HORIZONS)
.context("alloc diag_current_run_length_d")?;
let diag_sum_run_length_d = stream
.alloc_zeros::<u64>(n_backtests * N_HORIZONS)
.context("alloc diag_sum_run_length_d")?;
let diag_run_length_hist_d = stream
.alloc_zeros::<u32>(n_backtests * N_HORIZONS * 5)
.context("alloc diag_run_length_hist_d")?;
let diag_prev_dir_signed_d = stream
.alloc_zeros::<i8>(n_backtests * N_HORIZONS)
.context("alloc diag_prev_dir_signed_d")?;
let diag_conv_hist_d = stream
.alloc_zeros::<u32>(n_backtests * 10)
.context("alloc diag_conv_hist_d")?;
let diag_hold_hist_d = stream
.alloc_zeros::<u32>(n_backtests * 6)
.context("alloc diag_hold_hist_d")?;
let diag_outcome_n_d = stream
.alloc_zeros::<u32>(n_backtests * 10)
.context("alloc diag_outcome_n_d")?;
let diag_outcome_sum_pnl_d = stream
.alloc_zeros::<f32>(n_backtests * 10)
.context("alloc diag_outcome_sum_pnl_d")?;
let diag_outcome_n_wins_d = stream
.alloc_zeros::<u32>(n_backtests * 10)
.context("alloc diag_outcome_n_wins_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
@@ -619,6 +705,16 @@ impl LobSimCuda {
conviction_diff_var_ema_d,
conviction_sample_var_ema_d,
delta_floor_d,
diag_flip_count_d,
diag_current_run_length_d,
diag_sum_run_length_d,
diag_run_length_hist_d,
diag_prev_dir_signed_d,
diag_conv_hist_d,
diag_hold_hist_d,
diag_outcome_n_d,
diag_outcome_sum_pnl_d,
diag_outcome_n_wins_d,
})
}
@@ -847,6 +943,52 @@ impl LobSimCuda {
})
}
/// CRT.diag: end-of-run materialisation of the empirical measurement
/// battery. Reads all 8 host-visible diagnostic slots in one batch (the
/// 9th — `diag_current_run_length_d` — and 10th — `diag_prev_dir_signed_d`
/// — are transient kernel-internal state, not part of the summary).
///
/// Called ONCE at smoke termination from `BacktestHarness::run`. Each
/// `memcpy_dtoh` is a single device-to-host hop; per-event hot path
/// 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];
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())
.context("diag_sum_run_length DtoH")?;
self.stream.memcpy_dtoh(&self.diag_run_length_hist_d, run_length_hist.as_mut_slice())
.context("diag_run_length_hist DtoH")?;
self.stream.memcpy_dtoh(&self.diag_conv_hist_d, conv_hist.as_mut_slice())
.context("diag_conv_hist DtoH")?;
self.stream.memcpy_dtoh(&self.diag_hold_hist_d, hold_hist.as_mut_slice())
.context("diag_hold_hist DtoH")?;
self.stream.memcpy_dtoh(&self.diag_outcome_n_d, outcome_n.as_mut_slice())
.context("diag_outcome_n DtoH")?;
self.stream.memcpy_dtoh(&self.diag_outcome_sum_pnl_d, outcome_sum_pnl.as_mut_slice())
.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")?;
Ok(CrtDiagnostics {
flip_count,
sum_run_length,
run_length_hist,
conv_hist,
hold_hist,
outcome_n,
outcome_sum_pnl,
outcome_n_wins,
})
}
/// Upload per-backtest price-range bounds. Call ONCE before the first
/// apply_snapshot. min=0.0 / max=f32::INFINITY disables the check for
/// that backtest (default after LobSimCuda::new).
@@ -992,6 +1134,11 @@ impl LobSimCuda {
// CRT.1 C1.4: open branch snapshots conviction_at_entry +
// initializes conviction_ema mirror in open_trade_state.
.arg(&self.conviction_ema_d)
// CRT.diag Groups C + D: hold-time + outcome-by-entry-conviction.
.arg(&mut self.diag_hold_hist_d)
.arg(&mut self.diag_outcome_n_d)
.arg(&mut self.diag_outcome_sum_pnl_d)
.arg(&mut self.diag_outcome_n_wins_d)
.launch(cfg)?;
}
self.stream.synchronize()?;
@@ -1238,6 +1385,13 @@ impl LobSimCuda {
.arg(&mut self.conviction_sample_var_ema_d)
// CRT.1 C1.4: trajectory state for composite exit_signal.
.arg(&mut self.open_trade_state_d)
// CRT.diag Groups A + B: signal persistence + conviction histogram.
.arg(&mut self.diag_flip_count_d)
.arg(&mut self.diag_current_run_length_d)
.arg(&mut self.diag_sum_run_length_d)
.arg(&mut self.diag_run_length_hist_d)
.arg(&mut self.diag_prev_dir_signed_d)
.arg(&mut self.diag_conv_hist_d)
.arg(&n)
.launch(cfg)?;
}
@@ -1271,6 +1425,13 @@ impl LobSimCuda {
.arg(&mut self.conviction_sample_var_ema_d)
// CRT.1 C1.4: trajectory state for composite exit_signal.
.arg(&mut self.open_trade_state_d)
// CRT.diag Groups A + B: signal persistence + conviction histogram.
.arg(&mut self.diag_flip_count_d)
.arg(&mut self.diag_current_run_length_d)
.arg(&mut self.diag_sum_run_length_d)
.arg(&mut self.diag_run_length_hist_d)
.arg(&mut self.diag_prev_dir_signed_d)
.arg(&mut self.diag_conv_hist_d)
.arg(&n)
.launch(cfg)?;
}
@@ -1328,6 +1489,11 @@ impl LobSimCuda {
// CRT.1 C1.4: open branch snapshots conviction_at_entry +
// initializes conviction_ema mirror in open_trade_state.
.arg(&self.conviction_ema_d)
// CRT.diag Groups C + D: hold-time + outcome-by-entry-conviction.
.arg(&mut self.diag_hold_hist_d)
.arg(&mut self.diag_outcome_n_d)
.arg(&mut self.diag_outcome_sum_pnl_d)
.arg(&mut self.diag_outcome_n_wins_d)
.launch(cfg)?;
}
self.stream.synchronize()?;