/* chart_view.js — split from chart.js */ /** 用尾部 N 根合并进已有 K 线(同 timestamp 覆盖,更新则追加) */ function mergeKlineTail(existing, incoming) { if (!Array.isArray(incoming) || !incoming.length) { return Array.isArray(existing) ? existing : []; } if (!Array.isArray(existing) || !existing.length) { return incoming.slice(); } const out = existing.slice(); const barTs = (row) => { if (row && row.timestamp != null && row.timestamp !== '') { const n = Number(row.timestamp); if (!Number.isNaN(n)) return n; } const t = row && row.date != null ? new Date(row.date).getTime() : NaN; return Number.isNaN(t) ? null : t; }; for (let i = 0; i < incoming.length; i++) { const row = incoming[i]; const ts = barTs(row); if (ts == null) continue; let idx = -1; const scanFrom = Math.max(0, out.length - 8); for (let j = out.length - 1; j >= scanFrom; j--) { if (barTs(out[j]) === ts) { idx = j; break; } } if (idx >= 0) { out[idx] = Object.assign({}, out[idx], row); } else { const lastTs = barTs(out[out.length - 1]); if (lastTs == null || ts > lastTs) { out.push(row); } } } return out; } function updateChart(options) { options = options || {}; // 只显示旋转加载图标 $('#refreshLoadingSpinner').show(); // 获取参数 const dataSource = $('#dataSource').val() || 'crypto'; let symbol; if (dataSource === 'crypto') { symbol = $('#symbol').val() || 'BTC/USDT:USDT'; } else { symbol = $('#astockSymbol').val() || '000001'; } const timeframe = $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '4h'; const timezone = $('#timezone').val() || 'Asia/Shanghai'; const elementTimeframe = $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '1m'; const subSubTimeframe = $('#subSubTimeframe').val() || ''; // 确保时区参数有效 console.log('更新图表使用时区:', timezone, 'reason:', options.reason || (options.fromAutoRefresh ? 'auto' : 'manual')); console.log('数据源:', dataSource, '交易对/股票:', symbol); // 如果symbol为空,不发送请求 if (!symbol) { console.error('交易对/股票代码不能为空'); $('#refreshLoadingSpinner').hide(); return; } console.log(`更新图表: symbol=${symbol}, timeframe=${timeframe}, elementTimeframe=${elementTimeframe}, timezone=${timezone}`); // 获取开始和结束时间(如果已设置) let startTimeMs = null; let endTimeMs = null; if ($('#start_time').val()) { startTimeMs = new Date($('#start_time').val()).getTime(); } if ($('#end_time').val()) { endTimeMs = new Date($('#end_time').val()).getTime(); } // 自动刷新:取消进行中的上一请求,避免响应堆积 if (options.fromAutoRefresh && window._analyzeXhr && window._analyzeXhr.readyState !== 4) { try { window._analyzeXhr.abort(); } catch (e) {} } // 请求发出前冻结视窗(与自动刷新同一套;避免等响应时/setData 后 logical 索引漂移) try { if (tvWidget && tvWidget.mainChart) { window._preserveViewOnRefresh = captureChartViewState(tvWidget.mainChart); const prev = currentData && ( ($('#subSubPeriodKline').is(':checked') && currentData.sub_sub_kline_data) || ($('#elementPeriodKline').is(':checked') && currentData.element_kline_data) || currentData.kline_data ); window._preserveViewBarCount = Array.isArray(prev) ? prev.length : 0; console.log('📌 刷新前冻结视窗 bars=', window._preserveViewBarCount, window._preserveViewOnRefresh); } } catch (e) { window._preserveViewOnRefresh = null; window._preserveViewBarCount = 0; } 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); 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'); window._analyzeXhr = $.ajax({ url: '/api/klines/recent', data: { symbol: symbol, timeframe: timeframe, limit: 2, element_timeframe: elementTimeframe || undefined, sub_sub_timeframe: subSubTimeframe || undefined }, success: function(partial) { $('#refreshLoadingSpinner').hide(); if (requestId !== lastRequestId) return; if (!partial || !Array.isArray(partial.kline_data)) { console.warn('recent 响应无效,回退全量 analyze'); updateChart({ incremental: true, reason: 'recent-fallback' }); return; } currentData.kline_data = mergeKlineTail(currentData.kline_data, partial.kline_data); if (Array.isArray(partial.element_kline_data)) { currentData.element_kline_data = mergeKlineTail( currentData.element_kline_data, partial.element_kline_data ); if (partial.element_timeframe) { currentData.element_timeframe = partial.element_timeframe; } } if (Array.isArray(partial.sub_sub_kline_data)) { currentData.sub_sub_kline_data = mergeKlineTail( currentData.sub_sub_kline_data, partial.sub_sub_kline_data ); if (partial.sub_sub_timeframe) { currentData.sub_sub_timeframe = partial.sub_sub_timeframe; } } refreshChart(currentData, { incremental: true, skipTables: true }); }, error: function(jqXHR, textStatus, errorThrown) { $('#refreshLoadingSpinner').hide(); if (textStatus === 'abort') return; console.warn('recent 失败,回退全量 analyze:', errorThrown); updateChart({ incremental: true, reason: 'recent-error-fallback' }); } }); return; } // 手动 / 首拉:全量 analyze window._analyzeXhr = $.ajax({ url: '/api/analyze', data: { symbol: symbol, timeframe: timeframe, timezone: timezone, element_timeframe: elementTimeframe, sub_sub_timeframe: subSubTimeframe || undefined, start_time: startTimeMs, end_time: endTimeMs, elements_only: false, zone_kl_lines: parseInt($('#zoneKlLines').val()) || 1000, include_structure_zones: $('#showMainStructureZone').is(':checked') ? 1 : 0 // 威科夫随主分析一并返回;开关仅控制绘制,不再传 include_wyckoff }, success: function(data) { // 隐藏加载图标 $('#refreshLoadingSpinner').hide(); // 忽略过期响应 if (requestId !== lastRequestId) { return; } // 保存当前数据 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 绘制 // 换币对 / 手动分析 / 结构区:必须全量重建,否则会残留旧币对叠层 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 || symbolChanged || options.incremental === false) { wantIncremental = false; } refreshChart(data, { incremental: wantIncremental }); }, error: function(jqXHR, textStatus, errorThrown) { // 隐藏加载图标 $('#refreshLoadingSpinner').hide(); if (textStatus === 'abort') { return; } // 显示错误信息 console.error('加载数据失败:', errorThrown); // 自动刷新失败不弹窗打扰 if (!options.fromAutoRefresh) { alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown)); } } }); } function captureChartViewState(chart) { if (!chart || !chart.timeScale) return null; const ts = chart.timeScale(); const tsOptions = ts.options ? ts.options() : {}; return { barSpacing: tsOptions.barSpacing, rightOffset: tsOptions.rightOffset, scrollPosition: ts.scrollPosition ? ts.scrollPosition() : null, visibleRange: ts.getVisibleRange ? ts.getVisibleRange() : null, logicalRange: ts.getVisibleLogicalRange ? ts.getVisibleLogicalRange() : null }; } /** 将可见时间窗口限制在真实 K 线范围内,避免 to 落在右侧空白区导致锚到最右 */ function clampVisibleRangeToBarTimes(vr, firstTime, lastTime) { if (!vr || vr.from === undefined || vr.to === undefined) return vr; if (firstTime == null || lastTime == null) return vr; const f = Number(firstTime); const l = Number(lastTime); if (!isFinite(f) || !isFinite(l)) return vr; let from = Number(vr.from); let to = Number(vr.to); const span = Math.max(1, to - from); if (to > l) { to = l; from = to - span; } if (from < f) { from = f; to = from + span; } return { from: from, to: to }; } /** 尾部合并时用 update 代替 setData,避免 LWC 重置滚动位置 */ function applySeriesDataTail(series, points, tailOnly) { if (!series || typeof series.setData !== 'function' || !Array.isArray(points) || !points.length) { return; } if (tailOnly && typeof series.update === 'function' && points.length > 2) { points.slice(-4).forEach(function (p) { try { series.update(p); } catch (e) {} }); return; } series.setData(points); } function restoreChartViewState(charts, viewState, options) { // 全量重建备用:先缩放,再位置;不要在位置前写 rightOffset(会右边缘锚定) if (!viewState || !Array.isArray(charts) || charts.length === 0) return; options = options || {}; const preferTime = !!options.preferTime; const validCharts = charts.filter(c => c && c.timeScale); if (validCharts.length === 0) return; const mainChart = validCharts[0]; validCharts.forEach(c => { try { if (typeof viewState.barSpacing === 'number') { c.timeScale().applyOptions({ barSpacing: viewState.barSpacing }); } } catch (e) {} }); let restored = false; const syncFromMain = function () { try { const lrNow = mainChart.timeScale().getVisibleLogicalRange(); if (lrNow) { validCharts.forEach(c => { try { c.timeScale().setVisibleLogicalRange(lrNow); } catch (e) {} }); restored = true; } } catch (e) {} }; if (typeof viewState.scrollPosition === 'number') { try { mainChart.timeScale().scrollToPosition(viewState.scrollPosition, false); syncFromMain(); } catch (e) {} } const tryVisibleRange = function () { if (!viewState.visibleRange || viewState.visibleRange.from === undefined || viewState.visibleRange.to === undefined) { return false; } validCharts.forEach(c => { try { c.timeScale().setVisibleRange(viewState.visibleRange); restored = true; } catch (e) {} }); return restored; }; const tryLogicalRange = function () { if (!viewState.logicalRange || viewState.logicalRange.from === undefined || viewState.logicalRange.to === undefined) { return false; } validCharts.forEach(c => { try { c.timeScale().setVisibleLogicalRange(viewState.logicalRange); restored = true; } catch (e) {} }); return restored; }; if (!restored) { if (preferTime) { tryVisibleRange(); if (!restored) tryLogicalRange(); } else { tryLogicalRange(); if (!restored) tryVisibleRange(); } } } // 初始化图表