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
+38 -3
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import math
import re
import threading
import time
from typing import Any, Callable
@@ -628,7 +629,26 @@ def place_option_market_order(
return {"ok": False, "msg": _okx_trade_error_message(e)}
def fetch_option_positions(ex: ccxt.okx) -> list[dict[str, Any]]:
_OPTION_POSITIONS_CACHE: dict[str, Any] = {"updated_at": 0.0, "rows": None, "failed": False}
_OPTION_POSITIONS_CACHE_LOCK = threading.Lock()
_OPTION_POSITIONS_CACHE_TTL = 4.0
_OPTION_POSITIONS_STALE_OK_SEC = 30.0
def invalidate_option_positions_cache() -> None:
with _OPTION_POSITIONS_CACHE_LOCK:
_OPTION_POSITIONS_CACHE["updated_at"] = 0.0
_OPTION_POSITIONS_CACHE["failed"] = False
def fetch_option_positions(ex: ccxt.okx) -> list[dict[str, Any]] | None:
"""期权持仓:有仓返回列表,无仓返回 [],API 失败返回 None(短时回退缓存)."""
now = time.time()
with _OPTION_POSITIONS_CACHE_LOCK:
age = now - float(_OPTION_POSITIONS_CACHE["updated_at"] or 0.0)
cached = _OPTION_POSITIONS_CACHE["rows"]
if age < _OPTION_POSITIONS_CACHE_TTL and cached is not None and not _OPTION_POSITIONS_CACHE["failed"]:
return list(cached)
try:
rows = ex.private_get_account_positions({"instType": "OPTION"}).get("data") or []
out = []
@@ -639,9 +659,21 @@ def fetch_option_positions(ex: ccxt.okx) -> list[dict[str, Any]]:
if pos is None or abs(pos) < 1e-12:
continue
out.append(r)
with _OPTION_POSITIONS_CACHE_LOCK:
_OPTION_POSITIONS_CACHE["updated_at"] = now
_OPTION_POSITIONS_CACHE["rows"] = out
_OPTION_POSITIONS_CACHE["failed"] = False
return out
except Exception:
return []
with _OPTION_POSITIONS_CACHE_LOCK:
cached = _OPTION_POSITIONS_CACHE["rows"]
age = now - float(_OPTION_POSITIONS_CACHE["updated_at"] or 0.0)
if cached is not None and age < _OPTION_POSITIONS_STALE_OK_SEC:
return list(cached)
_OPTION_POSITIONS_CACHE["updated_at"] = now
_OPTION_POSITIONS_CACHE["rows"] = None
_OPTION_POSITIONS_CACHE["failed"] = True
return None
def fetch_option_position_history(
@@ -700,9 +732,12 @@ def resolve_option_close_from_history(
def fetch_options_unrealized_pnl_usdc(ex: ccxt.okx) -> float | None:
"""期权持仓未实现盈亏合计(USDC,统计口径与 USDT 1:1)."""
positions = fetch_option_positions(ex)
if positions is None:
return None
total = 0.0
found = False
for pos in fetch_option_positions(ex):
for pos in positions:
upl = _safe_float(pos.get("upl"))
if upl is None:
continue