Files
foxhunt/crates/ml-backtesting/cuda/resting_orders.cu
jgrusewski 95a77c4ac6 diag(ml-backtesting): max_hold enforcement counters localize why 86.5% of trades exceed the 60s cap
S2 cluster smoke ppcfk (29f7e923c) showed mean trade hold = 432s with
max_hold_ns=60s — 886/1024 trades exceed the cap that should hard-flat
them. Upload chain and pnl_track entry_ts write look structurally
correct on inspection. Need per-step counters to localize WHICH step
of the chain fails.

Adds 7 counters in stop_check_isv (kernel_calls, seen_zero, entry_ts_zero,
ts_underflow, elapsed_below_cap, would_fire, force_flat_written) plus 1
counter in seed_inflight_limits_batched (seed_saw_force_flat). Logged at
smoke end as a third nan_counters-like line.

Diagnostic only — no behavioral change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:35:20 +02:00

676 lines
31 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// resting_orders.cu — limit + stop + OCO machinery.
//
// Runs on every event (after book_update, before pnl_track). Per-block,
// single-writer (thread 0). Processes:
//
// 1. In-flight → resting promotion when arrival_ts_ns reached.
// Marketable on arrival? Cross immediately at the just-arrived book.
// 2. Resting limits: queue-decay against trade_signed_vol at level,
// check marketability against the new book (price moved through us).
// 3. Stop arming/triggering: best ask crossing trigger up → buy stop,
// best bid crossing trigger down → sell stop. On trigger:
// - StopMarket: walk book immediately, fill, emit OrderEvent
// - StopLimit: convert to a resting limit (allocate a LimitSlot)
// 4. OCO mutual exclusion: when one leg fills or stops triggers, free
// the paired slot via oco_pair index (0..31 = limit, 32..47 = stop).
//
// Per feedback_no_atomicadd.md: no atomics — single writer per block.
#include "lob_state.cuh"
#define KIND_LIMIT 1
#define KIND_IOC 2
#define KIND_FOK 3
#define KIND_STOP_MARKET 4
#define KIND_STOP_LIMIT 5
// Book walking helpers — same shape as order_match.cu but inlined here
// to keep this kernel's symbol set self-contained.
//
// min_px / max_px: per-backtest price-range bounds (from upload_price_range /
// S1.19). Levels with lvl_px outside [min_px, max_px] are MBP-10 sentinels
// for empty depth slots — consuming them writes avg_px=0 or avg_px>21M into
// pos.vwap_entry (194 zero + 216 huge sentinel writes, S1.22 root-cause).
__device__ static float walk_ask_for_buy(
const Book& book, float size, float& filled,
float min_px, float max_px
) {
float cost = 0.0f; filled = 0.0f;
#pragma unroll
for (int k = 0; k < 10 && size > 0.0f; ++k) {
const float lvl_sz = book.ask_sz[k];
const float lvl_px = book.ask_px[k];
if (lvl_sz <= 0.0f || !isfinite(lvl_sz) || !isfinite(lvl_px)
|| lvl_px < min_px || lvl_px > max_px) continue;
const float take = (size < lvl_sz) ? size : lvl_sz;
cost += take * lvl_px; filled += take; size -= take;
}
return cost;
}
__device__ static float walk_bid_for_sell(
const Book& book, float size, float& filled,
float min_px, float max_px
) {
float cost = 0.0f; filled = 0.0f;
#pragma unroll
for (int k = 0; k < 10 && size > 0.0f; ++k) {
const float lvl_sz = book.bid_sz[k];
const float lvl_px = book.bid_px[k];
if (lvl_sz <= 0.0f || !isfinite(lvl_sz) || !isfinite(lvl_px)
|| lvl_px < min_px || lvl_px > max_px) continue;
const float take = (size < lvl_sz) ? size : lvl_sz;
cost += take * lvl_px; filled += take; size -= take;
}
return cost;
}
// Mirror of decision_policy.cu's emit_event — duplicated to avoid
// a cross-kernel header dependency. Layout matches OrderEvent (24 B)
// in crates/ml-backtesting/src/order.rs.
__device__ static void emit_audit(
unsigned char* audit_base, unsigned int* head_d, int b, unsigned int ring_cap,
unsigned long long ts_ns, unsigned int slot_tag, unsigned char kind,
unsigned char side, unsigned short size, int px_ticks, int trigger_px_ticks
) {
const unsigned int idx = head_d[b] % ring_cap;
head_d[b] += 1;
unsigned char* rec = audit_base + ((size_t)b * ring_cap + idx) * 24;
*reinterpret_cast<unsigned long long*>(rec + 0) = ts_ns;
*reinterpret_cast<unsigned int*>(rec + 8) = slot_tag;
rec[12] = kind;
rec[13] = side;
*reinterpret_cast<unsigned short*>(rec + 14) = size;
*reinterpret_cast<int*>(rec + 16) = px_ticks;
*reinterpret_cast<int*>(rec + 20) = trigger_px_ticks;
}
// SlotTag pack: kind | (idx << 8) | (gen << 16). See src/order.rs.
__device__ static unsigned int pack_slot_tag(unsigned char kind, unsigned char idx, unsigned short gen) {
return (unsigned int)kind | ((unsigned int)idx << 8) | ((unsigned int)gen << 16);
}
// Free the slot referenced by an oco_pair byte: 0..31 = limits, 32..47 = stops.
__device__ static void cancel_oco_pair(Orders& orders, unsigned char oco_pair) {
if (oco_pair == 0xFF) return;
if (oco_pair < MAX_LIMITS) {
orders.limits[oco_pair].active = 0;
} else if (oco_pair - MAX_LIMITS < MAX_STOPS) {
orders.stops[oco_pair - MAX_LIMITS].active = 0;
}
}
// Apply a fill to per-backtest position state. Same VWAP-scale-in /
// counter-direction realised-P&L rule as order_match.cu's market-order
// path; isolated here as a __device__ helper.
//
// P4: per-fill cost integration. After the close-leg realized_pnl math
// runs (so the unwind P&L is computed from gross prices), deduct the
// fill cost from realized_pnl AND accumulate into total_fees_per_b[b].
// Net-of-cost semantics: isv_kelly_update_on_close reads realized_pnl
// delta which is now net of cost — Kelly state learns realistic returns.
//
// Plain `+=` for total_fees_per_b is safe under the single-writer-per-block
// convention enforced by `if (threadIdx.x != 0) return;` at the top of
// every block-per-backtest kernel.
//
// S1.20: NaN instrumentation counters.
// nan_avg_px — incremented when avg_px = total_cost/filled_lots is NaN/Inf.
// nan_realised — incremented when realised P&L in counter-direction branch is NaN/Inf.
// nan_realized_pnl — incremented when pos.realized_pnl becomes non-finite after += or -=.
// On NaN-producing events the function returns early WITHOUT poisoning pos state.
//
// S1.21: per-vwap-write-site counters.
// vwap_zero_open_flat / vwap_huge_open_flat — open-from-flat branch (line 131).
// vwap_zero_scale_in / vwap_huge_scale_in — scale-in branch (line 136).
// vwap_zero_flip / vwap_huge_flip — flip-beyond branch (line 150).
// last_bad_vwap / last_bad_path — capture site + value.
__device__ static void apply_fill_to_pos(
Pos& pos,
float filled_lots,
float total_cost,
unsigned char side,
int b,
const float* __restrict__ cost_per_lot_per_side_per_b,
float* __restrict__ total_fees_per_b,
unsigned int* __restrict__ nan_avg_px,
unsigned int* __restrict__ nan_realised,
unsigned int* __restrict__ nan_realized_pnl,
unsigned int* __restrict__ vwap_zero_open_flat,
unsigned int* __restrict__ vwap_huge_open_flat,
unsigned int* __restrict__ vwap_zero_scale_in,
unsigned int* __restrict__ vwap_huge_scale_in,
unsigned int* __restrict__ vwap_zero_flip,
unsigned int* __restrict__ vwap_huge_flip,
float* __restrict__ last_bad_vwap,
unsigned int* __restrict__ last_bad_path
) {
if (filled_lots <= 0.0f) return;
const float avg_px = total_cost / filled_lots;
if (!isfinite(avg_px)) {
nan_avg_px[b] += 1u;
return; // do NOT propagate NaN into pos state
}
const float sgn = (side == 0) ? 1.0f : -1.0f;
const float prev_pos_f = (float)pos.position_lots;
const float new_pos_f = prev_pos_f + sgn * filled_lots;
if (pos.position_lots == 0) {
pos.vwap_entry = avg_px;
// S1.21: open-from-flat write site.
if (pos.vwap_entry == 0.0f) {
vwap_zero_open_flat[b] += 1u;
last_bad_vwap[b] = 0.0f;
last_bad_path[b] = 1u;
} else if (!isfinite(pos.vwap_entry) || fabsf(pos.vwap_entry) > 21000000.0f) {
vwap_huge_open_flat[b] += 1u;
last_bad_vwap[b] = pos.vwap_entry;
last_bad_path[b] = 1u;
}
} else if ((prev_pos_f > 0.0f) == (sgn > 0.0f)) {
const float prev_notional = prev_pos_f * pos.vwap_entry;
const float new_notional = prev_notional + sgn * total_cost;
const float new_abs = (new_pos_f > 0.0f) ? new_pos_f : -new_pos_f;
pos.vwap_entry = (new_abs > 0.0f) ? (new_notional / (sgn * new_abs)) : 0.0f;
// S1.21: scale-in write site.
if (pos.vwap_entry == 0.0f) {
vwap_zero_scale_in[b] += 1u;
last_bad_vwap[b] = 0.0f;
last_bad_path[b] = 2u;
} else if (!isfinite(pos.vwap_entry) || fabsf(pos.vwap_entry) > 21000000.0f) {
vwap_huge_scale_in[b] += 1u;
last_bad_vwap[b] = pos.vwap_entry;
last_bad_path[b] = 3u;
}
} else {
const float prev_abs = (prev_pos_f > 0.0f) ? prev_pos_f : -prev_pos_f;
const float unwind = (filled_lots < prev_abs) ? filled_lots : prev_abs;
const float dir_was_long = (prev_pos_f > 0.0f) ? 1.0f : -1.0f;
const float realised = (avg_px - pos.vwap_entry) * dir_was_long * unwind;
if (!isfinite(realised)) {
nan_realised[b] += 1u;
return; // do not poison realized_pnl
}
pos.realized_pnl += realised;
if (!isfinite(pos.realized_pnl)) {
nan_realized_pnl[b] += 1u;
}
if (filled_lots > prev_abs) {
pos.vwap_entry = avg_px;
// S1.21: flip-beyond write site.
if (pos.vwap_entry == 0.0f) {
vwap_zero_flip[b] += 1u;
last_bad_vwap[b] = 0.0f;
last_bad_path[b] = 4u;
} else if (!isfinite(pos.vwap_entry) || fabsf(pos.vwap_entry) > 21000000.0f) {
vwap_huge_flip[b] += 1u;
last_bad_vwap[b] = pos.vwap_entry;
last_bad_path[b] = 5u;
}
}
}
pos.position_lots = (int)new_pos_f;
// P4: per-fill cost deduction (single-writer-per-block; plain += safe).
const float fill_cost = filled_lots * cost_per_lot_per_side_per_b[b];
pos.realized_pnl -= fill_cost;
if (!isfinite(pos.realized_pnl)) {
nan_realized_pnl[b] += 1u;
}
total_fees_per_b[b] += fill_cost;
}
// Free-slot search. Returns slot index 0..31 or -1 if all in use.
__device__ static int find_free_limit_slot(Orders& orders) {
#pragma unroll
for (int i = 0; i < MAX_LIMITS; ++i) {
if (orders.limits[i].active == 0) return i;
}
return -1;
}
// Main resting-orders processing kernel.
//
// `tick_size` is the price-to-tick conversion for audit-event recording.
extern "C" __global__ void resting_orders_step(
const Book* __restrict__ books, // [n_backtests]
unsigned char* orders_base, // [n_backtests * ORDERS_BYTES]
unsigned char* pos_base,
unsigned char* audit_base,
unsigned int* audit_head,
unsigned long long current_ts_ns,
float trade_signed_vol,
int orders_bytes,
int pos_bytes,
int ring_cap_events,
float tick_size,
// P4: per-fill cost integration (per-backtest arrays).
const float* __restrict__ cost_per_lot_per_side_per_b,
float* __restrict__ total_fees_per_b,
// Fix 3: session-gap force-close. Tracks last event ts per backtest.
unsigned long long* __restrict__ last_event_ts, // [n_backtests]
// S1.20: NaN instrumentation counters (per-backtest).
unsigned int* __restrict__ nan_avg_px,
unsigned int* __restrict__ nan_realised,
unsigned int* __restrict__ nan_realized_pnl,
// S1.21: per-vwap-write-site counters + last-bad-vwap capture.
unsigned int* __restrict__ vwap_zero_open_flat,
unsigned int* __restrict__ vwap_huge_open_flat,
unsigned int* __restrict__ vwap_zero_scale_in,
unsigned int* __restrict__ vwap_huge_scale_in,
unsigned int* __restrict__ vwap_zero_flip,
unsigned int* __restrict__ vwap_huge_flip,
unsigned int* __restrict__ vwap_session_gap_was_bad,
float* __restrict__ last_bad_vwap,
unsigned int* __restrict__ last_bad_path,
// S1.22: per-level price-range validation in walk_ask_for_buy/walk_bid_for_sell.
const float* __restrict__ min_reasonable_px, // [n_backtests]
const float* __restrict__ max_reasonable_px, // [n_backtests]
int n_backtests
) {
int b = blockIdx.x;
if (b >= n_backtests || threadIdx.x != 0) return;
const Book& book = books[b];
Orders& orders = *reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
Pos& pos = *reinterpret_cast<Pos*>(pos_base + (size_t)b * pos_bytes);
// S1.22: per-backtest price bounds for walk_* level filtering.
const float min_px = min_reasonable_px[b];
const float max_px = max_reasonable_px[b];
// Session-gap force-close: when event timestamps jump > 1 hour (weekend
// halt, session boundary, gap-fill), max_hold can't fire because no
// intervening events advance current_ts. Force-close all open positions
// before processing the new event's fills.
const unsigned long long SESSION_GAP_NS = 3600000000000ull; // 1 hour
const unsigned long long last_ts = last_event_ts[b];
if (last_ts > 0ull && current_ts_ns > last_ts
&& (current_ts_ns - last_ts) >= SESSION_GAP_NS
&& pos.position_lots != 0)
{
// Force-flat: zero position_lots directly. pnl_track_step (called by
// step_resting_orders after this kernel) will detect the prev!=0 && now==0
// transition via open_trade_state scratch and emit a close TradeRecord.
// No synthetic P&L is added — records show realised_pnl from whatever
// was accumulated before the gap (honest: cannot fill across a halt).
// S1.21: capture pre-close vwap_entry corruption if already bad.
if (pos.vwap_entry == 0.0f || !isfinite(pos.vwap_entry)
|| fabsf(pos.vwap_entry) > 21000000.0f)
{
vwap_session_gap_was_bad[b] += 1u;
last_bad_vwap[b] = pos.vwap_entry;
last_bad_path[b] = 6u;
}
pos.position_lots = 0;
}
last_event_ts[b] = current_ts_ns;
// (1) Promote in-flight limits to resting; (2) queue-decay; (3) marketability check.
const float trade_abs = (trade_signed_vol < 0.0f) ? -trade_signed_vol : trade_signed_vol;
for (int i = 0; i < MAX_LIMITS; ++i) {
LimitSlot& s = orders.limits[i];
if (s.active == 2 && current_ts_ns >= s.arrival_ts_ns) {
s.active = 1;
// Initialise queue position pessimistically: full level depth
// ahead of us at the just-arrived book (per spec §9 queue-decay
// risk row).
int level_idx = -1;
if (s.side == 0) {
#pragma unroll
for (int k = 0; k < 10; ++k) if (book.bid_px[k] == s.price) { level_idx = k; break; }
s.queue_position = (level_idx >= 0) ? book.bid_sz[level_idx] : 0.0f;
} else {
#pragma unroll
for (int k = 0; k < 10; ++k) if (book.ask_px[k] == s.price) { level_idx = k; break; }
s.queue_position = (level_idx >= 0) ? book.ask_sz[level_idx] : 0.0f;
}
}
if (s.active != 1) continue;
// Queue decay: trade flow on our side at our price level eats ahead-of-us first,
// then us. Only count trades on the SAME side as our resting order.
if (trade_abs > 0.0f) {
const bool same_side_trade = (s.side == 0 && trade_signed_vol < 0.0f) // resting bid + seller hits us
|| (s.side == 1 && trade_signed_vol > 0.0f); // resting ask + buyer lifts
if (same_side_trade) {
if (s.queue_position >= trade_abs) {
s.queue_position -= trade_abs;
} else {
// Sentinel-priced (market-like) orders use s.price = 1e9 (buy)
// or 0 (sell) so the marketability check below always crosses.
// The queue-decay arm MUST NOT use s.price for cost because it
// would inject the sentinel into avg_px (S1.23 root cause:
// zero_flat=194, huge_flat=216). Absorb the queue depletion
// only; the marketability check fires this same iteration via
// walk_* with book-derived prices.
const bool is_sentinel_price = (s.price < min_px) || (s.price > max_px);
if (is_sentinel_price) {
s.queue_position = 0.0f;
} else {
const float over = trade_abs - s.queue_position;
s.queue_position = 0.0f;
const float take = (over < s.size_remaining) ? over : s.size_remaining;
if (take > 0.0f) {
// Filled `take` lots at our limit price (queue position reached us).
const float cost = take * s.price;
apply_fill_to_pos(pos, take, cost, s.side,
b, cost_per_lot_per_side_per_b, total_fees_per_b,
nan_avg_px, nan_realised, nan_realized_pnl,
vwap_zero_open_flat, vwap_huge_open_flat,
vwap_zero_scale_in, vwap_huge_scale_in,
vwap_zero_flip, vwap_huge_flip,
last_bad_vwap, last_bad_path);
s.size_remaining -= take;
emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events,
current_ts_ns,
pack_slot_tag(0 /*SlotKind::Limit*/, (unsigned char)i, s.gen_counter),
1 /*SubmitLimit*/, s.side, (unsigned short)take,
(int)(s.price / tick_size + 0.5f), 0);
if (s.size_remaining <= 0.0f) {
cancel_oco_pair(orders, s.oco_pair);
s.active = 0;
continue;
}
}
}
}
}
}
// Marketability check: book moved through our price → cross at the
// arrival book (slippage-aware fill). Triggers on:
// buy limit @ p ≥ ask_px[0] — we'd be taking liquidity at our price or better
// sell limit @ p ≤ bid_px[0] — same on opposite side
const bool marketable = (s.side == 0 && s.price >= book.ask_px[0])
|| (s.side == 1 && s.price <= book.bid_px[0]);
if (marketable) {
float filled = 0.0f;
const float cost = (s.side == 0)
? walk_ask_for_buy (book, s.size_remaining, filled, min_px, max_px)
: walk_bid_for_sell(book, s.size_remaining, filled, min_px, max_px);
if (filled > 0.0f) {
apply_fill_to_pos(pos, filled, cost, s.side,
b, cost_per_lot_per_side_per_b, total_fees_per_b,
nan_avg_px, nan_realised, nan_realized_pnl,
vwap_zero_open_flat, vwap_huge_open_flat,
vwap_zero_scale_in, vwap_huge_scale_in,
vwap_zero_flip, vwap_huge_flip,
last_bad_vwap, last_bad_path);
s.size_remaining -= filled;
emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events,
current_ts_ns,
pack_slot_tag(0, (unsigned char)i, s.gen_counter),
s.kind /*Limit|Ioc|Fok*/, s.side, (unsigned short)filled,
(int)(s.price / tick_size + 0.5f), 0);
}
// IOC: any unfilled remainder cancelled immediately.
// FOK: if not fully filled, cancel everything (no partial leaves resting).
// Limit: leave residual resting at our price.
if (s.kind == KIND_IOC || (s.kind == KIND_FOK && s.size_remaining > 0.0f)) {
if (s.kind == KIND_FOK && filled > 0.0f) {
// FOK partial fill is forbidden — should not happen; defensive guard
// documents intent but the actual prevention belongs in submission code.
}
cancel_oco_pair(orders, s.oco_pair);
s.active = 0;
continue;
}
if (s.size_remaining <= 0.0f) {
cancel_oco_pair(orders, s.oco_pair);
s.active = 0;
continue;
}
}
}
// (4) Stop trigger detection.
for (int j = 0; j < MAX_STOPS; ++j) {
StopSlot& st = orders.stops[j];
if (st.active == 2 && current_ts_ns >= st.arrival_ts_ns) st.active = 1;
if (st.active != 1) continue;
const bool triggered = (st.side == 0 && book.ask_px[0] >= st.trigger_price)
|| (st.side == 1 && book.bid_px[0] <= st.trigger_price);
if (!triggered) continue;
st.active = 0;
if (st.kind == KIND_STOP_MARKET) {
float filled = 0.0f;
const float cost = (st.side == 0)
? walk_ask_for_buy (book, st.size, filled, min_px, max_px)
: walk_bid_for_sell(book, st.size, filled, min_px, max_px);
if (filled > 0.0f) {
apply_fill_to_pos(pos, filled, cost, st.side,
b, cost_per_lot_per_side_per_b, total_fees_per_b,
nan_avg_px, nan_realised, nan_realized_pnl,
vwap_zero_open_flat, vwap_huge_open_flat,
vwap_zero_scale_in, vwap_huge_scale_in,
vwap_zero_flip, vwap_huge_flip,
last_bad_vwap, last_bad_path);
emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events,
current_ts_ns,
pack_slot_tag(1 /*SlotKind::Stop*/, (unsigned char)j, st.gen_counter),
4 /*SubmitStopMarket*/, st.side, (unsigned short)filled,
0, (int)(st.trigger_price / tick_size + 0.5f));
}
cancel_oco_pair(orders, st.oco_pair);
} else if (st.kind == KIND_STOP_LIMIT) {
// Convert to a resting limit at st.limit_price (may not fill immediately
// if the limit is not marketable at current book).
const int free_idx = find_free_limit_slot(orders);
if (free_idx < 0) {
pos.submission_overflow += 1u;
cancel_oco_pair(orders, st.oco_pair);
continue;
}
LimitSlot& ns = orders.limits[free_idx];
ns.active = 1;
ns.side = st.side;
ns.kind = KIND_LIMIT;
ns.oco_pair = st.oco_pair;
ns.price = st.limit_price;
ns.size_remaining = st.size;
ns.queue_position = 0.0f; // marketable check next iter will fire if applicable
ns.arrival_ts_ns = current_ts_ns;
ns.gen_counter = (unsigned short)(ns.gen_counter + 1);
emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events,
current_ts_ns,
pack_slot_tag(1 /*SlotKind::Stop*/, (unsigned char)j, st.gen_counter),
5 /*SubmitStopLimit*/, st.side, (unsigned short)st.size,
(int)(st.limit_price / tick_size + 0.5f),
(int)(st.trigger_price / tick_size + 0.5f));
}
}
}
// Host-callable seed helpers — write a single slot. Single-block,
// single-thread. Used for fixture testing + as the kernel side of
// LobSimCuda::submit_limit. SlotKind::Limit only (Stop seed below).
extern "C" __global__ void seed_limit_slot(
unsigned char* orders_base,
int orders_bytes,
int b,
int slot_idx,
unsigned char side,
unsigned char kind,
unsigned char active_state, // 1=resting immediately, 2=in-flight
unsigned char oco_pair,
float price,
float size,
float queue_position_init,
unsigned long long arrival_ts_ns,
unsigned int* overflow_flag // [1] — set to 1 if slot_idx already in use
) {
if (threadIdx.x != 0 || blockIdx.x != 0) return;
Orders* orders = reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
if (slot_idx < 0 || slot_idx >= MAX_LIMITS) { *overflow_flag = 1u; return; }
LimitSlot& s = orders->limits[slot_idx];
if (s.active != 0) { *overflow_flag = 1u; return; }
s.active = active_state;
s.side = side;
s.kind = kind;
s.oco_pair = oco_pair;
s.price = price;
s.size_remaining = size;
s.queue_position = queue_position_init;
s.arrival_ts_ns = arrival_ts_ns;
s.gen_counter = (unsigned short)(s.gen_counter + 1);
}
extern "C" __global__ void seed_stop_slot(
unsigned char* orders_base,
int orders_bytes,
int b,
int slot_idx,
unsigned char side,
unsigned char kind,
unsigned char active_state,
unsigned char oco_pair,
float trigger_price,
float limit_price,
float size,
unsigned long long arrival_ts_ns,
unsigned int* overflow_flag
) {
if (threadIdx.x != 0 || blockIdx.x != 0) return;
Orders* orders = reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
if (slot_idx < 0 || slot_idx >= MAX_STOPS) { *overflow_flag = 1u; return; }
StopSlot& st = orders->stops[slot_idx];
if (st.active != 0) { *overflow_flag = 1u; return; }
st.active = active_state;
st.side = side;
st.kind = kind;
st.oco_pair = oco_pair;
st.trigger_price = trigger_price;
st.limit_price = limit_price;
st.size = size;
st.arrival_ts_ns = arrival_ts_ns;
st.gen_counter = (unsigned short)(st.gen_counter + 1);
}
// Try to allocate a limit slot from device-side submission code (e.g. C7
// decision kernel later) — returns slot index or -1 if all in use.
// Bumps pos.submission_overflow on failure. Single-block, single-thread.
extern "C" __global__ void submit_limit_alloc(
unsigned char* orders_base,
unsigned char* pos_base,
int orders_bytes,
int pos_bytes,
int b,
unsigned char side,
unsigned char kind,
unsigned char oco_pair,
float price,
float size,
unsigned long long arrival_ts_ns,
int* allocated_slot_out // [1] — written: slot idx or -1
) {
if (threadIdx.x != 0 || blockIdx.x != 0) return;
Orders* orders = reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
Pos* pos = reinterpret_cast<Pos*>(pos_base + (size_t)b * pos_bytes);
const int idx = find_free_limit_slot(*orders);
if (idx < 0) {
pos->submission_overflow += 1u;
*allocated_slot_out = -1;
return;
}
LimitSlot& s = orders->limits[idx];
s.active = 2; // in-flight
s.side = side;
s.kind = kind;
s.oco_pair = oco_pair;
s.price = price;
s.size_remaining = size;
s.queue_position = 0.0f;
s.arrival_ts_ns = arrival_ts_ns;
s.gen_counter = (unsigned short)(s.gen_counter + 1);
*allocated_slot_out = idx;
}
// P2: GPU-side seeder for in-flight LimitSlots driven by market_targets[b].
// Replaces the host-roundtrip loop in sim.rs::dispatch_latent_market_orders.
// Per-backtest single-writer (threadIdx.x == 0).
//
// §7 truth table:
// side=0, abs_sz=N → target signed position = +N (long)
// side=1, abs_sz=N → target signed position = -N (short)
// side=2 → no-op: preserve current position
// side=3 → force flat: target signed position = 0
//
// effective_position = pos.position_lots + Σ unfilled signed slot sizes
// where unfilled = active∈{1,2} (resting or in-flight).
// delta = target_signed effective_position.
// If delta == 0: nothing to do (idempotent re-submission under latency).
// If delta != 0: seed an in-flight order for |delta| lots on the
// appropriate side. If all 32 slots are occupied, increments
// pos.submission_overflow.
extern "C" __global__ void seed_inflight_limits_batched(
unsigned char* __restrict__ orders_base, // [n_backtests * orders_bytes]
int orders_bytes,
Pos* __restrict__ positions, // [n_backtests]
const int* __restrict__ market_targets, // [n_backtests * 2]
const unsigned int* __restrict__ latency_ns_per_b, // [n_backtests]
unsigned long long current_ts_ns,
// S2.1: max_hold enforcement diagnostic counter — incremented when this
// kernel reads market_targets[b*2+0] == 3 (force-flat from stop_check_isv).
unsigned int* __restrict__ mh_force_flat_seen_by_seed, // [n_backtests]
int n_backtests
) {
int b = blockIdx.x;
if (b >= n_backtests || threadIdx.x != 0) return;
const int side = market_targets[b * 2 + 0];
const int abs_sz = market_targets[b * 2 + 1];
// S2.1: count every time this kernel sees a force-flat target, regardless
// of whether the delta calculation produces a non-zero order (delta==0 means
// position is already flat, but the force-flat signal still arrived here).
if (side == 3) {
mh_force_flat_seen_by_seed[b] += 1u;
}
if (side == 2) return; // §7: no-op = preserve position
int target_signed;
if (side == 0) target_signed = +abs_sz;
else if (side == 1) target_signed = -abs_sz;
else target_signed = 0; // side == 3: force flat
Orders* orders = reinterpret_cast<Orders*>(orders_base + (size_t)b * orders_bytes);
// Effective position = filled position + all unfilled signed lots.
int effective = positions[b].position_lots;
#pragma unroll
for (int s_idx = 0; s_idx < MAX_LIMITS; ++s_idx) {
const LimitSlot& s = orders->limits[s_idx];
if (s.active == 0) continue;
const int slot_lots = (int)lroundf(s.size_remaining);
effective += (s.side == 0) ? slot_lots : -slot_lots;
}
const int delta = target_signed - effective;
if (delta == 0) return;
const int order_lots = (delta > 0) ? delta : -delta;
const int order_side = (delta > 0) ? 0 : 1;
// Find a free slot and seed an in-flight order.
for (int s_idx = 0; s_idx < MAX_LIMITS; ++s_idx) {
LimitSlot& s = orders->limits[s_idx];
if (s.active == 0) {
s.active = 2; // in-flight
s.side = (unsigned char)order_side;
s.kind = 1; // Limit
s.oco_pair = 0xFF;
s.price = (order_side == 0) ? 1.0e9f : 0.0f; // always crosses
s.size_remaining = (float)order_lots;
s.queue_position = 0.0f;
s.arrival_ts_ns = current_ts_ns + (unsigned long long)latency_ns_per_b[b];
s.gen_counter = (unsigned short)(s.gen_counter + 1);
return;
}
}
positions[b].submission_overflow += 1u;
}