修复模拟盘卖币握锁导致 database is locked 与中控超时离线。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-07 11:04:06 +08:00
parent 066c90fd96
commit 80ef5ceccb
7 changed files with 173 additions and 126 deletions
+10 -4
View File
@@ -1611,6 +1611,9 @@ def get_db():
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
try: try:
conn.execute("PRAGMA busy_timeout=30000") conn.execute("PRAGMA busy_timeout=30000")
# WAL:读(中控快照)与写(卖币/同步)可并行,避免 database is locked→全部超时离线
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA synchronous=NORMAL")
except Exception: except Exception:
pass pass
return conn return conn
@@ -5119,11 +5122,14 @@ def background_task():
while True: while True:
try: try:
auto_transfer_once_per_day() auto_transfer_once_per_day()
conn = get_db() # force_close 自带连接,勿套在另一未关闭连接上
force_close_before_reset() force_close_before_reset()
reconcile_external_closes(conn) conn = get_db()
conn.commit() try:
conn.close() reconcile_external_closes(conn)
conn.commit()
finally:
conn.close()
check_key_monitors() check_key_monitors()
check_order_monitors() check_order_monitors()
except Exception as e: except Exception as e:
+10 -25
View File
@@ -464,17 +464,12 @@ def maybe_sell_spot_after_close(
pass pass
parts = inst_id.split("-") parts = inst_id.split("-")
underlying = (parts[0] if parts else "ETH").upper() underlying = (parts[0] if parts else "ETH").upper()
conn = cfg["get_db"]() return _spot_bridge.sell_residual_after_option_flat(
try: cfg["get_db"],
from lib.options.options_db import init_options_tables ex,
underlying=underlying,
init_options_tables(conn) inst_id=inst_id,
return _spot_bridge.sell_residual_after_option_flat(conn, ex, underlying=underlying, inst_id=inst_id) )
finally:
try:
conn.close()
except Exception:
pass
def sweep_spot_after_options_flat( def sweep_spot_after_options_flat(
@@ -506,8 +501,6 @@ def sweep_spot_after_options_flat(
except Exception as e: except Exception as e:
return {"ok": False, "msg": f"持仓查询失败: {e}"} return {"ok": False, "msg": f"持仓查询失败: {e}"}
from lib.options.options_db import init_options_tables
sold: list[dict[str, Any]] = [] sold: list[dict[str, Any]] = []
for uly in ("BTC", "ETH"): for uly in ("BTC", "ETH"):
if uly in live_uly: if uly in live_uly:
@@ -518,22 +511,14 @@ def sweep_spot_after_options_flat(
continue continue
except Exception: except Exception:
continue continue
conn = cfg["get_db"]()
try: try:
init_options_tables(conn)
out = _spot_bridge.sell_residual_after_option_flat( out = _spot_bridge.sell_residual_after_option_flat(
conn, exchange, underlying=uly, inst_id=None cfg["get_db"],
exchange,
underlying=uly,
inst_id=None,
) )
sold.append({"underlying": uly, **(out or {})}) sold.append({"underlying": uly, **(out or {})})
try:
conn.commit()
except Exception:
pass
except Exception as e: except Exception as e:
sold.append({"underlying": uly, "ok": False, "msg": str(e)}) sold.append({"underlying": uly, "ok": False, "msg": str(e)})
finally:
try:
conn.close()
except Exception:
pass
return {"ok": True, "results": sold, "skipped": not sold} return {"ok": True, "results": sold, "skipped": not sold}
+6 -7
View File
@@ -573,16 +573,15 @@ def options_monitor_loop(
cfg=pe_cfg, cfg=pe_cfg,
ex=pe_cfg.get("exchange_options"), ex=pe_cfg.get("exchange_options"),
) )
if sync_trades_fn is not None:
try:
conn.commit()
except Exception:
pass
sync_trades_fn(conn)
conn.commit() conn.commit()
finally: finally:
conn.close() conn.close()
# 币本位扫尾卖回:必须在监控连接关闭之后 # 同步/卖币必须在监控连接关闭之后
if sync_trades_fn is not None:
try:
sync_trades_fn(None)
except Exception:
pass
try: try:
pe_sell = dict(profit_exit_cfg or {}) pe_sell = dict(profit_exit_cfg or {})
pe_sell.setdefault("send_wechat", send_wechat) pe_sell.setdefault("send_wechat", send_wechat)
+6
View File
@@ -336,6 +336,12 @@ def notify_options_close(
trade_id=head.get("id") if len(rows) == 1 else None, trade_id=head.get("id") if len(rows) == 1 else None,
premium_ccy=ccy, premium_ccy=ccy,
) )
# 推送企业微信前先提交,避免握锁等待 webhook(可达数秒)导致卖币/快照 database is locked
if conn is not None:
try:
conn.commit()
except Exception:
pass
ok = notify_options_send(cfg, msg) ok = notify_options_send(cfg, msg)
if ok and conn is not None: if ok and conn is not None:
for r in pending or rows: for r in pending or rows:
+58 -41
View File
@@ -437,8 +437,36 @@ def _sync_options_trades(
raw = raw_positions raw = raw_positions
live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")} live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")}
def _hist(inst_id: str): # 网络拉取历史必须在 DB 事务外,否则握锁期间卖币/中控快照会 database is locked
return fetch_option_position_history(ex, inst_id) pending_insts: list[str] = []
conn = cfg["get_db"]()
try:
init_options_tables(conn)
rows = conn.execute(
"SELECT inst_id FROM options_trades WHERE status = 'open'"
).fetchall()
for r in rows:
iid = str(r["inst_id"] or "").strip()
if iid and iid not in live_ids:
pending_insts.append(iid)
try:
conn.commit()
except Exception:
pass
finally:
conn.close()
hist_by_inst: dict[str, list] = {}
for iid in pending_insts:
try:
hist_by_inst[iid] = fetch_option_position_history(ex, iid) or []
except Exception:
hist_by_inst[iid] = []
hist_all = None
try:
hist_all = fetch_all_option_positions_history(ex, limit=200)
except Exception:
hist_all = None
conn = cfg["get_db"]() conn = cfg["get_db"]()
try: try:
@@ -447,17 +475,25 @@ def _sync_options_trades(
sync_open_options_trades( sync_open_options_trades(
conn, conn,
live_inst_ids=live_ids, live_inst_ids=live_ids,
fetch_history_fn=_hist, fetch_history_fn=lambda inst_id: hist_by_inst.get(str(inst_id) or [], []),
notify_cfg=cfg, notify_cfg=cfg,
) )
if hist_all is not None:
try:
backfill_closed_options_realized_pnl_from_history(conn, hist_all)
except Exception:
pass
conn.commit()
except Exception:
try: try:
hist_all = fetch_all_option_positions_history(ex, limit=200) conn.rollback()
backfill_closed_options_realized_pnl_from_history(conn, hist_all)
except Exception: except Exception:
pass pass
conn.commit()
finally: finally:
conn.close() try:
conn.close()
except Exception:
pass
try: try:
from lib.options.options_monitor_lib import sell_spot_after_options_sync from lib.options.options_monitor_lib import sell_spot_after_options_sync
@@ -1703,23 +1739,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
inst_id = (data.get("inst_id") or "").strip() or None inst_id = (data.get("inst_id") or "").strip() or None
from lib.options.options_spot_bridge_lib import sell_residual_after_option_flat from lib.options.options_spot_bridge_lib import sell_residual_after_option_flat
conn = cfg["get_db"]() out = sell_residual_after_option_flat(
try: cfg["get_db"],
init_options_tables(conn) ex,
try: underlying=underlying,
conn.commit() inst_id=inst_id,
except Exception: )
pass if out.get("ok"):
out = sell_residual_after_option_flat(conn, ex, underlying=underlying, inst_id=inst_id) _mark_balances_stale(cfg)
try: return jsonify(out)
conn.commit()
except Exception:
pass
if out.get("ok"):
_mark_balances_stale(cfg)
return jsonify(out)
finally:
conn.close()
@app.route("/api/options/convert/quote", methods=["POST"]) @app.route("/api/options/convert/quote", methods=["POST"])
@lr @lr
@@ -1979,24 +2007,13 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
return [] return []
return [cfg["format_position_row"](p) for p in raw] return [cfg["format_position_row"](p) for p in raw]
def _sync(conn): def _sync(_conn=None):
from lib.exchange.okx_options_lib import fetch_option_position_history # 不用监控线程传入的连接;force=False 走 15s 节流,卖币由监控循环在关闭连接后另扫
from lib.options.options_monitor_lib import reconcile_live_open_trades, sync_open_options_trades try:
_sync_options_trades(cfg, force=False)
ex = cfg.get("exchange_options") return 1
if ex is None: except Exception:
return 0 return 0
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return 0
live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")}
reconcile_live_open_trades(conn, live_inst_ids=live_ids)
return sync_open_options_trades(
conn,
live_inst_ids=live_ids,
fetch_history_fn=lambda inst_id: fetch_option_position_history(ex, inst_id),
notify_cfg=cfg,
)
def _target_close(inst_id: str) -> dict[str, Any]: def _target_close(inst_id: str) -> dict[str, Any]:
from lib.options.options_target_lib import close_option_by_bid_depth from lib.options.options_target_lib import close_option_by_bid_depth
+77 -38
View File
@@ -303,60 +303,99 @@ def rollback_bought_coin_to_usdt(
def sell_residual_after_option_flat( def sell_residual_after_option_flat(
conn: sqlite3.Connection, conn_or_get_db: Any,
ex: Any, ex: Any,
*, *,
underlying: str, underlying: str,
inst_id: str | None = None, inst_id: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""期权已平:卖掉本桥残留标的币;优先关闭 matching holding/pending 桥.""" """期权已平:卖掉本桥残留标的币;优先关闭 matching holding/pending 桥.
ensure_bridge_table(conn)
bridges = list_open_bridges(conn) 可传 get_db 可调用对象(推荐)或已打开的 Connection。
target = None 卖币(尤其模拟盘写钱包)期间绝不握着业务连接,否则会 database is locked。
for b in bridges: """
if str(b.get("status")) in (BRIDGE_HOLDING, BRIDGE_PENDING_SELL, BRIDGE_BOUGHT): get_db = conn_or_get_db if callable(conn_or_get_db) else None
if not underlying or str(b.get("underlying") or "").upper() == underlying.upper(): bid: int | None = None
target = b if get_db is not None:
break conn = get_db()
# 卖币(模拟盘)会另开连接写钱包,须先释放本连接事务锁,否则 database is locked try:
try: ensure_bridge_table(conn)
conn.commit() bridges = list_open_bridges(conn)
except Exception: for b in bridges:
pass 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
try:
conn.commit()
except Exception:
pass
finally:
try:
conn.close()
except Exception:
pass
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
try:
conn.commit()
except Exception:
pass
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying) sell = spot_market_sell_coin_to_usdt(ex, underlying=underlying)
if target is None: if bid is None:
if not sell.get("ok"): if not sell.get("ok"):
msg = str(sell.get("msg") or "") msg = str(sell.get("msg") or "")
if "无可用" in msg or "过小" in msg: if "无可用" in msg or "过小" in msg:
return {"ok": True, "msg": "无残留币需卖回", "skipped": True} return {"ok": True, "msg": "无残留币需卖回", "skipped": True}
return {"ok": False, "msg": msg, "bridge_status": BRIDGE_PENDING_SELL} return {"ok": False, "msg": msg, "bridge_status": BRIDGE_PENDING_SELL}
return {"ok": True, "sell": sell, "bridge_status": None} return {"ok": True, "sell": sell, "bridge_status": None}
bid = int(target["id"])
if not sell.get("ok"): def _apply_bridge(c: sqlite3.Connection) -> dict[str, Any]:
if not sell.get("ok"):
update_bridge(
c,
bid,
status=BRIDGE_PENDING_SELL,
inst_id=inst_id,
message=str(sell.get("msg") or "卖回 USDT 失败"),
)
return {
"ok": False,
"msg": sell.get("msg") or "卖回 USDT 失败",
"bridge_id": bid,
"bridge_status": BRIDGE_PENDING_SELL,
}
update_bridge( update_bridge(
conn, c,
bid, bid,
status=BRIDGE_PENDING_SELL, status=BRIDGE_CLOSED,
sell_ord_id=str(sell.get("ord_id") or ""),
coin_sold=_safe_float(sell.get("coin_sold")),
inst_id=inst_id, inst_id=inst_id,
message=str(sell.get("msg") or "卖回 USDT 失败"), message="期权已平,币已卖回 USDT",
close=True,
) )
return { return {"ok": True, "sell": sell, "bridge_id": bid, "bridge_status": BRIDGE_CLOSED}
"ok": False,
"msg": sell.get("msg") or "卖回 USDT 失败", if get_db is not None:
"bridge_id": bid, conn2 = get_db()
"bridge_status": BRIDGE_PENDING_SELL, try:
} return _apply_bridge(conn2)
update_bridge( finally:
conn, try:
bid, conn2.close()
status=BRIDGE_CLOSED, except Exception:
sell_ord_id=str(sell.get("ord_id") or ""), pass
coin_sold=_safe_float(sell.get("coin_sold")), return _apply_bridge(conn_or_get_db)
inst_id=inst_id,
message="期权已平,币已卖回 USDT",
close=True,
)
return {"ok": True, "sell": sell, "bridge_id": bid, "bridge_status": BRIDGE_CLOSED}
def bridge_blocks_new_open_msg(conn: sqlite3.Connection) -> str | None: def bridge_blocks_new_open_msg(conn: sqlite3.Connection) -> str | None:
+6 -11
View File
@@ -835,17 +835,12 @@ class SimBroker:
if margin_mode_from_inst_id(inst_id) == "coin": if margin_mode_from_inst_id(inst_id) == "coin":
uly = inst_id.split("-")[0].upper() if "-" in inst_id else "ETH" uly = inst_id.split("-")[0].upper() if "-" in inst_id else "ETH"
conn_s = self.get_db() sell_residual_after_option_flat(
try: self.get_db,
sell_residual_after_option_flat( exchange,
conn_s, underlying=uly,
exchange, inst_id=inst_id,
underlying=uly, )
inst_id=inst_id,
)
conn_s.commit()
finally:
conn_s.close()
except Exception: except Exception:
pass pass
settled.append( settled.append(