PINK: fix trade_seq always-zero — unique trade_id per position

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 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-08 18:23:31 +02:00
parent 62553424ab
commit c16b5aaaa4

View File

@@ -668,6 +668,7 @@ class ExecutionKernel:
self._backend = _get_rust().create(self.max_slots) self._backend = _get_rust().create(self.max_slots)
self._control_snapshot = self.control_plane.read() self._control_snapshot = self.control_plane.read()
self._last_settled_pnl: Dict[int, float] = {} self._last_settled_pnl: Dict[int, float] = {}
self._slot_was_closed: Dict[int, bool] = {}
self.projection.write_control(self._control_snapshot) self.projection.write_control(self._control_snapshot)
self.zinc_plane.update_control(self._control_snapshot) self.zinc_plane.update_control(self._control_snapshot)
self.state = KernelStateView(self) self.state = KernelStateView(self)
@@ -796,6 +797,7 @@ class ExecutionKernel:
self.state.refresh() self.state.refresh()
if intent.action == KernelCommandType.ENTER and outcome.accepted: if intent.action == KernelCommandType.ENTER and outcome.accepted:
self._last_settled_pnl[intent.slot_id] = 0.0 self._last_settled_pnl[intent.slot_id] = 0.0
self._slot_was_closed[intent.slot_id] = False
emitted_events = [] emitted_events = []
all_venue_transitions: List[KernelTransition] = [] all_venue_transitions: List[KernelTransition] = []
if outcome.accepted and intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}: if outcome.accepted and intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}:
@@ -943,6 +945,7 @@ class ExecutionKernel:
self.state.refresh() self.state.refresh()
if intent.action == KernelCommandType.ENTER and outcome.accepted: if intent.action == KernelCommandType.ENTER and outcome.accepted:
self._last_settled_pnl[intent.slot_id] = 0.0 self._last_settled_pnl[intent.slot_id] = 0.0
self._slot_was_closed[intent.slot_id] = False
emitted_events: List[VenueEvent] = [] emitted_events: List[VenueEvent] = []
all_venue_transitions: List[KernelTransition] = [] all_venue_transitions: List[KernelTransition] = []
if outcome.accepted and intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}: if outcome.accepted and intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}:
@@ -1040,6 +1043,14 @@ class ExecutionKernel:
if abs(incremental_pnl) > 1e-12: if abs(incremental_pnl) > 1e-12:
self.account.settle(incremental_pnl) self.account.settle(incremental_pnl)
self._last_settled_pnl[slot.slot_id] = slot.realized_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)] slots = [self._get_slot(i) for i in range(self.max_slots)]
self.account.observe_slots(slots) self.account.observe_slots(slots)
current = self._get_slot(slot.slot_id) current = self._get_slot(slot.slot_id)
@@ -1192,6 +1203,7 @@ class ExecutionKernel:
"open_positions": self.account.snapshot.open_positions, "open_positions": self.account.snapshot.open_positions,
"open_notional": self.account.snapshot.open_notional, "open_notional": self.account.snapshot.open_notional,
"leverage": self.account.snapshot.leverage, "leverage": self.account.snapshot.leverage,
"trade_seq": self.account.snapshot.trade_seq,
# V2 — kernel atomic K/E account (E rules; K is parallel check) # V2 — kernel atomic K/E account (E rules; K is parallel check)
"k_capital": rust_account.get("k_capital", self.account.snapshot.capital), "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), "k_realized_pnl": rust_account.get("k_realized_pnl", self.account.snapshot.realized_pnl),