Fix OKX abandon residual double-book and fill/residual ledger holes.

After dual-close leaves option pending, continue close_group instead of false residual; reject partial fills on wait timeout; LIVE residual settle allow_negative.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-29 20:20:36 +08:00
parent ec87cf2104
commit 44fd0371b9
7 changed files with 71 additions and 44 deletions
+2 -1
View File
@@ -115,7 +115,8 @@ class BinanceLiveExecutor(Matcher):
except Exception as e:
logger.exception("binance live open option failed")
msg = str(e)
if "orderId=" in msg or "orderId" in msg.lower():
# 仅当错误带明确 orderId= 时保留 opening(避免校验文案误卡槽)
if "orderId=" in msg:
return OpenResult(
ok=False,
detail=f"币安开期权未确认成交(保留 opening 防重复开,请核对交易所): {e}",
+5
View File
@@ -924,6 +924,11 @@ class OkxLiveExecutor(Matcher):
if dual.ok:
return dual
# close_group 可能已卖掉期权并落 option_closed_perp_pending;勿再记 residual,只续平永续
pos_after = self.current_position()
if str(pos_after.get("status") or "") == "option_closed_perp_pending":
return self.close_group(reason=reason, bypass_liquidity=True)
if require_deep_otm and not self.option_is_deep_otm():
return CloseResult(
ok=False,
+3 -5
View File
@@ -191,15 +191,13 @@ class OkxTradeClient:
if state in ("canceled", "failed"):
raise RuntimeError(f"OKX 订单失败 state={state} {last}")
time.sleep(0.3)
# 超时兜底:已 filled 或已有成交量+均价则回填,避免「有仓无账」
# 超时兜底:仅接受完全成交;部分成交不得当全成记账(会错张数/对冲)
state = str(last.get("state") or "")
avg = safe_float(last.get("avgPx"))
acc = safe_float(last.get("accFillSz")) or 0.0
if state == "filled" or (acc > 0 and avg and avg > 0):
if state == "filled" and avg and avg > 0:
logger.warning(
"OKX fill wait timeout but using last fill data ordId=%s state=%s",
"OKX fill wait timeout but order filled ordId=%s",
ord_id,
state,
)
return self._fill_from_order_row(inst_id, ord_id, last)
raise RuntimeError(f"OKX 订单未完全成交 ordId={ord_id} last={last}")
+4
View File
@@ -854,11 +854,15 @@ class Matcher:
opt_qty = float(row["option_qty_eth"])
opt_pnl = (of.fill_px - opt_entry) * opt_qty
opt_cash = of.notional - of.fee
from ..config import get_settings
self.ledger.apply_cash(
opt_cash,
kind="close_option",
group_id=group_id,
note=f"residual option expiry settle{' force' if force else ''}",
# LIVE 本地账本仅镜像;拒记会导致 residual 永久 pending
allow_negative=not get_settings().is_sim,
)
with self.db._lock:
+18
View File
@@ -241,6 +241,24 @@ class StrategyEngine:
close_data = r.data
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",
)
except Exception:
pass
elif st in ("open", "option_closed_perp_pending"):
# A:双腿(或续平永续)
r = self.matcher.close_group(reason="emergency", bypass_liquidity=True)