Fix LIVE SoT P0/P1: closing state machine, OO exchange fills, BN balances.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 15:54:45 +08:00
parent 99e58910d3
commit 8d67f3fc6c
10 changed files with 864 additions and 167 deletions
+81 -13
View File
@@ -477,19 +477,53 @@ class StrategyEngine:
)
except Exception:
pass
elif st == "closing":
close_oo = getattr(
self.matcher, "close_winning_oo_leave_residual", None
)
if callable(close_oo):
r = close_oo(reason="emergency_closing")
else:
r = self.matcher.close_group(
reason="emergency", bypass_liquidity=True
)
ok = r.ok
detail = r.detail
close_data = r.data
if r.ok:
self.enter_rest_after_close()
elif st in ("open", "option_closed_perp_pending"):
# A:双腿(或续平永续)
r = self.matcher.close_group(reason="emergency", bypass_liquidity=True)
if not r.ok and st == "open":
# B:砸不出期权时强制只平永续(不要求远虚)
abandon = getattr(self.matcher, "close_perp_abandon_option", None)
if callable(abandon):
try:
r2 = abandon(reason="emergency_perp", require_deep_otm=False)
except TypeError:
r2 = abandon(reason="emergency_perp")
if r2.ok:
r = r2
is_oo = (
str(pos.get("hedge_mode") or "") == "option_option"
or bool(pos.get("option2_inst_id"))
)
if is_oo and st == "open":
close_full = getattr(self.matcher, "close_oo_full", None)
if callable(close_full):
r = close_full(reason="emergency", bypass_liquidity=True)
else:
r = self.matcher.close_group(
reason="emergency", bypass_liquidity=True
)
else:
# A:双腿(或续平永续)
r = self.matcher.close_group(
reason="emergency", bypass_liquidity=True
)
if not r.ok and st == "open" and not is_oo:
# B:砸不出期权时强制只平永续(不要求远虚)
abandon = getattr(
self.matcher, "close_perp_abandon_option", None
)
if callable(abandon):
try:
r2 = abandon(
reason="emergency_perp", require_deep_otm=False
)
except TypeError:
r2 = abandon(reason="emergency_perp")
if r2.ok:
r = r2
ok = r.ok
detail = r.detail
close_data = r.data
@@ -948,6 +982,24 @@ class StrategyEngine:
)
return
if st_pos == "closing":
allowed, left = self._retry_allowed("closing")
if not allowed:
self._set_state(
phase="closing",
last_error=f"closing 收尾退避中,{left:.0f}s 后再试",
)
return
close_oo = getattr(self.matcher, "close_winning_oo_leave_residual", None)
if callable(close_oo):
r = await asyncio.to_thread(close_oo, reason="closing_retry")
self._note_retry_result("closing", ok=r.ok, detail=r.detail)
if r.ok:
self.enter_rest_after_close()
else:
self._set_state(phase="closing", last_error=r.detail)
return
if st_pos == "open":
upl = self.matcher.unrealized()
from .exits import lock_trade_exit_target, read_locked_exit_target
@@ -1001,8 +1053,24 @@ class StrategyEngine:
# 复用 ExitDecision 形态
from .exits import ExitDecision
should_semi = bool(semi_d.should_close)
if should_semi:
# 出场前要求持仓期权有买一,避免无对手盘硬平
opt_id = str(upl.get("option_inst_id") or "")
oq = None
try:
oq = self.matcher._quote_held_option(opt_id)
except Exception:
oq = None
if oq is None or oq.bid is None or float(oq.bid) <= 0:
should_semi = False
self._set_state(
phase="liquidity_wait",
last_error="半自动已达标但期权无买一,等待流动性",
)
return
decision = ExitDecision(
bool(semi_d.should_close),
should_semi,
str(semi_d.reason or ""),
float(semi_d.net_target or 0),
)
+9 -2
View File
@@ -70,8 +70,15 @@ def _live_balances() -> dict[str, float | None]:
ex = str(load_runtime_settings().exchange or "").strip().lower()
if ex in ("binance", "bn"):
# 币安资金接口尚未接入;返回 None 使可开判定为「未知」而非误用 OKX
logger.debug("open_capacity: binance live balance not wired; treating as unknown")
from ..live.binance_trade import BinanceTradeClient
client = BinanceTradeClient()
try:
bal = client.fetch_balances()
out["trading_usdt"] = _f(bal.get("trading_usdt"))
out["trading_usdc"] = _f(bal.get("trading_usdc"))
finally:
client.close()
else:
from ..live.okx_funds import OkxFundsClient