Files
crypto_okx/lib/market/nav_spot_tickers_lib.py

150 lines
5.0 KiB
Python

"""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 datetime import datetime, timedelta
from typing import Any, Callable, Optional
from zoneinfo import ZoneInfo
BJ = ZoneInfo("Asia/Shanghai")
ANCHOR_SOD_FIELD = "sodUtc0"
ANCHOR_CANDLE_BAR = "1Dutc"
ANCHOR_LABEL = "BJ 08:00"
# (base, display, swap instId, spot instId, ccxt swap symbol)
NAV_SPOT_SPECS = (
("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]:
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 _ticker_row(ex: Any, inst_id: str) -> Optional[dict[str, Any]]:
try:
rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
if rows and isinstance(rows[0], dict):
return rows[0]
except Exception:
pass
return None
def _candle_open(ex: Any, inst_id: str, bar: str) -> Optional[float]:
"""OKX candles: newest first; [ts, o, h, l, c, ...]."""
try:
rows = ex.public_get_market_candles(
{"instId": inst_id, "bar": bar, "limit": "1"}
).get("data") or []
if rows and isinstance(rows[0], (list, tuple)) and len(rows[0]) > 1:
return _safe_float(rows[0][1])
except Exception:
pass
return None
def _ccxt_last_and_anchor_open(ex: Any, symbol: str) -> tuple[Optional[float], Optional[float]]:
last = None
open_px = None
try:
t = ex.fetch_ticker(symbol) or {}
last = _safe_float(t.get("last") or t.get("close"))
info = t.get("info") if isinstance(t.get("info"), dict) else {}
open_px = _safe_float(info.get(ANCHOR_SOD_FIELD))
except Exception:
pass
return last, open_px
def _fetch_last_and_open(
ex: Any, swap_inst: str, spot_inst: str, swap_sym: str
) -> tuple[Optional[float], Optional[float]]:
"""Same instrument last + Beijing 08:00 open. Prefer USDT SWAP."""
for inst in (swap_inst, spot_inst):
row = _ticker_row(ex, inst)
if not row:
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
last, open_px = _ccxt_last_and_anchor_open(ex, spot_inst.replace("-", "/"))
if last is not None and open_px is not None:
return last, open_px
for inst, sym in ((swap_inst, swap_sym), (spot_inst, spot_inst.replace("-", "/"))):
row = _ticker_row(ex, inst)
last = _safe_float(row.get("last")) if row else None
if last is None:
last, _ = _ccxt_last_and_anchor_open(ex, sym)
open_px = _candle_open(ex, inst, ANCHOR_CANDLE_BAR)
if last is not None and open_px is not None:
return last, open_px
return None, None
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 last/change vs Beijing 08:00 open (sodUtc0)."""
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, display, swap_inst, spot_inst, swap_sym in NAV_SPOT_SPECS:
last, open_px = _fetch_last_and_open(exchange, swap_inst, spot_inst, swap_sym)
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": swap_inst,
"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": ANCHOR_LABEL,
"anchor_field": ANCHOR_SOD_FIELD,
}
)
return out