dita_v2+adapters: phantom-position root-cause fixes (UV audit §8)
bingx_direct.submit_intent: (1) empty/orderId-less ack (VST offline-symbol hole: code==0 + empty data) -> REJECTED, never fabricate fill from target_size; (2) honor upstream u- client id (intent.metadata promo_client_id/client_order_id) over self-minted p- id. rust_backend: EXIT asset-mismatch guard both sync+async paths — EXIT for asset X while slot holds Y -> NO_OPEN_POSITION/EXIT_ASSET_MISMATCH instead of asset-blind close (THETA 76bbf8ee cross-slot contamination). 9 regression tests, mutation-RED verified (guard flip).
This commit is contained in:
@@ -761,6 +761,36 @@ class ExecutionKernel:
|
||||
control=self.control,
|
||||
)
|
||||
|
||||
def _exit_asset_mismatch_outcome(self, intent: KernelIntent) -> Optional[KernelOutcome]:
|
||||
"""UV-FIX 2026-07-11: an EXIT must name the asset its slot holds.
|
||||
|
||||
All UV promotion intents share slot 0. During the 2026-07-10/11
|
||||
phantom-pair incident, an EXIT for asset X arriving while the slot
|
||||
held asset Y was accepted asset-blind — closing Y's venue position
|
||||
and orphaning Y's own later exit (NO_OPEN_POSITION). Reject the
|
||||
mismatch at the kernel boundary instead.
|
||||
"""
|
||||
if intent.action != KernelCommandType.EXIT:
|
||||
return None
|
||||
slot = self._get_slot(int(intent.slot_id))
|
||||
open_asset = str(getattr(slot, "asset", "") or "")
|
||||
if slot.is_open() and open_asset and open_asset != intent.asset:
|
||||
return KernelOutcome(
|
||||
accepted=False,
|
||||
slot_id=int(intent.slot_id),
|
||||
trade_id=intent.trade_id,
|
||||
state=slot.fsm_state,
|
||||
diagnostic_code=KernelDiagnosticCode.NO_OPEN_POSITION,
|
||||
severity=KernelSeverity.WARNING,
|
||||
details={
|
||||
"reason": "EXIT_ASSET_MISMATCH",
|
||||
"slot_asset": open_asset,
|
||||
"intent_asset": intent.asset,
|
||||
"intent_id": intent.intent_id,
|
||||
},
|
||||
)
|
||||
return None
|
||||
|
||||
def process_intent(self, intent: KernelIntent) -> KernelOutcome:
|
||||
self.zinc_plane.publish_intent(intent)
|
||||
if not (0 <= int(intent.slot_id) < self.max_slots):
|
||||
@@ -795,6 +825,9 @@ class ExecutionKernel:
|
||||
"asset": intent.asset,
|
||||
},
|
||||
)
|
||||
_mismatch = self._exit_asset_mismatch_outcome(intent)
|
||||
if _mismatch is not None:
|
||||
return _mismatch
|
||||
payload = _intent_to_payload(intent)
|
||||
result = _get_rust().process_intent(
|
||||
self._backend,
|
||||
@@ -943,6 +976,9 @@ class ExecutionKernel:
|
||||
details={"reason": "INVALID_INTENT", "field": name, "value": str(value),
|
||||
"intent_id": intent.intent_id, "action": intent.action.value, "asset": intent.asset},
|
||||
)
|
||||
_mismatch = self._exit_asset_mismatch_outcome(intent)
|
||||
if _mismatch is not None:
|
||||
return _mismatch
|
||||
# ── Rust FSM (sync, atomic, μs-fast — no await here) ─────────────────
|
||||
payload = _intent_to_payload(intent)
|
||||
result = _get_rust().process_intent(
|
||||
|
||||
Reference in New Issue
Block a user