From c16b5aaaa4a3f4f61b0cc22b9a6699861630dbf0 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 8 Jun 2026 18:23:31 +0200 Subject: [PATCH] =?UTF-8?q?PINK:=20fix=20trade=5Fseq=20always-zero=20?= =?UTF-8?q?=E2=80=94=20unique=20trade=5Fid=20per=20position?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit trade_seq was missing from the kernel account snapshot dict, so the intent engine always computed trade_id = "BTCUSDT-T-000000000001". The Rust FSM SLOT_BUSY guard only fires on *different* trade_ids; with the same ID it resets the slot and submits a new exchange order on each ENTER signal tick (~86 duplicate orders observed in one session). Fix: - Add _slot_was_closed dict to ExecutionKernel; set False on ENTER accepted (both sync/async), True on on_venue_event when slot.closed - Increment account.snapshot.trade_seq on the IDLE→CLOSED transition - Expose trade_seq in snapshot()["account"] so DecisionContext carries the correct counter → intent engine generates unique IDs per trade 451/451 tests green. Co-Authored-By: Claude Sonnet 4.6 --- prod/clean_arch/dita_v2/rust_backend.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/prod/clean_arch/dita_v2/rust_backend.py b/prod/clean_arch/dita_v2/rust_backend.py index b9f4d07..694e8cd 100644 --- a/prod/clean_arch/dita_v2/rust_backend.py +++ b/prod/clean_arch/dita_v2/rust_backend.py @@ -668,6 +668,7 @@ class ExecutionKernel: self._backend = _get_rust().create(self.max_slots) self._control_snapshot = self.control_plane.read() self._last_settled_pnl: Dict[int, float] = {} + self._slot_was_closed: Dict[int, bool] = {} self.projection.write_control(self._control_snapshot) self.zinc_plane.update_control(self._control_snapshot) self.state = KernelStateView(self) @@ -796,6 +797,7 @@ class ExecutionKernel: self.state.refresh() if intent.action == KernelCommandType.ENTER and outcome.accepted: self._last_settled_pnl[intent.slot_id] = 0.0 + self._slot_was_closed[intent.slot_id] = False emitted_events = [] all_venue_transitions: List[KernelTransition] = [] if outcome.accepted and intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}: @@ -943,6 +945,7 @@ class ExecutionKernel: self.state.refresh() if intent.action == KernelCommandType.ENTER and outcome.accepted: self._last_settled_pnl[intent.slot_id] = 0.0 + self._slot_was_closed[intent.slot_id] = False emitted_events: List[VenueEvent] = [] all_venue_transitions: List[KernelTransition] = [] if outcome.accepted and intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}: @@ -1040,6 +1043,14 @@ class ExecutionKernel: if abs(incremental_pnl) > 1e-12: self.account.settle(incremental_pnl) self._last_settled_pnl[slot.slot_id] = slot.realized_pnl + # Increment trade_seq when a slot transitions from open → closed so + # the next ENTER gets a unique trade_id. Without this increment the + # intent engine always generates "…-T-000000000001" and the Rust FSM + # resets (not SLOT_BUSYs) the slot on every duplicate ENTER signal. + was_closed = self._slot_was_closed.get(slot.slot_id, True) + if slot.closed and not was_closed: + self.account.snapshot.trade_seq += 1 + self._slot_was_closed[slot.slot_id] = slot.closed slots = [self._get_slot(i) for i in range(self.max_slots)] self.account.observe_slots(slots) current = self._get_slot(slot.slot_id) @@ -1192,6 +1203,7 @@ class ExecutionKernel: "open_positions": self.account.snapshot.open_positions, "open_notional": self.account.snapshot.open_notional, "leverage": self.account.snapshot.leverage, + "trade_seq": self.account.snapshot.trade_seq, # V2 — kernel atomic K/E account (E rules; K is parallel check) "k_capital": rust_account.get("k_capital", self.account.snapshot.capital), "k_realized_pnl": rust_account.get("k_realized_pnl", self.account.snapshot.realized_pnl),