From a5894a71960b102295d4d3e4dc6ac432cf1489a7 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 4 Jun 2026 18:17:31 +0200 Subject: [PATCH] PINK: FSM rollback on venue.submit failure via synthetic REJECTED event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When venue.submit() raises (BingX timeout / network error), the Rust FSM had already advanced to ORDER_REQUESTED/ENTRY_WORKING with no corresponding exchange order — stranding the slot. Every subsequent ENTER for a different asset hit SLOT_BUSY, preventing recovery without a restart. Restarts create a fresh IDLE kernel, leaving the orphaned exchange position unmanaged. Fix: catch submit exceptions, synthesise an ORDER_REJECT VenueEvent, feed it through on_venue_event() so the FSM rolls back to IDLE atomically. The slot is free on the next cycle with no orphan on the exchange. Co-Authored-By: Claude Sonnet 4.6 --- prod/clean_arch/dita_v2/rust_backend.py | 34 ++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/prod/clean_arch/dita_v2/rust_backend.py b/prod/clean_arch/dita_v2/rust_backend.py index 401febf..95a7b35 100644 --- a/prod/clean_arch/dita_v2/rust_backend.py +++ b/prod/clean_arch/dita_v2/rust_backend.py @@ -793,7 +793,39 @@ class ExecutionKernel: emitted_events = [] all_venue_transitions: List[KernelTransition] = [] if outcome.accepted and intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}: - emitted_events = self.venue.submit(intent) + try: + emitted_events = self.venue.submit(intent) + except Exception as _submit_exc: + # venue.submit() failed (e.g. BingX timeout). The Rust FSM already + # advanced to ORDER_REQUESTED / ENTRY_WORKING with no corresponding + # exchange order. Feed a synthetic REJECTED event so the FSM rolls + # back to IDLE — otherwise the slot is stranded and every subsequent + # ENTER with a different trade_id hits SLOT_BUSY forever. + import logging as _log + _log.getLogger(__name__).error( + "venue.submit failed (%s) — feeding synthetic REJECTED to roll back FSM slot=%d action=%s", + _submit_exc, intent.slot_id, intent.action.value, + ) + _reject_event = VenueEvent( + timestamp=datetime.now(timezone.utc), + event_id=f"{intent.trade_id}:submit_error", + trade_id=intent.trade_id, + slot_id=intent.slot_id, + kind=KernelEventKind.ORDER_REJECT, + status=VenueEventStatus.REJECTED, + venue_order_id="", + venue_client_id="", + side=intent.side, + asset=intent.asset, + price=0.0, + size=float(intent.target_size or 0.0), + filled_size=0.0, + remaining_size=float(intent.target_size or 0.0), + reason=f"VENUE_SUBMIT_ERROR:{_submit_exc}", + raw_payload={}, + metadata={"intent_id": intent.intent_id, "action": intent.action.value}, + ) + emitted_events = [_reject_event] for event in emitted_events: evt_outcome = self.on_venue_event(event) all_venue_transitions.extend(evt_outcome.transitions)