Fix nav ticker day-change: use SWAP sodUtc0 (Beijing 08:00) from same instrument.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-17 11:52:35 +08:00
parent de13869b38
commit 432adfb602
5 changed files with 68 additions and 124 deletions
+2 -2
View File
@@ -68,8 +68,8 @@
if (pctEl) pctEl.textContent = fmtSigned(t.change_pct, true); if (pctEl) pctEl.textContent = fmtSigned(t.change_pct, true);
const openHint = const openHint =
t.open != null t.open != null
? "开盘 " + fmtPrice(t.open) + " (" + (t.anchor || "BJ 08:00") + ")" ? "开盘 " + fmtPrice(t.open) + " · 北京 08:00 (sodUtc0)"
: t.anchor || "BJ 08:00"; : "北京 08:00 (sodUtc0)";
row.title = (t.symbol || "") + " " + openHint; row.title = (t.symbol || "") + " " + openHint;
} }
+1 -1
View File
@@ -149,7 +149,7 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
<script src="/static/instance_stats.js?v=5"></script> <script src="/static/instance_stats.js?v=5"></script>
{% include 'embed_boot_scripts.html' %} {% include 'embed_boot_scripts.html' %}
<script src="/static/options_expiry_countdown.js?v=1"></script> <script src="/static/options_expiry_countdown.js?v=1"></script>
<script src="/static/nav_spot_tickers.js?v=2"></script> <script src="/static/nav_spot_tickers.js?v=3"></script>
<script src="/static/instance_dashboard.js?v=5"></script> <script src="/static/instance_dashboard.js?v=5"></script>
<script src="/static/account_ledger.js?v=1"></script> <script src="/static/account_ledger.js?v=1"></script>
<script> <script>
+1 -1
View File
@@ -1830,7 +1830,7 @@ tickOrderHoldDurations();
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }}); setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
</script> </script>
<script src="/static/options_expiry_countdown.js?v=1"></script> <script src="/static/options_expiry_countdown.js?v=1"></script>
<script src="/static/nav_spot_tickers.js?v=2"></script> <script src="/static/nav_spot_tickers.js?v=3"></script>
<script src="/static/instance_dashboard.js?v=5"></script> <script src="/static/instance_dashboard.js?v=5"></script>
<script src="/static/account_ledger.js?v=1"></script> <script src="/static/account_ledger.js?v=1"></script>
<script> <script>
+1 -1
View File
@@ -1,5 +1,5 @@
{# 导航右侧 BTC/ETH 现货报价(相对北京 08:00 开盘) #} {# 导航右侧 BTC/ETH 现货报价(相对北京 08:00 开盘) #}
<div class="top-nav-spot-tickers" id="nav-spot-tickers" aria-label="BTC ETH 现货报价" title="相对今日北京时间 08:00 开盘"> <div class="top-nav-spot-tickers" id="nav-spot-tickers" aria-label="BTC ETH 报价" title="涨跌相对北京时间 08:00 开盘(OKX sodUtc0)">
<div class="nav-spot-row" data-base="ETH"> <div class="nav-spot-row" data-base="ETH">
<span class="nav-spot-ribbon" aria-hidden="true"></span> <span class="nav-spot-ribbon" aria-hidden="true"></span>
<span class="nav-spot-icon nav-spot-icon--eth" aria-hidden="true">Ξ</span> <span class="nav-spot-icon nav-spot-icon--eth" aria-hidden="true">Ξ</span>
+63 -119
View File
@@ -1,24 +1,27 @@
"""Nav bar BTC/ETH spot tickers vs Beijing 08:00 open (OKX 1D = UTC 00:00).""" """Nav bar BTC/ETH tickers vs Beijing 08:00 open.
OKX:
sodUtc0 / candle 1Dutc = UTC+0 open = Beijing 08:00 ← use this
sodUtc8 / candle 1D = UTC+8 open = Beijing 00:00 (default OKX daily K)
"""
from __future__ import annotations from __future__ import annotations
import threading
import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Callable, Optional from typing import Any, Callable, Optional
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
BJ = ZoneInfo("Asia/Shanghai") BJ = ZoneInfo("Asia/Shanghai")
# (base, OKX spot instId, display symbol, swap ccxt symbol, index instId) ANCHOR_SOD_FIELD = "sodUtc0"
NAV_SPOT_SPECS = ( ANCHOR_CANDLE_BAR = "1Dutc"
("ETH", "ETH-USDT", "ETHUSDT", "ETH/USDT:USDT", "ETH-USD"), ANCHOR_LABEL = "BJ 08:00"
("BTC", "BTC-USDT", "BTCUSDT", "BTC/USDT:USDT", "BTC-USD"),
)
_OPEN_CACHE_LOCK = threading.Lock() # (base, display, swap instId, spot instId, ccxt swap symbol)
_OPEN_CACHE: dict[str, dict[str, Any]] = {} NAV_SPOT_SPECS = (
_OPEN_CACHE_TTL_SEC = 120.0 ("ETH", "ETHUSDT", "ETH-USDT-SWAP", "ETH-USDT", "ETH/USDT:USDT"),
("BTC", "BTCUSDT", "BTC-USDT-SWAP", "BTC-USDT", "BTC/USDT:USDT"),
)
def _safe_float(v: Any) -> Optional[float]: def _safe_float(v: Any) -> Optional[float]:
@@ -42,131 +45,74 @@ def session_day_key(now: Optional[datetime] = None, reset_hour: int = 8) -> str:
return dt.strftime("%Y-%m-%d") return dt.strftime("%Y-%m-%d")
def _public_last(ex: Any, inst_id: str, last_key: str = "last") -> Optional[float]: def _ticker_row(ex: Any, inst_id: str) -> Optional[dict[str, Any]]:
try: try:
rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or [] rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
if rows and isinstance(rows[0], dict): if rows and isinstance(rows[0], dict):
return _safe_float(rows[0].get(last_key)) return rows[0]
except Exception: except Exception:
pass pass
return None return None
def _public_index_last(ex: Any, inst_id: str) -> Optional[float]: def _candle_open(ex: Any, inst_id: str, bar: str) -> Optional[float]:
try: """OKX candles: newest first; [ts, o, h, l, c, ...]."""
rows = ex.public_get_market_index_tickers({"instId": inst_id}).get("data") or []
if rows and isinstance(rows[0], dict):
return _safe_float(rows[0].get("idxPx"))
except Exception:
pass
return None
def _ccxt_last(ex: Any, symbol: str) -> Optional[float]:
try:
t = ex.fetch_ticker(symbol)
return _safe_float(t.get("last") or t.get("close"))
except Exception:
return None
def _fetch_spot_last(ex: Any, spot_inst: str, swap_sym: str, index_inst: str) -> Optional[float]:
# 1) 现货 2) 永续(本站常用) 3) 指数
last = _public_last(ex, spot_inst, "last")
if last is not None:
return last
last = _public_last(ex, f"{spot_inst}-SWAP", "last")
if last is not None:
return last
last = _ccxt_last(ex, swap_sym)
if last is not None:
return last
last = _ccxt_last(ex, spot_inst.replace("-", "/"))
if last is not None:
return last
return _public_index_last(ex, index_inst)
def _public_candle_open(ex: Any, inst_id: str, bar: str = "1D") -> Optional[float]:
try: try:
rows = ex.public_get_market_candles( rows = ex.public_get_market_candles(
{"instId": inst_id, "bar": bar, "limit": "1"} {"instId": inst_id, "bar": bar, "limit": "1"}
).get("data") or [] ).get("data") or []
if rows: if rows and isinstance(rows[0], (list, tuple)) and len(rows[0]) > 1:
row = rows[0] return _safe_float(rows[0][1])
if isinstance(row, (list, tuple)) and len(row) > 1:
return _safe_float(row[1])
except Exception: except Exception:
pass pass
return None return None
def _public_index_candle_open(ex: Any, inst_id: str) -> Optional[float]: def _ccxt_last_and_anchor_open(ex: Any, symbol: str) -> tuple[Optional[float], Optional[float]]:
last = None
open_px = None
try: try:
rows = ex.public_get_market_index_candles( t = ex.fetch_ticker(symbol) or {}
{"instId": inst_id, "bar": "1D", "limit": "1"} last = _safe_float(t.get("last") or t.get("close"))
).get("data") or [] info = t.get("info") if isinstance(t.get("info"), dict) else {}
if rows: open_px = _safe_float(info.get(ANCHOR_SOD_FIELD))
row = rows[0]
if isinstance(row, (list, tuple)) and len(row) > 1:
return _safe_float(row[1])
except Exception: except Exception:
pass pass
return None return last, open_px
def _ccxt_day_open(ex: Any, symbol: str) -> Optional[float]: def _fetch_last_and_open(
try: ex: Any, swap_inst: str, spot_inst: str, swap_sym: str
ohlcv = ex.fetch_ohlcv(symbol, timeframe="1d", limit=1) or [] ) -> tuple[Optional[float], Optional[float]]:
if ohlcv: """Same instrument last + Beijing 08:00 open. Prefer USDT SWAP."""
return _safe_float(ohlcv[-1][1]) for inst in (swap_inst, spot_inst):
except Exception: row = _ticker_row(ex, inst)
return None if not row:
return None continue
last = _safe_float(row.get("last"))
open_px = _safe_float(row.get(ANCHOR_SOD_FIELD))
if open_px is None:
open_px = _candle_open(ex, inst, ANCHOR_CANDLE_BAR)
if last is not None and open_px is not None:
return last, open_px
last, open_px = _ccxt_last_and_anchor_open(ex, swap_sym)
if last is not None and open_px is not None:
return last, open_px
def _fetch_day_open_bj8( last, open_px = _ccxt_last_and_anchor_open(ex, spot_inst.replace("-", "/"))
ex: Any, spot_inst: str, swap_sym: str, index_inst: str if last is not None and open_px is not None:
) -> Optional[float]: return last, open_px
"""OKX 1D candle open is UTC 00:00 = Beijing 08:00."""
open_px = _public_candle_open(ex, spot_inst)
if open_px is not None:
return open_px
open_px = _public_candle_open(ex, f"{spot_inst}-SWAP")
if open_px is not None:
return open_px
open_px = _ccxt_day_open(ex, swap_sym)
if open_px is not None:
return open_px
open_px = _ccxt_day_open(ex, spot_inst.replace("-", "/"))
if open_px is not None:
return open_px
return _public_index_candle_open(ex, index_inst)
for inst, sym in ((swap_inst, swap_sym), (spot_inst, spot_inst.replace("-", "/"))):
def _cached_day_open( row = _ticker_row(ex, inst)
ex: Any, cache_key: str, spot_inst: str, swap_sym: str, index_inst: str, day_key: str last = _safe_float(row.get("last")) if row else None
) -> Optional[float]: if last is None:
now_ts = time.time() last, _ = _ccxt_last_and_anchor_open(ex, sym)
with _OPEN_CACHE_LOCK: open_px = _candle_open(ex, inst, ANCHOR_CANDLE_BAR)
entry = _OPEN_CACHE.get(cache_key) if last is not None and open_px is not None:
if ( return last, open_px
entry return None, None
and entry.get("day_key") == day_key
and entry.get("open") is not None
and now_ts - float(entry.get("fetched_at") or 0) < _OPEN_CACHE_TTL_SEC
):
return _safe_float(entry["open"])
open_px = _fetch_day_open_bj8(ex, spot_inst, swap_sym, index_inst)
if open_px is not None:
with _OPEN_CACHE_LOCK:
_OPEN_CACHE[cache_key] = {
"day_key": day_key,
"open": open_px,
"fetched_at": now_ts,
}
return open_px
def build_nav_spot_tickers( def build_nav_spot_tickers(
@@ -175,15 +121,12 @@ def build_nav_spot_tickers(
reset_hour: int = 8, reset_hour: int = 8,
now_fn: Optional[Callable[[], datetime]] = None, now_fn: Optional[Callable[[], datetime]] = None,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Return ETH then BTC spot last/change vs Beijing 08:00 open.""" """Return ETH then BTC last/change vs Beijing 08:00 open (sodUtc0)."""
now = now_fn() if now_fn else datetime.now(BJ) now = now_fn() if now_fn else datetime.now(BJ)
day_key = session_day_key(now, reset_hour=reset_hour) day_key = session_day_key(now, reset_hour=reset_hour)
out: list[dict[str, Any]] = [] out: list[dict[str, Any]] = []
for base, spot_inst, display, swap_sym, index_inst in NAV_SPOT_SPECS: for base, display, swap_inst, spot_inst, swap_sym in NAV_SPOT_SPECS:
last = _fetch_spot_last(exchange, spot_inst, swap_sym, index_inst) last, open_px = _fetch_last_and_open(exchange, swap_inst, spot_inst, swap_sym)
open_px = _cached_day_open(
exchange, spot_inst, spot_inst, swap_sym, index_inst, day_key
)
change = None change = None
change_pct = None change_pct = None
if last is not None and open_px is not None and open_px != 0: if last is not None and open_px is not None and open_px != 0:
@@ -192,14 +135,15 @@ def build_nav_spot_tickers(
out.append( out.append(
{ {
"base": base, "base": base,
"inst_id": spot_inst, "inst_id": swap_inst,
"symbol": display, "symbol": display,
"last": round(last, 8) if last is not None else None, "last": round(last, 8) if last is not None else None,
"open": round(open_px, 8) if open_px is not None else None, "open": round(open_px, 8) if open_px is not None else None,
"change": round(change, 8) if change is not None else None, "change": round(change, 8) if change is not None else None,
"change_pct": round(change_pct, 4) if change_pct is not None else None, "change_pct": round(change_pct, 4) if change_pct is not None else None,
"session_day": day_key, "session_day": day_key,
"anchor": f"BJ {int(reset_hour):02d}:00", "anchor": ANCHOR_LABEL,
"anchor_field": ANCHOR_SOD_FIELD,
} }
) )
return out return out