135cdc13ba
Avoid load_markets hangs on weak networks; enable login-required sim defaults and document Baota Nginx reverse proxy. Co-authored-by: Cursor <cursoragent@cursor.com>
168 lines
4.9 KiB
Python
168 lines
4.9 KiB
Python
"""OKX 公共 REST 轻量拉取(绕开 ccxt load_markets).
|
|
|
|
部分线路上 Python requests 偶发连 www.okx.com 超时,而 curl -4 仍可用;
|
|
对冲页标记价/指数优先走本模块,避免整页空白.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import urllib.parse
|
|
import urllib.request
|
|
from typing import Any, Optional
|
|
|
|
|
|
OKX_BASE = "https://www.okx.com"
|
|
|
|
|
|
def _curl_get_json(url: str, *, timeout_sec: float = 8.0) -> dict[str, Any]:
|
|
proc = subprocess.run(
|
|
[
|
|
"curl",
|
|
"-4",
|
|
"-fsS",
|
|
"--connect-timeout",
|
|
str(max(1, int(timeout_sec))),
|
|
"--max-time",
|
|
str(max(2, int(timeout_sec + 2))),
|
|
url,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=max(3.0, timeout_sec + 3.0),
|
|
check=False,
|
|
)
|
|
if proc.returncode != 0:
|
|
raise RuntimeError((proc.stderr or proc.stdout or "curl failed").strip()[:240])
|
|
return json.loads(proc.stdout or "{}")
|
|
|
|
|
|
def _urllib_get_json(url: str, *, timeout_sec: float = 8.0) -> dict[str, Any]:
|
|
req = urllib.request.Request(url, headers={"User-Agent": "CoinOX/1.0"})
|
|
with urllib.request.urlopen(req, timeout=timeout_sec) as resp:
|
|
raw = resp.read().decode("utf-8", errors="replace")
|
|
return json.loads(raw or "{}")
|
|
|
|
|
|
def okx_public_get(
|
|
path: str,
|
|
params: Optional[dict[str, Any]] = None,
|
|
*,
|
|
timeout_sec: float = 8.0,
|
|
) -> dict[str, Any]:
|
|
q = urllib.parse.urlencode({k: v for k, v in (params or {}).items() if v is not None})
|
|
url = f"{OKX_BASE}{path}"
|
|
if q:
|
|
url = f"{url}?{q}"
|
|
last_err: Exception | None = None
|
|
for getter in (_curl_get_json, _urllib_get_json):
|
|
try:
|
|
data = getter(url, timeout_sec=timeout_sec)
|
|
if isinstance(data, dict):
|
|
return data
|
|
except Exception as e:
|
|
last_err = e
|
|
continue
|
|
raise RuntimeError(str(last_err or "okx public get failed"))
|
|
|
|
|
|
def _sf(v: Any) -> Optional[float]:
|
|
if v is None or v == "":
|
|
return None
|
|
try:
|
|
x = float(v)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
return x if x > 0 else None
|
|
|
|
|
|
def fetch_swap_mark_px(base: str) -> Optional[float]:
|
|
u = (base or "ETH").strip().upper() or "ETH"
|
|
try:
|
|
payload = okx_public_get(
|
|
"/api/v5/public/mark-price",
|
|
{"instId": f"{u}-USDT-SWAP"},
|
|
timeout_sec=8.0,
|
|
)
|
|
rows = payload.get("data") or []
|
|
if rows:
|
|
return _sf(rows[0].get("markPx"))
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def fetch_swap_ticker(base: str) -> dict[str, Any]:
|
|
"""返回 ETH/BTC USDT 永续公共 ticker 行(补齐 markPx)."""
|
|
u = (base or "ETH").strip().upper() or "ETH"
|
|
payload = okx_public_get(
|
|
"/api/v5/market/ticker",
|
|
{"instId": f"{u}-USDT-SWAP"},
|
|
timeout_sec=8.0,
|
|
)
|
|
rows = payload.get("data") or []
|
|
if not rows:
|
|
raise RuntimeError(payload.get("msg") or "ticker empty")
|
|
row = dict(rows[0])
|
|
if not _sf(row.get("markPx")):
|
|
mp = fetch_swap_mark_px(u)
|
|
if mp:
|
|
row["markPx"] = str(mp)
|
|
elif _sf(row.get("last")):
|
|
row["markPx"] = row.get("last")
|
|
return row
|
|
|
|
|
|
def fetch_index_px(uly: str) -> Optional[float]:
|
|
u = (uly or "ETH").strip().upper() or "ETH"
|
|
candidates = [u] if "-" in u else [f"{u}-USD", f"{u}-USDT"]
|
|
for inst in candidates:
|
|
try:
|
|
payload = okx_public_get(
|
|
"/api/v5/market/index-tickers",
|
|
{"instId": inst},
|
|
timeout_sec=8.0,
|
|
)
|
|
rows = payload.get("data") or []
|
|
if not rows:
|
|
continue
|
|
v = _sf(rows[0].get("idxPx"))
|
|
if v:
|
|
return v
|
|
except Exception:
|
|
continue
|
|
try:
|
|
row = fetch_swap_ticker(u.split("-")[0])
|
|
return _sf(row.get("markPx")) or _sf(row.get("last"))
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def fetch_option_instruments_public(inst_family: str) -> list[dict[str, Any]]:
|
|
family = str(inst_family or "").strip()
|
|
if not family:
|
|
return []
|
|
payload = okx_public_get(
|
|
"/api/v5/public/instruments",
|
|
{"instType": "OPTION", "instFamily": family},
|
|
timeout_sec=12.0,
|
|
)
|
|
rows = payload.get("data") or []
|
|
return [r for r in rows if isinstance(r, dict) and r.get("state") == "live"]
|
|
|
|
|
|
def fetch_option_tickers_public(inst_family: str) -> dict[str, dict[str, Any]]:
|
|
family = str(inst_family or "").strip()
|
|
if not family:
|
|
return {}
|
|
payload = okx_public_get(
|
|
"/api/v5/market/tickers",
|
|
{"instType": "OPTION", "instFamily": family},
|
|
timeout_sec=12.0,
|
|
)
|
|
out: dict[str, dict[str, Any]] = {}
|
|
for r in payload.get("data") or []:
|
|
if isinstance(r, dict) and r.get("instId"):
|
|
out[str(r["instId"])] = r
|
|
return out
|