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
+227 -23
View File
@@ -15,8 +15,11 @@ from .binance_trade import BinanceTradeClient
from .reconcile import (
assert_safe_to_open_live,
claim_open_slot,
exchange_option_abs_size,
perp_close_qty_eth_binance,
recover_stuck_opening,
release_open_slot_if_opening,
stamp_opening_intent,
)
from .symbols import live_settings, resolve_perp_inst_id
@@ -39,6 +42,17 @@ class BinanceLiveExecutor(Matcher):
return reason
return None
def _perp_margin_mode(self) -> str:
from ..config import get_settings
s = get_settings()
raw = str(
self.ledger.get_setting_str("perp_margin_mode", s.perp_margin_mode)
or s.perp_margin_mode
or "cross"
).strip().lower()
return "isolated" if raw == "isolated" else "cross"
def unrealized(self) -> dict:
base = super().unrealized()
if not base.get("has_position"):
@@ -106,6 +120,17 @@ class BinanceLiveExecutor(Matcher):
ct_mult = self._ct_mult(option_inst_id)
opt_contracts = contracts_for_eth(opt_qty, 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,
)
try:
opt_fill = client.place_option_market(
symbol=option_inst_id,
@@ -129,14 +154,30 @@ class BinanceLiveExecutor(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()
try:
if perp_side == "long":
side, pos_side = "BUY", "LONG"
else:
side, pos_side = "SELL", "SHORT"
leverage = self.ledger.get_setting_float("leverage", s.leverage)
try:
client.set_margin_type(perp_inst, mgn)
except Exception as e_mgn:
logger.warning("binance set_margin_type failed: %s", e_mgn)
try:
client.set_leverage(perp_inst, leverage)
except Exception as e_lev:
@@ -149,6 +190,21 @@ class BinanceLiveExecutor(Matcher):
)
except Exception as e:
logger.exception("binance live open perp failed (likely margin); rollback option")
try:
live_perp = client.get_perp_pos_sz(
perp_inst,
position_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_option_market(
symbol=option_inst_id,
@@ -220,8 +276,8 @@ class BinanceLiveExecutor(Matcher):
"""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
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
exec_mode, perp_margin_mode
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
"open",
@@ -238,6 +294,7 @@ class BinanceLiveExecutor(Matcher):
of_fee + pf_fee,
0.0,
"LIVE",
mgn,
),
)
self.db._conn.execute(
@@ -505,6 +562,101 @@ class BinanceLiveExecutor(Matcher):
data={"group_id": group_id, "exec_mode": "LIVE", "exchange": "binance"},
)
def recover_opening(self) -> CloseResult:
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:
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:
perp_qty = float(perp_total)
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:
@@ -513,6 +665,8 @@ class BinanceLiveExecutor(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"):
@@ -579,7 +733,34 @@ class BinanceLiveExecutor(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:
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(
"binance option already flat on exchange; skip resell: %s", e
)
elif is_expiry and intrinsic is not None:
of = option_expiry_settle(
intrinsic=float(intrinsic), qty_eth=opt_qty, fee_rate=fee_rate
)
@@ -601,18 +782,32 @@ class BinanceLiveExecutor(Matcher):
else:
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:
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:
@@ -625,16 +820,25 @@ class BinanceLiveExecutor(Matcher):
perp_inst=perp_inst,
perp_side=perp_side,
perp_qty_eth=perp_qty,
allow_db_fallback=not pending_perp_only,
)
perp_live = client.place_perp_market(
symbol=perp_inst,
side=side,
qty_eth=perp_qty_close,
position_side=pos_side,
reduce_only=True,
)
pf_px = float(perp_live.avg_px)
pf_fee = float(perp_live.fee)
if perp_qty_close <= 0:
pf_px = float(pos.get("perp_entry_px") or 0) or 0.0
pf_fee = 0.0
logger.warning(
"binance perp already flat; finalize without order group=%s",
group_id,
)
else:
perp_live = client.place_perp_market(
symbol=perp_inst,
side=side,
qty_eth=perp_qty_close,
position_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,