PINK DITAv2 L3: fix live LIMIT cancel (kernel order-id propagation + truth-based cancel)

L3 live validation surfaced a live-only defect: a working LIMIT order could not
be cancelled (MARKET never exercised cancel — synchronous fills).

Two coupled fixes:
- Rust FSM (lib.rs): propagate the venue's order id onto the active order for
  ALL order types and event kinds (ACK/partial/full fill) whenever the exchange
  provides one — orders are created at submit with an empty venue_order_id, so a
  later cancel had no real id to reference. Only fills empty ids, never overwrites.
  Requires recompiling libdita_v2_kernel.so.
- Backend (bingx_direct.py): add cancel(order) — a properly-signed DELETE by
  orderId (clientOrderId fallback) with TRUTH-BASED confirmation: BingX can return
  transient errors ("order not exist", dup-within-1s from an internal retry) even
  when the order was removed, so the cancel succeeds iff the order is no longer
  open on the venue. The venue adapter prefers this backend cancel over its raw
  signed_delete fallback (which failed signature with an empty id).

Validated:
- Offline: 63 + new cancel-truth unit tests green (no regression post-recompile).
- Live VST: resting SHORT LIMIT (+5%) rests as ENTRY_WORKING, confirmed as a LIMIT
  open order, cancel -> CANCEL_ACK -> IDLE, exchange flat (test_pink_limit_live.py).
- Live VST MARKET run-through re-validated post-recompile: PASS, exact capital
  reconciliation, two-phase rows visible (ORDER_REQUESTED + ENTRY_FILLED/EXIT).

LIMIT remains execution-infra only; PINK policy stays MARKET. BLUE untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-05-31 08:03:27 +02:00
parent 55ed6902d8
commit 0c15a7698e
4 changed files with 226 additions and 0 deletions

View File

@@ -441,6 +441,63 @@ class BingxDirectExecutionAdapter(ExecutionPort):
self._state = await self._refresh_exchange_state(intent.asset, include_history=True)
return receipt
async def cancel(self, order: Any, *, reason: str = "") -> dict[str, Any]:
"""Cancel a working order on the venue (resting LIMIT support).
Signs the DELETE with the same client used for order placement, keyed by
the venue orderId (propagated onto the slot order by the kernel on ACK)
with a clientOrderId fallback. Returns the raw BingX response for the
venue adapter to map into a CANCEL_ACK / CANCEL_REJECT event.
"""
asset = str((getattr(order, "metadata", None) or {}).get("asset") or "")
symbol = self._instrument_venue_symbol(asset) if asset else ""
params: dict[str, Any] = {
"symbol": symbol,
"recvWindow": str(int(self._config.recv_window_ms)),
}
venue_order_id = str(getattr(order, "venue_order_id", "") or "")
venue_client_id = str(getattr(order, "venue_client_id", "") or "")
if venue_order_id:
params["orderId"] = venue_order_id
elif venue_client_id:
params["clientOrderId"] = venue_client_id
else:
return {"status": "REJECTED", "msg": "no order id to cancel",
"orderId": venue_order_id, "clientOrderId": venue_client_id}
delete_resp: dict[str, Any] = {}
try:
resp = await self._client.signed_delete("/openApi/swap/v2/trade/order", params)
delete_resp = resp if isinstance(resp, dict) else {"status": "CANCELED"}
except BingxHttpError as exc:
delete_resp = {"status": "RATE_LIMITED" if _is_rate_limited_error(exc) else "ERROR", "msg": str(exc)}
# Truth-based confirmation: the cancel succeeded iff the order is no
# longer open on the venue. BingX can return transient errors (e.g.
# "order not exist", "same order number ... within 1 second" from an
# internal retry) even when the order was actually removed — so we trust
# exchange state, not the DELETE response.
still_open: bool | None = None
try:
oo = await self._client.signed_get("/openApi/swap/v2/trade/openOrders", {"symbol": symbol})
rows = oo if isinstance(oo, list) else (oo.get("data") or oo.get("orders") or [])
if isinstance(rows, dict):
rows = rows.get("orders") or []
ids = {str(r.get("orderId")) for r in rows if isinstance(r, dict)}
cids = {str(r.get("clientOrderId") or r.get("clientOrderID")) for r in rows if isinstance(r, dict)}
still_open = (venue_order_id in ids) if venue_order_id else (venue_client_id in cids)
except Exception:
still_open = None
if still_open is False:
return {"status": "CANCELED", "orderId": venue_order_id, "clientOrderId": venue_client_id}
if str(delete_resp.get("status", "")).upper() in {"CANCELED", "CANCELLED", "SUCCESS", "OK"}:
return {"status": "CANCELED", "orderId": venue_order_id, "clientOrderId": venue_client_id}
return {
"status": delete_resp.get("status", "REJECTED"),
"msg": delete_resp.get("msg", "cancel not confirmed"),
"orderId": venue_order_id, "clientOrderId": venue_client_id,
}
async def reconcile(self, symbol: str | None = None) -> ExchangeStateSnapshot:
# Recovery-only path: ask the venue for authoritative account/position/order state.
return await self._refresh_exchange_state(symbol, include_history=True)