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:
Codex
2026-07-11 22:29:02 +02:00
parent 3efb8749fe
commit 7bc13a6eb2
3 changed files with 259 additions and 4 deletions

View File

@@ -594,7 +594,13 @@ class BingxDirectExecutionAdapter(ExecutionPort):
_action_char = "e" if intent.action == DecisionAction.ENTER else "x"
_ts36 = self._base36(int(time.time() * 1000))
_rand4 = uuid.uuid4().hex[:4]
client_order_id = f"p-{_action_char}-{_ts36}-{_rand4}"
# UV-FIX 2026-07-11: honor an upstream-minted client id (the u- prefix
# from the UV seam) when the intent carries one — venue orders then join
# back to exec_journal rows by id, and the sentinel u- check holds.
# Same charset/length rules apply (metadata id is trimmed to 40).
_meta = intent.metadata or {}
_meta_cid = str(_meta.get("client_order_id") or _meta.get("promo_client_id") or "")
client_order_id = _meta_cid[:40] if _meta_cid else f"p-{_action_char}-{_ts36}-{_rand4}"
# DUAL-LEVERAGE TRANSLATION (prod/bingx/leverage.py, SYSTEM BIBLE §6):
# intent.leverage is the STRATEGY conviction (fractional, 0.5–9.0) and
# already sized the quantity. At-exchange leverage is derived from it
@@ -653,6 +659,34 @@ class BingxDirectExecutionAdapter(ExecutionPort):
)
ack = BingxOrderAck.from_http(ack_payload if isinstance(ack_payload, dict) else {})
ack_row = dict(unwrap_order_payload(ack_payload)) if isinstance(ack_payload, dict) else {}
# UV-FIX 2026-07-11: an accepted order ALWAYS carries the venue's
# orderId. VST answers an order on an offline/delisted symbol with
# code==0 + EMPTY data (observed BAND/CELR 2026-07-10/11) — the
# unwrapped ack is {} and, before this guard, fell through as
# "ACKED" while fill_qty below fabricated a full fill from
# intent.target_size. The kernel FSM then opened a position that
# never existed at the venue (root of the phantom-position pairs).
# (code!=0 envelopes raise BingxHttpError inside signed_post and
# take the except-branch below; this guard covers the code==0
# empty-ack hole, plus any raw envelope leak, belt-and-braces.)
_body_code = int(ack_payload.get("code") or 0) if isinstance(ack_payload, dict) else 0
_order_handle = (
ack_row.get("orderId") or ack_row.get("orderID") or ack_row.get("order_id")
if isinstance(ack_row, dict) else None
)
if _body_code != 0 or not _order_handle:
LOGGER.warning(
"order POST venue-dead ack symbol=%s code=%s orderId=%r msg=%.160s"
" — REJECTED (no fabricated fill)",
symbol, _body_code, _order_handle,
str((ack_payload or {}).get("msg") or "") if isinstance(ack_payload, dict) else "",
)
ack_row = {
"status": "REJECTED",
"msg": "venue returned no orderId (symbol offline/delisted?)",
"symbol": symbol,
"clientOrderId": client_order_id,
}
status = str(ack_row.get("status") or ack.status or "ACKED")
LOGGER.debug("order ACK: status=%s orderId=%s executedQty=%s side=%s",
status, ack_row.get("orderId"), ack_row.get("executedQty"), ack_row.get("side"))
@@ -665,7 +699,7 @@ class BingxDirectExecutionAdapter(ExecutionPort):
if value > 0:
fill_price = value
break
if fill_price <= 0 and self._state is not None:
if fill_price <= 0 and status != "REJECTED" and self._state is not None:
fill_price = next(
(float(row.get("markPrice") or row.get("avgPrice") or 0.0)
for row in self._state.open_positions.values()
@@ -688,8 +722,14 @@ class BingxDirectExecutionAdapter(ExecutionPort):
# BingX REST ACK does not include commission. WS FILL_SETTLED will deliver
# the actual fee later and update the fee_source to "WS_SETTLED".
# Until then, log an estimate so CH rows are never blank on this field.
fill_qty = float(ack_row.get("executedQty") or ack_row.get("filledQty") or
getattr(intent, "target_size", 0.0) or 0.0)
# UV-FIX 2026-07-11: never fabricate a fill for a rejected order — the
# target_size fallback applies only to accepted MARKET acks whose fill
# arrives later via WS (BingX REST ack omits executedQty on those).
if status in ("REJECTED", "RATE_LIMITED"):
fill_qty = 0.0
else:
fill_qty = float(ack_row.get("executedQty") or ack_row.get("filledQty") or
getattr(intent, "target_size", 0.0) or 0.0)
if is_limit:
# LIMIT orders *may* rest and fill as maker — optimistic estimate.
fee_rate = 0.0002 # BingX perpetuals maker fee 0.02%