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:
+7
-1
@@ -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
@@ -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 线"}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user