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