From 09e5fe1131bb737ecb1838deefda780c6b959e17 Mon Sep 17 00:00:00 2001 From: luisleo526 Date: Wed, 8 Jul 2026 03:42:21 +0800 Subject: [PATCH] fix(fills): normalize qty_percent when freezing a deferred layered exit qty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reconcile_deferred_layered_exits froze each leg's qty to its reserved slice but left the 100% sibling's qty_percent at 100. On a later re-arm of the partial sibling, the compute_exit_reserved_qty guard read that stale 100 as a still-pending FULL exit, dropped the re-issued partial, and the 100% leg re-expanded to flatten the whole position at its stop — over-closing a layered TP1(50%)+TP2(100%) bracket that was armed while flat (the deferred path; live-armed groups normalize at arm time). Set qty_percent consistent with the qty reconcile already froze (mirrors the live-armed normalization in compute_exit_reserved_qty). The change is gated by reconcile's leg_count>=2 + has_partial condition, so live-armed layered exits and non-layered brackets are untouched. Gate (R7, fresh context): cmake pineforge clean; ctest 79/79; run_corpus 239 exc/12 strong/1 anomaly fail=0 (byte-identical corpus); data sweep 0 up/0 down across 8 qty_percent sentinels + 2 3commas DCA/grid, run_err=0. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/engine_fills.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/engine_fills.cpp b/src/engine_fills.cpp index 4d8a3f4..da8b465 100644 --- a/src/engine_fills.cpp +++ b/src/engine_fills.cpp @@ -934,6 +934,14 @@ void BacktestEngine::reconcile_deferred_layered_exits(const std::string& entry_i double res = std::min(requested, avail); if (res <= kQtyEpsilon) continue; // nothing left to reserve; leave deferred o.qty = res; + // Keep qty_percent consistent with the qty we just froze. A deferred + // 100% sibling capped here to the remaining slice must not keep + // qty_percent=100, or a later same-bar/next-bar re-arm of a partial + // sibling reads it as a still-pending FULL exit (compute_exit_reserved_ + // qty guard), drops the re-issued partial, and the 100% leg re-expands + // to flatten the whole position. Mirrors the live-armed normalization + // at engine_strategy_commands.cpp (reserved_qty_out / live_pos * 100). + if (live_pos > kQtyEpsilon) o.qty_percent = (res / live_pos) * 100.0; o.requested_partial = res < live_pos - kFullQtyEps; reserved += res; }