Throttle OKX amp-stats candle pagination and retry on 429.

Add page pauses, exponential backoff, and cooldown before swap fallback to avoid rate limits.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-23 02:57:24 +08:00
parent 0a9e3aa95c
commit 910c938d0a
5 changed files with 142 additions and 35 deletions
+77 -25
View File
@@ -7,6 +7,7 @@ from __future__ import annotations
import csv
import io
import statistics
import time
from datetime import date, datetime, timedelta
from typing import Any, Callable, Optional
from zoneinfo import ZoneInfo
@@ -401,6 +402,51 @@ def _parse_okx_candle_row(row: list) -> Optional[dict[str, Any]]:
return {"ts": ts, "o": o, "h": h, "l": l, "c": c}
def _okx_get_json(
client: httpx.Client,
url: str,
params: dict[str, str],
*,
retries: int = 8,
) -> dict[str, Any]:
"""GET OKX 公共行情;遇 429 指数退避重试."""
last_err: Optional[BaseException] = None
for attempt in range(max(1, int(retries))):
try:
r = client.get(url, params=params)
if r.status_code == 429:
wait = min(12.0, 0.7 * (2**attempt))
time.sleep(wait)
last_err = httpx.HTTPStatusError(
f"429 Too Many Requests for url '{r.url}'",
request=r.request,
response=r,
)
continue
r.raise_for_status()
body = r.json()
if not isinstance(body, dict):
raise RuntimeError("OKX 返回非对象 JSON")
return body
except httpx.HTTPStatusError as exc:
status = exc.response.status_code if exc.response is not None else None
if status == 429 and attempt + 1 < retries:
wait = min(12.0, 0.7 * (2**attempt))
time.sleep(wait)
last_err = exc
continue
raise
except httpx.TransportError as exc:
if attempt + 1 < retries:
time.sleep(min(8.0, 0.5 * (2**attempt)))
last_err = exc
continue
raise
if last_err is not None:
raise last_err
raise RuntimeError("OKX 请求失败")
def fetch_okx_candles(
*,
url: str,
@@ -412,10 +458,13 @@ def fetch_okx_candles(
timeout: float = 30.0,
history_url: Optional[str] = None,
max_pages: int = 200,
page_pause_sec: float = 0.12,
history_page_pause_sec: float = 0.22,
) -> list[dict[str, Any]]:
"""拉取 [since_ms, until_ms] 覆盖的 K 线(含边界).
OKX 近期接口约仅 1440 根;更早需 history_* 端点续拉.
分页带间隔,429 自动退避重试.
"""
own = client is None
client = client or httpx.Client(
@@ -428,40 +477,33 @@ def fetch_okx_candles(
after: Optional[str] = None
active_url = url
switched_history = False
empty_streak = 0
for _ in range(max(20, int(max_pages))):
for page_i in range(max(20, int(max_pages))):
if page_i > 0:
pause = history_page_pause_sec if switched_history or "history" in active_url else page_pause_sec
if pause > 0:
time.sleep(pause)
params: dict[str, str] = {"instId": inst_id, "bar": bar, "limit": "100"}
if after:
params["after"] = after
r = client.get(active_url, params=params)
r.raise_for_status()
body = r.json()
body = _okx_get_json(client, active_url, params)
if str(body.get("code") or "") not in ("0", "0.0", ""):
raise RuntimeError(body.get("msg") or f"OKX error {body.get('code')}")
data = body.get("data") or []
if not data:
empty_streak += 1
# 近期接口到头 → 切历史端点再试
if (
history_url
and not switched_history
and after is not None
):
if history_url and not switched_history and after is not None:
active_url = history_url
switched_history = True
empty_streak = 0
time.sleep(max(history_page_pause_sec, 0.35))
continue
break
empty_streak = 0
oldest_ts = None
newest_in_page = None
for row in data:
parsed = _parse_okx_candle_row(row)
if not parsed:
continue
ts = int(parsed["ts"])
oldest_ts = ts if oldest_ts is None else min(oldest_ts, ts)
newest_in_page = ts if newest_in_page is None else max(newest_in_page, ts)
if ts < since_ms - 3600 * 1000:
continue
if ts > until_ms + 3600 * 1000:
@@ -476,6 +518,7 @@ def fetch_okx_candles(
if history_url and not switched_history:
active_url = history_url
switched_history = True
time.sleep(max(history_page_pause_sec, 0.35))
continue
break
after = str(oldest_ts)
@@ -488,6 +531,7 @@ def fetch_okx_candles(
):
active_url = history_url
switched_history = True
time.sleep(max(history_page_pause_sec, 0.35))
return [out[k] for k in sorted(out.keys())]
finally:
if own:
@@ -508,6 +552,7 @@ def fetch_symbol_bars(
bars = fetch_fn(inst_id=meta["index_inst"], since_ms=since_ms, until_ms=until_ms)
return bars, f"okx_index:{meta['index_inst']}", meta["index_inst"]
index_err: Optional[BaseException] = None
try:
bars = fetch_okx_candles(
url=OKX_INDEX_CANDLES,
@@ -518,18 +563,25 @@ def fetch_symbol_bars(
)
if bars:
return bars, f"okx_index:{meta['index_inst']}", meta["index_inst"]
except Exception:
bars = []
except Exception as exc:
index_err = exc
# 指数侧已触发限频时先冷却,再降级永续,避免连环 429
time.sleep(1.2)
bars = fetch_okx_candles(
url=OKX_SWAP_CANDLES,
history_url=OKX_HISTORY_SWAP_CANDLES,
inst_id=meta["swap_inst"],
since_ms=since_ms,
until_ms=until_ms,
)
try:
bars = fetch_okx_candles(
url=OKX_SWAP_CANDLES,
history_url=OKX_HISTORY_SWAP_CANDLES,
inst_id=meta["swap_inst"],
since_ms=since_ms,
until_ms=until_ms,
)
except Exception as exc:
detail = f"index={index_err}; swap={exc}" if index_err else str(exc)
raise RuntimeError(f"OKX K线拉取失败({detail})") from exc
if not bars:
raise RuntimeError("OKX 指数与永续 K 线均无数据")
detail = f"index={index_err}" if index_err else "empty"
raise RuntimeError(f"OKX 指数与永续 K 线均无数据({detail})")
return bars, f"okx_swap:{meta['swap_inst']}", meta["swap_inst"]