diff --git a/web/static/js/app/chart_sync.js b/web/static/js/app/chart_sync.js index 2a3ae2d..11c7bb9 100644 --- a/web/static/js/app/chart_sync.js +++ b/web/static/js/app/chart_sync.js @@ -365,6 +365,15 @@ function updateTradingViewData() { applyPosition(''); setTimeout(function () { applyPosition('@0'); }, 0); setTimeout(function () { applyPosition('@50'); }, 50); + // 增量 setData 常不触发可见时间范围回调,但价格轴会变:补刷分型竖边 + var bumpFxVert = function () { + if (typeof window._redrawFxBoxVerticalOverlay === 'function') { + window._redrawFxBoxVerticalOverlay(); + } + }; + bumpFxVert(); + setTimeout(bumpFxVert, 0); + setTimeout(bumpFxVert, 50); } console.log('增量更新图表完成'); diff --git a/web/static/js/app/chart_tv_overlays.js b/web/static/js/app/chart_tv_overlays.js index 62ed7cd..be114e9 100644 --- a/web/static/js/app/chart_tv_overlays.js +++ b/web/static/js/app/chart_tv_overlays.js @@ -58,10 +58,8 @@ function safeOverlayLineSetData(series, points) { 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; - } + // 竖边不用折线(任意时间差都会斜),改走 canvas + if (t0 === t1) return; if (t0 > t1) { series.setData([{ time: t1, value: v1 }, { time: t0, value: v0 }]); } else { @@ -72,7 +70,147 @@ function safeOverlayLineSetData(series, points) { } } +function pushFxBoxVertical(time, lo, hi, color) { + if (!window._fxBoxVerticals) window._fxBoxVerticals = []; + var t = Math.floor(Number(time)); + var a = Number(lo), b = Number(hi); + if (!isFinite(t) || !isFinite(a) || !isFinite(b) || a === b) return; + window._fxBoxVerticals.push({ + time: t, + lo: Math.min(a, b), + hi: Math.max(a, b), + color: color || '#888' + }); +} + +function getMainPriceSeries() { + if (!window.tvWidget || !tvWidget.series) return null; + var s = tvWidget.series; + return s.candleSeries || s.klcSeries || s.barSeries || s.heikinSeries || s.renkoSeries || + s.lineSeries || s.areaSeries || s.baselineSeries || null; +} + +function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { + if (!mainChart || !mainChartContainer) return; + if (typeof window._fxBoxOverlayCleanup === 'function') { + try { window._fxBoxOverlayCleanup(); } catch (e) {} + window._fxBoxOverlayCleanup = null; + } + var canvas = mainChartContainer.querySelector('.fx-box-vert-overlay'); + if (!canvas) { + canvas = document.createElement('canvas'); + canvas.className = 'fx-box-vert-overlay'; + canvas.style.cssText = 'position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;z-index:6;'; + if (getComputedStyle(mainChartContainer).position === 'static') { + mainChartContainer.style.position = 'relative'; + } + mainChartContainer.appendChild(canvas); + } + var lastSig = ''; + var watchRaf = null; + var cleaned = false; + var redrawPending = false; + var quant = function (v) { + if (v == null || !isFinite(Number(v))) return 'n'; + return String(Math.round(Number(v))); + }; + // LWC 4 无 priceScale 订阅:采样坐标变化(含增量 setData 后自动缩放) + var sampleSig = function () { + var boxes = window._fxBoxVerticals || []; + var series = getMainPriceSeries(); + if (!series || !boxes.length) return '0'; + var ts = mainChart.timeScale(); + var a = boxes[0]; + var b = boxes[boxes.length - 1]; + return [ + boxes.length, + quant(ts.timeToCoordinate(a.time)), + quant(series.priceToCoordinate(a.hi)), + quant(series.priceToCoordinate(a.lo)), + quant(ts.timeToCoordinate(b.time)), + quant(series.priceToCoordinate(b.hi)), + quant(series.priceToCoordinate(b.lo)) + ].join('|'); + }; + var redraw = function () { + var boxes = window._fxBoxVerticals || []; + var series = getMainPriceSeries(); + var rect = mainChartContainer.getBoundingClientRect(); + var dpr = window.devicePixelRatio || 1; + canvas.width = Math.max(1, Math.floor(rect.width * dpr)); + canvas.height = Math.max(1, Math.floor(rect.height * dpr)); + canvas.style.width = rect.width + 'px'; + canvas.style.height = rect.height + 'px'; + var ctx2 = canvas.getContext('2d'); + if (!ctx2) return; + ctx2.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx2.clearRect(0, 0, rect.width, rect.height); + if (!series || !boxes.length) { + lastSig = sampleSig(); + return; + } + var ts = mainChart.timeScale(); + for (var i = 0; i < boxes.length; i++) { + var box = boxes[i]; + var x = ts.timeToCoordinate(box.time); + var y1 = series.priceToCoordinate(box.hi); + var y2 = series.priceToCoordinate(box.lo); + if (x == null || y1 == null || y2 == null) continue; + ctx2.beginPath(); + ctx2.strokeStyle = box.color; + ctx2.lineWidth = 1; + ctx2.setLineDash([4, 3]); + ctx2.moveTo(Math.round(x) + 0.5, y1); + ctx2.lineTo(Math.round(x) + 0.5, y2); + ctx2.stroke(); + } + ctx2.setLineDash([]); + lastSig = sampleSig(); + }; + var scheduleRedraw = function () { + if (cleaned || redrawPending) return; + redrawPending = true; + requestAnimationFrame(function () { + redrawPending = false; + if (!cleaned) redraw(); + }); + }; + var watch = function () { + if (cleaned) return; + watchRaf = requestAnimationFrame(watch); + var sig = sampleSig(); + if (sig !== lastSig) scheduleRedraw(); + }; + try { mainChart.timeScale().subscribeVisibleLogicalRangeChange(scheduleRedraw); } catch (e) {} + try { mainChart.timeScale().subscribeVisibleTimeRangeChange(scheduleRedraw); } catch (e) {} + var ro = null; + if (typeof ResizeObserver !== 'undefined') { + ro = new ResizeObserver(scheduleRedraw); + ro.observe(mainChartContainer); + } + window._redrawFxBoxVerticalOverlay = scheduleRedraw; + window._fxBoxOverlayCleanup = function () { + if (cleaned) return; + cleaned = true; + if (watchRaf != null) { + try { cancelAnimationFrame(watchRaf); } catch (e) {} + watchRaf = null; + } + window._redrawFxBoxVerticalOverlay = null; + try { mainChart.timeScale().unsubscribeVisibleLogicalRangeChange(scheduleRedraw); } catch (e) {} + try { mainChart.timeScale().unsubscribeVisibleTimeRangeChange(scheduleRedraw); } catch (e) {} + if (ro) try { ro.disconnect(); } catch (e) {} + try { if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas); } catch (e) {} + }; + if (!window._tvInitCleanups) window._tvInitCleanups = []; + window._tvInitCleanups.push(window._fxBoxOverlayCleanup); + scheduleRedraw(); + setTimeout(scheduleRedraw, 50); + watchRaf = requestAnimationFrame(watch); +} + function chartTvRenderOverlays(ctx) { + window._fxBoxVerticals = []; var symbol = ctx.symbol; var timeframe = ctx.timeframe; var symbolConfig = ctx.symbolConfig; @@ -1726,28 +1864,11 @@ function chartTvRenderOverlays(ctx) { }); safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); - const leftSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); - - const rightSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, // 虚线 - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor); + pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor); if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = []; - tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries); } } @@ -1932,28 +2053,11 @@ function chartTvRenderOverlays(ctx) { }); safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); - const leftSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); - - const rightSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor); + pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor); if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = []; - tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries); } } @@ -2085,28 +2189,11 @@ function chartTvRenderOverlays(ctx) { }); safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); - const leftSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); - - const rightSeries = mainChart.addLineSeries({ - color: boxColor, - lineWidth: 1, - lineStyle: 2, - lastValueVisible: false, - priceLineVisible: false, - crosshairMarkerVisible: false, - }); - safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor); + pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor); if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = []; - tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries); } } } catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); } @@ -2409,4 +2496,11 @@ function chartTvRenderOverlays(ctx) { } } } + + // KLC 分型框竖边:canvas 真竖线(LWC 折线做不到不斜) + try { + syncFxBoxVerticalOverlay(mainChart, mainChartContainer); + } catch (e) { + console.warn('分型竖边 overlay 失败:', e); + } } diff --git a/web/static/js/app/chart_view.js b/web/static/js/app/chart_view.js index 569201f..0b7f9e7 100644 --- a/web/static/js/app/chart_view.js +++ b/web/static/js/app/chart_view.js @@ -110,8 +110,17 @@ function updateChart(options) { const requestId = ++lastRequestId; const chartsReady = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart); const hasBaseline = !!(currentData && Array.isArray(currentData.kline_data) && currentData.kline_data.length); - // 自动刷新常态:只拉最近 2 根;fullAnalyze(约每 1 分钟)走全量 analyze 更新缠论 - const useRecentTail = !!(options.fromAutoRefresh && !options.fullAnalyze && chartsReady && hasBaseline); + const baselineSymbol = (currentData && currentData.symbol) || window._lastChartSymbol || ''; + // 自动刷新常态:只拉最近 2 根;换币对后基线不一致则禁止尾部合并(否则会叠旧缠论) + // fullAnalyze(约每 1 分钟)走全量 analyze 更新缠论 + const useRecentTail = !!( + options.fromAutoRefresh && + !options.fullAnalyze && + chartsReady && + hasBaseline && + baselineSymbol && + baselineSymbol === symbol + ); if (useRecentTail) { console.log('自动刷新 → /api/klines/recent limit=2'); @@ -187,24 +196,28 @@ function updateChart(options) { } // 保存当前数据 + const prevSymbol = (currentData && currentData.symbol) || window._lastChartSymbol || ''; if (currentData) { // 覆盖前断开旧引用,帮助GC尽快回收 delete currentData.original_kline_data; delete currentData.original_macd; } currentData = data; + window._lastChartSymbol = symbol; window._lastFullAnalyzeAt = Date.now(); if (typeof renderWyckoffCycleSummary === 'function') { renderWyckoffCycleSummary(); } - // 有图则增量;笔/段/中枢/结构区只在全量 init 绘制,fullAnalyze 必须重建 + // 有图则增量;笔/段/中枢/结构区只在全量 init 绘制 + // 换币对 / 手动分析 / 结构区:必须全量重建,否则会残留旧币对叠层 const ready = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart); const structureZonesOn = $('#showMainStructureZone').is(':checked'); + const symbolChanged = !!(prevSymbol && prevSymbol !== symbol); let wantIncremental = options.incremental !== undefined ? !!options.incremental : (ready || !!options.fromAutoRefresh); - if (structureZonesOn || options.fullAnalyze) { + if (structureZonesOn || options.fullAnalyze || symbolChanged || options.incremental === false) { wantIncremental = false; } refreshChart(data, { incremental: wantIncremental }); diff --git a/web/templates/index.html b/web/templates/index.html index 427cca8..f2c962e 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -982,7 +982,7 @@