修复期权币本位高危缺陷:开仓估量、卖回封顶、握库超时与条件单嵌套锁。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-07 12:00:08 +08:00
parent ea3e029eb9
commit 77fe0fa679
16 changed files with 724 additions and 201 deletions
+100 -19
View File
@@ -276,12 +276,23 @@ def spot_market_sell_coin_to_usdt(
underlying: str,
coin_amount: float | None = None,
) -> dict[str, Any]:
"""交易账户:市价卖出标的币换 USDT.始终卖光交易户可用余额(coin_amount 仅兼容旧调用,不参与定量)."""
"""交易账户:市价卖出标的币换 USDT.
coin_amount 有值时按 min(可用, coin_amount) 卖,避免误卖无关余额;
未传时卖光该币可用(空仓扫尾).
"""
ccy = (underlying or "ETH").upper()
avail = fetch_trading_coin_available(ex, ccy)
if avail is None or float(avail) <= 0:
return {"ok": False, "msg": f"交易账户无可用 {ccy}"}
amt = float(avail)
if coin_amount is not None:
try:
lim = float(coin_amount)
except (TypeError, ValueError):
lim = 0.0
if lim > 0:
amt = min(amt, lim)
if amt <= 0:
return {"ok": False, "msg": f"{ccy} 数量须大于 0"}
min_sz = spot_min_sell_sz(ex, ccy)
@@ -334,14 +345,29 @@ def rollback_bought_coin_to_usdt(
reason: str = "",
coin_amount: float | None = None,
) -> dict[str, Any]:
"""买币后开期权失败:卖回 USDT 并关闭桥.卖光交易户可用标的币."""
_ = coin_amount # 兼容旧签名;定量以交易户可用为准
"""买币后开期权失败:按本桥买入量封顶卖回 USDT 并关闭桥."""
ensure_bridge_table(conn)
cap = coin_amount
if cap is None:
try:
row = conn.execute(
"SELECT coin_bought FROM options_spot_bridge WHERE id=?",
(int(bridge_id),),
).fetchone()
if row is not None:
cap = _safe_float(row["coin_bought"] if hasattr(row, "keys") else row[0])
except Exception:
cap = None
# 卖币(模拟盘)会另开连接写钱包,须先释放本连接事务锁
try:
conn.commit()
except Exception:
pass
sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying)
sell = spot_market_sell_coin_to_usdt(
ex,
underlying=underlying,
coin_amount=float(cap) if cap is not None and float(cap) > 0 else None,
)
if not sell.get("ok"):
update_bridge(
conn,
@@ -376,16 +402,34 @@ def sell_residual_after_option_flat(
"""
get_db = conn_or_get_db if callable(conn_or_get_db) else None
bid: int | None = None
sell_cap: float | None = None
want_inst = str(inst_id or "").strip()
def _pick_bridge(bridges: list[dict[str, Any]]) -> dict[str, Any] | None:
uly_u = (underlying or "").upper()
matched: list[dict[str, Any]] = []
for b in bridges:
if str(b.get("status")) not in (BRIDGE_HOLDING, BRIDGE_PENDING_SELL, BRIDGE_BOUGHT):
continue
if uly_u and str(b.get("underlying") or "").upper() != uly_u:
continue
matched.append(b)
if not matched:
return None
if want_inst:
for b in matched:
if str(b.get("inst_id") or "").strip() == want_inst:
return b
return matched[0]
if get_db is not None:
conn = get_db()
try:
ensure_bridge_table(conn)
bridges = list_open_bridges(conn)
for b in bridges:
if str(b.get("status")) in (BRIDGE_HOLDING, BRIDGE_PENDING_SELL, BRIDGE_BOUGHT):
if not underlying or str(b.get("underlying") or "").upper() == underlying.upper():
bid = int(b["id"])
break
target = _pick_bridge(list_open_bridges(conn))
if target is not None:
bid = int(target["id"])
sell_cap = _safe_float(target.get("coin_bought"))
try:
conn.commit()
except Exception:
@@ -398,12 +442,10 @@ def sell_residual_after_option_flat(
else:
conn = conn_or_get_db
ensure_bridge_table(conn)
bridges = list_open_bridges(conn)
for b in bridges:
if str(b.get("status")) in (BRIDGE_HOLDING, BRIDGE_PENDING_SELL, BRIDGE_BOUGHT):
if not underlying or str(b.get("underlying") or "").upper() == underlying.upper():
bid = int(b["id"])
break
target = _pick_bridge(list_open_bridges(conn))
if target is not None:
bid = int(target["id"])
sell_cap = _safe_float(target.get("coin_bought"))
try:
conn.commit()
except Exception:
@@ -414,7 +456,10 @@ def sell_residual_after_option_flat(
avail_chk = fetch_trading_coin_available(ex, underlying)
except Exception:
avail_chk = None
if avail_chk is not None and not is_sellable_spot_coin(ex, underlying, avail_chk):
dust_amt = avail_chk
if dust_amt is None and sell_cap is not None:
dust_amt = sell_cap
if dust_amt is not None and not is_sellable_spot_coin(ex, underlying, dust_amt):
dust_msg = "粉尘低于现货最小可卖,已忽略"
if bid is None:
return {"ok": True, "msg": dust_msg, "skipped": True, "dust": True}
@@ -446,8 +491,13 @@ def sell_residual_after_option_flat(
pass
return _close_dust(conn_or_get_db)
# 有桥时按 coin_bought 封顶,避免误卖无关余额;无桥空仓扫尾仍卖光可用
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying)
sell = spot_market_sell_coin_to_usdt(
ex,
underlying=underlying,
coin_amount=sell_cap if (bid is not None and sell_cap is not None and sell_cap > 0) else None,
)
if bid is None:
if not sell.get("ok"):
msg = str(sell.get("msg") or "")
@@ -527,12 +577,34 @@ def clear_dust_pending_sell_bridges(
get_db = conn_or_get_db if callable(conn_or_get_db) else None
def _pending(c: sqlite3.Connection) -> list[dict[str, Any]]:
# pending_sell 始终可清粉尘;holding 仅在无对应标的期权仓时清(避免误关持仓中桥)
return [
b
for b in list_open_bridges(c)
if str(b.get("status") or "") == BRIDGE_PENDING_SELL
if str(b.get("status") or "") in (BRIDGE_PENDING_SELL, BRIDGE_HOLDING)
]
live_uly: set[str] = set()
positions_ok = False
try:
from lib.exchange.okx_options_lib import fetch_option_positions
raw_pos = fetch_option_positions(ex)
if raw_pos is not None:
positions_ok = True
for p in raw_pos or []:
try:
if abs(float(p.get("pos") or 0)) <= 1e-12:
continue
except (TypeError, ValueError):
continue
inst = str(p.get("instId") or p.get("inst_id") or "").strip()
if inst:
live_uly.add(inst.split("-")[0].upper())
except Exception:
positions_ok = False
live_uly = set()
if get_db is not None:
conn = get_db()
try:
@@ -556,12 +628,21 @@ def clear_dust_pending_sell_bridges(
to_close: list[dict[str, Any]] = []
for b in pending:
st = str(b.get("status") or "")
uly = str(b.get("underlying") or "ETH").upper() or "ETH"
# holding 仅在「确认无该标的期权仓」时才可按粉尘关闭;持仓查询失败绝不关 holding
if st == BRIDGE_HOLDING and (not positions_ok or uly in live_uly):
continue
try:
avail = fetch_trading_coin_available(ex, uly)
except Exception:
continue
# 余额查不到时:用桥上记录的买入量判断粉尘,避免永久拦开仓
if avail is None:
cb = _safe_float(b.get("coin_bought"))
if cb is None or is_sellable_spot_coin(ex, uly, cb):
continue
to_close.append(b)
continue
if is_sellable_spot_coin(ex, uly, avail):
continue