Files
foxhunt/crates/ml-backtesting/cuda/resting_orders.cu
jgrusewski 7c850a6c02 arch(crt-1): no-trade band in seed_inflight — sparse action at signal cadence (C1.3)
Per spec §4.0 + plan C1.3. Without this gate, every fractional change
in target_lots seeds an order via target-delta semantics; combined with
the continuous controller invocation (every event after A1) this
generates hyperactivity even with the multi-horizon §4.4 conviction
formula (C1.2). v2 Gate-1 failures confirmed: scalar EMA + amplification
clamp + per-event controller = 155k trades on a 2M-event smoke.

The band absorbs fractional target adjustments so most events produce
no order. When the signal genuinely shifts and target moves enough to
cross the band, the kernel seeds. Continuous evaluation, sparse action.

Greenfields atomic refactor — single config knob threaded through every
layer in one commit:
  SweepBase.delta_floor: f32  (YAML, default 1.0 = 1 lot)
  SimVariant.delta_floor: Option<f32>  (per-variant override)
  ResolvedSimVariant.delta_floor: f32
  UniformSimParams.delta_floor: f32
  BatchedSimConfig.delta_floor: Vec<f32>
  LobSimCuda.delta_floor_d: CudaSlice<f32>
  seed_inflight_limits_batched kernel param + skip-if-below-floor logic

New accessor: LobSimCuda::read_inflight_count(b) — counts active != 0
limit slots for backtest b. Not cfg(test); follows existing read_limit_slot
pattern.

Test: no_trade_band_blocks_micro_delta verifies that a second decision
with the same strong-bullish alpha (same target, effective = in-flight
lots) produces delta=0 and does not re-seed. max_lots=1 keeps the
arithmetic unambiguous.

Existing tests: all UniformSimParams struct literals updated with
delta_floor=0.0 (band disabled) so existing behaviour is preserved.
harness.rs from_uniform path uses delta_floor=1.0 (production default).

Hot-path discipline: the delta_floor_d upload happens once per run in
the existing config-upload block (alongside threshold, cost, max_hold_ns);
the kernel reads the device slot per-event without any host roundtrip. No
memcpy_htod / dtoh / dtov / synchronize introduced on the per-event path.

Per pearl_controller_anchors_isv_driven, this floor is currently a
config constant; CRT.2 (Phase 2) makes it ISV-derived from rolling
spread cost / signal volatility.

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

730 lines
34 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]
// S2.2: max_hold force-close at event rate.
const unsigned long long* __restrict__ max_hold_ns_per_b, // [n_backtests]
const unsigned char* __restrict__ open_trade_state, // [n_backtests * 64] (CRT.1 §7)
// 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;
// S2.2: max_hold force-close at event rate.
// Runs every event so the 60s cap is actually enforceable.
// A1: decision_stride removed; all enforcement is event-rate.
const unsigned long long max_hold = max_hold_ns_per_b[b];
if (max_hold > 0ull && pos.position_lots != 0) {
const unsigned long long entry_ts =
*reinterpret_cast<const unsigned long long*>(
open_trade_state + (size_t)b * 64); // OPEN_TRADE_STATE_BYTES = 64 (CRT.1 §7)
if (entry_ts > 0ull && current_ts_ns >= entry_ts
&& (current_ts_ns - entry_ts) >= max_hold) {
// S2.3: realize PnL on the force-close. apply_fill_to_pos's
// counter-direction branch handles the unwind:
// realised = (avg_px vwap) × dir × unwind
// Synthesize the closing fill at top-of-book on the closing side:
// long → sell at bid[0], short → buy at ask[0].
// book[b]'s top-of-book is already validated by book_update's skip
// (book_update_apply_snapshot rejects bad-top snapshots), so
// close_px is guaranteed in [min_px, max_px] and > 0.
const int pos_lots = pos.position_lots;
const bool is_long = pos_lots > 0;
const float close_px = is_long ? book.bid_px[0] : book.ask_px[0];
const float lots_abs = is_long ? (float)pos_lots : -(float)pos_lots;
const float total_cost = lots_abs * close_px;
const unsigned char close_side = is_long ? 1 : 0; // sell to close long, buy to close short
apply_fill_to_pos(
pos, lots_abs, total_cost, close_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
);
// After apply_fill_to_pos with unwind == prev_abs, position_lots = 0.
}
}
// (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]
// CRT.1 C1.3: no-trade band in lots. Seeding is skipped when
// |target_signed effective| < delta_floor_per_b[b].
const float* __restrict__ delta_floor_per_b, // [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;
// CRT.1 C1.3: no-trade band. Skip seeding when |delta| is below the
// configured floor. Most events produce fractional target changes that
// aren't worth the spread; the band absorbs them so the controller can
// fire every event without generating an order on every fractional move.
// Per spec §4.0 (v3 architecture reset). Without this gate, the v2
// Gate-1 catastrophe (155k trades from 2M events) recurs regardless of
// conviction smoothing.
const float abs_delta_f = (delta > 0) ? (float)delta : (float)(-delta);
if (abs_delta_f < delta_floor_per_b[b]) return;
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;
}