first commit
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user