pnl_track_step runs after each matching pass, compares per-block
position-state-now against persisted OpenTradeState (24 B scratch)
and either:
- records entry context (entry_ts_ns, entry_px_x100, entry_size,
realised_at_open) on open transition (prev==0, now!=0); or
- emits a 40-byte TradeRecord into the per-block trade-log buffer
on close transition (prev!=0, now==0), reconstructing implied
exit_px from the realized P&L delta and converting to USD ×100
fixed-point ($50/index-point × 100 = ×5000 multiplier).
Multi-fill averaging (scale-in then partial close) deferred to v2 —
v1 covers the clean open→close case the spec calls out as primary.
LobSimCuda owns three new buffers: open_trade_state_d (n × 24),
trade_log_d (n × TRADE_LOG_CAP × 40), trade_log_head_d (n × u32).
submit_market now takes current_ts_ns and chains pnl_track_step
internally; step_pnl_track() exposed for caller-driven orchestration.
read_trade_records(backtest_idx) drains the per-block ring as
Vec<TradeRecord>; LSP-pinned 40-byte Pod struct from C2 lines up
1:1 with the kernel's hand-rolled byte writes.
pnl_accounting_buy_close fixture: buy 4 lots @ ask top (5500.00),
book moves +5 to bid top 5505.00, sell 4 to close. Expected
realized_pnl = (5505 − 5500.00) × 4 = $20 in price-units, which is
$20 × $50/contract × 100 = 100000 USD ×100 fixed-point. PASS within
$1 fixed-point tolerance.
All 5 Ring 1 fixtures green on RTX 3050.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
106 lines
5.3 KiB
Plaintext
106 lines
5.3 KiB
Plaintext
// pnl_track.cu — segment_complete detector + TradeRecord emission.
|
||
//
|
||
// Runs after every matching pass. Compares previous position state
|
||
// (persisted in OpenTradeState scratch) against current Pos.position_lots
|
||
// to detect transitions:
|
||
// prev == 0, now != 0 → entry: record entry context into scratch
|
||
// prev != 0, now == 0 → close: emit a TradeRecord with the spread
|
||
// between entry and exit P&L delta
|
||
//
|
||
// Single-writer (thread 0) per block.
|
||
//
|
||
// OpenTradeState layout (24 bytes per backtest):
|
||
// 0..8 entry_ts_ns (u64)
|
||
// 8..12 entry_px_x100 (i32 — entry VWAP × 100, fixed-point)
|
||
// 12..16 entry_size (i32 — signed, +long -short)
|
||
// 16..20 entry_realized_at_open (f32 — pos.realized_pnl when entry happened)
|
||
// 20..21 horizon_idx (u8)
|
||
// 21..24 padding
|
||
//
|
||
// TradeRecord layout (40 bytes — MUST match src/order.rs::TradeRecord):
|
||
// 0..8 entry_ts_ns
|
||
// 8..16 exit_ts_ns
|
||
// 16..20 entry_px_ticks (×100)
|
||
// 20..24 exit_px_ticks (×100)
|
||
// 24..28 size_lots (signed)
|
||
// 28..32 fees_usd_fp (placeholder, zero in v1)
|
||
// 32..36 realised_pnl_usd_fp (×100; price-units × $50 × 100 = ×5000)
|
||
// 36..37 horizon_idx
|
||
// 37..38 strategy_id
|
||
// 38..40 padding
|
||
|
||
#include "lob_state.cuh"
|
||
|
||
#define OPEN_TRADE_STATE_BYTES 24
|
||
#define TRADE_RECORD_BYTES 40
|
||
|
||
extern "C" __global__ void pnl_track_step(
|
||
unsigned char* pos_base, // [n_backtests * 24 bytes]
|
||
unsigned char* open_trade_state, // [n_backtests * 24 bytes]
|
||
unsigned char* trade_log_base, // [n_backtests * cap * 40 bytes]
|
||
unsigned int* trade_log_head, // [n_backtests]
|
||
unsigned long long current_ts_ns,
|
||
int trade_log_cap,
|
||
int n_backtests
|
||
) {
|
||
int b = blockIdx.x;
|
||
if (b >= n_backtests || threadIdx.x != 0) return;
|
||
|
||
Pos* pos = reinterpret_cast<Pos*>(pos_base + (size_t)b * sizeof(Pos));
|
||
unsigned char* st = open_trade_state + (size_t)b * OPEN_TRADE_STATE_BYTES;
|
||
int prev_size = *reinterpret_cast<int*>(st + 12);
|
||
int now_size = pos->position_lots;
|
||
|
||
if (prev_size == 0 && now_size != 0) {
|
||
// Open entry — snapshot context.
|
||
*reinterpret_cast<unsigned long long*>(st + 0) = current_ts_ns;
|
||
*reinterpret_cast<int*>(st + 8) = (int)(pos->vwap_entry * 100.0f + 0.5f);
|
||
*reinterpret_cast<int*>(st + 12) = now_size;
|
||
*reinterpret_cast<float*>(st + 16) = pos->realized_pnl;
|
||
st[20] = 0; // horizon_idx; set by decision kernel in C7
|
||
} else if (prev_size != 0 && now_size == 0) {
|
||
// Close — emit TradeRecord.
|
||
const unsigned int idx = trade_log_head[b] % (unsigned int)trade_log_cap;
|
||
trade_log_head[b] += 1;
|
||
unsigned char* rec = trade_log_base + ((size_t)b * trade_log_cap + idx) * TRADE_RECORD_BYTES;
|
||
|
||
const unsigned long long entry_ts = *reinterpret_cast<unsigned long long*>(st + 0);
|
||
const int entry_px_x100 = *reinterpret_cast<int*>(st + 8);
|
||
const int entry_size = *reinterpret_cast<int*>(st + 12);
|
||
const float realized_at_open = *reinterpret_cast<float*>(st + 16);
|
||
const unsigned char horizon_idx = st[20];
|
||
|
||
// Realized P&L delta = pos.realized_pnl_now − pos.realized_pnl_at_open.
|
||
// In price-units × lots.
|
||
const float segment_realized = pos->realized_pnl - realized_at_open;
|
||
// Convert to USD ×100 fixed-point: × $50/index-point × 100 = ×5000.
|
||
const int realised_usd_fp = (int)(segment_realized * 5000.0f + (segment_realized >= 0.0f ? 0.5f : -0.5f));
|
||
// Exit price reconstructed from realized delta: realized = (exit − entry) × dir × |size|.
|
||
// For sanity we record pos.vwap_entry at close time (the residual avg if any),
|
||
// but for a clean open→close that's the same as entry_px. We store the
|
||
// implied exit price instead, derived from the realized delta.
|
||
const float entry_px = (float)entry_px_x100 / 100.0f;
|
||
const float size_abs = (entry_size > 0) ? (float)entry_size : (float)(-entry_size);
|
||
const float dir = (entry_size > 0) ? 1.0f : -1.0f;
|
||
const float exit_px = (size_abs > 0.0f) ? (entry_px + segment_realized * dir / size_abs) : entry_px;
|
||
|
||
*reinterpret_cast<unsigned long long*>(rec + 0) = entry_ts;
|
||
*reinterpret_cast<unsigned long long*>(rec + 8) = current_ts_ns;
|
||
*reinterpret_cast<int*>(rec + 16) = entry_px_x100;
|
||
*reinterpret_cast<int*>(rec + 20) = (int)(exit_px * 100.0f + 0.5f);
|
||
*reinterpret_cast<int*>(rec + 24) = entry_size;
|
||
*reinterpret_cast<int*>(rec + 28) = 0; // fees_usd_fp placeholder
|
||
*reinterpret_cast<int*>(rec + 32) = realised_usd_fp;
|
||
rec[36] = horizon_idx;
|
||
rec[37] = 0; // strategy_id placeholder
|
||
rec[38] = 0;
|
||
rec[39] = 0;
|
||
|
||
// Reset open-trade scratch.
|
||
#pragma unroll
|
||
for (int i = 0; i < OPEN_TRADE_STATE_BYTES; ++i) st[i] = 0;
|
||
}
|
||
// prev != 0 AND now != 0 (scale-in or partial close): leave entry
|
||
// scratch in place. Multi-fill averaging is a v2 refinement.
|
||
}
|