Add BTC/ETH spot tickers on nav, anchored to Beijing 08:00 open.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""Nav bar BTC/ETH spot tickers vs Beijing 08:00 open (OKX 1D = UTC 00:00)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Callable, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
BJ = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
# (base, OKX spot instId, display symbol)
|
||||
NAV_SPOT_SPECS = (
|
||||
("ETH", "ETH-USDT", "ETHUSDT"),
|
||||
("BTC", "BTC-USDT", "BTCUSDT"),
|
||||
)
|
||||
|
||||
_OPEN_CACHE_LOCK = threading.Lock()
|
||||
_OPEN_CACHE: dict[str, dict[str, Any]] = {}
|
||||
_OPEN_CACHE_TTL_SEC = 120.0
|
||||
|
||||
|
||||
def _safe_float(v: Any) -> Optional[float]:
|
||||
try:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def session_day_key(now: Optional[datetime] = None, reset_hour: int = 8) -> str:
|
||||
"""Trading day key: before reset_hour BJ belongs to previous calendar day."""
|
||||
dt = now or datetime.now(BJ)
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=BJ)
|
||||
else:
|
||||
dt = dt.astimezone(BJ)
|
||||
if dt.hour < int(reset_hour):
|
||||
dt = dt - timedelta(days=1)
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _fetch_spot_last(ex: Any, inst_id: str) -> Optional[float]:
|
||||
try:
|
||||
rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
|
||||
if rows and isinstance(rows[0], dict):
|
||||
return _safe_float(rows[0].get("last"))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
# ccxt spot id: BTC-USDT → BTC/USDT
|
||||
sym = inst_id.replace("-", "/")
|
||||
t = ex.fetch_ticker(sym)
|
||||
return _safe_float(t.get("last"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _fetch_day_open_bj8(ex: Any, inst_id: str) -> Optional[float]:
|
||||
"""OKX 1D candle open is UTC 00:00 = Beijing 08:00."""
|
||||
try:
|
||||
rows = ex.public_get_market_candles(
|
||||
{"instId": inst_id, "bar": "1D", "limit": "1"}
|
||||
).get("data") or []
|
||||
if rows:
|
||||
row = rows[0]
|
||||
# OKX: [ts, o, h, l, c, vol, volCcy, volCcyQuote, confirm]
|
||||
if isinstance(row, (list, tuple)) and len(row) > 1:
|
||||
return _safe_float(row[1])
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
sym = inst_id.replace("-", "/")
|
||||
ohlcv = ex.fetch_ohlcv(sym, timeframe="1d", limit=1) or []
|
||||
if ohlcv:
|
||||
return _safe_float(ohlcv[-1][1])
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _cached_day_open(ex: Any, inst_id: str, day_key: str) -> Optional[float]:
|
||||
now_ts = time.time()
|
||||
with _OPEN_CACHE_LOCK:
|
||||
entry = _OPEN_CACHE.get(inst_id)
|
||||
if (
|
||||
entry
|
||||
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, inst_id)
|
||||
if open_px is not None:
|
||||
with _OPEN_CACHE_LOCK:
|
||||
_OPEN_CACHE[inst_id] = {
|
||||
"day_key": day_key,
|
||||
"open": open_px,
|
||||
"fetched_at": now_ts,
|
||||
}
|
||||
return open_px
|
||||
|
||||
|
||||
def build_nav_spot_tickers(
|
||||
exchange: Any,
|
||||
*,
|
||||
reset_hour: int = 8,
|
||||
now_fn: Optional[Callable[[], datetime]] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return ETH then BTC spot last/change vs Beijing 08:00 open."""
|
||||
now = now_fn() if now_fn else datetime.now(BJ)
|
||||
day_key = session_day_key(now, reset_hour=reset_hour)
|
||||
out: list[dict[str, Any]] = []
|
||||
for base, inst_id, display in NAV_SPOT_SPECS:
|
||||
last = _fetch_spot_last(exchange, inst_id)
|
||||
open_px = _cached_day_open(exchange, inst_id, day_key)
|
||||
change = None
|
||||
change_pct = None
|
||||
if last is not None and open_px is not None and open_px != 0:
|
||||
change = last - open_px
|
||||
change_pct = (change / open_px) * 100.0
|
||||
out.append(
|
||||
{
|
||||
"base": base,
|
||||
"inst_id": inst_id,
|
||||
"symbol": display,
|
||||
"last": round(last, 8) if last 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_pct": round(change_pct, 4) if change_pct is not None else None,
|
||||
"session_day": day_key,
|
||||
"anchor": f"BJ {int(reset_hour):02d}:00",
|
||||
}
|
||||
)
|
||||
return out
|
||||
Reference in New Issue
Block a user