Fix LIVE SoT P0/P1: closing state machine, OO exchange fills, BN balances.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 15:54:45 +08:00
parent 99e58910d3
commit 8d67f3fc6c
10 changed files with 864 additions and 167 deletions
+187 -57
View File
@@ -528,29 +528,52 @@ class BinanceLiveExecutor(Matcher):
option_inst_id = str(pos.get("option_inst_id") or "")
opt_contracts = float(pos.get("option_qty_contracts") or 0)
opt_qty = float(pos.get("option_qty_eth") or 0)
if not option_inst_id or opt_contracts <= 0:
if not option_inst_id:
return CloseResult(ok=False, detail="half_open 缺期权合约信息")
client = self._client()
try:
opt_live = client.place_option_market(
symbol=option_inst_id,
side="SELL",
quantity=opt_contracts,
reduce_only=True,
ex_sz = exchange_option_abs_size(client, option_inst_id)
if ex_sz is None:
return CloseResult(ok=False, detail="half_open:无法核对交易所期权仓位")
if ex_sz <= 1e-8:
of_px, of_fee, of_notional = 0.0, 0.0, 0.0
opt_contracts = 0.0
opt_qty = 0.0
opt_entry = float(pos.get("option_entry_px") or 0)
else:
opt_contracts = float(ex_sz)
opt_qty = eth_from_contracts(opt_contracts, self._ct_mult(option_inst_id))
try:
opt_live = client.place_option_market(
symbol=option_inst_id,
side="SELL",
quantity=opt_contracts,
reduce_only=True,
)
except Exception as e:
return CloseResult(ok=False, detail=f"half_open 平期权失败: {e}")
of_px = float(opt_live.avg_px)
of_fee = float(opt_live.fee)
filled = float(opt_live.sz) if opt_live.sz and float(opt_live.sz) > 0 else 0.0
if filled > 0:
opt_contracts = filled
opt_qty = eth_from_contracts(
opt_contracts, self._ct_mult(option_inst_id)
)
of_notional = of_px * opt_qty
ex_left = exchange_option_abs_size(client, option_inst_id)
if ex_left is None or ex_left > 1e-8:
return CloseResult(
ok=False,
detail=f"half_open:卖后仍有仓或无法核对 left={ex_left}",
)
opt_entry = float(pos.get("option_entry_px") or of_px)
self.ledger.apply_cash(
of_notional - of_fee,
kind="close_option",
group_id=group_id or None,
note="LIVE-BN repair half_open",
allow_negative=True,
)
except Exception as e:
return CloseResult(ok=False, detail=f"half_open 平期权失败: {e}")
of_px = float(opt_live.avg_px)
of_fee = float(opt_live.fee)
of_notional = of_px * opt_qty
opt_entry = float(pos.get("option_entry_px") or of_px)
self.ledger.apply_cash(
of_notional - of_fee,
kind="close_option",
group_id=group_id or None,
note="LIVE-BN repair half_open",
allow_negative=True,
)
now = int(time.time() * 1000)
with self.db._lock:
if group_id:
@@ -961,7 +984,8 @@ class BinanceLiveExecutor(Matcher):
if err:
return CloseResult(ok=False, detail=err)
pos = self.current_position()
if str(pos.get("status") or "") != "open" or not pos.get("group_id"):
st = str(pos.get("status") or "")
if st not in ("open", "closing") or not pos.get("group_id"):
return CloseResult(ok=False, detail="无期期持仓可平")
if not (
str(pos.get("hedge_mode") or "") == "option_option"
@@ -1022,6 +1046,13 @@ class BinanceLiveExecutor(Matcher):
),
)
self.db._conn.commit()
from ..sim.pnl import summarize_fills_pnl
fill_rows = self.db.fetchall(
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
)
summary = summarize_fills_pnl(list(fill_rows))
net = float(summary.get("net_pnl") or 0.0)
with self.db._lock:
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
@@ -1030,7 +1061,7 @@ class BinanceLiveExecutor(Matcher):
"closed",
int(time.time() * 1000),
reason,
0.0,
net,
f"oo full close {reason} exchange_flat_mirror",
group_id,
),
@@ -1050,7 +1081,7 @@ class BinanceLiveExecutor(Matcher):
return CloseResult(
ok=True,
detail="oo_full_closed_live_bn",
data={"group_id": group_id, "reason": reason, "net": 0.0},
data={"group_id": group_id, "reason": reason, "net": net},
)
def close_winning_oo_leave_residual(
@@ -1060,31 +1091,46 @@ class BinanceLiveExecutor(Matcher):
if err:
return CloseResult(ok=False, detail=err)
pos = self.current_position()
if str(pos.get("status") or "") == "closing":
return super().close_winning_oo_leave_residual(
reason=reason, skip_market=True
)
if str(pos.get("status") or "") != "open" or not pos.get("option2_inst_id"):
st = str(pos.get("status") or "")
if st == "closing":
return self._finish_oo_win_after_exchange(reason=reason, pos=pos)
if st != "open" or not pos.get("option2_inst_id"):
return CloseResult(ok=False, detail="无期期持仓")
upl = self.unrealized()
call_upl = float(upl.get("option_upl") or 0)
put_upl = float(upl.get("option2_upl") or 0)
if call_upl >= put_upl and call_upl > 0:
win_leg = "option"
win_id = str(pos["option_inst_id"])
win_contracts = float(pos.get("option_qty_contracts") or 0)
elif put_upl > 0:
win_leg = "option2"
win_id = str(pos["option2_inst_id"])
win_contracts = float(pos.get("option2_qty_contracts") or 0)
else:
return CloseResult(ok=False, detail="无明确盈利腿")
client = self._client()
ex_sz = exchange_option_abs_size(client, win_id)
if ex_sz is None:
return CloseResult(ok=False, detail="期期平盈利腿:无法核对交易所仓位")
if ex_sz <= 1e-8:
return self._finish_oo_win_after_exchange(
reason=reason,
pos=pos,
win_leg=win_leg,
fill_px=0.0,
fill_fee=0.0,
fill_c=0.0,
)
with self.db._lock:
self.db._conn.execute(
"UPDATE positions SET status='closing' WHERE id=1 AND status='open'"
)
self.db._conn.commit()
try:
self._client().place_option_market(
symbol=win_id, side="SELL", quantity=win_contracts
live = client.place_option_market(
symbol=win_id,
side="SELL",
quantity=float(ex_sz),
reduce_only=True,
)
except Exception as e:
with self.db._lock:
@@ -1093,8 +1139,62 @@ class BinanceLiveExecutor(Matcher):
)
self.db._conn.commit()
return CloseResult(ok=False, detail=f"期期平盈利腿失败: {e}")
fill_c = float(live.sz) if live.sz and float(live.sz) > 0 else 0.0
return self._finish_oo_win_after_exchange(
reason=reason,
pos=self.current_position(),
win_leg=win_leg,
fill_px=float(live.avg_px),
fill_fee=float(live.fee),
fill_c=fill_c,
)
def _finish_oo_win_after_exchange(
self,
*,
reason: str,
pos: dict,
win_leg: str | None = None,
fill_px: float | None = None,
fill_fee: float | None = None,
fill_c: float | None = None,
) -> CloseResult:
client = self._client()
call_id = str(pos.get("option_inst_id") or "")
put_id = str(pos.get("option2_inst_id") or "")
if not win_leg:
c_sz = exchange_option_abs_size(client, call_id) if call_id else None
p_sz = exchange_option_abs_size(client, put_id) if put_id else None
if c_sz is None or p_sz is None:
return CloseResult(
ok=False, detail="closing 收尾:无法核对交易所两腿仓位"
)
if c_sz <= 1e-8 and p_sz > 1e-8:
win_leg = "option"
elif p_sz <= 1e-8 and c_sz > 1e-8:
win_leg = "option2"
elif c_sz <= 1e-8 and p_sz <= 1e-8:
return self.close_oo_full(reason=reason, bypass_liquidity=True)
else:
return CloseResult(
ok=False, detail="closing 收尾:盈利腿仍在交易所,请重试卖出"
)
win_id = call_id if win_leg == "option" else put_id
ex_win = exchange_option_abs_size(client, win_id)
if ex_win is None:
return CloseResult(ok=False, detail="closing 收尾:无法核对盈利腿仓位")
if ex_win > 1e-8:
return CloseResult(
ok=False,
detail=f"closing 收尾:盈利腿仍有仓 {ex_win},禁止本地清仓",
)
return super().close_winning_oo_leave_residual(
reason=reason, skip_market=True
reason=reason,
skip_market=True,
live_fill_px=0.0 if fill_px is None else float(fill_px),
live_fill_fee=0.0 if fill_fee is None else float(fill_fee),
live_fill_contracts=fill_c,
live_win_leg=win_leg,
)
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
@@ -1109,6 +1209,18 @@ class BinanceLiveExecutor(Matcher):
return self.recover_opening()
if st == "half_open":
return self.repair_half_open()
if st == "closing":
if pos.get("option2_inst_id") or str(pos.get("hedge_mode") or "") == "option_option":
return self.close_winning_oo_leave_residual(
reason=reason or "closing_retry"
)
return CloseResult(ok=False, detail="closing 非期期状态,请人工核对")
is_oo = (
str(pos.get("hedge_mode") or "") == "option_option"
or bool(pos.get("option2_inst_id"))
)
if is_oo and st == "open":
return self.close_oo_full(reason=reason, bypass_liquidity=bypass_liquidity)
if st not in ("open", "option_closed_perp_pending") or not pos.get("group_id"):
return CloseResult(ok=False, detail="无持仓可平")
@@ -1158,9 +1270,9 @@ class BinanceLiveExecutor(Matcher):
of_fee = float(prev["fee"] or 0)
of_notional = float(prev["notional"] or (of_px * opt_qty))
else:
of_px = float(intrinsic) if intrinsic is not None else 0.0
of_px = 0.0
of_fee = 0.0
of_notional = of_px * opt_qty
of_notional = 0.0
self._ensure_option_closed_perp_pending(
group_id=group_id,
option_inst_id=option_inst_id,
@@ -1176,9 +1288,17 @@ class BinanceLiveExecutor(Matcher):
of_slip = 0.0
option_apply_cash = False
elif is_expiry:
of_px = float(intrinsic) if intrinsic is not None else 0.0
ex_opt = exchange_option_abs_size(client, option_inst_id)
if ex_opt is not None and ex_opt > 1e-8:
logger.warning(
"bn expiry: option still on exchange sz=%.4f group=%s; "
"skip option, close perp only",
ex_opt,
group_id,
)
of_px = 0.0
of_fee = 0.0
of_notional = of_px * opt_qty
of_notional = 0.0
of_slip = 0.0
option_apply_cash = False
logger.info(
@@ -1960,35 +2080,45 @@ class BinanceLiveExecutor(Matcher):
perp_qty_eth=perp_qty,
allow_db_fallback=False,
)
if perp_qty_close is None or perp_qty_close <= 0:
if perp_qty_close is None:
return CloseResult(
ok=False,
detail="币安平永续失败: 无法取得有效永续仓位数量",
detail="币安平永续失败: 无法核对交易所永续仓位",
)
if perp_qty_close > 0:
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)
perp_qty = float(perp_qty_close)
else:
pf_px = 0.0
pf_fee = 0.0
logger.warning(
"bn abandon: perp already flat; archive option residual group=%s",
group_id,
)
perp_live = client.place_perp_market(
symbol=perp_inst,
side=side,
qty_eth=perp_qty_close,
position_side=pos_side,
reduce_only=True,
)
except Exception as e:
return CloseResult(ok=False, detail=f"币安平永续失败: {e}")
pf_px = float(perp_live.avg_px)
pf_fee = float(perp_live.fee)
if perp_side == "long":
perp_pnl = (pf_px - perp_entry) * perp_qty
perp_pnl = (pf_px - perp_entry) * perp_qty if pf_px else 0.0
else:
perp_pnl = (perp_entry - pf_px) * perp_qty
perp_pnl = (perp_entry - pf_px) * perp_qty if pf_px else 0.0
self.ledger.apply_cash(
perp_pnl - pf_fee,
kind="close_perp",
group_id=group_id,
note=f"LIVE-BN close perp abandon option {reason}",
allow_negative=True,
)
if abs(perp_pnl) + abs(pf_fee) > 1e-12:
self.ledger.apply_cash(
perp_pnl - pf_fee,
kind="close_perp",
group_id=group_id,
note=f"LIVE-BN close perp abandon option {reason}",
allow_negative=True,
)
strike = self._group_strike(group_id, option_inst_id)
g = self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (group_id,))