fix(hub): 修复行情区 K 线 Gate 分页与图表 unexpected base

Gate OHLCV 分页在接近当前时间时停止并容错 from>to;分页失败时用 limit 兜底。chart.js 为 priceFormat 增加整数 base,setData 失败时回退默认精度。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-03 17:28:42 +08:00
parent e2bf58cfd3
commit c56326734e
4 changed files with 84 additions and 7 deletions
+7 -1
View File
@@ -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,
+28 -3
View File
@@ -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 线"}
+21 -3
View File
@@ -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;
+28
View File
@@ -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