PINK Phase 0: FET -$5,990 fix batch — leverage-free PnL, true fill prices, reconcile baseline anchors
Defects fix (FET -$5,990 replay, 2026-06-11): - realized_pnl() and mark_price(): PnL = qty × Δprice, side-signed; no ×leverage inflation (was 3× every leg). - BingX MARKET fill events carry true fill price (avgPrice/lastFillPrice), never the order's nominal price (protective bound ±20-25% from mark, poisoned PnL to -$5,990 on a +$164 round-trip). - Fill routing by ORDER IDENTITY first, FSM state second — late entry-remainder fills during EXIT_WORKING no longer misclassify as exits. - Entry basis = VWAP across entry fills, not last fill price. - reconcile_from_slots / restore_state: re-anchor _last_settled_pnl / _slot_was_closed to adopted slot state (cross-restart double-book of carried PnL). - ACCOUNT_UPDATE with wallet_balance=0 dropped (margin-only frames no longer zero e_available_margin). - Foreign-fill skip on shared VST account (PRODGREEN collision filter). - exec_router TTL: entry-requote venue-truth gate (recent own fill + live exchange position probes prevent double-entry). - bingx_direct: openOrders fetched BEFORE positions (sequential ordering prevents dangerous tear → double-entries). - Dual-leverage translation via map_internal_conviction_to_exchange_leverage() (strategy conviction → integer at-exchange leverage, bankers rounding). - BLUE-parity alpha components wired: asset picker (IRP universe ranking) + alpha sizer (cubic-convex dynamic leverage, 0.5-8.0 range). - ch_writer: date_time_input_format=best_effort on insert URLs; flush error logging at WARNING with counter. - blue_parity.price_of(): hyphen-tolerant fallback (FET-USDT → FETUSDT). - Fill test updated to incremental filled_size semantics (BingX WS lastFilledQty). - Env-override base URLs, supervisord autorestart, per-asset DC histories, single-slot invariant, fill-attribution filter. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
@@ -26,7 +26,7 @@ from prod.bingx.enums import BingxEnvironment
|
||||
from prod.bingx.http import BingxHttpError
|
||||
from prod.bingx.http import BingxHttpClient
|
||||
from prod.bingx.instrument_provider import BingxInstrumentProvider
|
||||
from prod.bingx.leverage import normalize_bingx_leverage_value
|
||||
from prod.bingx.leverage import map_internal_conviction_to_exchange_leverage
|
||||
from prod.bingx.schemas import BingxOrderAck
|
||||
from prod.bingx.schemas import unwrap_order_payload
|
||||
from prod.clean_arch.dita import Intent, TradeSide, DecisionAction
|
||||
@@ -448,12 +448,22 @@ class BingxDirectExecutionAdapter(ExecutionPort):
|
||||
the others. Historical calls (allOrders, allFillOrders) are gated
|
||||
on ``include_history`` and also gathered.
|
||||
"""
|
||||
balance_task = self._safe_get("/openApi/swap/v2/user/balance")
|
||||
positions_task = self._safe_get("/openApi/swap/v2/user/positions")
|
||||
orders_task = self._safe_get("/openApi/swap/v2/trade/openOrders")
|
||||
|
||||
balance_payload, positions_payload, open_orders_payload = await asyncio.gather(
|
||||
balance_task, positions_task, orders_task,
|
||||
# SNAPSHOT CONSISTENCY ORDER (2026-06-10, operator-mandated atomicity):
|
||||
# openOrders MUST complete BEFORE positions is fetched. The snapshot
|
||||
# is assembled from separate REST calls and cannot be truly atomic, but
|
||||
# this ordering makes the dangerous tear unrepresentable: if an order
|
||||
# fills between the two fetches, it still APPEARS in open_orders
|
||||
# (conservative: treated as working) while the resulting position is
|
||||
# ALSO visible in positions. The deadly combination — absent from
|
||||
# open_orders AND absent from positions for a filled order — cannot
|
||||
# occur. Consumers may therefore reason "order gone ⇒ its outcome is
|
||||
# visible in positions/fills" ONLY because of this ordering.
|
||||
# (Previously all three were gathered concurrently → torn snapshots →
|
||||
# double-entry incidents 15:20 and 17:24 UTC.)
|
||||
open_orders_payload = await self._safe_get("/openApi/swap/v2/trade/openOrders")
|
||||
balance_payload, positions_payload = await asyncio.gather(
|
||||
self._safe_get("/openApi/swap/v2/user/balance"),
|
||||
self._safe_get("/openApi/swap/v2/user/positions"),
|
||||
)
|
||||
|
||||
all_orders_payload: Any = []
|
||||
@@ -585,8 +595,13 @@ class BingxDirectExecutionAdapter(ExecutionPort):
|
||||
_ts36 = self._base36(int(time.time() * 1000))
|
||||
_rand4 = uuid.uuid4().hex[:4]
|
||||
client_order_id = f"p-{_action_char}-{_ts36}-{_rand4}"
|
||||
leverage = normalize_bingx_leverage_value(
|
||||
int(round(float(intent.leverage or self._config.default_leverage))),
|
||||
# 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
|
||||
# via the linear conviction map → integer [1, cap], bankers rounding.
|
||||
# Plain round(conviction) here previously pinned every trade at the cap.
|
||||
leverage = map_internal_conviction_to_exchange_leverage(
|
||||
float(intent.leverage or self._config.default_leverage),
|
||||
exchange_max=self._config.exchange_leverage_cap,
|
||||
)
|
||||
|
||||
@@ -624,7 +639,10 @@ class BingxDirectExecutionAdapter(ExecutionPort):
|
||||
}
|
||||
if is_limit:
|
||||
payload["price"] = self._format_price(intent.asset, limit_price)
|
||||
payload["timeInForce"] = "GTC"
|
||||
# Exec-router maker quotes send PostOnly so a crossing quote is
|
||||
# rejected by the venue instead of paying taker silently.
|
||||
_tif = str((intent.metadata or {}).get("_time_in_force", "GTC") or "GTC")
|
||||
payload["timeInForce"] = _tif if _tif in ("GTC", "IOC", "FOK", "PostOnly") else "GTC"
|
||||
if reduce_only:
|
||||
payload["reduceOnly"] = "true"
|
||||
LOGGER.debug("order POST: action=%s side=%s symbol=%s qty=%s reduceOnly=%s",
|
||||
|
||||
Reference in New Issue
Block a user