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:
+225
-24
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user