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
+225 -24
View File
@@ -17,8 +17,11 @@ from .okx_trade import OkxTradeClient
from .reconcile import (
assert_safe_to_open_live,
claim_open_slot,
exchange_option_abs_size,
perp_close_contracts_okx,
recover_stuck_opening,
release_open_slot_if_opening,
stamp_opening_intent,
)
from .symbols import live_settings, resolve_perp_inst_id
@@ -132,6 +135,18 @@ class OkxLiveExecutor(Matcher):
ct_mult = self._ct_mult(option_inst_id)
opt_contracts = contracts_for_eth(opt_qty, ct_mult)
# 意图先落库:崩溃后仍可 recover(含 option_inst_id
stamp_opening_intent(
self.db,
group_id=group_id,
option_inst_id=option_inst_id,
option_side=option_side,
perp_side=perp_side,
option_qty_eth=opt_qty,
option_qty_contracts=float(opt_contracts),
entry_index_px=entry_index_px,
)
# 期权:买入,张数 = contracts
try:
opt_fill = client.place_market(
@@ -158,6 +173,17 @@ class OkxLiveExecutor(Matcher):
)
opt_contracts = filled_opt_contracts
opt_qty = eth_from_contracts(opt_contracts, ct_mult)
stamp_opening_intent(
self.db,
group_id=group_id,
option_inst_id=option_inst_id,
option_side=option_side,
perp_side=perp_side,
option_qty_eth=opt_qty,
option_qty_contracts=float(opt_contracts),
entry_index_px=entry_index_px,
option_entry_px=float(opt_fill.avg_px),
)
# 永续市价:按产品假设,失败原因实质为保证金不足 → 必须回滚期权
mgn = self._perp_margin_mode()
@@ -190,6 +216,21 @@ class OkxLiveExecutor(Matcher):
)
except Exception as e:
logger.exception("live open perp failed (likely margin); rollback option")
# 永续可能已成交:先查仓,有仓则不得回滚期权
try:
live_perp = client.get_perp_pos_sz(
perp_inst, pos_side=("long" if perp_side == "long" else "short")
)
except Exception:
live_perp = None
if live_perp is not None and live_perp > 1e-8:
return OpenResult(
ok=False,
detail=(
f"永续可能已成交但未确认成交明细(保留 opening): {e}; "
f"ex_perp={live_perp}"
),
)
try:
client.place_market(
inst_id=option_inst_id,
@@ -544,6 +585,109 @@ class OkxLiveExecutor(Matcher):
data={"group_id": group_id, "exec_mode": "LIVE"},
)
def recover_opening(self) -> CloseResult:
"""恢复 stuck opening(交易所对账后 half_open/open/清槽)。"""
err = self._guard_live()
if err:
return CloseResult(ok=False, detail=err)
r = recover_stuck_opening(self)
if r is None:
return CloseResult(ok=False, detail="非 opening 状态")
return r
def _promote_opening_to_open(
self,
*,
pos: dict,
perp_inst: str,
opt_sz: float,
perp_total: float,
) -> CloseResult:
"""opening + 交易所双边有仓 → 落本地 open(用 stamp/设置数量)。"""
s = live_settings()
group_id = str(pos.get("group_id") or f"RCV-{int(time.time())}")
option_inst_id = str(pos.get("option_inst_id") or "")
option_side = str(pos.get("option_side") or "call")
perp_side = str(pos.get("perp_side") or "long")
of_px = float(pos.get("option_entry_px") or 0) or 0.0
opt_qty = float(pos.get("option_qty_eth") or 0)
opt_contracts = float(pos.get("option_qty_contracts") or opt_sz or 0)
if opt_qty <= 0 and opt_contracts > 0:
opt_qty = eth_from_contracts(opt_contracts, self._ct_mult(option_inst_id))
perp_qty = float(pos.get("perp_qty_eth") or 0) or float(
self.ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth)
)
if perp_total > 0:
try:
ct_val = self._client().get_ct_val(perp_inst, inst_type="SWAP")
if ct_val > 0:
perp_qty = float(perp_total) * float(ct_val)
except Exception:
pass
entry_index = float(pos.get("entry_index_px") or 0) or 0.0
# 永续入场价未知时用指数近似(仅恢复镜像)
pf_px = entry_index if entry_index > 0 else of_px
initial_premium = of_px * opt_qty
mgn = self._perp_margin_mode()
now = int(time.time() * 1000)
with self.db._lock:
existing = self.db._conn.execute(
"SELECT group_id FROM groups WHERE group_id=?", (group_id,)
).fetchone()
if existing is None:
self.db._conn.execute(
"""INSERT INTO groups(
group_id, status, bias, option_side, perp_side, option_inst_id, perp_inst_id,
strike, expiry_ymd, entry_index_px, initial_premium, open_at_ms, fees, slip_cost,
exec_mode, perp_margin_mode, note
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
"open",
"recover",
option_side,
perp_side,
option_inst_id,
perp_inst,
None,
None,
entry_index,
initial_premium,
now,
0.0,
0.0,
"LIVE",
mgn,
"recover_opening both legs",
),
)
self.db._conn.execute(
"""UPDATE positions SET
group_id=?, perp_side=?, perp_qty_eth=?, perp_entry_px=?,
option_inst_id=?, option_side=?, option_qty_eth=?, option_qty_contracts=?,
option_entry_px=?, entry_index_px=?, initial_premium=?, status='open'
WHERE id=1""",
(
group_id,
perp_side,
perp_qty,
pf_px,
option_inst_id,
option_side,
opt_qty,
opt_contracts,
of_px,
entry_index,
initial_premium,
),
)
self.db._conn.commit()
return CloseResult(
ok=True,
detail="recover_opening: 已提升为 open",
data={"group_id": group_id, "exec_mode": "LIVE"},
)
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
err = self._guard_live()
if err:
@@ -552,6 +696,8 @@ class OkxLiveExecutor(Matcher):
s = live_settings()
pos = self.current_position()
st = str(pos.get("status") or "")
if st == "opening":
return self.recover_opening()
if st == "half_open":
return self.repair_half_open()
if st not in ("open", "option_closed_perp_pending") or not pos.get("group_id"):
@@ -619,7 +765,35 @@ class OkxLiveExecutor(Matcher):
opt_qty = eth_from_contracts(opt_contracts, self._ct_mult(option_inst_id))
of_notional = of_px * opt_qty
except Exception as e:
if is_expiry and intrinsic is not None:
# 交易所期权可能已空(上次卖出成功但未 mark):跳过再卖,直接 pending
ex_opt = exchange_option_abs_size(client, option_inst_id)
if ex_opt is not None and ex_opt <= 1e-8:
prev = self.db.fetchone(
"""SELECT fill_px, fee, notional, slip FROM fills
WHERE group_id=? AND leg='option' AND action='close'
ORDER BY id DESC LIMIT 1""",
(group_id,),
)
if prev is not None:
of_px = float(prev["fill_px"])
of_fee = float(prev["fee"] or 0)
of_notional = float(prev["notional"] or (of_px * opt_qty))
of_slip = float(prev["slip"] or 0)
elif is_expiry and intrinsic is not None:
of = option_expiry_settle(
intrinsic=float(intrinsic), qty_eth=opt_qty, fee_rate=fee_rate
)
of_px, of_fee, of_notional = of.fill_px, of.fee, of.notional
of_slip = 0.0
else:
of_px = float(pos.get("option_entry_px") or 0) or 0.0
of_fee = 0.0
of_notional = of_px * opt_qty
of_slip = 0.0
logger.warning(
"option already flat on exchange; skip resell: %s", e
)
elif is_expiry and intrinsic is not None:
# 到期后交易所可能已不能交易:用本地结算,仍进入 pending 再平永续
of = option_expiry_settle(
intrinsic=float(intrinsic), qty_eth=opt_qty, fee_rate=fee_rate
@@ -643,42 +817,69 @@ class OkxLiveExecutor(Matcher):
return CloseResult(ok=False, detail=f"实盘平期权失败: {e}")
# 期权已平(或到期本地结算):立刻落 pending,避免永续失败后重试再卖期权
self._mark_option_closed_perp_pending(
group_id=group_id,
option_inst_id=option_inst_id,
opt_qty=opt_qty,
opt_contracts=opt_contracts,
of_px=of_px,
of_fee=of_fee,
of_notional=of_notional,
of_slip=of_slip,
reason=reason,
st_now = str(self.current_position().get("status") or "")
prev_close = self.db.fetchone(
"""SELECT id FROM fills
WHERE group_id=? AND leg='option' AND action='close'
ORDER BY id DESC LIMIT 1""",
(group_id,),
)
if st_now == "option_closed_perp_pending" or prev_close is not None:
# 已入账过平期权:只保证 pending,禁止二次现金
if st_now != "option_closed_perp_pending":
with self.db._lock:
self.db._conn.execute(
"UPDATE positions SET status='option_closed_perp_pending' WHERE id=1"
)
self.db._conn.commit()
else:
self._mark_option_closed_perp_pending(
group_id=group_id,
option_inst_id=option_inst_id,
opt_qty=opt_qty,
opt_contracts=opt_contracts,
of_px=of_px,
of_fee=of_fee,
of_notional=of_notional,
of_slip=of_slip,
reason=reason,
)
pending_perp_only = True
try:
ct_val = client.get_ct_val(perp_inst, inst_type="SWAP")
# pending 路径:交易所已空则禁止用 DB 数量再下单
perp_sz = perp_close_contracts_okx(
client,
perp_inst=perp_inst,
perp_side=perp_side,
perp_qty_eth=perp_qty,
ct_val=ct_val,
allow_db_fallback=not pending_perp_only,
)
if perp_side == "long":
side, pos_side = "sell", "long"
if perp_sz <= 0:
# 永续已在交易所平掉:用入场价近似 finalize(净盈亏由对账校正)
pf_px = float(pos.get("perp_entry_px") or 0) or 0.0
pf_fee = 0.0
logger.warning(
"perp already flat on exchange; finalize without order group=%s",
group_id,
)
else:
side, pos_side = "buy", "short"
perp_live = client.place_market(
inst_id=perp_inst,
side=side,
sz=str(perp_sz),
td_mode=self._perp_margin_mode_for_group(group_id),
pos_side=pos_side,
reduce_only=True,
)
pf_px = float(perp_live.avg_px)
pf_fee = float(perp_live.fee)
if perp_side == "long":
side, pos_side = "sell", "long"
else:
side, pos_side = "buy", "short"
perp_live = client.place_market(
inst_id=perp_inst,
side=side,
sz=str(perp_sz),
td_mode=self._perp_margin_mode_for_group(group_id),
pos_side=pos_side,
reduce_only=True,
)
pf_px = float(perp_live.avg_px)
pf_fee = float(perp_live.fee)
except Exception as e:
return CloseResult(
ok=False,