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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user