修复期权币本位高危缺陷:开仓估量、卖回封顶、握库超时与条件单嵌套锁。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user