diff --git a/prod/clean_arch/dita_v2/bingx_venue.py b/prod/clean_arch/dita_v2/bingx_venue.py index 04e5dfc..37f21f2 100644 --- a/prod/clean_arch/dita_v2/bingx_venue.py +++ b/prod/clean_arch/dita_v2/bingx_venue.py @@ -302,29 +302,39 @@ class BingxVenueAdapter(VenueAdapter): client_order_id: str = "", details: dict[str, Any] | None = None, ) -> None: - plane = self._telemetry_plane - publish = getattr(plane, "publish_venue", None) if plane is not None else None - if publish is None: - return - slot_id = 0 - trade_id = "" - asset = "" - side = TradeSide.FLAT - action = "" - intent_id = "" - if intent is not None: - slot_id = int(getattr(intent, "slot_id", 0) or 0) - trade_id = str(getattr(intent, "trade_id", "") or "") - asset = str(getattr(intent, "asset", "") or "") - side = getattr(intent, "side", TradeSide.FLAT) - action = str(getattr(intent, "action", "") or "") - intent_id = str(getattr(intent, "intent_id", "") or "") - if order is not None: - slot_id = int(order.metadata.get("slot_id", slot_id) or slot_id) - trade_id = str(order.internal_trade_id or trade_id) - asset = str(order.metadata.get("asset") or asset) - side = order.side or side + # TOTAL: this method cannot raise. It runs INLINE on the order path — + # submit()/submit_async() call it immediately after the venue has already + # accepted the order. An exception escaping here reaches the caller's + # submit guard, which synthesises a REJECTED event and rolls the FSM back + # while the venue keeps the position (orphan; 6 of them on 2026-07-13). + # + # The guard used to cover only publish(), leaving the attribute/coercion + # block below it unprotected — which is how a signature skew orphaned live + # positions past a defence that existed precisely to prevent it. Everything + # that can raise now lives inside the try. Observability never vetoes a fill. try: + plane = self._telemetry_plane + publish = getattr(plane, "publish_venue", None) if plane is not None else None + if publish is None: + return + slot_id = 0 + trade_id = "" + asset = "" + side = TradeSide.FLAT + action = "" + intent_id = "" + if intent is not None: + slot_id = int(getattr(intent, "slot_id", 0) or 0) + trade_id = str(getattr(intent, "trade_id", "") or "") + asset = str(getattr(intent, "asset", "") or "") + side = getattr(intent, "side", TradeSide.FLAT) + action = str(getattr(intent, "action", "") or "") + intent_id = str(getattr(intent, "intent_id", "") or "") + if order is not None: + slot_id = int(order.metadata.get("slot_id", slot_id) or slot_id) + trade_id = str(order.internal_trade_id or trade_id) + asset = str(order.metadata.get("asset") or asset) + side = order.side or side publish( VenueTelemetrySnapshot( phase=phase, @@ -350,8 +360,14 @@ class BingxVenueAdapter(VenueAdapter): details=dict(details or {}), ) ) - except Exception: - pass + except Exception as exc: + import logging as _log + + _log.getLogger(__name__).warning( + "FIX(bingx_venue): telemetry publish failed (phase=%s) — swallowed, " + "order path unaffected: %s", + phase, exc, + ) def _call_backend(self, method_name: str, *args: Any, **kwargs: Any) -> Any: method = getattr(self.backend, method_name, None)