PINK: FSM rollback on venue.submit failure via synthetic REJECTED event

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 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-04 18:17:31 +02:00
parent 9acaeafc8b
commit a5894a7196

View File

@@ -793,7 +793,39 @@ class ExecutionKernel:
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}:
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: for event in emitted_events:
evt_outcome = self.on_venue_event(event) evt_outcome = self.on_venue_event(event)
all_venue_transitions.extend(evt_outcome.transitions) all_venue_transitions.extend(evt_outcome.transitions)