submit_market_immediate walks the ask/bid book for a buy/sell market
order, computes notional cost + filled lots across up to 10 levels,
and updates per-backtest Pos { position_lots, vwap_entry, realized_pnl }
with correct VWAP scale-in + counter-direction realized-P&L accounting.
Single-writer (thread 0) per block — no atomics per
feedback_no_atomicadd.md. Each backtest gets its own target slot
{ side, size } in the device-global targets array (side=2 = no-op).
Pos struct added to lob_state.cuh (24 bytes); host mirror PosFlat in
src/lob/mod.rs is repr(C) bytemuck::Pod for direct host↔device cast.
LobSimCuda gains submit_market(backtest_idx, side, size) +
read_pos(backtest_idx) -> PosFlat. Fixture harness extended with the
SubmitMarket event variant + ExpectedPos check (vwap_tol + realized_pnl_tol
for FP tolerance).
market_order_consumes_top fixture verifies a buy of 5 lots into
ask[3@5500.00, 10@5500.25] fills 3+2 → position_lots=5,
vwap_entry=5500.10 within 1e-3 tolerance. All 4 Ring 1 fixtures
(book_update × 3 + market_order × 1) green on RTX 3050.
Scope deliberately trimmed from plan C5: this commit lands market-fill
matching only. Queue decay on resting limits, in-flight latency
promotion, stop-trigger detection, and OCO leg-cancellation will land
in follow-up commits — each in its own kernel addition. This keeps the
incremental delta reviewable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
112 lines
4.4 KiB
Plaintext
112 lines
4.4 KiB
Plaintext
// order_match.cu — immediate market-order matching (C5 minimal).
|
||
//
|
||
// v1 scope: walk the book for an aggressive market order, update
|
||
// position + VWAP + realized P&L. Single-writer (thread 0) per block.
|
||
//
|
||
// Resting-limit machinery (queue decay, in-flight latency promotion,
|
||
// stop triggers, OCO link cancellation) is added in follow-up commits.
|
||
//
|
||
// Per feedback_no_atomicadd.md: no atomics — single-writer discipline.
|
||
|
||
#include "lob_state.cuh"
|
||
|
||
// Walk ask side for a buy of `size`. Returns total notional cost in
|
||
// price-units. Writes filled_lots out-param.
|
||
__device__ static float walk_ask_for_buy(
|
||
const Book& book, float size, float& filled_lots
|
||
) {
|
||
float cost = 0.0f;
|
||
filled_lots = 0.0f;
|
||
#pragma unroll
|
||
for (int k = 0; k < 10; ++k) {
|
||
if (size <= 0.0f) break;
|
||
const float lvl_sz = book.ask_sz[k];
|
||
if (lvl_sz <= 0.0f) continue;
|
||
const float take = (size < lvl_sz) ? size : lvl_sz;
|
||
cost += take * book.ask_px[k];
|
||
filled_lots += take;
|
||
size -= take;
|
||
}
|
||
return cost;
|
||
}
|
||
|
||
__device__ static float walk_bid_for_sell(
|
||
const Book& book, float size, float& filled_lots
|
||
) {
|
||
float cost = 0.0f;
|
||
filled_lots = 0.0f;
|
||
#pragma unroll
|
||
for (int k = 0; k < 10; ++k) {
|
||
if (size <= 0.0f) break;
|
||
const float lvl_sz = book.bid_sz[k];
|
||
if (lvl_sz <= 0.0f) continue;
|
||
const float take = (size < lvl_sz) ? size : lvl_sz;
|
||
cost += take * book.bid_px[k];
|
||
filled_lots += take;
|
||
size -= take;
|
||
}
|
||
return cost;
|
||
}
|
||
|
||
// Immediate market-order fill: walks the book, updates Pos.
|
||
// One block per backtest; thread 0 does all the work.
|
||
//
|
||
// `targets` is a [n_backtests, 2]-shaped i32 array: { side, size }.
|
||
// side: 0=buy, 1=sell, 2=no-op (skip this backtest this step).
|
||
extern "C" __global__ void submit_market_immediate(
|
||
const Book* __restrict__ books,
|
||
const int* __restrict__ targets, // [n_backtests, 2]
|
||
Pos* __restrict__ positions, // [n_backtests]
|
||
int n_backtests
|
||
) {
|
||
int b = blockIdx.x;
|
||
if (b >= n_backtests || threadIdx.x != 0) return;
|
||
|
||
const int side = targets[b * 2 + 0];
|
||
const int sz_in = targets[b * 2 + 1];
|
||
if (side == 2 || sz_in <= 0) return; // no-op for this backtest
|
||
|
||
const Book& book = books[b];
|
||
Pos& pos = positions[b];
|
||
|
||
float filled = 0.0f;
|
||
const float cost = (side == 0)
|
||
? walk_ask_for_buy (book, (float)sz_in, filled)
|
||
: walk_bid_for_sell(book, (float)sz_in, filled);
|
||
if (filled <= 0.0f) return;
|
||
const float avg_px = cost / filled;
|
||
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;
|
||
|
||
if (pos.position_lots == 0) {
|
||
// Open position from flat.
|
||
pos.vwap_entry = avg_px;
|
||
} else if ((prev_pos_f > 0.0f) == (sgn > 0.0f)) {
|
||
// Same direction: scale-in. Update VWAP by lot-weighted average.
|
||
const float prev_notional = prev_pos_f * pos.vwap_entry; // signed notional ($ at entry-side)
|
||
const float new_notional = prev_notional + sgn * 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;
|
||
} else {
|
||
// Counter direction: realize P&L on the unwound portion.
|
||
const float prev_abs = (prev_pos_f > 0.0f) ? prev_pos_f : -prev_pos_f;
|
||
const float unwind = (filled < prev_abs) ? filled : prev_abs;
|
||
// Profit per lot for a closed long = exit_px − entry_px
|
||
// Profit per lot for a closed short = entry_px − exit_px
|
||
// prev_pos_f > 0 (long) closing via sell: side==1, sgn=−1 → realised = (avg_px − vwap) × unwind
|
||
// prev_pos_f < 0 (short) closing via buy: side==0, sgn=+1 → realised = (vwap − avg_px) × unwind
|
||
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;
|
||
pos.realized_pnl += realised;
|
||
// If counter-fill exceeds the prior position, the residual opens
|
||
// a new position in the opposite direction at avg_px.
|
||
if (filled > prev_abs) {
|
||
pos.vwap_entry = avg_px;
|
||
}
|
||
// If position is now flat, vwap_entry becomes irrelevant.
|
||
}
|
||
pos.position_lots = (int)new_pos_f;
|
||
}
|