Recover stuck opening and harden LIVE open/close reconcile.

Stamp open intent, recover opening from exchange option/perp state, skip resell/reopen when already flat, and persist Binance margin mode.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-29 20:32:45 +08:00
parent 44fd0371b9
commit e2a19a1614
8 changed files with 893 additions and 104 deletions
+71 -17
View File
@@ -242,23 +242,32 @@ class StrategyEngine:
if r.ok:
self.enter_rest_after_close()
elif st == "opening":
# 开仓未确认:不自动清槽(可能期权已成交);紧急全平仅告警,防重复开
ok = False
detail = (
"stuck opening:请核对交易所期权/永续后人工处理"
"(已占槽防重复开,紧急全平不会自动释放)"
)
close_data = {"status": "opening"}
try:
from ..notify import wecom
wecom.notify_fault(
title="紧急全平遇到 stuck opening",
detail=detail,
dedupe_key="emergency:opening",
recover = getattr(self.matcher, "recover_opening", None)
if callable(recover):
r = recover()
ok = r.ok
detail = r.detail
close_data = r.data
st2 = str(self.matcher.current_position().get("status") or "")
if r.ok and st2 in ("flat",):
self.enter_rest_after_close()
else:
ok = False
detail = (
"stuck opening:请核对交易所期权/永续后人工处理"
"(已占槽防重复开)"
)
except Exception:
pass
close_data = {"status": "opening"}
try:
from ..notify import wecom
wecom.notify_fault(
title="紧急全平遇到 stuck opening",
detail=detail,
dedupe_key="emergency:opening",
)
except Exception:
pass
elif st in ("open", "option_closed_perp_pending"):
# A:双腿(或续平永续)
r = self.matcher.close_group(reason="emergency", bypass_liquidity=True)
@@ -534,6 +543,46 @@ class StrategyEngine:
pos = self.matcher.current_position()
st_pos = str(pos.get("status") or "flat")
if st_pos == "opening":
allowed, left = self._retry_allowed("opening")
if not allowed:
self._set_state(
phase="opening_stuck",
last_error=f"opening 恢复退避中,{left:.0f}s 后再试",
)
return
recover = getattr(self.matcher, "recover_opening", None)
if callable(recover):
r = await asyncio.to_thread(recover)
self._note_retry_result("opening", ok=r.ok, detail=r.detail)
if r.ok:
self._set_state(phase="idle", last_error=None)
if "half_open" in (r.detail or "") or "提升为 open" in (r.detail or ""):
pass # 继续本 tick 后续逻辑由下一轮处理
try:
from ..notify import wecom
wecom.notify_fault(
title="opening 已自动恢复",
detail=r.detail,
dedupe_key=f"opening_ok:{r.detail[:60]}",
)
except Exception:
pass
else:
self._set_state(phase="opening_stuck", last_error=r.detail)
try:
from ..notify import wecom
wecom.notify_fault(
title="opening 恢复失败",
detail=r.detail,
dedupe_key=f"opening_fail:{r.detail[:60]}",
)
except Exception:
pass
return
if st_pos == "half_open":
allowed, left = self._retry_allowed("half_open")
if not allowed:
@@ -733,7 +782,12 @@ class StrategyEngine:
except Exception:
logger.exception("wecom notify_open failed")
else:
self._set_state(phase="idle", last_error=r.detail)
# 保留 opening 时勿伪装 idle
pos_after = self.matcher.current_position()
if str(pos_after.get("status") or "") == "opening":
self._set_state(phase="opening_stuck", last_error=r.detail)
else:
self._set_state(phase="idle", last_error=r.detail)
try:
from ..notify import wecom