diff --git a/docs/振幅统计说明.md b/docs/振幅统计说明.md index 4ed6d23..a29538a 100644 --- a/docs/振幅统计说明.md +++ b/docs/振幅统计说明.md @@ -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 线;收益列红绿着色 | diff --git a/lib/hub/amp_stats_lib.py b/lib/hub/amp_stats_lib.py index 7e13c5c..4b349d3 100644 --- a/lib/hub/amp_stats_lib.py +++ b/lib/hub/amp_stats_lib.py @@ -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, diff --git a/manual_trading_hub/static/amp_stats.js b/manual_trading_hub/static/amp_stats.js index 6707f38..2c7a179 100644 --- a/manual_trading_hub/static/amp_stats.js +++ b/manual_trading_hub/static/amp_stats.js @@ -178,7 +178,7 @@ const profit = r.profit == null || r.profit === "" ? "—" - : `${esc(r.profit)}`; + : `${esc(r.profit)}`; const trClass = r.is_weekend ? ' class="amp-row-weekend"' : ""; return ( `` + diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index c89d53e..b8bdc67 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -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 { diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 217e1dd..e9cbbe0 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -16,7 +16,7 @@ - + @@ -1508,7 +1508,7 @@ - + diff --git a/tests/test_amp_stats_lib.py b/tests/test_amp_stats_lib.py index a56bfe1..a9cdc37 100644 --- a/tests/test_amp_stats_lib.py +++ b/tests/test_amp_stats_lib.py @@ -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)