PINK DITAv2 L0-L2: two-phase persistence + async-fill pump + LIMIT wiring
Execution-infra only (policy stays MARKET; algorithmic integrity untouched). L0 — two-phase (request->result) persistence (pink_clickhouse.py): - Split persist_step into persist_request (policy_events + trade_reconstruction ORDER_REQUESTED) and persist_result (state snapshot + per-fill lifecycle rows). - Lifecycle rows (ENTRY_FILLED/EXIT/trade_events/trade_exit_legs) gated on evidence of an actual fill (FULL/PARTIAL_FILL event, closed slot, or size drop vs _leg_state) -> a resting LIMIT (ACK only) emits no terminal rows. - Add persist_fill_events: synthesizes a minimal decision/intent from slot+event for async fills and routes through persist_result. L1 — async-fill pump (pink_direct.py): - PinkDirectRuntime.pump_venue_events(): venue.reconcile() -> kernel.on_venue_event (capital settles, FSM advances), persists applied fills; kernel dedups duplicates (no double-settle). Called at the start of step(). L2 — LIMIT placement (bingx_direct.py): - submit_intent now honors _order_type/_limit_price from intent metadata (was hardcoded MARKET): LIMIT -> type=LIMIT + price + GTC; MARKET default; invalid limit price falls back to MARKET. Offline: 63 passed (persistence/groundwork/pump/limit-payload/runtime/accounting/ flaws/kernel). MARKET path unchanged; resting LIMIT now correct end-to-end offline. Live VST validation (L3) pending. BLUE untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
73
prod/tests/test_bingx_direct_limit_order.py
Normal file
73
prod/tests/test_bingx_direct_limit_order.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user