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

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
+147 -23
View File
@@ -27,6 +27,43 @@ from lib.options.options_spot_bridge_lib import (
)
def _sell_excess_bridge_coin(
cfg: dict[str, Any],
ex: Any,
conn: Any,
*,
bridge_id: int,
underlying: str,
keep_coin: float,
coin_bought: float,
) -> dict[str, Any] | None:
"""开仓后卖掉多买的币:卖量不超过 bridge 买入量,并保留权利金所需 keep_coin."""
try:
keep = max(0.0, float(keep_coin))
except (TypeError, ValueError):
keep = 0.0
try:
bought = max(0.0, float(coin_bought))
except (TypeError, ValueError):
bought = 0.0
excess_cap = max(0.0, bought - keep)
if excess_cap <= 0:
return None
try:
conn.commit()
except Exception:
pass
avail = _spot_bridge.fetch_trading_coin_available(ex, underlying)
if avail is None:
return None
sell_amt = min(float(avail), excess_cap)
if not _spot_bridge.is_sellable_spot_coin(ex, underlying, sell_amt):
return None
return _spot_bridge.spot_market_sell_coin_to_usdt(
ex, underlying=underlying, coin_amount=sell_amt
)
def coin_budget_preview(cfg: dict[str, Any], ex: Any) -> dict[str, Any]:
from lib.exchange.okx_options_lib import fetch_options_balances
@@ -69,19 +106,33 @@ def open_coin_option_buy_full(
parts = inst_id.split("-")
underlying = (parts[0] if parts else "ETH").upper()
# 先清粉尘 pending 桥(自管短连接),再开业务连接,避免模拟盘读余额嵌套锁库
# 先清粉尘桥 + 空仓扫尾卖回(短连接),再开业务连接
try:
_spot_bridge.clear_dust_pending_sell_bridges(cfg["get_db"], ex)
except Exception:
pass
try:
sweep_spot_after_options_flat(cfg, ex)
except Exception:
pass
conn: Any = None
conn = cfg["get_db"]()
try:
init_options_tables(conn)
try:
conn.commit()
except Exception:
pass
block = _spot_bridge.bridge_blocks_new_open_msg(conn)
if block:
return {"ok": False, "msg": block, "can_open": False}
# 网络检查前再提交,避免握锁拉持仓/行情
try:
conn.commit()
except Exception:
pass
compound_block = compound_full_single_position_block_msg(
ex, fetch_positions=cfg.get("fetch_option_positions")
)
@@ -198,14 +249,45 @@ def open_coin_option_buy_full(
}
coin_bought = max(0.0, float(coin_after) - float(coin_before or 0))
if coin_bought <= 0:
# 落账延迟时退化为用当前可用,但仍写入上限提示
coin_bought = float(coin_after)
if coin_bought <= 0:
rb = _spot_bridge.rollback_bought_coin_to_usdt(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买入量无效"
)
return {"ok": False, "msg": "买币后可用增量无效", "rollback": rb, "budget": budget_info}
# 落账延迟:短重试取增量;绝不把整户余额当成「本次买入」
for _ in range(3):
time.sleep(0.6)
try:
from lib.exchange.okx_options_lib import invalidate_options_balance_cache
invalidate_options_balance_cache()
except Exception:
pass
again = _spot_bridge.fetch_trading_coin_available(ex, underlying)
if again is None:
continue
coin_bought = max(0.0, float(again) - float(coin_before or 0))
if coin_bought > 0:
coin_after = again
break
if coin_bought <= 0:
# 订单回执有数量则用之,否则回滚
try:
hinted = float(buy.get("coin_bought") or 0)
except (TypeError, ValueError):
hinted = 0.0
if hinted > 0:
coin_bought = hinted
else:
rb = _spot_bridge.rollback_bought_coin_to_usdt(
conn, ex, bridge_id=bridge_id, underlying=underlying, reason="买入量无效"
)
return {
"ok": False,
"msg": "买币后可用增量无效",
"rollback": rb,
"budget": budget_info,
}
_spot_bridge.update_bridge(conn, bridge_id, coin_bought=float(coin_bought))
try:
conn.commit()
except Exception:
pass
sizing = calc_sheets_from_coin_balance(
quote_per_unit=float(ask),
@@ -303,6 +385,17 @@ def open_coin_option_buy_full(
fill_timeout = max(2.0, float(os.getenv("OKX_OPTIONS_OPEN_FILL_TIMEOUT_SEC") or "12"))
except (TypeError, ValueError):
fill_timeout = 12.0
# 等待成交可能十余秒:先提交并释放业务连接,避免锁死中控/卖币
try:
conn.commit()
except Exception:
pass
try:
conn.close()
except Exception:
pass
conn = None
fill = wait_option_order_full_fill(
ex,
inst_id=inst_id,
@@ -311,6 +404,9 @@ def open_coin_option_buy_full(
timeout_sec=fill_timeout,
cancel_on_timeout=True,
)
conn = cfg["get_db"]()
init_options_tables(conn)
if not fill.get("ok"):
filled_n = int(fill.get("filled_sheets") or 0)
if filled_n <= 0:
@@ -328,11 +424,27 @@ def open_coin_option_buy_full(
premium_paid = round(float(ask) * eth_amount, 8)
premium_ccy = premium_ccy_for_mode("coin", underlying)
# 部分成交/减半张数后卖掉多余买币(按 bridge coin_bought 封顶)
need_coin = float(premium_paid)
try:
_sell_excess_bridge_coin(
cfg,
ex,
conn,
bridge_id=bridge_id,
underlying=underlying,
keep_coin=need_coin,
coin_bought=float(coin_bought),
)
except Exception:
pass
_spot_bridge.update_bridge(
conn,
bridge_id,
status=BRIDGE_HOLDING,
inst_id=inst_id,
coin_bought=float(need_coin),
message="期权持仓中",
)
@@ -408,10 +520,11 @@ def open_coin_option_buy_full(
"fill": fill,
}
finally:
try:
conn.close()
except Exception:
pass
if conn is not None:
try:
conn.close()
except Exception:
pass
def _insert_coin_trade(conn: Any, **kwargs: Any) -> int:
@@ -502,19 +615,30 @@ def sweep_spot_after_options_flat(
return {"ok": False, "msg": "exchange 未就绪"}
live_uly: set[str] = set()
try:
rows = cfg["fetch_option_positions"](exchange) or []
for p in rows:
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 not inst:
continue
live_uly.add(inst.split("-")[0].upper())
rows = cfg["fetch_option_positions"](exchange)
except Exception as e:
# 持仓查询失败:只清 pending 粉尘桥,绝不卖币
try:
_spot_bridge.clear_dust_pending_sell_bridges(cfg["get_db"], exchange)
except Exception:
pass
return {"ok": False, "msg": f"持仓查询失败: {e}"}
if rows is None:
try:
_spot_bridge.clear_dust_pending_sell_bridges(cfg["get_db"], exchange)
except Exception:
pass
return {"ok": False, "msg": "持仓查询失败: 空响应"}
for p in rows 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 not inst:
continue
live_uly.add(inst.split("-")[0].upper())
# 先关掉只剩粉尘的 pending 桥,避免拦开仓又看不到重试按钮
try: