"""OKX REST 只读行情。禁止任何交易类接口。""" from __future__ import annotations from typing import Any import httpx def safe_float(v: Any) -> float | None: if v is None or v == "": return None try: return float(v) except (TypeError, ValueError): return None class OkxRestClient: """仅调用公开行情 / 公共接口。""" # 硬黑名单:防止误用交易路径 _FORBIDDEN_PREFIXES = ( "/api/v5/trade", "/api/v5/account", "/api/v5/asset", "/api/v5/users", ) def __init__( self, base_url: str = "https://www.okx.com", timeout: float = 15.0, proxy: str | None = None, ) -> None: self.base_url = base_url.rstrip("/") self.proxy = (proxy or "").strip() or None self._client = httpx.Client( base_url=self.base_url, timeout=timeout, proxy=self.proxy, headers={"Accept": "application/json", "User-Agent": "market_intel/0.1"}, ) def close(self) -> None: self._client.close() def __enter__(self) -> OkxRestClient: return self def __exit__(self, *args: object) -> None: self.close() def _get(self, path: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]: for bad in self._FORBIDDEN_PREFIXES: if path.startswith(bad): raise RuntimeError(f"forbidden trading path: {path}") r = self._client.get(path, params=params or {}) r.raise_for_status() body = r.json() if str(body.get("code")) != "0": raise RuntimeError(f"OKX REST error code={body.get('code')} msg={body.get('msg')}") data = body.get("data") or [] return [x for x in data if isinstance(x, dict)] def _get_raw(self, path: str, params: dict[str, Any] | None = None) -> list[Any]: for bad in self._FORBIDDEN_PREFIXES: if path.startswith(bad): raise RuntimeError(f"forbidden trading path: {path}") r = self._client.get(path, params=params or {}) r.raise_for_status() body = r.json() if str(body.get("code")) != "0": raise RuntimeError(f"OKX REST error code={body.get('code')} msg={body.get('msg')}") data = body.get("data") or [] return data if isinstance(data, list) else [] def fetch_option_instruments(self, inst_family: str) -> list[dict[str, Any]]: rows = self._get( "/api/v5/public/instruments", {"instType": "OPTION", "instFamily": inst_family}, ) return [r for r in rows if str(r.get("state") or "").lower() == "live"] def fetch_index_ticker(self, inst_id: str) -> float | None: rows = self._get("/api/v5/market/index-tickers", {"instId": inst_id}) if not rows: return None return safe_float(rows[0].get("idxPx")) def fetch_index_at( self, inst_id: str, target_ts_ms: int ) -> tuple[float | None, int | None]: """ 用 1m 历史指数 K 线取最接近 target 的收盘价。 OKX: /api/v5/market/history-index-candles candle: [ts, o, h, l, c, confirm, ...] """ # before = 请求此时间戳之前的数据;取到期前后窗口 before = int(target_ts_ms) + 60_000 after = int(target_ts_ms) - 10 * 60_000 rows = self._get_raw( "/api/v5/market/history-index-candles", { "instId": inst_id, "bar": "1m", "before": str(before), "after": str(after), "limit": "20", }, ) best_px: float | None = None best_ts: int | None = None best_delta: int | None = None for row in rows: if not isinstance(row, (list, tuple)) or len(row) < 5: continue ts = safe_float(row[0]) close = safe_float(row[4]) if ts is None or close is None: continue ts_i = int(ts) delta = abs(ts_i - int(target_ts_ms)) if best_delta is None or delta < best_delta: best_delta = delta best_px = close best_ts = ts_i if best_delta is not None and best_delta > 5 * 60_000: return None, None return best_px, best_ts def fetch_books( self, inst_id: str, sz: int = 5 ) -> tuple[float | None, float | None, float | None, float | None, int | None]: """返回 ask, bid, ask_sz, bid_sz, ts_ms。""" rows = self._get( "/api/v5/market/books", {"instId": inst_id, "sz": str(max(1, min(int(sz), 400)))}, ) if not rows: return None, None, None, None, None row = rows[0] ts = safe_float(row.get("ts")) ts_ms = int(ts) if ts is not None else None asks = row.get("asks") or [] bids = row.get("bids") or [] ask = ask_sz = bid = bid_sz = None if asks and isinstance(asks[0], (list, tuple)) and len(asks[0]) >= 2: ask = safe_float(asks[0][0]) ask_sz = safe_float(asks[0][1]) if bids and isinstance(bids[0], (list, tuple)) and len(bids[0]) >= 2: bid = safe_float(bids[0][0]) bid_sz = safe_float(bids[0][1]) return ask, bid, ask_sz, bid_sz, ts_ms