fix(ml-backtesting): skip queue-decay fill for sentinel-priced market-like orders — ACTUAL root cause of vwap=0/huge sentinels

S1.20-S1.22 chased a wrong hypothesis (walk_* deep-level filter) for
3 rounds. The actual bug: resting_orders.cu:344 fills at cost = take *
s.price in the queue-decay maturation arm. Market-like orders seed at
line 644 with sentinel s.price = 1e9 (buy) / 0 (sell), counting on the
marketability check at lines 372-... to fire first via walk_*. But
queue_position initializes to 0 (no book level matches the sentinel),
so on the first same-side trade volume the queue-decay arm fires and
injects the sentinel into cost: buy cost = take * 1e9 -> avg_px =
1e9 -> huge_flat=216, sell cost = take * 0 -> avg_px = 0 ->
zero_flat=194. Same pattern explains flip=9+6.

Fix: detect s.price outside [min_reasonable_px, max_reasonable_px] in
the queue-decay arm; absorb queue_position depletion without filling.
Marketability check fires in the same iteration via walk_* with proper
book prices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-20 14:03:05 +02:00
parent 76ec68c1c9
commit 29f7e923c6

View File

@@ -336,29 +336,41 @@ extern "C" __global__ void resting_orders_step(
if (s.queue_position >= trade_abs) { if (s.queue_position >= trade_abs) {
s.queue_position -= trade_abs; s.queue_position -= trade_abs;
} else { } else {
const float over = trade_abs - s.queue_position; // Sentinel-priced (market-like) orders use s.price = 1e9 (buy)
s.queue_position = 0.0f; // or 0 (sell) so the marketability check below always crosses.
const float take = (over < s.size_remaining) ? over : s.size_remaining; // The queue-decay arm MUST NOT use s.price for cost because it
if (take > 0.0f) { // would inject the sentinel into avg_px (S1.23 root cause:
// Filled `take` lots at our limit price (queue position reached us). // zero_flat=194, huge_flat=216). Absorb the queue depletion
const float cost = take * s.price; // only; the marketability check fires this same iteration via
apply_fill_to_pos(pos, take, cost, s.side, // walk_* with book-derived prices.
b, cost_per_lot_per_side_per_b, total_fees_per_b, const bool is_sentinel_price = (s.price < min_px) || (s.price > max_px);
nan_avg_px, nan_realised, nan_realized_pnl, if (is_sentinel_price) {
vwap_zero_open_flat, vwap_huge_open_flat, s.queue_position = 0.0f;
vwap_zero_scale_in, vwap_huge_scale_in, } else {
vwap_zero_flip, vwap_huge_flip, const float over = trade_abs - s.queue_position;
last_bad_vwap, last_bad_path); s.queue_position = 0.0f;
s.size_remaining -= take; const float take = (over < s.size_remaining) ? over : s.size_remaining;
emit_audit(audit_base, audit_head, b, (unsigned int)ring_cap_events, if (take > 0.0f) {
current_ts_ns, // Filled `take` lots at our limit price (queue position reached us).
pack_slot_tag(0 /*SlotKind::Limit*/, (unsigned char)i, s.gen_counter), const float cost = take * s.price;
1 /*SubmitLimit*/, s.side, (unsigned short)take, apply_fill_to_pos(pos, take, cost, s.side,
(int)(s.price / tick_size + 0.5f), 0); b, cost_per_lot_per_side_per_b, total_fees_per_b,
if (s.size_remaining <= 0.0f) { nan_avg_px, nan_realised, nan_realized_pnl,
cancel_oco_pair(orders, s.oco_pair); vwap_zero_open_flat, vwap_huge_open_flat,
s.active = 0; vwap_zero_scale_in, vwap_huge_scale_in,
continue; 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;
}
} }
} }
} }