Fix intermittent loss of options positions and realtime PnL on refresh.

Use stale-while-revalidate for positions API and UI, throttle sync calls, and avoid overwriting displayed PnL with null on transient failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-09 18:09:58 +08:00
parent 5a97e14f3e
commit f99900ac40
7 changed files with 146 additions and 17 deletions
+2
View File
@@ -53,6 +53,8 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
return {"ok": False, "enabled": True, "msg": reason or "期权 API 未配置"}
try:
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return {"ok": False, "enabled": True, "msg": "获取期权持仓失败"}
positions = [cfg["format_position_row"](p) for p in raw]
upl_total = 0.0
has_upl = False
+40 -3
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import os
import threading
import time
from typing import Any
from flask import Flask, jsonify, redirect, request, url_for
@@ -142,14 +143,34 @@ def _budget_full_usdc(cfg: dict[str, Any], ex: Any) -> tuple[float | None, str]:
return float(raw), ""
def _sync_options_trades(cfg: dict[str, Any]) -> None:
_OPTIONS_SYNC_LOCK = threading.Lock()
_OPTIONS_SYNC_LAST_AT = 0.0
_OPTIONS_SYNC_INTERVAL_SEC = 15.0
def _sync_options_trades(
cfg: dict[str, Any],
*,
raw_positions: list[dict[str, Any]] | None = None,
force: bool = False,
) -> None:
ex = cfg.get("exchange_options")
if ex is None:
return
now = time.time()
with _OPTIONS_SYNC_LOCK:
if not force and now - _OPTIONS_SYNC_LAST_AT < _OPTIONS_SYNC_INTERVAL_SEC:
return
_OPTIONS_SYNC_LAST_AT = now
from lib.exchange.okx_options_lib import fetch_option_position_history
from lib.options.options_monitor_lib import reconcile_live_open_trades, sync_open_options_trades
raw = cfg["fetch_option_positions"](ex)
if raw_positions is None:
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return
else:
raw = raw_positions
live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")}
def _hist(inst_id: str):
@@ -348,6 +369,10 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
conn.commit()
finally:
conn.close()
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
invalidate_option_positions_cache()
_sync_options_trades(cfg, force=True)
return jsonify({"ok": True, "order": order, "sizing": sizing})
@app.route("/api/options/positions")
@@ -356,8 +381,10 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
ex, err = _require_options_ex(cfg)
if ex is None:
return jsonify({"ok": False, "msg": err})
_sync_options_trades(cfg)
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
_sync_options_trades(cfg, raw_positions=raw)
rows = [cfg["format_position_row"](p) for p in raw]
conn = cfg["get_db"]()
try:
@@ -396,6 +423,8 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
if not use_market and (bid is None or bid <= 0):
return jsonify({"ok": False, "msg": "暂无买一价,无法限价平仓"})
raw_positions = cfg["fetch_option_positions"](ex)
if raw_positions is None:
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
pos = next((p for p in raw_positions if str(p.get("instId")) == inst_id), None)
if not pos:
return jsonify({"ok": False, "msg": "未找到持仓"})
@@ -465,6 +494,10 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
conn.commit()
finally:
conn.close()
from lib.exchange.okx_options_lib import invalidate_option_positions_cache
invalidate_option_positions_cache()
_sync_options_trades(cfg, force=True)
return jsonify({"ok": True, "order": order, "bid": bid, "sheets": close_sheets})
@app.route("/api/options/convert/quote", methods=["POST"])
@@ -712,6 +745,8 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
if ex is None:
return []
raw = cfg["fetch_option_positions"](ex)
if raw is None:
return []
return [cfg["format_position_row"](p) for p in raw]
def _sync(conn):
@@ -722,6 +757,8 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
if ex is None:
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(
+1 -1
View File
@@ -137,4 +137,4 @@
</div>
</div>
<script src="/static/options_expiry_countdown.js?v=1"></script>
<script src="/static/options_panel.js?v=16"></script>
<script src="/static/options_panel.js?v=17"></script>