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:
+3
-1
@@ -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 线;收益列红绿着色 |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}>` +
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user