Files
foxhunt/crates/ml-backtesting/cuda/lob_state.cuh
jgrusewski 1fb2decb79 feat(ml-backtesting): resting orders + stops + OCO + audit ring (C11)
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>
2026-05-18 09:15:24 +02:00

83 lines
3.6 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.
// 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 × (1win_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