From c56326734ececd53cd8332f38b4655910cdf3741 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 3 Jun 2026 17:28:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(hub):=20=E4=BF=AE=E5=A4=8D=E8=A1=8C?= =?UTF-8?q?=E6=83=85=E5=8C=BA=20K=20=E7=BA=BF=20Gate=20=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E4=B8=8E=E5=9B=BE=E8=A1=A8=20unexpected=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate OHLCV 分页在接近当前时间时停止并容错 from>to;分页失败时用 limit 兜底。chart.js 为 priceFormat 增加整数 base,setData 失败时回退默认精度。 Co-authored-by: Cursor --- hub_kline_store.py | 8 +++++++- hub_ohlcv_lib.py | 31 +++++++++++++++++++++++++++--- manual_trading_hub/static/chart.js | 24 ++++++++++++++++++++--- tests/test_hub_ohlcv_lib.py | 28 +++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/hub_kline_store.py b/hub_kline_store.py index 78020d1..7b4ba8b 100644 --- a/hub_kline_store.py +++ b/hub_kline_store.py @@ -256,7 +256,13 @@ def resolve_chart_bars( if need_fetch: since = fetch_start_ms - if db_rows and not force_refresh and newest_ok and len(db_rows) >= need: + # 仅当库内根数已够且缺口在尾部时做增量拉取;否则全量回看,避免 Gate from>to + if ( + db_rows + and not force_refresh + and newest_ok + and len(db_rows) >= need + ): since = max(0, int(newest_db) - period_ms * 2) remote = remote_fetch( symbol=sym, diff --git a/hub_ohlcv_lib.py b/hub_ohlcv_lib.py index 90ce0f5..19cf201 100644 --- a/hub_ohlcv_lib.py +++ b/hub_ohlcv_lib.py @@ -275,18 +275,33 @@ def _paginate_fetch_ohlcv( else: since = max(0, int(time.time() * 1000) - want * period_ms) + now_ms = int(time.time() * 1000) guard = 0 prev_since = None while len(collected) < want and guard < 80: guard += 1 + if since >= now_ms: + break req_limit = min(chunk_max, want - len(collected)) - batch = exchange.fetch_ohlcv( - ex_sym, timeframe=tf, since=since, limit=req_limit - ) + try: + batch = exchange.fetch_ohlcv( + ex_sym, timeframe=tf, since=since, limit=req_limit + ) + except Exception as e: + err = str(e).lower() + if collected and ( + "from" in err + and "to" in err + or "invalid request parameter" in err + ): + break + raise if not batch: break collected.extend(batch) next_since = _next_since_from_batch(batch, period_ms) + if next_since >= now_ms: + break if prev_since is not None and next_since <= prev_since: break prev_since = since @@ -385,6 +400,16 @@ def fetch_ohlcv_for_hub( if len(merged) > want: merged = merged[-want:] + if not merged: + try: + tail = exchange.fetch_ohlcv( + ex_sym, timeframe=tf, limit=min(want, 300) + ) + merged = _bars_to_dicts(tail or []) + if len(merged) > want: + merged = merged[-want:] + except Exception: + pass if not merged: return {"ok": False, "msg": "交易所未返回 K 线"} diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index 8ce220b..fa97ab8 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -1013,7 +1013,18 @@ const minMove = tick != null && Number.isFinite(Number(tick)) && Number(tick) > 0 ? Number(tick) : 0.01; const precision = decimalsFromTick(minMove) ?? 2; - return { type: "price", precision: precision, minMove: minMove }; + const fmt = { type: "price", precision: precision, minMove: minMove }; + // 避免 minMove 浮点导致 lightweight-charts 报 "unexpected base" + if (minMove > 0 && minMove < 1) { + const inv = 1 / minMove; + if (Number.isFinite(inv) && inv >= 1 && inv <= 1e15) { + const base = Math.round(inv); + if (base > 0 && Math.abs(inv - base) / Math.max(inv, 1) < 1e-6) { + fmt.base = base; + } + } + } + return fmt; } function applyChartPriceFormat() { @@ -1507,8 +1518,15 @@ applyChartPriceFormat(); lastCandles = data.candles; indexCandles(lastCandles); - candleSeries.setData(lastCandles); - volumeSeries.setData(buildVolumeData(lastCandles)); + try { + candleSeries.setData(lastCandles); + volumeSeries.setData(buildVolumeData(lastCandles)); + } catch (setErr) { + priceTick = null; + applyChartPriceFormat(); + candleSeries.setData(lastCandles); + volumeSeries.setData(buildVolumeData(lastCandles)); + } applyChartRightGap(); if (resetView) { lastViewKey = vKey; diff --git a/tests/test_hub_ohlcv_lib.py b/tests/test_hub_ohlcv_lib.py index 5337da0..1580aa3 100644 --- a/tests/test_hub_ohlcv_lib.py +++ b/tests/test_hub_ohlcv_lib.py @@ -163,6 +163,34 @@ class TestHubOhlcvLib(unittest.TestCase): self.assertTrue(bars_spacing_matches_timeframe(bars, "12h")) self.assertEqual(ex.calls[0]["timeframe"], "1h") + def test_pagination_stops_when_next_since_reaches_now(self): + """Gate 等:分页 since 不得越过当前时间,避免 from>to。""" + from hub_ohlcv_lib import TIMEFRAME_MS + + step = TIMEFRAME_MS["1d"] + now_ms = int(__import__("time").time() * 1000) + # 最后一页最后一根 K 的 next_since 将 >= now_ms,应停止不再请求 + last_open = ((now_ms // step) - 2) * step + page = [ + [last_open - step, 1.0, 1.1, 0.9, 1.0, 10.0], + [last_open, 1.1, 1.2, 1.0, 1.1, 11.0], + ] + ex = _FakeExchange([page]) + + out = fetch_ohlcv_for_hub( + symbol="ONDO/USDT", + timeframe="1d", + since_ms=last_open - step * 5, + limit=10, + normalize_symbol_input=lambda s: str(s).strip().upper(), + normalize_exchange_symbol=lambda s: f"{s}:USDT" if ":" not in s else s, + ensure_markets_loaded=lambda: None, + exchange=ex, + ) + self.assertTrue(out.get("ok")) + self.assertGreaterEqual(len(out.get("bars") or []), 2) + self.assertLessEqual(len(ex.calls), 4) + def test_aggregate_ohlcv_bars_buckets(self): from hub_ohlcv_lib import TIMEFRAME_MS