diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js index 02a2e5d..323aaf6 100644 --- a/lib/common/static/options_review.js +++ b/lib/common/static/options_review.js @@ -168,10 +168,33 @@ } function reloadAll() { + // 先无同步出列表(统计同理),再后台同步;避免模拟盘握库+拉行情把列表卡死在「加载中」 setSyncStatus("读取本地记录…"); - loadTrades({ sync: true }); + loadTrades({ sync: false }); loadReviewed({ sync: false }); loadStats(); + backgroundSync(); + } + + function backgroundSync() { + setSyncStatus("同步本地记录…"); + fetch("/api/options/review/sync", { credentials: "same-origin", method: "POST" }) + .then(function (r) { + return r.json(); + }) + .then(function (data) { + if (data && data.ok === false) { + setSyncStatus("同步跳过,已显示本地缓存"); + return; + } + setSyncStatus("本地记录已加载"); + loadTrades({ sync: false, soft: true }); + loadReviewed({ sync: false, soft: true }); + loadStats(); + }) + .catch(function () { + setSyncStatus("同步跳过,已显示本地缓存"); + }); } function isHedgeSource(sourceType) { @@ -451,6 +474,7 @@ endListLoad(wrap); }) .catch(function () { + if (doSync) setSyncStatus("加载失败"); tbody.innerHTML = '加载失败'; endListLoad(wrap); }); diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index b6ea588..4fa4e5f 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -302,6 +302,11 @@ def sync_options_from_local_trades( LIMIT 500 """ ).fetchall() + # 读完先提交,后续拉指数不与写事务叠在一起 + try: + conn.commit() + except Exception: + pass def _index_px(underly: str) -> float | None: u = (underly or "ETH").strip().upper() or "ETH" @@ -356,6 +361,18 @@ def sync_options_from_local_trades( inserted = updated = skipped = 0 idx_cache: dict[str, float | None] = {} + need_idx: set[str] = set() + for r in rows: + inst0 = str(r["inst_id"] or "") + underly0 = str(r["underlying"] or (inst0.split("-")[0] if inst0 else "ETH") or "ETH") + ccy0 = str(r["premium_ccy"] or "").strip().upper() + if not ccy0: + ccy0 = premium_ccy_for_mode(margin_mode_from_inst_id(inst0), underly0) + if ccy0 in ("ETH", "BTC"): + need_idx.add(underly0) + for underly0 in need_idx: + idx_cache[underly0] = _index_px(underly0) + for r in rows: trade_id = int(r["id"]) history_key = f"local_opt:{trade_id}" @@ -364,8 +381,6 @@ def sync_options_from_local_trades( ccy = str(r["premium_ccy"] or "").strip().upper() if not ccy: ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly) - if underly not in idx_cache: - idx_cache[underly] = _index_px(underly) idx = idx_cache.get(underly) pnl = _safe_float(r["realized_pnl"]) if pnl is None: @@ -644,6 +659,11 @@ def ensure_local_review_synced( note_swap = repair_swapped_closed_at_signal_note(conn) except Exception as e: note_swap = {"ok": False, "msg": str(e)} + # 提交后再拉历史/行情,避免模拟盘 broker 二次开库与本连接互锁 + try: + conn.commit() + except Exception: + pass if backfill_exchange_pnl and ex is not None: try: from lib.exchange.okx_options_lib import fetch_all_option_positions_history @@ -653,8 +673,12 @@ def ensure_local_review_synced( hist = fetch_all_option_positions_history(ex, limit=200) backfill_closed_options_realized_pnl_from_history(conn, hist) + conn.commit() except Exception: - pass + try: + conn.rollback() + except Exception: + pass try: out = sync_all_review_sources(conn, ex=ex, from_exchange=False) except sqlite3.OperationalError as e: @@ -662,6 +686,8 @@ def ensure_local_review_synced( if isinstance(out, dict): out["ts_migrate"] = ts_migrate out["note_swap"] = note_swap + if out.get("ok") is not False: + out["ok"] = True return out diff --git a/lib/options/options_review_register.py b/lib/options/options_review_register.py index cae60d9..e82d0c9 100644 --- a/lib/options/options_review_register.py +++ b/lib/options/options_review_register.py @@ -275,7 +275,16 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s ex, _err = _require_ex(cfg) result = ensure_local_review_synced(conn, ex=ex if ex is not None else None) conn.commit() - return jsonify(result) + if isinstance(result, dict) and result.get("ok") is not False: + result["ok"] = True + return jsonify(result if isinstance(result, dict) else {"ok": True, "result": result}) + except Exception as e: + print(f"[options_review_sync] {e}", flush=True) + try: + conn.rollback() + except Exception: + pass + return jsonify({"ok": False, "msg": f"同步失败: {e}"}), 500 finally: conn.close() @@ -284,16 +293,21 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s def api_options_review_trades(): conn = cfg["get_db"]() try: - # 翻页可跳过同步,仅刷新当前卡片列表 - do_sync = (request.args.get("sync") or "1").strip().lower() not in ( - "0", - "false", - "no", + # 默认不同步:列表先读缓存;完整同步走 POST /api/options/review/sync + do_sync = (request.args.get("sync") or "0").strip().lower() in ( + "1", + "true", + "yes", ) if do_sync: try: ex, _err = _require_ex(cfg) - ensure_local_review_synced(conn, ex=ex if ex is not None else None) + # 列表路径不做历史回填,避免模拟盘握库二次开连接卡死 + ensure_local_review_synced( + conn, + ex=ex if ex is not None else None, + backfill_exchange_pnl=False, + ) conn.commit() except Exception as sync_err: # 同步撞锁时仍返回已有列表,避免整页 500 @@ -303,6 +317,8 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s except Exception: pass init_options_review_tables(conn) + else: + init_options_review_tables(conn) filt = dict( source_type=_review_source_for_mode(request.args.get("source_type")), underlying=(request.args.get("underlying") or "").strip() or None, diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index 76011c3..f64b4a0 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -456,4 +456,4 @@ - +