fix(hub): 修复币安行情区 unexpected base 价格精度

normalize_price_tick 对齐 tick 为 10^-n;chart.js 使用整数 base 并在 applyOptions 失败时回退安全 priceFormat。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-03 17:36:51 +08:00
parent c56326734e
commit 2d8f65bf1d
3 changed files with 75 additions and 33 deletions
+48 -32
View File
@@ -1009,35 +1009,49 @@
return Math.max(0, Math.min(12, Math.round(-Math.log10(minMove))));
}
const SAFE_PRICE_FORMAT = { type: "price", precision: 4, minMove: 0.0001, base: 10000 };
function tickToPriceFormat(tick) {
const minMove =
tick != null && Number.isFinite(Number(tick)) && Number(tick) > 0 ? Number(tick) : 0.01;
const precision = decimalsFromTick(minMove) ?? 2;
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;
}
try {
if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) {
return { type: "price", precision: 2, minMove: 0.01, base: 100 };
}
const raw = Number(tick);
if (raw >= 1) {
return { type: "price", precision: 0, minMove: 1 };
}
let prec = decimalsFromTick(raw);
if (prec == null || prec < 0) prec = 4;
prec = Math.min(8, Math.max(0, Math.floor(prec)));
const base = Math.pow(10, prec);
if (!Number.isFinite(base) || base < 1 || base > 1e12) {
return SAFE_PRICE_FORMAT;
}
return { type: "price", precision: prec, minMove: 1 / base, base: base };
} catch (e) {
return SAFE_PRICE_FORMAT;
}
}
function applyPriceFormatToSeries(series, pf) {
if (!series || !series.applyOptions) return;
try {
series.applyOptions({ priceFormat: pf });
} catch (e) {
series.applyOptions({ priceFormat: SAFE_PRICE_FORMAT });
}
return fmt;
}
function applyChartPriceFormat() {
const pf = tickToPriceFormat(priceTick);
if (candleSeries && candleSeries.applyOptions) {
candleSeries.applyOptions({ priceFormat: pf });
}
if (indSeries.ema21 && indSeries.ema21.applyOptions) {
indSeries.ema21.applyOptions({ priceFormat: pf });
}
if (indSeries.ema55 && indSeries.ema55.applyOptions) {
indSeries.ema55.applyOptions({ priceFormat: pf });
let pf = SAFE_PRICE_FORMAT;
try {
pf = tickToPriceFormat(priceTick);
} catch (e) {
pf = SAFE_PRICE_FORMAT;
}
applyPriceFormatToSeries(candleSeries, pf);
applyPriceFormatToSeries(indSeries.ema21, pf);
applyPriceFormatToSeries(indSeries.ema55, pf);
if (chart) {
chart.applyOptions({
localization: {
@@ -1288,7 +1302,7 @@
wickDownColor: "#ff4d6d",
lastValueVisible: false,
priceLineVisible: false,
priceFormat: tickToPriceFormat(priceTick),
priceFormat: SAFE_PRICE_FORMAT,
};
if (typeof chart.addCandlestickSeries === "function") {
@@ -1515,18 +1529,16 @@
}
priceTick = data.price_tick;
applyChartPriceFormat();
lastCandles = data.candles;
indexCandles(lastCandles);
try {
candleSeries.setData(lastCandles);
volumeSeries.setData(buildVolumeData(lastCandles));
} catch (setErr) {
applyChartPriceFormat();
} catch (fmtErr) {
priceTick = null;
applyChartPriceFormat();
candleSeries.setData(lastCandles);
volumeSeries.setData(buildVolumeData(lastCandles));
}
lastCandles = data.candles;
indexCandles(lastCandles);
candleSeries.setData(lastCandles);
volumeSeries.setData(buildVolumeData(lastCandles));
applyChartRightGap();
if (resetView) {
lastViewKey = vKey;
@@ -1538,7 +1550,11 @@
updateVisibleRangeMarkers();
syncPosContextForView(exKey, sym);
showLatestOhlcv();
updateIndicators();
try {
updateIndicators();
} catch (indErr) {
/* 指标序列 priceFormat 异常时不阻断主图 */
}
scheduleChartResize();
const limit = data.limit || lastCandles.length;