修复复盘列表先读缓存再后台同步,避免模拟盘握库卡死加载。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-07 13:50:04 +08:00
parent 2158a30179
commit d7b3f7a8e7
4 changed files with 78 additions and 12 deletions
+25 -1
View File
@@ -168,10 +168,33 @@
} }
function reloadAll() { function reloadAll() {
// 先无同步出列表(统计同理),再后台同步;避免模拟盘握库+拉行情把列表卡死在「加载中」
setSyncStatus("读取本地记录…"); setSyncStatus("读取本地记录…");
loadTrades({ sync: true }); loadTrades({ sync: false });
loadReviewed({ sync: false }); loadReviewed({ sync: false });
loadStats(); 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) { function isHedgeSource(sourceType) {
@@ -451,6 +474,7 @@
endListLoad(wrap); endListLoad(wrap);
}) })
.catch(function () { .catch(function () {
if (doSync) setSyncStatus("加载失败");
tbody.innerHTML = '<tr><td colspan="7" class="muted">加载失败</td></tr>'; tbody.innerHTML = '<tr><td colspan="7" class="muted">加载失败</td></tr>';
endListLoad(wrap); endListLoad(wrap);
}); });
+28 -2
View File
@@ -302,6 +302,11 @@ def sync_options_from_local_trades(
LIMIT 500 LIMIT 500
""" """
).fetchall() ).fetchall()
# 读完先提交,后续拉指数不与写事务叠在一起
try:
conn.commit()
except Exception:
pass
def _index_px(underly: str) -> float | None: def _index_px(underly: str) -> float | None:
u = (underly or "ETH").strip().upper() or "ETH" u = (underly or "ETH").strip().upper() or "ETH"
@@ -356,6 +361,18 @@ def sync_options_from_local_trades(
inserted = updated = skipped = 0 inserted = updated = skipped = 0
idx_cache: dict[str, float | None] = {} 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: for r in rows:
trade_id = int(r["id"]) trade_id = int(r["id"])
history_key = f"local_opt:{trade_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() ccy = str(r["premium_ccy"] or "").strip().upper()
if not ccy: if not ccy:
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly) 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) idx = idx_cache.get(underly)
pnl = _safe_float(r["realized_pnl"]) pnl = _safe_float(r["realized_pnl"])
if pnl is None: if pnl is None:
@@ -644,6 +659,11 @@ def ensure_local_review_synced(
note_swap = repair_swapped_closed_at_signal_note(conn) note_swap = repair_swapped_closed_at_signal_note(conn)
except Exception as e: except Exception as e:
note_swap = {"ok": False, "msg": str(e)} note_swap = {"ok": False, "msg": str(e)}
# 提交后再拉历史/行情,避免模拟盘 broker 二次开库与本连接互锁
try:
conn.commit()
except Exception:
pass
if backfill_exchange_pnl and ex is not None: if backfill_exchange_pnl and ex is not None:
try: try:
from lib.exchange.okx_options_lib import fetch_all_option_positions_history from lib.exchange.okx_options_lib import fetch_all_option_positions_history
@@ -653,6 +673,10 @@ def ensure_local_review_synced(
hist = fetch_all_option_positions_history(ex, limit=200) hist = fetch_all_option_positions_history(ex, limit=200)
backfill_closed_options_realized_pnl_from_history(conn, hist) backfill_closed_options_realized_pnl_from_history(conn, hist)
conn.commit()
except Exception:
try:
conn.rollback()
except Exception: except Exception:
pass pass
try: try:
@@ -662,6 +686,8 @@ def ensure_local_review_synced(
if isinstance(out, dict): if isinstance(out, dict):
out["ts_migrate"] = ts_migrate out["ts_migrate"] = ts_migrate
out["note_swap"] = note_swap out["note_swap"] = note_swap
if out.get("ok") is not False:
out["ok"] = True
return out return out
+23 -7
View File
@@ -275,7 +275,16 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
ex, _err = _require_ex(cfg) ex, _err = _require_ex(cfg)
result = ensure_local_review_synced(conn, ex=ex if ex is not None else None) result = ensure_local_review_synced(conn, ex=ex if ex is not None else None)
conn.commit() 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: finally:
conn.close() 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(): def api_options_review_trades():
conn = cfg["get_db"]() conn = cfg["get_db"]()
try: try:
# 翻页可跳过同步,仅刷新当前卡片列表 # 默认不同步:列表先读缓存;完整同步走 POST /api/options/review/sync
do_sync = (request.args.get("sync") or "1").strip().lower() not in ( do_sync = (request.args.get("sync") or "0").strip().lower() in (
"0", "1",
"false", "true",
"no", "yes",
) )
if do_sync: if do_sync:
try: try:
ex, _err = _require_ex(cfg) 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() conn.commit()
except Exception as sync_err: except Exception as sync_err:
# 同步撞锁时仍返回已有列表,避免整页 500 # 同步撞锁时仍返回已有列表,避免整页 500
@@ -303,6 +317,8 @@ def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: s
except Exception: except Exception:
pass pass
init_options_review_tables(conn) init_options_review_tables(conn)
else:
init_options_review_tables(conn)
filt = dict( filt = dict(
source_type=_review_source_for_mode(request.args.get("source_type")), source_type=_review_source_for_mode(request.args.get("source_type")),
underlying=(request.args.get("underlying") or "").strip() or None, underlying=(request.args.get("underlying") or "").strip() or None,
@@ -456,4 +456,4 @@
</section> </section>
</div> </div>
<script src="/static/options_review.js?v=25"></script> <script src="/static/options_review.js?v=26"></script>