From 9cf625c413d68f499cbc21f022119010d39ecb0b Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sat, 8 Aug 2026 17:06:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=B0=8F=E5=91=A8=E6=9C=9F?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E6=97=B6=E5=AF=B9=E9=BD=90=E6=A0=87=E8=AE=B0?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=20LWC=20Value=20is=20null?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主周期笔/KLC 分型标记在切到 1m/2m 主图时未对齐 K 线 time;过滤均线无效点并钳制视窗恢复。顺带统一 BI 中枢计算路径。 Co-authored-by: Cursor --- web/services/runtime/analyze.py | 4 +- web/static/js/app/chart_sync.js | 35 ++++++++- web/static/js/app/chart_tv_overlays.js | 101 +++++++++++++++++++++---- web/static/js/app/chart_tv_shell.js | 8 +- web/static/js/app/ui.js | 11 +-- web/static/js/charts.js | 26 +++++-- web/static/js/indicators.js | 5 +- web/templates/index.html | 12 +-- 8 files changed, 159 insertions(+), 43 deletions(-) diff --git a/web/services/runtime/analyze.py b/web/services/runtime/analyze.py index d0ce88b..db2db5c 100644 --- a/web/services/runtime/analyze.py +++ b/web/services/runtime/analyze.py @@ -25,8 +25,8 @@ def analyze_chan(df, symbol=None, timeframe=None): zs_list = chan.calculate_seg_zs(seg_list) # 计算笔中枢(BI中枢)并拍平成列表 - #bi_zs_list = chan.cal_bi_zs_list_pure(bi_list) - bi_zs_list = chan.cal_bi_zs(seg_list) + bi_zs_list = chan.cal_bi_zs_list_pure(bi_list) + #bi_zs_list = chan.cal_bi_zs(seg_list) bsp_list = [] if len(bi_zs_list) > 0: bsp_list = chan.find_all_bsp(bi_list, bi_zs_list) diff --git a/web/static/js/app/chart_sync.js b/web/static/js/app/chart_sync.js index cf80957..2a3ae2d 100644 --- a/web/static/js/app/chart_sync.js +++ b/web/static/js/app/chart_sync.js @@ -87,6 +87,21 @@ function updateTradingViewData() { }); } + // LWC 不允许 null/NaN;时间用整秒,避免 Line 渲染抛 Value is null + candles = (candles || []).filter(function (c) { + return c && c.time != null && + isFinite(Number(c.open)) && isFinite(Number(c.high)) && + isFinite(Number(c.low)) && isFinite(Number(c.close)); + }).map(function (c) { + return { + time: Math.floor(Number(c.time)), + open: Number(c.open), + high: Number(c.high), + low: Number(c.low), + close: Number(c.close) + }; + }); + const newBarCount = candles.length; const barDelta = (oldBarCount > 0 && newBarCount > 0) ? (newBarCount - oldBarCount) : 0; @@ -306,14 +321,28 @@ function updateTradingViewData() { const applyPosition = function (tag) { let ok = false; - if (lr && lr.from !== undefined && lr.to !== undefined) { + if (lr && lr.from !== undefined && lr.to !== undefined && newBarCount > 0) { + // 视窗超出当前 K 线数量时,LWC Line 绘制会抛 Value is null + const span = Math.max(1, lr.to - lr.from); + let to = lr.to; + let from = lr.from; + const maxTo = newBarCount - 1 + 8; + if (to > maxTo) { + to = maxTo; + from = to - span; + } + if (from < -8) { + from = -8; + to = from + span; + } + const clamped = { from: from, to: to }; charts.forEach(c => { try { - c.timeScale().setVisibleLogicalRange({ from: lr.from, to: lr.to }); + c.timeScale().setVisibleLogicalRange(clamped); ok = true; } catch (e) {} }); - if (ok) console.log('🔄 恢复位置 logical' + (tag || '') + ':', lr); + if (ok) console.log('🔄 恢复位置 logical' + (tag || '') + ':', clamped); } if (!ok && vr && vr.from !== undefined && vr.to !== undefined) { charts.forEach(c => { diff --git a/web/static/js/app/chart_tv_overlays.js b/web/static/js/app/chart_tv_overlays.js index beb421d..62ed7cd 100644 --- a/web/static/js/app/chart_tv_overlays.js +++ b/web/static/js/app/chart_tv_overlays.js @@ -1,5 +1,77 @@ /* chart_tv_overlays.js — structure zones / wyckoff / BSP / FX / bollinger */ +/** 标记 time 必须落在主 series 的 K 线 time 上,否则 LWC 会抛 Value is null */ +function alignMarkersToCandles(markers, candles) { + if (!Array.isArray(markers) || !markers.length) return []; + if (!Array.isArray(candles) || !candles.length) return []; + var times = []; + for (var i = 0; i < candles.length; i++) { + var ct = candles[i] && candles[i].time; + if (ct == null || !isFinite(Number(ct))) continue; + times.push(Math.floor(Number(ct))); + } + if (!times.length) return []; + var set = {}; + for (var j = 0; j < times.length; j++) set[times[j]] = true; + var nearest = function (target) { + var best = times[0]; + var bestDiff = Math.abs(best - target); + // 两端夹逼:大数据量时比全扫略好 + var lo = 0, hi = times.length - 1; + while (lo <= hi) { + var mid = (lo + hi) >> 1; + var t = times[mid]; + var d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + if (t < target) lo = mid + 1; + else hi = mid - 1; + } + if (lo < times.length) { + var d2 = Math.abs(times[lo] - target); + if (d2 < bestDiff) best = times[lo]; + } + if (hi >= 0) { + var d3 = Math.abs(times[hi] - target); + if (d3 < bestDiff) best = times[hi]; + } + return best; + }; + var out = []; + for (var k = 0; k < markers.length; k++) { + var m = markers[k]; + if (!m || m.time == null || !isFinite(Number(m.time))) continue; + var t0 = Math.floor(Number(m.time)); + var aligned = set[t0] ? t0 : nearest(t0); + var copy = Object.assign({}, m, { time: aligned }); + out.push(copy); + } + return out; +} + +function safeOverlayLineSetData(series, points) { + if (!series || typeof series.setData !== 'function' || !Array.isArray(points) || points.length < 2) return; + try { + var a = points[0], b = points[1]; + if (!a || !b || a.time == null || b.time == null) return; + var t0 = Math.floor(Number(a.time)); + var t1 = Math.floor(Number(b.time)); + var v0 = Number(a.value); + var v1 = Number(b.value); + if (!isFinite(t0) || !isFinite(t1) || !isFinite(v0) || !isFinite(v1)) return; + if (t0 === t1) { + // 竖边:同 time 两点 LWC 不接受,跳过(横边仍保留) + return; + } + if (t0 > t1) { + series.setData([{ time: t1, value: v1 }, { time: t0, value: v0 }]); + } else { + series.setData([{ time: t0, value: v0 }, { time: t1, value: v1 }]); + } + } catch (e) { + console.warn('叠层线 setData 跳过:', e && e.message ? e.message : e); + } +} + function chartTvRenderOverlays(ctx) { var symbol = ctx.symbol; var timeframe = ctx.timeframe; @@ -1642,7 +1714,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); const bottomSeries = mainChart.addLineSeries({ color: boxColor, @@ -1652,7 +1724,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); const leftSeries = mainChart.addLineSeries({ color: boxColor, @@ -1662,8 +1734,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - // 左边竖线:同一 time 上下两个点(和你已有ZS绘制写法保持一致) - leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); const rightSeries = mainChart.addLineSeries({ color: boxColor, @@ -1673,7 +1744,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = []; tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); @@ -1849,7 +1920,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); const bottomSeries = mainChart.addLineSeries({ color: boxColor, @@ -1859,7 +1930,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); const leftSeries = mainChart.addLineSeries({ color: boxColor, @@ -1869,7 +1940,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); const rightSeries = mainChart.addLineSeries({ color: boxColor, @@ -1879,7 +1950,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = []; tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); @@ -2002,7 +2073,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); const bottomSeries = mainChart.addLineSeries({ color: boxColor, @@ -2012,7 +2083,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); const leftSeries = mainChart.addLineSeries({ color: boxColor, @@ -2022,7 +2093,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); const rightSeries = mainChart.addLineSeries({ color: boxColor, @@ -2032,7 +2103,7 @@ function chartTvRenderOverlays(ctx) { priceLineVisible: false, crosshairMarkerVisible: false, }); - rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = []; tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); @@ -2180,7 +2251,7 @@ function chartTvRenderOverlays(ctx) { else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries; if (targetSeries) { try { - targetSeries.setMarkers(combinedMarkers); + targetSeries.setMarkers(alignMarkersToCandles(combinedMarkers, candles)); } catch (e) { console.warn('设置主系列标记失败(可能series已释放):', e); } @@ -2309,7 +2380,7 @@ function chartTvRenderOverlays(ctx) { else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries; if (targetSeries2) { try { - targetSeries2.setMarkers(onlyMainAndU); + targetSeries2.setMarkers(alignMarkersToCandles(onlyMainAndU, candles)); } catch (e) { console.warn('设置主系列标记失败(可能series已释放):', e); } diff --git a/web/static/js/app/chart_tv_shell.js b/web/static/js/app/chart_tv_shell.js index 8038930..97b53b5 100644 --- a/web/static/js/app/chart_tv_shell.js +++ b/web/static/js/app/chart_tv_shell.js @@ -38,7 +38,7 @@ function chartTvBuildShell(ctx) { } candles = klineDataSource.map((kline) => { const date = new Date(kline.date); - const timestamp = date.getTime() / 1000; + const timestamp = Math.floor(date.getTime() / 1000); return { time: timestamp, open: parseFloat(kline.open), @@ -46,7 +46,7 @@ function chartTvBuildShell(ctx) { low: parseFloat(kline.low), close: parseFloat(kline.close), }; - }); + }).filter((c) => isFinite(c.time) && isFinite(c.open) && isFinite(c.high) && isFinite(c.low) && isFinite(c.close)); } else { if (!currentData.kline_data || !Array.isArray(currentData.kline_data)) { console.error('主周期K线数据不存在或不是数组:', currentData.kline_data); @@ -54,7 +54,7 @@ function chartTvBuildShell(ctx) { } candles = currentData.kline_data.map((kline) => { const date = new Date(kline.date); - const timestamp = date.getTime() / 1000; + const timestamp = Math.floor(date.getTime() / 1000); return { time: timestamp, open: parseFloat(kline.open), @@ -62,7 +62,7 @@ function chartTvBuildShell(ctx) { low: parseFloat(kline.low), close: parseFloat(kline.close), }; - }); + }).filter((c) => isFinite(c.time) && isFinite(c.open) && isFinite(c.high) && isFinite(c.low) && isFinite(c.close)); } // 根据交易对类型过滤数据(仅用于显示优化) diff --git a/web/static/js/app/ui.js b/web/static/js/app/ui.js index 3e5d3aa..ddf3429 100644 --- a/web/static/js/app/ui.js +++ b/web/static/js/app/ui.js @@ -874,14 +874,14 @@ $('#showElementMacdDiv').change(function() { refreshChartOnly(); }); -// 绑定分型类型显示开关 +// 绑定分型类型显示开关(与笔一致:全量重建,避免增量路径标记未对齐) $('#showKlcFxType').change(function() { - refreshChartOnly(); + updateChartDisplay(); }); // 绑定小周期分型显示开关 $('#showElementKlcFxType').change(function() { - refreshChart(currentData); + updateChartDisplay(); }); @@ -894,10 +894,7 @@ $('#showElementBollinger').change(function() { updateChartDisplay(); }); -// 绑定K线周期切换 -$('input[name="klinePeriod"]').change(function() { - refreshChart(currentData); -}); +// K线周期切换由 macd_ui.js 统一走 updateChartDisplay(勿再绑 refreshChart,会重复且易漏对齐) // 绑定主图U显示开关 $('#toggleUOnMain').change(function() { diff --git a/web/static/js/charts.js b/web/static/js/charts.js index 53ac532..835efdf 100644 --- a/web/static/js/charts.js +++ b/web/static/js/charts.js @@ -4,6 +4,16 @@ window.App.Charts = (function() { // 依赖 Indicators const Indicators = (window.App && window.App.Indicators) || {}; + function sanitizeLinePoints(points) { + if (!Array.isArray(points)) return []; + return points.filter(function (p) { + return p && p.time != null && p.value != null && + isFinite(Number(p.time)) && isFinite(Number(p.value)); + }).map(function (p) { + return { time: Math.floor(Number(p.time)), value: Number(p.value) }; + }); + } + function addMovingAveragesToChart(candleData) { if (!window.tvWidget || !tvWidget.mainChart || !candleData || candleData.length === 0) return; if (!window.movingAverages) return; @@ -21,6 +31,8 @@ window.App.Charts = (function() { try { const maData = Indicators.calculateMA(candleData, maConfig.type, maConfig.length, maConfig.source); const smoothedData = maConfig.smoothType !== 'none' ? (window.applySmoothToMA ? window.applySmoothToMA(maData, maConfig.smoothType, maConfig.smoothLength) : maData) : maData; + const cleanData = sanitizeLinePoints(smoothedData); + if (!cleanData.length) return; const maSeries = tvWidget.mainChart.addLineSeries({ color: maConfig.color, lineWidth: maConfig.lineWidth || 2, @@ -30,8 +42,8 @@ window.App.Charts = (function() { priceLineVisible: false, crosshairMarkerVisible: true, }); - maSeries.setData(smoothedData); - maConfig.data = smoothedData; + maSeries.setData(cleanData); + maConfig.data = cleanData; tvWidget.series.maSeries.push(maSeries); } catch(e) {} }); @@ -51,12 +63,16 @@ window.App.Charts = (function() { if (!bbConfig.visible) return; try { const bbData = Indicators.calculateBB(candleData, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, bbConfig.source); + const upper = sanitizeLinePoints(bbData.map(item => ({ time: item.time, value: item.upper }))); + const middle = sanitizeLinePoints(bbData.map(item => ({ time: item.time, value: item.middle }))); + const lower = sanitizeLinePoints(bbData.map(item => ({ time: item.time, value: item.lower }))); + if (!upper.length || !middle.length || !lower.length) return; const upperSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.upperColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true }); const middleSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.middleColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true }); const lowerSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.lowerColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true }); - upperSeries.setData(bbData.map(item => ({ time: item.time, value: item.upper }))); - middleSeries.setData(bbData.map(item => ({ time: item.time, value: item.middle }))); - lowerSeries.setData(bbData.map(item => ({ time: item.time, value: item.lower }))); + upperSeries.setData(upper); + middleSeries.setData(middle); + lowerSeries.setData(lower); bbConfig.data = bbData; tvWidget.series.bbSeries.push(upperSeries, middleSeries, lowerSeries); } catch(e) {} diff --git a/web/static/js/indicators.js b/web/static/js/indicators.js index 6085042..9aef74b 100644 --- a/web/static/js/indicators.js +++ b/web/static/js/indicators.js @@ -63,7 +63,10 @@ window.App.Indicators = (function() { default: value = sourceData[i]; } - result.push({ time: data[i].time, value }); + if (value == null || !isFinite(value) || data[i].time == null || !isFinite(Number(data[i].time))) { + continue; + } + result.push({ time: Math.floor(Number(data[i].time)), value: Number(value) }); } return result; } diff --git a/web/templates/index.html b/web/templates/index.html index 128bba3..427cca8 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -22,8 +22,8 @@ - - + + - + - + - + - +