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:
|
if need_fetch:
|
||||||
since = fetch_start_ms
|
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)
|
since = max(0, int(newest_db) - period_ms * 2)
|
||||||
remote = remote_fetch(
|
remote = remote_fetch(
|
||||||
symbol=sym,
|
symbol=sym,
|
||||||
|
|||||||
+28
-3
@@ -275,18 +275,33 @@ def _paginate_fetch_ohlcv(
|
|||||||
else:
|
else:
|
||||||
since = max(0, int(time.time() * 1000) - want * period_ms)
|
since = max(0, int(time.time() * 1000) - want * period_ms)
|
||||||
|
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
guard = 0
|
guard = 0
|
||||||
prev_since = None
|
prev_since = None
|
||||||
while len(collected) < want and guard < 80:
|
while len(collected) < want and guard < 80:
|
||||||
guard += 1
|
guard += 1
|
||||||
|
if since >= now_ms:
|
||||||
|
break
|
||||||
req_limit = min(chunk_max, want - len(collected))
|
req_limit = min(chunk_max, want - len(collected))
|
||||||
batch = exchange.fetch_ohlcv(
|
try:
|
||||||
ex_sym, timeframe=tf, since=since, limit=req_limit
|
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:
|
if not batch:
|
||||||
break
|
break
|
||||||
collected.extend(batch)
|
collected.extend(batch)
|
||||||
next_since = _next_since_from_batch(batch, period_ms)
|
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:
|
if prev_since is not None and next_since <= prev_since:
|
||||||
break
|
break
|
||||||
prev_since = since
|
prev_since = since
|
||||||
@@ -385,6 +400,16 @@ def fetch_ohlcv_for_hub(
|
|||||||
if len(merged) > want:
|
if len(merged) > want:
|
||||||
merged = 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:
|
if not merged:
|
||||||
return {"ok": False, "msg": "交易所未返回 K 线"}
|
return {"ok": False, "msg": "交易所未返回 K 线"}
|
||||||
|
|
||||||
|
|||||||
@@ -1013,7 +1013,18 @@
|
|||||||
const minMove =
|
const minMove =
|
||||||
tick != null && Number.isFinite(Number(tick)) && Number(tick) > 0 ? Number(tick) : 0.01;
|
tick != null && Number.isFinite(Number(tick)) && Number(tick) > 0 ? Number(tick) : 0.01;
|
||||||
const precision = decimalsFromTick(minMove) ?? 2;
|
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() {
|
function applyChartPriceFormat() {
|
||||||
@@ -1507,8 +1518,15 @@
|
|||||||
applyChartPriceFormat();
|
applyChartPriceFormat();
|
||||||
lastCandles = data.candles;
|
lastCandles = data.candles;
|
||||||
indexCandles(lastCandles);
|
indexCandles(lastCandles);
|
||||||
candleSeries.setData(lastCandles);
|
try {
|
||||||
volumeSeries.setData(buildVolumeData(lastCandles));
|
candleSeries.setData(lastCandles);
|
||||||
|
volumeSeries.setData(buildVolumeData(lastCandles));
|
||||||
|
} catch (setErr) {
|
||||||
|
priceTick = null;
|
||||||
|
applyChartPriceFormat();
|
||||||
|
candleSeries.setData(lastCandles);
|
||||||
|
volumeSeries.setData(buildVolumeData(lastCandles));
|
||||||
|
}
|
||||||
applyChartRightGap();
|
applyChartRightGap();
|
||||||
if (resetView) {
|
if (resetView) {
|
||||||
lastViewKey = vKey;
|
lastViewKey = vKey;
|
||||||
|
|||||||
@@ -163,6 +163,34 @@ class TestHubOhlcvLib(unittest.TestCase):
|
|||||||
self.assertTrue(bars_spacing_matches_timeframe(bars, "12h"))
|
self.assertTrue(bars_spacing_matches_timeframe(bars, "12h"))
|
||||||
self.assertEqual(ex.calls[0]["timeframe"], "1h")
|
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):
|
def test_aggregate_ohlcv_bars_buckets(self):
|
||||||
from hub_ohlcv_lib import TIMEFRAME_MS
|
from hub_ohlcv_lib import TIMEFRAME_MS
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user