Fix amp-stats long-range candles via OKX history endpoints.

Recent candles cap near 60d; continue with history-index/history candles and color profit green/red.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-23 02:53:36 +08:00
parent b64c742fc9
commit 0a9e3aa95c
6 changed files with 101 additions and 7 deletions
+3 -1
View File
@@ -51,7 +51,8 @@
汇总必含:最大振幅(及日期)、开→高/开→低的最大与均值等。
K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / BTC-USD),失败再降级永续标记。
K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / BTC-USD),失败再降级永续标记。
近期 K 线接口约仅 **1440** 根(1H≈60 天);更长周期自动续拉 `history-index-candles` / `history-candles`
---
@@ -106,3 +107,4 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
| 2026-07-23 | 首版上线说明 |
| 2026-07-23 | 买跨对照:可设双边权利金、越过占比与收盘盈亏 |
| 2026-07-23 | 周末筛选/标注、止盈点(≥)、日表收益列 |
| 2026-07-23 | 长周期续拉 history K 线;收益列红绿着色 |
+45 -3
View File
@@ -40,7 +40,9 @@ PERIOD_DAYS: dict[str, int] = {
}
OKX_INDEX_CANDLES = "https://www.okx.com/api/v5/market/index-candles"
OKX_HISTORY_INDEX_CANDLES = "https://www.okx.com/api/v5/market/history-index-candles"
OKX_SWAP_CANDLES = "https://www.okx.com/api/v5/market/candles"
OKX_HISTORY_SWAP_CANDLES = "https://www.okx.com/api/v5/market/history-candles"
def normalize_symbol(raw: str) -> str:
@@ -408,8 +410,13 @@ def fetch_okx_candles(
bar: str = "1H",
client: Optional[httpx.Client] = None,
timeout: float = 30.0,
history_url: Optional[str] = None,
max_pages: int = 200,
) -> list[dict[str, Any]]:
"""拉取 [since_ms, until_ms] 覆盖的 K 线(含边界).OKX 返回新→旧."""
"""拉取 [since_ms, until_ms] 覆盖的 K 线(含边界).
OKX 近期接口约仅 1440 根;更早需 history_* 端点续拉.
"""
own = client is None
client = client or httpx.Client(
timeout=timeout,
@@ -419,25 +426,42 @@ def fetch_okx_candles(
try:
out: dict[int, dict[str, Any]] = {}
after: Optional[str] = None
for _ in range(80):
active_url = url
switched_history = False
empty_streak = 0
for _ in range(max(20, int(max_pages))):
params: dict[str, str] = {"instId": inst_id, "bar": bar, "limit": "100"}
if after:
params["after"] = after
r = client.get(url, params=params)
r = client.get(active_url, params=params)
r.raise_for_status()
body = r.json()
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
):
active_url = history_url
switched_history = True
empty_streak = 0
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:
@@ -447,7 +471,23 @@ def fetch_okx_candles(
break
if oldest_ts <= since_ms:
break
# 无新进度时避免死循环
if after is not None and str(oldest_ts) == after:
if history_url and not switched_history:
active_url = history_url
switched_history = True
continue
break
after = str(oldest_ts)
# 近期接口返回变少且仍未覆盖 since → 切历史
if (
history_url
and not switched_history
and len(data) < 100
and oldest_ts > since_ms
):
active_url = history_url
switched_history = True
return [out[k] for k in sorted(out.keys())]
finally:
if own:
@@ -471,6 +511,7 @@ def fetch_symbol_bars(
try:
bars = fetch_okx_candles(
url=OKX_INDEX_CANDLES,
history_url=OKX_HISTORY_INDEX_CANDLES,
inst_id=meta["index_inst"],
since_ms=since_ms,
until_ms=until_ms,
@@ -482,6 +523,7 @@ def fetch_symbol_bars(
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,
+1 -1
View File
@@ -178,7 +178,7 @@
const profit =
r.profit == null || r.profit === ""
? "—"
: `<span class="${pnlClass(r.profit)}">${esc(r.profit)}</span>`;
: `<span class="amp-pnl ${pnlClass(r.profit)}">${esc(r.profit)}</span>`;
const trClass = r.is_weekend ? ' class="amp-row-weekend"' : "";
return (
`<tr${trClass}>` +
+2
View File
@@ -10972,6 +10972,8 @@ html[data-theme="light"] .hub-logs-card-hint {
.amp-sum-v { font-size: 14px; font-weight: 600; color: var(--text); }
.amp-sum-v.is-pos { color: var(--green); }
.amp-sum-v.is-neg { color: var(--red); }
.amp-pnl.is-pos { color: var(--green); font-weight: 600; }
.amp-pnl.is-neg { color: var(--red); font-weight: 600; }
.amp-straddle { margin-bottom: 4px; }
.amp-table tr.amp-row-weekend td { background: rgba(255, 180, 60, 0.08); }
.amp-wd-tag {
+2 -2
View File
@@ -16,7 +16,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260723-amp-tp" />
<link rel="stylesheet" href="/assets/app.css?v=20260723-amp-hist" />
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
<script src="/assets/account_risk_badge.js?v=4"></script>
@@ -1508,7 +1508,7 @@
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
<script src="/assets/dashboard.js?v=20260720-dash-sl-tp"></script>
<script src="/assets/strategy.js?v=9"></script>
<script src="/assets/amp_stats.js?v=3"></script>
<script src="/assets/amp_stats.js?v=4"></script>
<script src="/assets/help.js?v=1"></script>
<script src="/assets/logs.js?v=1"></script>
<script src="/assets/ai_review_render.js?v=3"></script>
+48
View File
@@ -191,6 +191,54 @@ class AmpStatsLibTests(unittest.TestCase):
self.assertEqual(reframed["rows"][0]["profit"], -2)
self.assertIn("收益", build_export_csv(reframed))
def test_fetch_switches_to_history_endpoint(self):
"""近期接口到头后应切 history 续拉."""
from lib.hub.amp_stats_lib import fetch_okx_candles
calls: list[str] = []
class FakeResp:
def __init__(self, data):
self._data = data
def raise_for_status(self):
return None
def json(self):
return {"code": "0", "data": self._data}
class FakeClient:
def get(self, url, params=None):
calls.append(url)
after = (params or {}).get("after")
# recent: only 2 pages then empty; history continues
if "history" not in url:
if after is None:
return FakeResp([["2000", "1", "2", "0.5", "1.5"], ["1900", "1", "2", "0.5", "1.5"]])
if after == "1900":
return FakeResp([]) # recent exhausted
return FakeResp([])
# history
if after == "1900":
return FakeResp([["1800", "1", "2", "0.5", "1.5"], ["1000", "1", "2", "0.5", "1.5"]])
return FakeResp([])
def close(self):
return None
bars = fetch_okx_candles(
url="https://www.okx.com/api/v5/market/index-candles",
history_url="https://www.okx.com/api/v5/market/history-index-candles",
inst_id="ETH-USD",
since_ms=1000,
until_ms=3000,
client=FakeClient(),
max_pages=10,
)
self.assertTrue(any("history-index-candles" in u for u in calls))
self.assertGreaterEqual(len(bars), 3)
self.assertEqual(bars[0]["ts"], 1000)
def test_compute_with_mock_fetch(self):
now = datetime(2026, 7, 22, 18, 0, tzinfo=TZ)