Treat exchange as LIVE SoT: expiry closes perp only, no invented option settles.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-08 15:44:15 +08:00
parent 4fe15c41fa
commit 99e58910d3
8 changed files with 1010 additions and 303 deletions
+364 -131
View File
@@ -9,7 +9,7 @@ from ..config import get_settings
from ..env_store import live_ready
from ..sim.liquidity import contracts_for_eth, eth_from_contracts
from ..sim.matcher import CloseResult, Matcher, OpenResult
from ..sim.pricing import option_expiry_settle, option_intrinsic
from ..sim.pricing import option_intrinsic
from ..strategy.session import get_session
from .binance_trade import BinanceTradeClient
from .reconcile import (
@@ -626,10 +626,14 @@ class BinanceLiveExecutor(Matcher):
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))
opt_contracts = float(opt_sz) if opt_sz > 0 else float(
pos.get("option_qty_contracts") or 0
)
opt_qty = (
eth_from_contracts(opt_contracts, self._ct_mult(option_inst_id))
if opt_contracts > 0
else float(pos.get("option_qty_eth") or 0)
)
perp_qty = float(pos.get("perp_qty_eth") or 0) or float(
self.ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth)
)
@@ -916,24 +920,139 @@ class BinanceLiveExecutor(Matcher):
data={"hedge_mode": "option_option", "exec_mode": "LIVE"},
)
def live_sell_oo_both(self, *, bypass_liquidity: bool = False) -> None:
def live_sell_oo_both(
self, *, bypass_liquidity: bool = False, reason: str = ""
) -> None:
if str(reason or "") == "expiry":
logger.info("bn live_sell_oo_both: skip on expiry (exchange auto-settle)")
return
pos = self.current_position()
client = self._client()
for inst, contracts in (
for inst, _contracts in (
(str(pos.get("option_inst_id") or ""), float(pos.get("option_qty_contracts") or 0)),
(str(pos.get("option2_inst_id") or ""), float(pos.get("option2_qty_contracts") or 0)),
):
if not inst or contracts <= 0:
if not inst:
continue
ex_sz = exchange_option_abs_size(client, inst)
if ex_sz is None:
if not bypass_liquidity:
raise RuntimeError(f"期期卖腿查仓失败: {inst}")
continue
if ex_sz <= 1e-8:
continue
try:
client.place_option_market(
symbol=inst, side="SELL", quantity=contracts
symbol=inst,
side="SELL",
quantity=float(ex_sz),
reduce_only=True,
)
except Exception:
logger.exception("bn live_sell_oo_both failed inst=%s", inst)
if not bypass_liquidity:
raise
def close_oo_full(
self, *, reason: str = "expiry", bypass_liquidity: bool = False
) -> CloseResult:
"""期期 LIVE 全平:以交易所空仓为准;到期不卖期权。"""
err = self._guard_live()
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"):
return CloseResult(ok=False, detail="无期期持仓可平")
if not (
str(pos.get("hedge_mode") or "") == "option_option"
or pos.get("option2_inst_id")
):
return CloseResult(ok=False, detail="非期期持仓")
group_id = str(pos["group_id"])
client = self._client()
legs = [
("option", str(pos.get("option_inst_id") or ""), float(pos.get("option_qty_eth") or 0), float(pos.get("option_qty_contracts") or 0)),
("option2", str(pos.get("option2_inst_id") or ""), float(pos.get("option2_qty_eth") or 0), float(pos.get("option2_qty_contracts") or 0)),
]
if reason != "expiry":
try:
self.live_sell_oo_both(bypass_liquidity=bypass_liquidity, reason=reason)
except Exception as e:
return CloseResult(ok=False, detail=f"期期全平卖腿失败: {e}")
for _leg, inst, _qty, _c in legs:
if not inst:
continue
ex_sz = exchange_option_abs_size(client, inst)
if ex_sz is None:
return CloseResult(
ok=False, detail=f"期期全平无法核对交易所仓位: {inst}"
)
if ex_sz > 1e-8:
return CloseResult(
ok=False,
detail=(
f"期期全平等待交易所{'到期结算' if reason == 'expiry' else '成交'}"
f": {inst} 仍有 {ex_sz}"
),
)
now = int(time.time() * 1000)
for i, (leg, inst, qty, contracts) in enumerate(legs):
if not inst:
continue
with self.db._lock:
self.db._conn.execute(
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
leg,
"close",
"flat",
inst,
qty,
contracts,
0.0,
0.0,
0.0,
0.0,
0.0,
now + i,
"LIVE",
),
)
self.db._conn.commit()
with self.db._lock:
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
note=? WHERE group_id=?""",
(
"closed",
int(time.time() * 1000),
reason,
0.0,
f"oo full close {reason} exchange_flat_mirror",
group_id,
),
)
self.db._conn.execute(
"""UPDATE positions SET
group_id=NULL, perp_side=NULL, perp_qty_eth=0, perp_entry_px=NULL,
option_inst_id=NULL, option_side=NULL, option_qty_eth=0,
option_qty_contracts=0, option_entry_px=NULL, entry_index_px=NULL,
initial_premium=0, exit_target_usdt=NULL, status='flat',
hedge_mode=NULL, option2_inst_id=NULL, option2_side=NULL,
option2_qty_eth=0, option2_qty_contracts=0, option2_entry_px=NULL,
strike2=NULL, initial_premium2=NULL
WHERE id=1"""
)
self.db._conn.commit()
return CloseResult(
ok=True,
detail="oo_full_closed_live_bn",
data={"group_id": group_id, "reason": reason, "net": 0.0},
)
def close_winning_oo_leave_residual(
self, *, reason: str = "target_oo_win"
) -> CloseResult:
@@ -1003,7 +1122,6 @@ class BinanceLiveExecutor(Matcher):
perp_inst = resolve_perp_inst_id(self.db, group_id=group_id)
client = self._client()
is_expiry = reason == "expiry"
fee_rate = self._fee_rate()
pending_perp_only = st == "option_closed_perp_pending"
sess = get_session()
@@ -1020,27 +1138,79 @@ class BinanceLiveExecutor(Matcher):
of_fee = 0.0
of_slip = 0.0
of_notional = 0.0
option_apply_cash = True
perp_already_flat = False
if pending_perp_only:
# 期权已在上次成交并入账;只读上次平期权 fill
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 None:
if prev is None and not is_expiry:
return CloseResult(
ok=False,
detail="option_closed_perp_pending 缺期权平仓记录,请人工核对",
)
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 = 0.0 # LIVE 不计模拟滑点
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))
else:
of_px = float(intrinsic) if intrinsic is not None else 0.0
of_fee = 0.0
of_notional = of_px * opt_qty
self._ensure_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=0.0,
reason=reason,
apply_cash=False,
)
of_slip = 0.0
option_apply_cash = False
elif is_expiry:
of_px = float(intrinsic) if intrinsic is not None else 0.0
of_fee = 0.0
of_notional = of_px * opt_qty
of_slip = 0.0
option_apply_cash = False
logger.info(
"bn expiry: skip option order, close perp only group=%s", group_id
)
self._ensure_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,
apply_cash=False,
)
pending_perp_only = True
else:
# 含到期:优先交易所真实平期权;失败且无内在价值时可本地结算
ex_opt_pre = exchange_option_abs_size(client, option_inst_id)
if ex_opt_pre is not None and ex_opt_pre > 1e-8:
opt_contracts = float(ex_opt_pre)
opt_qty = eth_from_contracts(
opt_contracts, self._ct_mult(option_inst_id)
)
try:
if ex_opt_pre is not None and ex_opt_pre <= 1e-8:
raise RuntimeError("option already flat on exchange")
if opt_contracts <= 0:
return CloseResult(
ok=False, detail="币安平期权失败: 无有效张数"
)
opt_live = client.place_option_market(
symbol=option_inst_id,
side="SELL",
@@ -1049,10 +1219,31 @@ class BinanceLiveExecutor(Matcher):
)
of_px = float(opt_live.avg_px)
of_fee = float(opt_live.fee)
filled_c = float(opt_live.sz) if opt_live.sz and opt_live.sz > 0 else opt_contracts
opt_contracts = filled_c
opt_qty = eth_from_contracts(opt_contracts, self._ct_mult(option_inst_id))
of_notional = of_px * opt_qty
filled_c = (
float(opt_live.sz) if opt_live.sz and opt_live.sz > 0 else 0.0
)
if filled_c <= 1e-12:
ex_after = exchange_option_abs_size(client, option_inst_id)
if ex_after is None:
return CloseResult(
ok=False,
detail="币安平期权失败: 成交张数未知且无法核对仓位",
)
if ex_after > 1e-8:
return CloseResult(
ok=False,
detail=f"币安平期权失败: 未确认成交仍有仓 {ex_after}",
)
of_px = 0.0
of_fee = 0.0
of_notional = 0.0
option_apply_cash = False
else:
opt_contracts = filled_c
opt_qty = eth_from_contracts(
opt_contracts, self._ct_mult(option_inst_id)
)
of_notional = of_px * opt_qty
except Exception as e:
ex_opt = exchange_option_abs_size(client, option_inst_id)
if ex_opt is not None and ex_opt <= 1e-8:
@@ -1067,33 +1258,16 @@ class BinanceLiveExecutor(Matcher):
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
option_apply_cash = False
else:
of_px = float(pos.get("option_entry_px") or 0) or 0.0
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.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
)
of_px, of_fee, of_notional = (
of.fill_px,
of.fee,
of.notional,
)
of_slip = 0.0 # LIVE 不计模拟滑点
logger.warning(
"expiry option exchange close failed, local settle: %s", e
)
elif not bypass_liquidity:
return CloseResult(
ok=False,
@@ -1103,32 +1277,18 @@ class BinanceLiveExecutor(Matcher):
else:
return CloseResult(ok=False, detail=f"币安平期权失败: {e}")
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,),
self._ensure_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,
apply_cash=option_apply_cash,
)
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:
@@ -1141,7 +1301,7 @@ class BinanceLiveExecutor(Matcher):
perp_inst=perp_inst,
perp_side=perp_side,
perp_qty_eth=perp_qty,
allow_db_fallback=not pending_perp_only,
allow_db_fallback=False,
)
if perp_qty_close is None:
return CloseResult(
@@ -1149,8 +1309,9 @@ class BinanceLiveExecutor(Matcher):
detail="期权已平,永续待平(无法核对交易所仓位,禁止空仓 finalize)",
)
if perp_qty_close <= 0:
pf_px = float(pos.get("perp_entry_px") or 0) or 0.0
pf_px = 0.0
pf_fee = 0.0
perp_already_flat = True
logger.warning(
"binance perp already flat; finalize without order group=%s",
group_id,
@@ -1165,14 +1326,14 @@ class BinanceLiveExecutor(Matcher):
)
pf_px = float(perp_live.avg_px)
pf_fee = float(perp_live.fee)
perp_qty = float(perp_qty_close)
pos = {**pos, "perp_qty_eth": perp_qty}
except Exception as e:
return CloseResult(
ok=False,
detail=f"期权已平,永续待平(option_closed_perp_pending): {e}",
)
# 期权已在 _mark_option_closed_perp_pending 入账/写 fill(含到期本地结算),
# 此处 pending_perp_only 必为 True;勿再按 is_expiry 二次入账。
return self._finalize_dual_close(
pos=pos,
group_id=group_id,
@@ -1187,7 +1348,52 @@ class BinanceLiveExecutor(Matcher):
pf_fee=pf_fee,
reason=reason,
option_fill_already_written=bool(pending_perp_only),
skip_option_cash=bool(pending_perp_only),
skip_option_cash=True,
skip_perp_cash=bool(perp_already_flat),
skip_perp_fill=bool(perp_already_flat),
settle_index_px=spot,
)
def _ensure_option_closed_perp_pending(
self,
*,
group_id: str,
option_inst_id: str,
opt_qty: float,
opt_contracts: float,
of_px: float,
of_fee: float,
of_notional: float,
of_slip: float,
reason: str,
apply_cash: bool = True,
) -> None:
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()
return
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,
apply_cash=apply_cash,
)
def _mark_option_closed_perp_pending(
@@ -1202,14 +1408,16 @@ class BinanceLiveExecutor(Matcher):
of_notional: float,
of_slip: float,
reason: str,
apply_cash: bool = True,
) -> None:
self.ledger.apply_cash(
of_notional - of_fee,
kind="close_option",
group_id=group_id,
note=f"LIVE-BN close option pending perp {reason}",
allow_negative=True,
)
if apply_cash:
self.ledger.apply_cash(
of_notional - of_fee,
kind="close_option",
group_id=group_id,
note=f"LIVE-BN close option pending perp {reason}",
allow_negative=True,
)
now = int(time.time() * 1000)
with self.db._lock:
self.db._conn.execute(
@@ -1236,9 +1444,14 @@ class BinanceLiveExecutor(Matcher):
self.db._conn.execute(
"UPDATE positions SET status='option_closed_perp_pending' WHERE id=1"
)
note = (
f"option_closed_perp_pending:{reason}"
if apply_cash
else f"option_closed_perp_pending:{reason}:no_cash_exchange_sot"
)
self.db._conn.execute(
"UPDATE groups SET fees=COALESCE(fees,0)+?, note=? WHERE group_id=?",
(of_fee, f"option_closed_perp_pending:{reason}", group_id),
(of_fee if apply_cash else 0.0, note, group_id),
)
self.db._conn.commit()
@@ -1259,13 +1472,16 @@ class BinanceLiveExecutor(Matcher):
reason: str,
option_fill_already_written: bool,
skip_option_cash: bool,
skip_perp_cash: bool = False,
skip_perp_fill: bool = False,
settle_index_px: float | None = None,
) -> CloseResult:
s = live_settings()
perp_inst = resolve_perp_inst_id(self.db, group_id=group_id)
perp_side = str(pos["perp_side"])
perp_qty = float(pos["perp_qty_eth"])
opt_entry = float(pos["option_entry_px"])
perp_entry = float(pos["perp_entry_px"] or pf_px)
opt_entry = float(pos["option_entry_px"] or 0)
perp_entry = float(pos["perp_entry_px"] or pf_px or 0)
opt_pnl = (of_px - opt_entry) * opt_qty
if perp_side == "long":
perp_pnl = (pf_px - perp_entry) * perp_qty
@@ -1280,18 +1496,23 @@ class BinanceLiveExecutor(Matcher):
note=f"LIVE-BN close option {reason}",
allow_negative=True,
)
self.ledger.apply_cash(
perp_pnl - pf_fee,
kind="close_perp",
group_id=group_id,
note=f"LIVE-BN close perp {reason}",
allow_negative=True,
)
if not skip_perp_cash:
self.ledger.apply_cash(
perp_pnl - pf_fee,
kind="close_perp",
group_id=group_id,
note=f"LIVE-BN close perp {reason}",
allow_negative=True,
)
now = int(time.time() * 1000)
g = self.db.fetchone("SELECT * FROM groups WHERE group_id=?", (group_id,))
base_fees = float((g["fees"] if g else 0) or 0)
fees = base_fees + (0.0 if skip_option_cash else of_fee) + pf_fee
fees = (
base_fees
+ (0.0 if skip_option_cash else of_fee)
+ (0.0 if skip_perp_cash else pf_fee)
)
# LIVE:真实成交价已含盘口冲击,不另计/不计模拟滑点
of_slip = 0.0
slip = 0.0
@@ -1320,27 +1541,28 @@ class BinanceLiveExecutor(Matcher):
"LIVE",
),
)
self.db._conn.execute(
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
"perp",
"close",
"flat",
perp_inst,
perp_qty,
None,
pf_px,
pf_px,
pf_fee,
0.0,
pf_px * perp_qty,
now + 1,
"LIVE",
),
)
if not skip_perp_fill:
self.db._conn.execute(
"""INSERT INTO fills(group_id, leg, action, side, inst_id, qty_eth, qty_contracts,
base_px, fill_px, fee, slip, notional, ts_ms, exec_mode)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
group_id,
"perp",
"close",
"flat",
perp_inst,
perp_qty,
None,
pf_px,
pf_px,
pf_fee,
0.0,
pf_px * perp_qty,
now + 1,
"LIVE",
),
)
fills = self.db._conn.execute(
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
).fetchall()
@@ -1348,10 +1570,13 @@ class BinanceLiveExecutor(Matcher):
net = summary.get("net_pnl")
if net is None:
net = opt_pnl + perp_pnl - of_fee - pf_fee
close_index = (
float(settle_index_px) if settle_index_px is not None else None
)
self.db._conn.execute(
"""UPDATE groups SET status=?, close_at_ms=?, close_reason=?, realized_pnl=?,
fees=?, slip_cost=? WHERE group_id=?""",
("closed", now, reason, float(net), fees, slip, group_id),
fees=?, slip_cost=?, settle_index_px=COALESCE(?, settle_index_px) WHERE group_id=?""",
("closed", now, reason, float(net), fees, slip, close_index, group_id),
)
self.db._conn.execute(
"""UPDATE positions SET
@@ -1415,7 +1640,10 @@ class BinanceLiveExecutor(Matcher):
client = self._client()
ex_sz = exchange_option_abs_size(client, option_inst_id)
if ex_sz is None:
return row
logger.warning(
"residual %s: exchange size unknown, skip until query ok", group_id
)
return None
ct = self._ct_mult(option_inst_id)
local_c = float(row.get("option_qty_contracts") or 0)
if local_c <= 0:
@@ -1443,7 +1671,7 @@ class BinanceLiveExecutor(Matcher):
booked is not None,
)
return None
if local_c > ex_sz + 1e-8:
if abs(local_c - ex_sz) > 1e-8:
rem_eth = eth_from_contracts(float(ex_sz), ct)
init = float(row.get("initial_premium") or 0)
local_eth = float(row.get("option_qty_eth") or 0)
@@ -1536,11 +1764,13 @@ class BinanceLiveExecutor(Matcher):
if filled_c <= 1e-12:
return None
ex_left = exchange_option_abs_size(client, option_inst_id)
remaining = (
max(0.0, float(ex_left))
if ex_left is not None
else max(0.0, opt_contracts - filled_c)
)
if ex_left is None:
logger.warning(
"residual close %s: filled but remaining size unknown; leave pending",
row.get("group_id"),
)
return None
remaining = max(0.0, float(ex_left))
fill_eth = eth_from_contracts(filled_c, self._ct_mult(option_inst_id))
now_ms = int(time.time() * 1000)
tag = "manual" if skip_premium_ratio else "mid"
@@ -1571,7 +1801,12 @@ class BinanceLiveExecutor(Matcher):
option_inst_id = str(row.get("option_inst_id") or "")
client = self._client()
ex_sz = exchange_option_abs_size(client, option_inst_id)
if ex_sz is not None and ex_sz <= 1e-8:
if ex_sz is None:
logger.warning(
"residual flatten %s: exchange size unknown", row.get("group_id")
)
return None
if ex_sz <= 1e-8:
return {
"fill_px": 0.0,
"fee": 0.0,
@@ -1583,17 +1818,14 @@ class BinanceLiveExecutor(Matcher):
"exec_mode": "LIVE",
"close_reason": "emergency" if force else "expiry",
}
opt_contracts = float(row.get("option_qty_contracts") or 0)
if ex_sz is not None and ex_sz > 0:
opt_contracts = float(ex_sz)
if opt_contracts <= 0:
opt_contracts = float(
contracts_for_eth(
float(row.get("option_qty_eth") or 0),
self._ct_mult(option_inst_id),
)
or 0
if not force:
logger.info(
"residual expiry %s: exchange still holds %.4f, wait auto-settle",
row.get("group_id"),
ex_sz,
)
return None
opt_contracts = float(ex_sz)
if opt_contracts <= 0:
return None
oq = self._quote_held_option(option_inst_id)
@@ -1726,6 +1958,7 @@ class BinanceLiveExecutor(Matcher):
perp_inst=perp_inst,
perp_side=perp_side,
perp_qty_eth=perp_qty,
allow_db_fallback=False,
)
if perp_qty_close is None or perp_qty_close <= 0:
return CloseResult(