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