Two orthogonal bugs that compounded to produce the exit_px=±21474836 sentinel in cluster smokes (315 trades baseline, 500 trades pearl): Bug A — NaN/Inf book propagation: - apply_snapshot_kernel passes through NaN/Inf prices from MBP-10 predecoded data (session boundaries, gap-fills). - walk_ask_for_buy / walk_bid_for_sell accumulate cost += take * NaN. - apply_fill_to_pos: avg_px = NaN; realized_pnl += NaN → permanent NaN. - Subsequent close: (int)(NaN-derived * 5000) → i32::MAX sentinel. - Fix: zero-out non-finite levels in apply_snapshot_kernel; add isfinite() guards in walk helpers as defense-in-depth; ATR update guards against non-finite mid. Bug B — pnl_track close branch doesn't reset scratch: - Scratch reset (full 24-byte memset to 0) was already present in the current code; no source change required for Bug B. Tests: - book_nan_inf_prices_dont_corrupt_realized_pnl: NaN at ask[3] + Inf at bid[5] survives the fill pipeline without making realized_pnl non-finite. - pnl_track_resets_scratch_on_close: open+close+5 flat events emits exactly 1 record; second open+close emits exactly 1 more (=2 total). 14/14 stop_controller tests pass; all other ml-backtesting test suites unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74 lines
3.1 KiB
Plaintext
74 lines
3.1 KiB
Plaintext
// book_update.cu — MBP-10 snapshot apply.
|
||
//
|
||
// Replaces the per-block book state with the broadcast snapshot. The
|
||
// full top-10 image overwrites the previous state (MBP-10 semantics).
|
||
// One block per backtest; thread tid handles one of 10 levels per side
|
||
// (10 threads used out of 32 in the warp; remainder idle).
|
||
//
|
||
// Per feedback_no_atomicadd.md: no atomics — single-writer per level.
|
||
// Per spec §2: same snapshot broadcast to every backtest in v1.
|
||
|
||
#include "lob_state.cuh"
|
||
|
||
// Books are laid out [n_backtests, 4 fields × 10 levels].
|
||
// Field order: bid_px, bid_sz, ask_px, ask_sz (one contiguous Book per backtest).
|
||
extern "C" __global__ void book_update_apply_snapshot(
|
||
const float* __restrict__ bid_px_in, // [10] — broadcast
|
||
const float* __restrict__ bid_sz_in, // [10]
|
||
const float* __restrict__ ask_px_in, // [10]
|
||
const float* __restrict__ ask_sz_in, // [10]
|
||
Book* __restrict__ books_out, // [n_backtests]
|
||
float* __restrict__ prev_mid, // [n_backtests]
|
||
float* __restrict__ atr_mid_ema, // [n_backtests]
|
||
int n_backtests
|
||
) {
|
||
int b = blockIdx.x;
|
||
int tid = threadIdx.x;
|
||
if (b >= n_backtests) return;
|
||
|
||
if (tid < 10) {
|
||
Book& bk = books_out[b];
|
||
const float bp = bid_px_in[tid];
|
||
const float bs = bid_sz_in[tid];
|
||
const float ap = ask_px_in[tid];
|
||
const float az = ask_sz_in[tid];
|
||
// Sanitize: level is valid only if price + size are both finite AND size > 0.
|
||
// Otherwise zero BOTH so walk_*_for_* (which skips lvl_sz <= 0) treats it as empty.
|
||
// Real MBP-10 data at session boundaries / gap-fills can contain NaN or Inf.
|
||
const bool bid_ok = isfinite(bp) && isfinite(bs) && bs > 0.0f;
|
||
const bool ask_ok = isfinite(ap) && isfinite(az) && az > 0.0f;
|
||
bk.bid_px[tid] = bid_ok ? bp : 0.0f;
|
||
bk.bid_sz[tid] = bid_ok ? bs : 0.0f;
|
||
bk.ask_px[tid] = ask_ok ? ap : 0.0f;
|
||
bk.ask_sz[tid] = ask_ok ? az : 0.0f;
|
||
}
|
||
|
||
// Thread 0 handles per-backtest ATR-EMA update against the broadcast mid.
|
||
if (tid == 0) {
|
||
const float mid_raw = 0.5f * (bid_px_in[0] + ask_px_in[0]);
|
||
if (!isfinite(mid_raw)) {
|
||
// Bad top-of-book (NaN/Inf from session boundary); skip ATR update.
|
||
// prev_mid and atr_mid_ema remain unchanged.
|
||
} else {
|
||
const float mid = mid_raw;
|
||
const float prev = prev_mid[b];
|
||
if (prev == 0.0f) {
|
||
// First-observation bootstrap (pearl_first_observation_bootstrap).
|
||
prev_mid[b] = mid;
|
||
// atr_mid_ema stays 0 until event 2.
|
||
} else {
|
||
const float delta = fabsf(mid - prev);
|
||
const float ema = atr_mid_ema[b];
|
||
if (ema == 0.0f) {
|
||
// First non-zero delta: replace directly.
|
||
atr_mid_ema[b] = delta;
|
||
} else {
|
||
// Wiener-α=0.4 EMA (pearl_wiener_alpha_floor_for_nonstationary).
|
||
atr_mid_ema[b] = 0.4f * delta + 0.6f * ema;
|
||
}
|
||
prev_mid[b] = mid;
|
||
}
|
||
}
|
||
}
|
||
}
|