Picks up the deferred work from C5/C7 trim notes. Adds:
cuda/lob_state.cuh — LimitSlot (32 B) + StopSlot (32 B) + Orders
(limits[32] + stops[16] = 1536 B/backtest). u64-aligned with
explicit _pad[6] on both slot structs so the Rust mirrors
(LimitSlotFlat / StopSlotFlat) bytemuck::Pod-derive without
panics about implicit padding.
cuda/resting_orders.cu — three kernels:
resting_orders_step (runs per event after book_update):
1. In-flight → resting promotion at arrival_ts_ns. Queue position
initialised pessimistically (full level depth ahead) per spec §9.
2. Queue decay against same-side trade_signed_vol at level: ahead-
of-us first then us; partial-fill emits OrderEvent + pos update.
3. Marketability check: book moved through our price → cross at
the just-arrived book (slippage-aware fill walks levels).
IOC cancels remainder; FOK does too (partial-fill not allowed).
4. Stop trigger detection: best ask up for buy-stop, best bid
down for sell-stop. StopMarket walks book; StopLimit converts
to a new LimitSlot (overflow → submission_overflow++).
5. OCO mutual exclusion: oco_pair byte (0..31 = limit, 32..47 =
stop) — when one leg fills/triggers, the paired slot is freed.
seed_limit_slot / seed_stop_slot — host-side single-slot seeders
for fixture testing + as a v1 entry point until the decision
kernel learns to emit resting orders. Both set an overflow_flag
if the target slot is already in use (gen_counter bumped on
successful seed for SlotTag freshness).
src/sim.rs — wires three new methods:
seed_limit_order(b, slot, side, kind, active_state, oco_pair,
price, size, queue_position_init, arrival_ts_ns)
seed_stop_order(b, slot, side, kind, active_state, oco_pair,
trigger_price, limit_price, size, arrival_ts_ns)
step_resting_orders(current_ts_ns, trade_signed_vol)
→ launches resting_orders_step kernel + chains step_pnl_track
so any fill that closes a position emits a TradeRecord.
Plus read_limit_slot / read_stop_slot / read_audit_records for
fixture inspection.
Audit-ring buffers (deferred from C2 originally) now allocated:
audit_d (n × 256 × 24 B) + audit_head_d (n × u32). Both
resting_orders.cu and the decision kernel's WriteOrder path
populate it via the inlined pack_slot_tag + emit_audit helpers.
Four new GPU fixtures:
limit_rest_marketable_fill — resting buy at 5500 with book at
ask 5500 fills immediately on step_resting (3 lots, vwap=5500).
stop_trigger — buy 4 lots @ 5500, seed sell-stop at trigger=5495,
book moves so bid=5494.50: stop triggers, walks book, position
closes flat, TradeRecord emitted, stop slot active=0.
oco_one_cancels_other — pair {buy@5495, sell@5505} with oco_pair
cross-linked: book moves so bid=5505.50, sell leg fills (pos=-2),
buy leg auto-cancelled. Both slots end active=0.
submission_overflow — host loop fills all 32 limit slots, then 33rd
seed_limit_order call must return Err (slot 0 already in use).
All 10 GPU fixtures green on RTX 3050 (6 original + 4 new). Ring 2
fuzz at N ∈ {1, 16, 256} still green. 33 lib unit tests still green.
The decision kernel (C7) and submit_market_immediate (C5) paths
are unchanged — they continue to use the "immediate fill" route.
A follow-up commit can teach the decision kernel to emit resting
limits via submit_limit_alloc (already defined in resting_orders.cu).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
83 lines
3.6 KiB
Plaintext
83 lines
3.6 KiB
Plaintext
// Per-block shared-mem layout for the LOB simulator.
|
||
// One block = one parallel backtest. See spec §2 + §5b.
|
||
//
|
||
// MUST stay in sync with crates/ml-backtesting/src/lob/mod.rs's host mirrors.
|
||
|
||
#ifndef LOB_STATE_CUH
|
||
#define LOB_STATE_CUH
|
||
|
||
#define N_HORIZONS 5
|
||
#define MAX_LIMITS 32
|
||
#define MAX_STOPS 16
|
||
|
||
struct Book {
|
||
float bid_px[10];
|
||
float bid_sz[10];
|
||
float ask_px[10];
|
||
float ask_sz[10];
|
||
};
|
||
|
||
// Per-backtest position + cumulative P&L. See spec §2 (CUDA shared-mem layout).
|
||
// Host mirror in crates/ml-backtesting/src/lob/mod.rs::PosFlat — MUST stay in sync.
|
||
struct Pos {
|
||
int position_lots; // signed (+ long, − short)
|
||
float vwap_entry; // running entry-price VWAP for current open position
|
||
float realized_pnl; // cumulative realized P&L in price-units × lots
|
||
float peak_equity; // for drawdown tracking
|
||
unsigned int submission_overflow; // diagnostic counter
|
||
unsigned int open_horizon_mask; // bit h set if currently open position was authorized by horizon h
|
||
};
|
||
|
||
// Per-horizon adaptive Kelly state. Lives per (backtest, horizon).
|
||
// See spec §5 (per-horizon ISV-Kelly).
|
||
struct IsvKellyState {
|
||
float pnl_ema_win; // Wiener-α blended EMA of winning trade returns (price units)
|
||
float pnl_ema_loss; // EMA of losing trade returns (positive magnitude)
|
||
float win_rate_ema;
|
||
unsigned int n_trades_seen;
|
||
float realised_return_var; // Welford running variance over per-trade returns
|
||
float recent_sharpe; // composite: (μ × win_rate − μ_loss × (1−win_rate)) / sqrt(var)
|
||
};
|
||
|
||
// Resting-order slots. See spec §2 (CUDA shared-mem layout) and §3
|
||
// (order types). Host mirror in src/lob/mod.rs::{LimitSlotFlat,StopSlotFlat}.
|
||
// LimitSlot.kind / StopSlot.kind values mirror OrderEventKind:
|
||
// Limit=1, Ioc=2, Fok=3 for LimitSlot
|
||
// StopMarket=4, StopLimit=5 for StopSlot
|
||
struct LimitSlot {
|
||
unsigned char active; // 0=free, 1=resting, 2=in-flight (latency)
|
||
unsigned char side; // 0=buy, 1=sell
|
||
unsigned char kind; // 1=Limit, 2=Ioc, 3=Fok
|
||
unsigned char oco_pair; // 0xFF=no pair, else paired-slot global index
|
||
// (limits: 0..31; stops: 32..47)
|
||
float price; // limit price in price-units (e.g. 5500.25)
|
||
float size_remaining; // contracts left unfilled
|
||
float queue_position; // contracts ahead of us at this level (queue decay)
|
||
unsigned long long arrival_ts_ns; // when in-flight order becomes resting
|
||
unsigned short gen_counter; // bumped on each slot allocation for SlotTag freshness
|
||
unsigned char _pad[6]; // pad-to-8 for u64-aligned arrays of this struct
|
||
}; // 32 bytes
|
||
|
||
struct StopSlot {
|
||
unsigned char active; // 0=free, 1=armed, 2=in-flight
|
||
unsigned char side; // 0=buy, 1=sell
|
||
unsigned char kind; // 4=StopMarket, 5=StopLimit
|
||
unsigned char oco_pair; // 0xFF=no pair, else paired-slot global index
|
||
float trigger_price;
|
||
float limit_price; // 0.0 for StopMarket; price for StopLimit
|
||
float size;
|
||
unsigned long long arrival_ts_ns;
|
||
unsigned short gen_counter;
|
||
unsigned char _pad[6]; // pad-to-8 for u64-aligned arrays
|
||
}; // 32 bytes
|
||
|
||
#define MAX_LIMITS 32
|
||
#define MAX_STOPS 16
|
||
|
||
struct Orders {
|
||
LimitSlot limits[MAX_LIMITS]; // 32 × 32 = 1024 B
|
||
StopSlot stops[MAX_STOPS]; // 16 × 32 = 512 B
|
||
}; // 1536 B total
|
||
|
||
#endif // LOB_STATE_CUH
|