"""L2 — LIMIT order payload wiring in BingxDirectExecutionAdapter.submit_intent. The venue adapter forwards _order_type/_limit_price in the intent metadata; the backend must place a LIMIT order (type=LIMIT + price + GTC) when asked, and keep MARKET as the default. Offline unit test of payload construction — the signed_post client is stubbed to capture the order payload; no exchange contact. """ from __future__ import annotations import asyncio from datetime import datetime, timezone from types import SimpleNamespace from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter from prod.clean_arch.dita import DecisionAction, Intent, TradeSide def _adapter(captured: dict): a = BingxDirectExecutionAdapter.__new__(BingxDirectExecutionAdapter) a._config = SimpleNamespace(recv_window_ms=5000, default_leverage=1, exchange_leverage_cap=3) a._client_order_run_id = "test" a._entry_client_order_seq = 0 a._exit_client_order_seq = 0 a._state = SimpleNamespace(open_positions={}, account={}) async def _signed_post(path, params): if path.endswith("/trade/order"): captured["order"] = dict(params) return {"orderId": "1", "status": "NEW"} a._client = SimpleNamespace(signed_post=_signed_post) a._instrument_venue_symbol = lambda asset: "BTC-USDT" a._format_quantity = lambda asset, q: f"{float(q)}" a._format_price = lambda asset, p: f"{float(p)}" async def _refresh(asset, include_history=True): return a._state a._refresh_exchange_state = _refresh return a def _intent(metadata: dict) -> Intent: return Intent( timestamp=datetime.now(timezone.utc), trade_id="T1", decision_id="D1", asset="BTCUSDT", action=DecisionAction.ENTER, side=TradeSide.SHORT, reason="TEST", target_size=0.01, leverage=2.0, reference_price=100.0, confidence=0.5, exit_leg_ratios=(1.0,), metadata=metadata, ) def test_limit_intent_places_limit_order(): captured: dict = {} asyncio.run(_adapter(captured).submit_intent(_intent({"_order_type": "LIMIT", "_limit_price": 95.0}))) o = captured["order"] assert o["type"] == "LIMIT", o assert "price" in o and float(o["price"]) == 95.0, o assert o.get("timeInForce") == "GTC", o def test_market_intent_places_market_order(): captured: dict = {} asyncio.run(_adapter(captured).submit_intent(_intent({}))) o = captured["order"] assert o["type"] == "MARKET", o assert "price" not in o, o def test_limit_without_valid_price_falls_back_to_market(): captured: dict = {} asyncio.run(_adapter(captured).submit_intent(_intent({"_order_type": "LIMIT", "_limit_price": 0.0}))) assert captured["order"]["type"] == "MARKET", captured["order"] # --- cancel: truth-based confirmation (trust exchange state over the response) --- def _cancel_adapter(*, open_after: list): a = BingxDirectExecutionAdapter.__new__(BingxDirectExecutionAdapter) a._config = SimpleNamespace(recv_window_ms=5000) a._instrument_venue_symbol = lambda asset: "TRX-USDT" async def _signed_delete(path, params): # Simulate BingX returning a transient error even when the order is removed. return {"status": "REJECTED", "msg": "order not exist"} async def _signed_get(path, params): return {"data": {"orders": open_after}} a._client = SimpleNamespace(signed_delete=_signed_delete, signed_get=_signed_get) return a def _order(oid="2060963645141028864"): return SimpleNamespace(venue_order_id=oid, venue_client_id="T:i", metadata={"asset": "TRXUSDT"}) def test_cancel_succeeds_when_order_gone_despite_error_response(): a = _cancel_adapter(open_after=[]) # order no longer open resp = asyncio.run(a.cancel(_order())) assert resp["status"] == "CANCELED", resp def test_cancel_rejected_when_order_still_open(): a = _cancel_adapter(open_after=[{"orderId": "2060963645141028864", "status": "PENDING"}]) resp = asyncio.run(a.cancel(_order())) assert resp["status"] != "CANCELED", resp