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:
@@ -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}:
|
||||||
|
try:
|
||||||
emitted_events = self.venue.submit(intent)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user