From 29f7e923c6480022de85ad7e242f0d504f739963 Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 20 May 2026 14:03:05 +0200 Subject: [PATCH] =?UTF-8?q?fix(ml-backtesting):=20skip=20queue-decay=20fil?= =?UTF-8?q?l=20for=20sentinel-priced=20market-like=20orders=20=E2=80=94=20?= =?UTF-8?q?ACTUAL=20root=20cause=20of=20vwap=3D0/huge=20sentinels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/ml-backtesting/cuda/resting_orders.cu | 58 ++++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/crates/ml-backtesting/cuda/resting_orders.cu b/crates/ml-backtesting/cuda/resting_orders.cu index dcc39fccf..94c2bee9f 100644 --- a/crates/ml-backtesting/cuda/resting_orders.cu +++ b/crates/ml-backtesting/cuda/resting_orders.cu @@ -336,29 +336,41 @@ extern "C" __global__ void resting_orders_step( if (s.queue_position >= trade_abs) { s.queue_position -= trade_abs; } else { - const float over = trade_abs - s.queue_position; - s.queue_position = 0.0f; - const float take = (over < s.size_remaining) ? over : s.size_remaining; - if (take > 0.0f) { - // Filled `take` lots at our limit price (queue position reached us). - const float cost = take * s.price; - apply_fill_to_pos(pos, take, cost, s.side, - b, cost_per_lot_per_side_per_b, total_fees_per_b, - nan_avg_px, nan_realised, nan_realized_pnl, - vwap_zero_open_flat, vwap_huge_open_flat, - vwap_zero_scale_in, vwap_huge_scale_in, - 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; + // Sentinel-priced (market-like) orders use s.price = 1e9 (buy) + // or 0 (sell) so the marketability check below always crosses. + // The queue-decay arm MUST NOT use s.price for cost because it + // would inject the sentinel into avg_px (S1.23 root cause: + // zero_flat=194, huge_flat=216). Absorb the queue depletion + // only; the marketability check fires this same iteration via + // walk_* with book-derived prices. + const bool is_sentinel_price = (s.price < min_px) || (s.price > max_px); + if (is_sentinel_price) { + s.queue_position = 0.0f; + } else { + const float over = trade_abs - s.queue_position; + s.queue_position = 0.0f; + const float take = (over < s.size_remaining) ? over : s.size_remaining; + if (take > 0.0f) { + // Filled `take` lots at our limit price (queue position reached us). + const float cost = take * s.price; + apply_fill_to_pos(pos, take, cost, s.side, + b, cost_per_lot_per_side_per_b, total_fees_per_b, + nan_avg_px, nan_realised, nan_realized_pnl, + vwap_zero_open_flat, vwap_huge_open_flat, + vwap_zero_scale_in, vwap_huge_scale_in, + 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; + } } } }