/* chart_tv.js — split from chart.js */ function initTradingView(symbol, timeframe) { try { // 在重新初始化前,尝试释放旧图表与系列资源,避免 GPU 内存累积 try { if (tvWidget && tvWidget.state && tvWidget.state.isInitialized) { // 主图 if (tvWidget.mainChart && typeof tvWidget.mainChart.remove === 'function') { tvWidget.mainChart.remove(); } // 成交量 if (tvWidget.volumeChart && typeof tvWidget.volumeChart.remove === 'function') { tvWidget.volumeChart.remove(); } // 旧 MACD(若存在) if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') { tvWidget.macdChart.remove(); } // 新 ChanMACD(若存在) if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') { tvWidget.chanMacdChart.remove(); } // ATR if (tvWidget.atrChart && typeof tvWidget.atrChart.remove === 'function') { tvWidget.atrChart.remove(); } } } catch (e) { console.warn('释放旧图表资源失败(可忽略):', e); } console.log('初始化TradingView图表:', symbol, timeframe); // 获取当前交易对的配置 const symbolConfig = getSymbolConfig(symbol); console.log('交易对配置:', symbolConfig); // 检查数据是否存在 if (!currentData || !currentData.kline_data) { console.error('数据加载失败或不存在'); return; } // 检查使用哪一档K线数据:次次周期 / 小周期 / 主周期 const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') && currentData.sub_sub_kline_data && Array.isArray(currentData.sub_sub_kline_data); const useElementPeriod = $('#elementPeriodKline').is(':checked') && currentData.element_kline_data && Array.isArray(currentData.element_kline_data); // 输出K线周期选择状态 const klinePeriodLabel = useSubSubPeriod ? '次次周期' : (useElementPeriod ? '小周期' : '主周期'); console.log('K线周期选择:', klinePeriodLabel); console.log('当前选择时区:', $('#timezone').val()); console.log('交易对类型:', symbolConfig.type); let candles = []; const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? (currentData.element_kline_data || []) : (currentData.kline_data || [])); if (useSubSubPeriod || useElementPeriod) { if (!klineDataSource.length) { console.error(useSubSubPeriod ? '次次周期K线数据不存在或为空' : '小周期K线数据不存在或为空', klineDataSource); return; } candles = klineDataSource.map((kline) => { const date = new Date(kline.date); const timestamp = date.getTime() / 1000; return { time: timestamp, open: parseFloat(kline.open), high: parseFloat(kline.high), low: parseFloat(kline.low), close: parseFloat(kline.close), }; }); } else { if (!currentData.kline_data || !Array.isArray(currentData.kline_data)) { console.error('主周期K线数据不存在或不是数组:', currentData.kline_data); return; } candles = currentData.kline_data.map((kline) => { const date = new Date(kline.date); const timestamp = date.getTime() / 1000; return { time: timestamp, open: parseFloat(kline.open), high: parseFloat(kline.high), low: parseFloat(kline.low), close: parseFloat(kline.close), }; }); } // 根据交易对类型过滤数据(仅用于显示优化) if (symbolConfig.type === 'a_stock' && timeframe.includes('m')) { // 对于A股分钟级数据,过滤非交易时间 const originalLength = candles.length; candles = filterTradingHours(candles, symbolConfig); console.log(`A股数据过滤: ${originalLength} -> ${candles.length} 条记录`); } // 清除图表容器(释放旧 DOM 与 Canvas) const chartRoot = document.getElementById('tradingview_chart'); if (chartRoot) chartRoot.innerHTML = ''; // 重置图表对象 tvWidget = { mainChart: null, volumeChart: null, macdChart: null, series: { candleSeries: null, lineSeries: null, volumeSeries: null, macdLineSeries: null, signalLineSeries: null, histogramSeries: null, mainBiSeries: [], mainUncompletedBiSeries: [], mainSegSeries: [], mainUncompletedSegSeries: [], mainZsSeries: [], mainUncompletedZsSeries: [], elementBiSeries: [], elementUncompletedBiSeries: [], elementSegSeries: [], elementUncompletedSegSeries: [], elementZsSeries: [], elementUncompletedZsSeries: [], subSubBiSeries: [], subSubUncompletedBiSeries: [], subSubSegSeries: [], subSubUncompletedSegSeries: [], subSubZsSeries: [], subSubUncompletedZsSeries: [], tradePointSeries: [], mainBollingerSeries: [], elementBollingerSeries: [], maSeries: [], // 添加均线系列数组 bbSeries: [], // 添加布林带系列数组 ema52Series: [] // 添加EMA52系列数组 }, state: { isInitialized: false, visibleRange: null, logicalRange: null } }; // 设置父容器样式 const container = document.getElementById('tradingview_chart'); container.style.position = 'relative'; container.style.width = '100%'; container.style.height = '100%'; // 是否显示MACD const showMacd = $('#showMacd').is(':checked'); const showOriginalKline = $('#showOriginalKline').is(':checked'); // 创建主图容器 const mainChartContainer = document.createElement('div'); mainChartContainer.style.width = '100%'; mainChartContainer.style.position = 'absolute'; mainChartContainer.style.top = '0'; mainChartContainer.style.left = '0'; mainChartContainer.style.right = '0'; // 创建成交量副图容器 const volumeChartContainer = document.createElement('div'); volumeChartContainer.style.width = '100%'; volumeChartContainer.style.position = 'absolute'; volumeChartContainer.style.left = '0'; volumeChartContainer.style.right = '0'; volumeChartContainer.style.borderTop = '1px solid #e0e0e0'; // 添加ATR图表容器 const atrChartContainer = document.createElement('div'); atrChartContainer.style.width = '100%'; atrChartContainer.style.position = 'absolute'; atrChartContainer.style.left = '0'; atrChartContainer.style.right = '0'; atrChartContainer.style.borderTop = '1px solid #e0e0e0'; // 如果需要显示MACD,创建MACD容器 let macdChartContainer = null; let chanMacdChartContainer = null; if (showMacd) { // 仅显示新的 ChanMACD 图:让其占用原 MACD+ChanMACD 的整体高度 // 新布局:主图(40%) → ChanMACD(30%) → 成交量(17.5%) → ATR(12.5%) mainChartContainer.style.height = '40%'; // 隐藏旧 MACD 容器(不创建) // 创建 ChanMACD 容器占据原 MACD+ChanMACD 高度(30%) chanMacdChartContainer = document.createElement('div'); chanMacdChartContainer.style.width = '100%'; chanMacdChartContainer.style.height = '30%'; chanMacdChartContainer.style.position = 'absolute'; chanMacdChartContainer.style.top = '40%'; chanMacdChartContainer.style.left = '0'; chanMacdChartContainer.style.right = '0'; chanMacdChartContainer.style.borderTop = '1px solid #e0e0e0'; chanMacdChartContainer.style.zIndex = '10'; // 水印:便于区分是新的 ChanMACD 子图 const chanMacdWatermark = document.createElement('div'); chanMacdWatermark.textContent = 'ChanMACD'; chanMacdWatermark.style.position = 'absolute'; chanMacdWatermark.style.top = '4px'; chanMacdWatermark.style.left = '8px'; chanMacdWatermark.style.fontSize = '11px'; chanMacdWatermark.style.color = '#888'; chanMacdWatermark.style.pointerEvents = 'none'; chanMacdChartContainer.appendChild(chanMacdWatermark); // 成交量位于 ChanMACD 之下 volumeChartContainer.style.top = '70%'; volumeChartContainer.style.height = '17.5%'; // ATR 位于最底部 atrChartContainer.style.top = '87.5%'; atrChartContainer.style.height = '12.5%'; } else { // 不显示MACD时的高度 - 主图、成交量图和ATR图分配 mainChartContainer.style.height = '55%'; // 主图占55% volumeChartContainer.style.top = '55%'; volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5% atrChartContainer.style.top = '77.5%'; // ATR图从77.5%位置开始 atrChartContainer.style.height = '22.5%'; // ATR图占22.5% } container.appendChild(mainChartContainer); container.appendChild(volumeChartContainer); container.appendChild(atrChartContainer); if (showMacd) { // 只追加新的 ChanMACD 容器 container.appendChild(chanMacdChartContainer); } // 防止同步过程中的无限循环 let syncInProgress = false; // 创建统一的图表选项 const createChartOptions = (showTimeScale = true, chartType = 'main') => { // 根据图表类型确定高度 let chartHeight; if (chartType === 'main') { chartHeight = mainChartContainer.clientHeight; } else if (chartType === 'volume') { chartHeight = volumeChartContainer.clientHeight; } else if (chartType === 'atr') { chartHeight = atrChartContainer.clientHeight; } else if (chartType === 'macd') { chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0; } else if (chartType === 'chanmacd') { chartHeight = chanMacdChartContainer ? chanMacdChartContainer.clientHeight : 0; } else { chartHeight = mainChartContainer.clientHeight; } const baseOptions = { width: mainChartContainer.clientWidth, height: chartHeight, layout: { background: { color: '#ffffff' }, textColor: '#333', }, grid: { vertLines: { color: '#f0f0f0' }, horzLines: { color: '#f0f0f0' }, }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal, // 添加十字线工具提示本地化配置 horzLine: { labelVisible: true, }, vertLine: { labelVisible: true, // 自定义时间格式化 labelFormatter: (time) => { const selectedTimezone = $('#timezone').val(); try { const date = new Date(time * 1000); if (symbolConfig.type === 'a_stock') { // A股使用中国时区格式 return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } else { return date.toLocaleString('zh-CN', { timeZone: selectedTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } } catch (e) { console.error('十字线时间格式化错误:', e); return new Date(time * 1000).toLocaleString(); } }, }, }, rightPriceScale: { borderColor: '#ddd', scaleMargins: { top: 0.1, bottom: 0.1, }, // 为标签留出更多空间,防止遮挡 minimumWidth: 80, }, // 添加左边距配置 leftPriceScale: { visible: false, }, // 添加本地化选项,确保所有时间显示都使用选定的时区 localization: { timeFormatter: (time) => { const selectedTimezone = $('#timezone').val(); try { const date = new Date(time * 1000); if (symbolConfig.type === 'a_stock') { // A股使用中国时区格式 return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } else { return date.toLocaleString('zh-CN', { timeZone: selectedTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } } catch (e) { console.error('全局时间格式化错误:', e); return new Date(time * 1000).toLocaleString(); } } }, timeScale: { timeVisible: true, secondsVisible: false, visible: showTimeScale, borderColor: '#ddd', barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10, // 确保所有图表使用相同的边距设置 rightOffset: 12, // 移除可能影响拖动的固定边缘设置 // fixLeftEdge: true, // fixRightEdge: true, lockVisibleTimeRangeOnResize: true, tickMarkFormatter: (time) => { const selectedTimezone = symbolConfig.type === 'a_stock' ? 'Asia/Shanghai' : $('#timezone').val(); try { // 使用完整的配置确保时区正确应用 const date = new Date(time * 1000); console.log('格式化时间:', time, '转换为:', date.toISOString(), '时区:', selectedTimezone); return date.toLocaleString('zh-CN', { timeZone: selectedTimezone, month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch (e) { console.error('时间格式化错误:', e); // 如果时区格式化失败,返回简单格式 return new Date(time * 1000).toLocaleString(); } }, }, }; // 根据交易对类型调整配置 return adjustChartForSymbolType(baseOptions, symbolConfig); }; // 创建主图表 const mainChart = LightweightCharts.createChart(mainChartContainer, createChartOptions(true, 'main')); // 创建成交量图表 - 只显示底部的时间轴 const volumeChart = LightweightCharts.createChart(volumeChartContainer, createChartOptions(false, 'volume')); // 创建ATR图表 const atrChart = LightweightCharts.createChart(atrChartContainer, createChartOptions(false, 'atr')); // 创建MACD图表(如果需要):仅创建新的 ChanMACD 图 let macdChart = null; let chanMacdChart = null; if (showMacd) { chanMacdChart = LightweightCharts.createChart(chanMacdChartContainer, createChartOptions(false, 'chanmacd')); } // 创建主价格系列并设置数据(支持多种图表类型) (function(){ const klineType = ($('#klineType').val() || 'candlestick'); // 先清空旧的主系列引用 tvWidget.series.candleSeries = null; tvWidget.series.lineSeries = null; tvWidget.series.barSeries = null; tvWidget.series.areaSeries = null; tvWidget.series.baselineSeries = null; tvWidget.series.renkoSeries = null; tvWidget.series.heikinSeries = null; if (klineType === 'candlestick') { const series = mainChart.addCandlestickSeries({ upColor: '#28a745', downColor: '#dc3545', borderVisible: false, wickUpColor: '#28a745', wickDownColor: '#dc3545', }); series.setData(candles); tvWidget.series.candleSeries = series; } else if (klineType === 'renko') { const series = mainChart.addCandlestickSeries({ upColor: '#28a745', downColor: '#dc3545', borderVisible: false, wickUpColor: '#28a745', wickDownColor: '#dc3545', }); const bricks = buildRenkoFromCandles(candles); series.setData(bricks); tvWidget.series.renkoSeries = series; } else if (klineType === 'heikin') { const series = mainChart.addCandlestickSeries({ upColor: '#28a745', downColor: '#dc3545', borderVisible: false, wickUpColor: '#28a745', wickDownColor: '#dc3545', }); const hk = buildHeikinFromCandles(candles); series.setData(hk); tvWidget.series.heikinSeries = series; } else if (klineType === 'bar') { const series = mainChart.addBarSeries({ upColor: '#28a745', downColor: '#dc3545', thinBars: false }); series.setData(candles); tvWidget.series.barSeries = series; } else if (klineType === 'line') { const series = mainChart.addLineSeries({ color: '#2962FF', lineWidth: 2, crosshairMarkerVisible: true, lastValueVisible: true, priceLineVisible: true, }); const lineData = candles.map(c => ({ time: c.time, value: c.close })); series.setData(lineData); tvWidget.series.lineSeries = series; } else if (klineType === 'area') { const series = mainChart.addAreaSeries({ topColor: 'rgba(41, 98, 255, 0.4)', bottomColor: 'rgba(41, 98, 255, 0.0)', lineColor: '#2962FF', lineWidth: 2, }); const areaData = candles.map(c => ({ time: c.time, value: c.close })); series.setData(areaData); tvWidget.series.areaSeries = series; } else if (klineType === 'baseline') { const series = mainChart.addBaselineSeries({ baseValue: { type: 'price', price: candles.length ? candles[candles.length - 1].close : 0 }, topLineColor: '#26a69a', bottomLineColor: '#ef5350', topFillColor1: 'rgba(38, 166, 154, 0.28)', topFillColor2: 'rgba(38, 166, 154, 0.05)', bottomFillColor1: 'rgba(239, 83, 80, 0.28)', bottomFillColor2: 'rgba(239, 83, 80, 0.05)' }); const baseData = candles.map(c => ({ time: c.time, value: c.close })); series.setData(baseData); tvWidget.series.baselineSeries = series; } else if (klineType === 'klc') { // KLC显示模式 - 使用蜡烛线显示KLC数据 const series = mainChart.addCandlestickSeries({ upColor: '#28a745', downColor: '#dc3545', borderVisible: false, wickUpColor: '#28a745', wickDownColor: '#dc3545', }); // 使用KLC数据创建蜡烛图 const klcCandles = buildKLCFromAnalysis(currentData); series.setData(klcCandles); tvWidget.series.klcSeries = series; } })(); // 转换成交量数据 - 与K线周期一致 let volumes = []; const volumeDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data); console.log('成交量数据源选择:', klinePeriodLabel); console.log('成交量数据长度:', volumeDataSource.length); if (volumeDataSource && Array.isArray(volumeDataSource)) { volumes = volumeDataSource.map(kline => { // 使用与K线和MACD完全相同的时间戳计算方式 const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); return { time: timestamp, value: parseFloat(kline.volume), color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)', }; }); console.log('处理后的成交量数据点数:', volumes.length); } // 添加成交量图表 const volumeSeries = volumeChart.addHistogramSeries({ color: '#26a69a', priceFormat: { type: 'volume', }, title: '成交量', }); volumeSeries.setData(volumes); tvWidget.series.volumeSeries = volumeSeries; // 添加ATR图表 const atrLineSeries = atrChart.addLineSeries({ color: '#FF9800', lineWidth: 2, title: 'ATR', lastValueVisible: false, priceLineVisible: false, }); // 准备ATR数据 const atrData = []; const atrKlineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data); const atrDataSource = useSubSubPeriod ? (currentData.sub_sub_atr || currentData.atr) : (useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr); console.log('ATR数据源选择:', klinePeriodLabel); console.log('ATR数据长度:', atrDataSource ? atrDataSource.length : 0); console.log('K线数据长度:', atrKlineDataSource ? atrKlineDataSource.length : 0); if (atrDataSource && Array.isArray(atrDataSource) && atrKlineDataSource && Array.isArray(atrKlineDataSource)) { // 关键修复:为每个K线时间点都创建ATR数据点,包括没有ATR值的前期数据 for (let i = 0; i < atrKlineDataSource.length; i++) { const kline = atrKlineDataSource[i]; const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); // 为每个时间点都添加数据以保持时间轴对齐,但ATR为0时不显示 if (atrDataSource[i] !== undefined) { if (atrDataSource[i] > 0) { // ATR有效值,正常显示 atrData.push({ time: timestamp, value: atrDataSource[i] }); } else { // ATR为0,添加时间点但不显示线条(使用undefined作为value) atrData.push({ time: timestamp, value: undefined }); } } } console.log('处理后的ATR数据点数:', atrData.length); console.log('ATR数据样本:', atrData.slice(0, 5)); } console.log('处理后的ATR数据点数:', atrData.length); atrLineSeries.setData(atrData); tvWidget.series.atrLineSeries = atrLineSeries; // 旧 MACD 图已移除,不再绘制(保留占位但彻底禁用) if (FEATURES.legacyMacd && showMacd && currentData.macd && currentData.kline_data && Array.isArray(currentData.kline_data)) { // 创建MACD线 const macdLineSeries = macdChart.addLineSeries({ color: '#2962FF', lineWidth: 1, title: 'MACD', lastValueVisible: false, // 禁用最后值标签,防止遮挡 priceLineVisible: false, // 禁用价格线 }); // 创建信号线 const signalLineSeries = macdChart.addLineSeries({ color: '#FF6B6B', lineWidth: 1, title: 'Signal', lastValueVisible: false, // 禁用最后值标签,防止遮挡 priceLineVisible: false, // 禁用价格线 }); // 创建直方图 const histogramSeries = macdChart.addHistogramSeries({ color: '#26a69a', title: 'Histogram', priceFormat: { type: 'price', precision: 4, }, }); // 提取MACD数据 - 使用和K线数据相同的时间处理逻辑 const macdData = []; const signalData = []; const histogramData = []; // 使用与K线数据相同的数据源来确保时间对齐 const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data); const macdDataSource = useElementPeriod ? (currentData.element_macd || currentData.macd) : // 如果有次周期MACD数据则使用,否则使用主周期 currentData.macd; // 主周期使用主周期MACD数据 console.log('MACD数据源选择:', useElementPeriod ? '次周期' : '主周期'); console.log('K线数据长度:', klineDataSource.length); console.log('MACD数据:', macdDataSource); for (let i = 0; i < klineDataSource.length; i++) { const kline = klineDataSource[i]; // 使用与K线完全相同的时间戳计算方式 const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); if (macdDataSource && macdDataSource.macd && macdDataSource.macd[i] !== undefined) { macdData.push({ time: timestamp, value: macdDataSource.macd[i] }); signalData.push({ time: timestamp, value: macdDataSource.signal[i] }); // 设置直方图颜色 const histValue = macdDataSource.histogram[i]; histogramData.push({ time: timestamp, value: histValue, color: histValue >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)' }); } } console.log('处理后的MACD数据点数:', macdData.length); macdLineSeries.setData(macdData); signalLineSeries.setData(signalData); histogramSeries.setData(histogramData); tvWidget.series.macdLineSeries = macdLineSeries; tvWidget.series.signalLineSeries = signalLineSeries; tvWidget.series.histogramSeries = histogramSeries; } // 添加ChanMACD图表 console.log('ChanMACD图表创建条件检查:', { showMacd: showMacd, chanMacdChart: !!chanMacdChart, hasMacd: !!currentData.macd, hasKlineData: !!currentData.kline_data, isArray: Array.isArray(currentData.kline_data) }); // 在创建 ChanMACD 前,确保一次性同步 U 显示开关到全局(默认不显示) if (typeof window.showUOnMain === 'undefined') { window.showUOnMain = $('#toggleUOnMain').is(':checked'); } if (typeof window.showUOnElement === 'undefined') { window.showUOnElement = $('#toggleUOnElement').is(':checked'); } if (typeof window.showUOnSubSub === 'undefined') { window.showUOnSubSub = $('#toggleUOnSubSub').is(':checked'); } if (showMacd && chanMacdChart && ((useSubSubPeriod && currentData.sub_sub_macd) || (useElementPeriod && currentData.element_macd) || currentData.macd) && (useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data))) { console.log('✅ 开始创建 ChanMACD 系列'); // 创建ChanMACD线系列 const chanMacdLineSeries = chanMacdChart.addLineSeries({ color: '#2962FF', lineWidth: 1, title: 'ChanMACD', lastValueVisible: false, priceLineVisible: false, }); // 创建ChanMACD信号线系列 const chanMacdSignalSeries = chanMacdChart.addLineSeries({ color: '#FF6B6B', lineWidth: 1, title: 'ChanSignal', lastValueVisible: false, priceLineVisible: false, }); // 创建ChanMACD柱状图系列 const chanMacdHistSeries = chanMacdChart.addHistogramSeries({ color: '#26a69a', title: 'ChanHistogram', priceFormat: { type: 'price', precision: 4, }, }); // 设置ChanMACD图表的字体大小 chanMacdChart.applyOptions({ layout: { fontSize: 10, // 设置更小的字体大小 }, rightPriceScale: { fontSize: 10, // 设置右侧价格轴的字体大小 }, timeScale: { fontSize: 10, // 设置时间轴的字体大小 }, }); // 使用与主图一致的数据源(小周期开启时使用小周期MACD与K线) const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data); const macdDataSource = useSubSubPeriod ? (currentData.sub_sub_macd || currentData.macd) : (useElementPeriod ? (currentData.element_macd || currentData.macd) : currentData.macd); // 准备ChanMACD数据 const chanMacdData = []; const chanSignalData = []; const chanHistData = []; console.log('ChanMACD数据源检查:', { klineDataSourceLength: klineDataSource.length, macdDataSource: !!macdDataSource, macdLength: macdDataSource ? macdDataSource.macd.length : 0 }); console.log('ChanMACD数据源检查:', { klineDataSourceLength: klineDataSource.length, macdDataSource: !!macdDataSource, macdLength: macdDataSource ? macdDataSource.macd.length : 0 }); for (let i = 0; i < klineDataSource.length; i++) { const kline = klineDataSource[i]; if (kline && kline.date && i < macdDataSource.macd.length && macdDataSource.macd[i] !== null && macdDataSource.macd[i] !== undefined) { // 使用与K线完全相同的时间戳计算方式 const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); chanMacdData.push({ time: timestamp, value: macdDataSource.macd[i] }); chanSignalData.push({ time: timestamp, value: macdDataSource.signal[i] }); chanHistData.push({ time: timestamp, value: macdDataSource.histogram[i], color: macdDataSource.histogram[i] >= 0 ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)' }); } } console.log('ChanMACD数据处理完成:', { chanMacdDataLength: chanMacdData.length, chanSignalDataLength: chanSignalData.length, chanHistDataLength: chanHistData.length, sampleData: chanMacdData.length > 0 ? chanMacdData[0] : null }); // 设置ChanMACD数据 console.log('ChanMACD数据长度:', chanMacdData.length, chanSignalData.length, chanHistData.length); if (chanMacdData.length > 0) { chanMacdLineSeries.setData(chanMacdData); chanMacdSignalSeries.setData(chanSignalData); chanMacdHistSeries.setData(chanHistData); console.log('✅ ChanMACD数据设置成功'); } else { console.warn('⚠️ ChanMACD数据为空,无法设置数据'); } // 保存到tvWidget tvWidget.series.chanMacdLineSeries = chanMacdLineSeries; tvWidget.series.chanMacdSignalSeries = chanMacdSignalSeries; tvWidget.series.chanMacdHistSeries = chanMacdHistSeries; console.log('✅ ChanMACD图表系列已保存到tvWidget'); // 添加ChanMACD分析标注 // 根据主/次周期开关与各自的"显示U"独立控制 const cm = useSubSubPeriod ? (currentData.sub_sub_chan_macd || currentData.chan_macd) : (useElementPeriod ? (currentData.element_chan_macd || currentData.chan_macd) : currentData.chan_macd); // 默认不显示,必须用户勾选对应复选框 const allowU = useSubSubPeriod ? !!window.showUOnSubSub : (useElementPeriod ? !!window.showUOnElement : !!window.showUOnMain); if (cm && allowU) { console.log('添加ChanMACD分析标注:', { segListLength: cm.seg_list ? cm.seg_list.length : 0, unittfListLength: cm.unittf_list ? cm.unittf_list.length : 0, histsetListLength: cm.histset_list ? cm.histset_list.length : 0 }); // 详细检查段数据 if (cm.seg_list && cm.seg_list.length > 0) { console.log('段数据详情:', cm.seg_list.slice(0, 3)); // 显示前3个段 } else { console.log('⚠️ 段数据为空或不存在'); } addAllChanMacdMarkers( cm.seg_list || [], cm.unittf_list || [], cm.histset_list || [], { high_position_list: cm.high_position_list || [], high_empty_list: cm.high_empty_list || [], low_position_list: cm.low_position_list || [], low_empty_list: cm.low_empty_list || [], return_zero_list: cm.return_zero_list || [], cross0_up_list: cm.cross0_up_list || [], cross0_down_list: cm.cross0_down_list || [] } ); // 同时从主/次周期的 klu_list 提取 SD/CD 标记,分别使用不同样式 try { const mainCm = currentData.chan_macd || {}; const elementCm = currentData.element_chan_macd || {}; const mainMarkers = []; const elementMarkers = []; // 基于时间构建 MACD 值映射,便于按时间快速获取对应的 MACD 值 const buildMacdTimeMap = (macdObj, klineArr) => { const map = new Map(); if (!macdObj || !klineArr || !Array.isArray(klineArr)) return map; for (let i = 0; i < klineArr.length; i++) { const k = klineArr[i]; if (!k || !k.date) continue; const t = Math.floor(new Date(k.date).getTime() / 1000); const val = (macdObj.macd && macdObj.macd[i] !== undefined && macdObj.macd[i] !== null) ? macdObj.macd[i] : null; map.set(t, val); } return map; }; const mainMacdMap = buildMacdTimeMap(currentData.macd, currentData.kline_data); const elementMacdMap = buildMacdTimeMap( (currentData.element_macd || currentData.macd), (currentData.element_kline_data || currentData.kline_data) ); // 主周期 U 标记(蓝/橙,与原样式一致) if (window.showUOnMain && Array.isArray(mainCm.klu_list)) { mainCm.klu_list.forEach((item) => { if (!item || !item.time) return; const ts = Math.floor(new Date(item.time).getTime() / 1000); if (isNaN(ts)) return; if (Number(item.separate_div) > 0) { const macdVal = mainMacdMap.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; mainMarkers.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); } if (item.continue_div === true) { const macdVal = mainMacdMap.get(ts); const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar'; mainMarkers.push({ time: ts, position: posCd, color: '#ff9800', shape: 'arrowDown', text: 'CD', size: 0.6 }); } if (item.near0_return && Number(item.near0_return) > 0) { mainMarkers.push({ time: ts, position: 'belowBar', color: '#8bc34a', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 }); } }); } // 次周期 U 标记(使用不同配色以区分) if (window.showUOnElement && Array.isArray(elementCm.klu_list)) { elementCm.klu_list.forEach((item) => { if (!item || !item.time) return; const ts = Math.floor(new Date(item.time).getTime() / 1000); if (isNaN(ts)) return; if (Number(item.separate_div) > 0) { const macdVal = elementMacdMap.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; elementMarkers.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); } if (item.continue_div === true) { const macdVal = elementMacdMap.get(ts); const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar'; elementMarkers.push({ time: ts, position: posCd, color: '#4caf50', shape: 'arrowDown', text: 'CD', size: 0.6 }); } if (item.near0_return && Number(item.near0_return) > 0) { elementMarkers.push({ time: ts, position: 'belowBar', color: '#009688', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 }); } }); } const subSubCm = currentData.sub_sub_chan_macd || {}; const subSubMarkers = []; const subSubMacdMap = buildMacdTimeMap(currentData.macd, currentData.kline_data); if (window.showUOnSubSub && Array.isArray(subSubCm.klu_list)) { subSubCm.klu_list.forEach((item) => { if (!item || !item.time) return; const ts = Math.floor(new Date(item.time).getTime() / 1000); if (isNaN(ts)) return; if (Number(item.separate_div) > 0) { const macdVal = subSubMacdMap.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; subSubMarkers.push({ time: ts, position: posSd, color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); } if (item.continue_div === true) { const macdVal = subSubMacdMap.get(ts); const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar'; subSubMarkers.push({ time: ts, position: posCd, color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 }); } if (item.near0_return && Number(item.near0_return) > 0) { subSubMarkers.push({ time: ts, position: 'belowBar', color: '#00695c', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 }); } }); } window.kluDivMarkersSubSub = subSubMarkers; // 保存到全局,供主图合并标记使用 window.kluDivMarkersMain = mainMarkers; window.kluDivMarkersElement = elementMarkers; } catch (e) { console.warn('处理 KLU 背驰标记出错:', e); window.kluDivMarkersMain = []; window.kluDivMarkersElement = []; window.kluDivMarkersSubSub = []; } } else { console.log('⚠️ 没有ChanMACD分析数据'); // 无数据时清空本次的 KLU 背驰标记 window.kluDivMarkersMain = []; window.kluDivMarkersElement = []; window.kluDivMarkersSubSub = []; } } else { console.log('⚠️ ChanMACD图表创建条件不满足'); } // 独立于当前显示周期:计算主/次周期 SD/CD 标记(用于主图合并显示) try { const mainCmAll = currentData.chan_macd || {}; const elementCmAll = currentData.element_chan_macd || {}; const mainMarkersAll = []; const elementMarkersAll = []; // 构建 MACD 时间映射,用于依据 MACD 正负决定 SD/CD 的显示上下位置 const buildMacdTimeMapAll = (macdObj, klineArr) => { const map = new Map(); if (!macdObj || !klineArr || !Array.isArray(klineArr)) return map; for (let i = 0; i < klineArr.length; i++) { const k = klineArr[i]; if (!k || !k.date) continue; const t = Math.floor(new Date(k.date).getTime() / 1000); const val = (macdObj.macd && macdObj.macd[i] !== undefined && macdObj.macd[i] !== null) ? macdObj.macd[i] : null; map.set(t, val); } return map; }; const mainMacdMapAll = buildMacdTimeMapAll(currentData.macd, currentData.kline_data); const elementMacdMapAll = buildMacdTimeMapAll( (currentData.element_macd || currentData.macd), (currentData.element_kline_data || currentData.kline_data) ); if ((typeof window.showUOnMain === 'undefined' ? false : window.showUOnMain) && Array.isArray(mainCmAll.klu_list)) { mainCmAll.klu_list.forEach((item) => { if (!item || !item.time) return; const ts = Math.floor(new Date(item.time).getTime() / 1000); if (isNaN(ts)) return; if (Number(item.separate_div) > 0) { const macdVal = mainMacdMapAll.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; mainMarkersAll.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); } if (item.continue_div === true) { const macdVal = mainMacdMapAll.get(ts); const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar'; mainMarkersAll.push({ time: ts, position: posCd, color: '#ff9800', shape: 'arrowDown', text: 'CD', size: 0.6 }); } if (item.near0_return && Number(item.near0_return) > 0) { mainMarkersAll.push({ time: ts, position: 'belowBar', color: '#8bc34a', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 }); } }); } if ((typeof window.showUOnElement === 'undefined' ? false : window.showUOnElement) && Array.isArray(elementCmAll.klu_list)) { elementCmAll.klu_list.forEach((item) => { if (!item || !item.time) return; const ts = Math.floor(new Date(item.time).getTime() / 1000); if (isNaN(ts)) return; if (Number(item.separate_div) > 0) { const macdVal = elementMacdMapAll.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; elementMarkersAll.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); } if (item.continue_div === true) { const macdVal = elementMacdMapAll.get(ts); const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar'; elementMarkersAll.push({ time: ts, position: posCd, color: '#4caf50', shape: 'arrowDown', text: 'CD', size: 0.6 }); } if (item.near0_return && Number(item.near0_return) > 0) { elementMarkersAll.push({ time: ts, position: 'belowBar', color: '#009688', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 }); } }); } window.kluDivMarkersMain = mainMarkersAll; window.kluDivMarkersElement = elementMarkersAll; const subSubCmAll = currentData.sub_sub_chan_macd || {}; const subSubMarkersAll = []; if (window.showUOnSubSub && Array.isArray(subSubCmAll.klu_list)) { subSubCmAll.klu_list.forEach((item) => { if (!item || !item.time) return; const ts = Math.floor(new Date(item.time).getTime() / 1000); if (isNaN(ts)) return; if (Number(item.separate_div) > 0) { subSubMarkersAll.push({ time: ts, position: 'aboveBar', color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); } if (item.continue_div === true) { subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 }); } if (item.near0_return && Number(item.near0_return) > 0) { subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#00695c', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 }); } }); } window.kluDivMarkersSubSub = subSubMarkersAll; } catch (e) { console.warn('独立计算 KLU 背驰标记出错:', e); window.kluDivMarkersMain = []; window.kluDivMarkersElement = []; window.kluDivMarkersSubSub = []; } // 实现三图联动滚动 // 同步图表的时间范围 function syncCharts(sourceChart, sourceContainer) { // 防止无限循环 - 使用更精确的检查 if (syncInProgress) { console.log('🔄 同步正在进行中,跳过此次同步'); return; } syncInProgress = true; console.log('🚀 开始同步图表,来源:', sourceChart === mainChart ? '主图' : sourceChart === volumeChart ? '成交量图' : sourceChart === atrChart ? 'ATR图' : sourceChart === macdChart ? 'MACD图' : sourceChart === chanMacdChart ? 'ChanMACD图' : '未知图表'); try { if (sourceChart && sourceChart.timeScale) { const logicalRange = sourceChart.timeScale().getVisibleLogicalRange(); if (logicalRange && logicalRange.from !== undefined && logicalRange.to !== undefined) { console.log('📊 同步时间范围:', logicalRange); // 同步主图 if (sourceChart !== mainChart && mainChart && mainChart.timeScale) { try { mainChart.timeScale().setVisibleLogicalRange(logicalRange); console.log('✅ 主图同步完成'); } catch (e) { console.error('❌ 主图同步失败:', e); } } // 同步成交量图 if (sourceChart !== volumeChart && volumeChart && volumeChart.timeScale) { try { volumeChart.timeScale().setVisibleLogicalRange(logicalRange); console.log('✅ 成交量图同步完成'); } catch (e) { console.error('❌ 成交量图同步失败:', e); } } // 同步ATR图 if (sourceChart !== atrChart && atrChart && atrChart.timeScale) { try { atrChart.timeScale().setVisibleLogicalRange(logicalRange); console.log('✅ ATR图同步完成'); } catch (e) { console.error('❌ ATR图同步失败:', e); } } // 同步MACD图 if (showMacd && macdChart && sourceChart !== macdChart && macdChart.timeScale) { try { macdChart.timeScale().setVisibleLogicalRange(logicalRange); console.log('✅ MACD图同步完成'); } catch (e) { console.error('❌ MACD图同步失败:', e); } } // 同步ChanMACD图 if (showMacd && chanMacdChart && sourceChart !== chanMacdChart && chanMacdChart.timeScale) { try { chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange); console.log('✅ ChanMACD图同步完成'); } catch (e) { console.error('❌ ChanMACD图同步失败:', e); } } // 保存当前的可见范围到全局状态 if (tvWidget && tvWidget.state) { tvWidget.state.logicalRange = logicalRange; } } else { console.warn('⚠️ 无效的逻辑范围:', logicalRange); } } else { console.warn('⚠️ 无效的源图表或时间刻度'); } } catch (e) { console.error('💥 同步图表出错:', e); } // 立即重置同步标志,提高响应速度 setTimeout(() => { syncInProgress = false; console.log('🔓 同步标志已重置'); }, 1); } // 用于跟踪所有图表的拖动状态 let localDragStates = { main: false, volume: false, atr: false, macd: false, chanmacd: false }; // 全局鼠标抬起事件(只添加一次) document.addEventListener('mouseup', () => { // 重置所有拖动状态 Object.keys(localDragStates).forEach(key => { if (localDragStates[key]) { console.log(`全局鼠标抬起,重置${key}图表拖动状态`); localDragStates[key] = false; } }); }); // 为每个图表添加事件监听 const addChartSyncEvents = (chartContainer, chart) => { console.log('为图表添加同步事件监听:', chart === mainChart ? '主图' : chart === volumeChart ? '成交量图' : chart === atrChart ? 'ATR图' : chart === macdChart ? 'MACD图' : chart === chanMacdChart ? 'ChanMACD图' : '未知图表'); // 确定当前图表类型 const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : chart === atrChart ? 'atr' : chart === macdChart ? 'macd' : chart === chanMacdChart ? 'chanmacd' : 'unknown'; // 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法) chart.timeScale().subscribeVisibleTimeRangeChange(() => { // 使用图表特定的同步标志防止递归 if (!syncInProgress) { console.log('✅ 检测到时间范围变化,触发同步:', chartType, '当前范围:', chart.timeScale().getVisibleLogicalRange()); syncCharts(chart, chartContainer); } else { console.log('⏸️ 同步进行中,跳过时间范围变化事件:', chartType); } }); // 备用的DOM事件监听(用于调试和额外保障) let isScrolling = false; // 鼠标按下事件 chartContainer.addEventListener('mousedown', (e) => { localDragStates[chartType] = true; console.log('鼠标按下开始拖动:', chartType); }); // 鼠标抬起事件 chartContainer.addEventListener('mouseup', (e) => { if (localDragStates[chartType]) { localDragStates[chartType] = false; console.log('鼠标抬起,结束拖动:', chartType); } }); // 鼠标离开事件 chartContainer.addEventListener('mouseleave', (e) => { if (localDragStates[chartType]) { localDragStates[chartType] = false; console.log('鼠标离开容器,结束拖动:', chartType); } }); // 滚轮缩放事件(保持原有逻辑) chartContainer.addEventListener('wheel', (e) => { if (!isScrolling) { isScrolling = true; console.log('滚轮缩放:', chartType); setTimeout(() => { if (!syncInProgress) { syncCharts(chart, chartContainer); } isScrolling = false; }, 50); } }); }; // 添加事件监听 addChartSyncEvents(mainChartContainer, mainChart); addChartSyncEvents(volumeChartContainer, volumeChart); addChartSyncEvents(atrChartContainer, atrChart); if (showMacd && macdChart) { addChartSyncEvents(macdChartContainer, macdChart); } if (showMacd && chanMacdChart) { addChartSyncEvents(chanMacdChartContainer, chanMacdChart); } // 窗口大小变化时重绘图表 window.addEventListener('resize', () => { // 调整主图大小 mainChart.applyOptions({ width: mainChartContainer.clientWidth, height: mainChartContainer.clientHeight }); // 调整成交量图大小 volumeChart.applyOptions({ width: volumeChartContainer.clientWidth, height: volumeChartContainer.clientHeight }); // 调整ATR图大小 atrChart.applyOptions({ width: atrChartContainer.clientWidth, height: atrChartContainer.clientHeight }); // 调整MACD图大小 if (showMacd && macdChart && macdChartContainer) { macdChart.applyOptions({ width: macdChartContainer.clientWidth, height: macdChartContainer.clientHeight }); } // 调整ChanMACD图大小 if (showMacd && chanMacdChart && chanMacdChartContainer) { chanMacdChart.applyOptions({ width: chanMacdChartContainer.clientWidth, height: chanMacdChartContainer.clientHeight }); } // 重新同步 - 使用主图作为同步源 setTimeout(() => { if (mainChart) { syncCharts(mainChart, mainChartContainer); } }, 200); }); // 显示笔的绘制 - 分别处理主周期、次周期和次次周期 if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked') || $('#showSubSubBi').is(':checked')) { console.log('绘制笔 - 已启用'); let biLines = []; // 主周期笔 if ($('#showMainBi').is(':checked') && currentData.bi_list && currentData.bi_list.length > 0) { console.log(`绘制主周期笔数据,共${currentData.bi_list.length}条`); // 清空已有的主周期笔系列 tvWidget.series.mainBiSeries = []; tvWidget.series.mainUncompletedBiSeries = []; // 遍历处理每个笔 currentData.bi_list.forEach(function(bi) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000); const endTime = Math.floor(new Date(bi.end_time).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('主周期笔时间转换错误:', bi.start_time, bi.end_time); return; } const startPrice = parseFloat(bi.start_price); const endPrice = parseFloat(bi.end_price); if (isNaN(startPrice) || isNaN(endPrice)) { console.error('主周期笔价格转换错误:', bi.start_price, bi.end_price); return; } // 添加线段 biLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: bi.direction === 1 ? '#dc3545' : '#28a745', // 主周期笔颜色 lineWidth: 1, }); // 添加到图表对象 tvWidget.series.mainBiSeries.push({ time: startTime, value: startPrice, color: bi.direction === 1 ? '#dc3545' : '#28a745', lineWidth: 1 }); // 在笔的末端添加macd_div值标记 if (bi.macd_div && bi.macd_div !== 0 && $('#showMainMacdDiv').is(':checked')) { console.log(`添加主周期macd_div标记: ${bi.macd_div.toFixed(2)}, 在时间点: ${endTime}`); const macdDivLabel = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false, color: 'transparent', // 设置为透明色 lineWidth: 0, // 线宽为0 }); // 添加一个透明的数据点用于承载标记 macdDivLabel.setData([ { time: endTime, value: endPrice } ]); // 主周期MACD背离标记根据笔方向显示,远离K线避免与分型重叠 const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar'; const textColor = bi.macd_div > 0 ? '#dc3545' : '#28a745'; // 只使用标记,不添加数据点 macdDivLabel.setMarkers([ { time: endTime, position: markerPosition, color: textColor, text: `${bi.macd_div.toFixed(2)}`, // 添加M前缀区分 size: 0.6, // 更小的尺寸,远离分型标记 } ]); } } catch (e) { console.error('主周期笔处理出错:', e); } }); } // 次周期笔 if ($('#showElementBi').is(':checked') && currentData.element_bi_list && currentData.element_bi_list.length > 0) { console.log(`绘制次周期笔数据,共${currentData.element_bi_list.length}条`); // 清空已有的次周期笔系列 tvWidget.series.elementBiSeries = []; tvWidget.series.elementUncompletedBiSeries = []; currentData.element_bi_list.forEach(function(bi) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000); const endTime = Math.floor(new Date(bi.end_time).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('次周期笔时间转换错误:', bi.start_time, bi.end_time); return; } const startPrice = parseFloat(bi.start_price); const endPrice = parseFloat(bi.end_price); if (isNaN(startPrice) || isNaN(endPrice)) { console.error('次周期笔价格转换错误:', bi.start_price, bi.end_price); return; } // 添加线段 biLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: bi.direction === 1 ? '#9c27b0' : '#673ab7', // 次周期笔颜色 lineWidth: 1, }); // 添加到图表对象 tvWidget.series.elementBiSeries.push({ time: startTime, value: startPrice, color: bi.direction === 1 ? '#9c27b0' : '#673ab7', lineWidth: 1 }); // 在笔的末端添加macd_div值标记 if (bi.macd_div && bi.macd_div !== 0 && $('#showElementMacdDiv').is(':checked')) { console.log(`添加元素周期macd_div标记: ${bi.macd_div.toFixed(2)}, 在时间点: ${endTime}`); const macdDivLabel = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false, color: 'transparent', // 设置为透明色 lineWidth: 0, // 线宽为0 }); // 添加一个透明的数据点用于承载标记 macdDivLabel.setData([ { time: endTime, value: endPrice } ]); // 次周期MACD背离标记使用不同位置,进一步避免重叠 const markerPosition = bi.direction === 1 ? 'aboveBar' : 'belowBar'; const textColor = bi.macd_div > 0 ? '#9c27b0' : '#673ab7'; // 只使用标记,不添加数据点 macdDivLabel.setMarkers([ { time: endTime, position: markerPosition, color: textColor, text: `${bi.macd_div.toFixed(2)}`, // 添加E前缀区分次周期 size: 0.4, // 更小的尺寸,让分型标记有更多空间 } ]); } } catch (e) { console.error('次周期笔处理出错:', e); } }); } // 绘制未完成笔 - 主周期 if ($('#showMainBi').is(':checked') && currentData.uncompleted_bi_list && currentData.uncompleted_bi_list.length > 0) { console.log(`绘制主周期未完成笔数据,共${currentData.uncompleted_bi_list.length}条`); currentData.uncompleted_bi_list.forEach(function(bi) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000); // 未完成笔的结束时间设为当前K线的最后时间 const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('主周期未完成笔时间转换错误:', bi.start_time); return; } const startPrice = parseFloat(bi.start_price); if (isNaN(startPrice)) { console.error('主周期未完成笔价格转换错误:', bi.start_price); return; } // 根据笔的方向确定终点价格 let endPrice; const latestKline = currentData.kline_data[currentData.kline_data.length-1]; if (bi.direction === 1) { // 向上笔,终点为最新K线的最高点 endPrice = parseFloat(latestKline.high); } else { // 向下笔,终点为最新K线的最低点 endPrice = parseFloat(latestKline.low); } // 添加未完成笔(红色虚线) biLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: '#FF0000', // 红色 lineWidth: 1, lineStyle: 2 // 虚线 }); // 添加到图表对象 tvWidget.series.mainUncompletedBiSeries.push({ time: startTime, value: startPrice, color: '#FF0000', lineWidth: 1, lineStyle: 2 }); } catch (e) { console.error('主周期未完成笔处理出错:', e); } }); } // 绘制未完成笔 - 次周期 if ($('#showElementBi').is(':checked') && currentData.element_uncompleted_bi_list && currentData.element_uncompleted_bi_list.length > 0) { console.log(`绘制次周期未完成笔数据,共${currentData.element_uncompleted_bi_list.length}条`); currentData.element_uncompleted_bi_list.forEach(function(bi) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000); // 未完成笔的结束时间设为当前K线的最后时间 const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('次周期未完成笔时间转换错误:', bi.start_time); return; } const startPrice = parseFloat(bi.start_price); if (isNaN(startPrice)) { console.error('次周期未完成笔价格转换错误:', bi.start_price); return; } // 根据笔的方向确定终点价格 let endPrice; const latestKline = currentData.kline_data[currentData.kline_data.length-1]; if (bi.direction === 1) { // 向上笔,终点为最新K线的最高点 endPrice = parseFloat(latestKline.high); } else { // 向下笔,终点为最新K线的最低点 endPrice = parseFloat(latestKline.low); } // 添加未完成笔(红色虚线) biLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: '#FF0000', // 红色 lineWidth: 1, lineStyle: 2 // 虚线 }); // 添加到图表对象 tvWidget.series.elementUncompletedBiSeries.push({ time: startTime, value: startPrice, color: '#FF0000', lineWidth: 1, lineStyle: 2 }); } catch (e) { console.error('次周期未完成笔处理出错:', e); } }); } // 次次周期笔 if ($('#showSubSubBi').is(':checked') && currentData.sub_sub_bi_list && currentData.sub_sub_bi_list.length > 0) { tvWidget.series.subSubBiSeries = []; tvWidget.series.subSubUncompletedBiSeries = []; currentData.sub_sub_bi_list.forEach(function(bi) { try { const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000); const endTime = bi.end_time ? Math.floor(new Date(bi.end_time).getTime() / 1000) : 0; if (isNaN(startTime) || !endTime) return; const startPrice = parseFloat(bi.start_price); const endPrice = parseFloat(bi.end_price); if (isNaN(startPrice) || isNaN(endPrice)) return; biLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: bi.direction === 1 ? '#00897b' : '#26a69a', lineWidth: 1, lineStyle: 0 }); tvWidget.series.subSubBiSeries.push({ time: startTime, value: startPrice, color: '#00897b', lineWidth: 1 }); } catch (e) { console.error('次次周期笔处理出错:', e); } }); } // 次次周期未完成笔 if ($('#showSubSubBi').is(':checked') && currentData.sub_sub_uncompleted_bi_list && currentData.sub_sub_uncompleted_bi_list.length > 0) { if (!tvWidget.series.subSubBiSeries) tvWidget.series.subSubBiSeries = []; if (!tvWidget.series.subSubUncompletedBiSeries) tvWidget.series.subSubUncompletedBiSeries = []; const klineData = currentData.kline_data || []; const lastTime = klineData.length ? Math.floor(new Date(klineData[klineData.length-1].date).getTime() / 1000) : 0; currentData.sub_sub_uncompleted_bi_list.forEach(function(bi) { try { const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000); if (isNaN(startTime) || !lastTime) return; const startPrice = parseFloat(bi.start_price); if (isNaN(startPrice)) return; const lastK = klineData[klineData.length-1]; const endPrice = bi.direction === 1 ? parseFloat(lastK.high) : parseFloat(lastK.low); biLines.push({ startTime: startTime, endTime: lastTime, startPrice: startPrice, endPrice: endPrice, color: '#00695c', lineWidth: 1, lineStyle: 2 }); tvWidget.series.subSubUncompletedBiSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 1, lineStyle: 2 }); } catch (e) { console.error('次次周期未完成笔处理出错:', e); } }); } // 添加所有笔到图表 // 1m 等小周期叠加到大周期时,笔数量会非常大;每条笔创建一个 series 会导致主图渲染退化 // 这里限制绘制数量,优先保留最新笔,避免把主K线和其他元素“挤没” const MAX_BI_LINE_SERIES = 800; if (biLines.length > MAX_BI_LINE_SERIES) { console.warn(`BI线条过多(${biLines.length}),仅绘制最新 ${MAX_BI_LINE_SERIES} 条以保障主图稳定`); } const linesToDraw = biLines.length > MAX_BI_LINE_SERIES ? biLines.slice(-MAX_BI_LINE_SERIES) : biLines; linesToDraw.forEach(line => { try { if (!Number.isFinite(line.startTime) || !Number.isFinite(line.endTime)) return; if (!Number.isFinite(line.startPrice) || !Number.isFinite(line.endPrice)) return; if (line.endTime <= line.startTime) return; const lineSeries = mainChart.addLineSeries({ color: line.color, lineWidth: line.lineWidth, lineStyle: line.lineStyle || 0, // 支持虚线样式 lastValueVisible: false, priceLineVisible: false, }); lineSeries.setData([ { time: line.startTime, value: line.startPrice }, { time: line.endTime, value: line.endPrice } ]); } catch (e) { console.warn('绘制BI线段失败,已跳过单条异常数据:', e); } }); } else { console.log('绘制笔 - 已禁用'); } // 显示线段的绘制 - 分别处理主周期、次周期和次次周期 if ($('#showMainSeg').is(':checked') || $('#showElementSeg').is(':checked') || $('#showSubSubSeg').is(':checked')) { console.log('绘制线段 - 已启用'); let segLines = []; // 主周期线段 if ($('#showMainSeg').is(':checked') && currentData.seg_list && currentData.seg_list.length > 0) { console.log(`绘制主周期线段数据,共${currentData.seg_list.length}条`); // 清空已有的主周期线段系列 tvWidget.series.mainSegSeries = []; tvWidget.series.mainUncompletedSegSeries = []; currentData.seg_list.forEach(function(seg) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000); const endTime = Math.floor(new Date(seg.end_time).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('主周期线段时间转换错误:', seg.start_time, seg.end_time); return; } const startPrice = parseFloat(seg.start_price); const endPrice = parseFloat(seg.end_price); if (isNaN(startPrice) || isNaN(endPrice)) { console.error('主周期线段价格转换错误:', seg.start_price, seg.end_price); return; } // 添加线段 segLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: seg.direction === 1 ? '#FF6B6B' : '#4CAF50', // 主周期线段颜色 lineWidth: 2, }); // 添加到图表对象 tvWidget.series.mainSegSeries.push({ time: startTime, value: startPrice, color: seg.direction === 1 ? '#FF6B6B' : '#4CAF50', lineWidth: 2 }); } catch (e) { console.error('主周期线段处理出错:', e); } }); } // 次周期线段 if ($('#showElementSeg').is(':checked') && currentData.element_seg_list && currentData.element_seg_list.length > 0) { console.log(`绘制次周期线段数据,共${currentData.element_seg_list.length}条`); // 清空已有的次周期线段系列 tvWidget.series.elementSegSeries = []; tvWidget.series.elementUncompletedSegSeries = []; currentData.element_seg_list.forEach(function(seg) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000); const endTime = Math.floor(new Date(seg.end_time).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('次周期线段时间转换错误:', seg.start_time, seg.end_time); return; } const startPrice = parseFloat(seg.start_price); const endPrice = parseFloat(seg.end_price); if (isNaN(startPrice) || isNaN(endPrice)) { console.error('次周期线段价格转换错误:', seg.start_price, seg.end_price); return; } // 添加线段 segLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: seg.direction === 1 ? '#673ab7' : '#9c27b0', // 次周期线段颜色 lineWidth: 2, }); // 添加到图表对象 tvWidget.series.elementSegSeries.push({ time: startTime, value: startPrice, color: seg.direction === 1 ? '#673ab7' : '#9c27b0', lineWidth: 2 }); } catch (e) { console.error('次周期线段处理出错:', e); } }); } // 绘制未完成线段 - 主周期 if ($('#showMainSeg').is(':checked') && currentData.uncompleted_seg_list && currentData.uncompleted_seg_list.length > 0) { console.log(`绘制主周期未完成线段数据,共${currentData.uncompleted_seg_list.length}条`); currentData.uncompleted_seg_list.forEach(function(seg) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000); if (isNaN(startTime)) { console.error('主周期未完成线段时间转换错误:', seg.start_time); return; } const startPrice = parseFloat(seg.start_price); if (isNaN(startPrice)) { console.error('主周期未完成线段价格转换错误:', seg.start_price); return; } let endTime, endPrice; if (seg.end_time && seg.end_price) { // 有结束时间和价格的未完成线段(倒数第二个等) endTime = Math.floor(new Date(seg.end_time).getTime() / 1000); endPrice = parseFloat(seg.end_price); if (isNaN(endTime) || isNaN(endPrice)) { console.error('主周期未完成线段结束时间或价格转换错误:', seg.end_time, seg.end_price); return; } } else { // 没有结束时间和价格的未完成线段(最后一个) endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(endTime)) { console.error('主周期未完成线段结束时间转换错误'); return; } // 根据线段的方向确定终点价格 const latestKline = currentData.kline_data[currentData.kline_data.length-1]; if (seg.direction === 1) { // 向上线段,终点为最新K线的最高点 endPrice = parseFloat(latestKline.high); } else { // 向下线段,终点为最新K线的最低点 endPrice = parseFloat(latestKline.low); } } // 添加未完成线段(红色虚线) segLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: '#FF0000', // 红色 lineWidth: 2, lineStyle: 2 // 虚线 }); // 添加到图表对象 tvWidget.series.mainUncompletedSegSeries.push({ time: startTime, value: startPrice, color: '#FF0000', lineWidth: 2, lineStyle: 2 }); } catch (e) { console.error('主周期未完成线段处理出错:', e); } }); } // 绘制未完成线段 - 次周期 if ($('#showElementSeg').is(':checked') && currentData.element_uncompleted_seg_list && currentData.element_uncompleted_seg_list.length > 0) { console.log(`绘制次周期未完成线段数据,共${currentData.element_uncompleted_seg_list.length}条`); currentData.element_uncompleted_seg_list.forEach(function(seg) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000); if (isNaN(startTime)) { console.error('次周期未完成线段时间转换错误:', seg.start_time); return; } const startPrice = parseFloat(seg.start_price); if (isNaN(startPrice)) { console.error('次周期未完成线段价格转换错误:', seg.start_price); return; } let endTime, endPrice; if (seg.end_time && seg.end_price) { // 有结束时间和价格的未完成线段(倒数第二个等) endTime = Math.floor(new Date(seg.end_time).getTime() / 1000); endPrice = parseFloat(seg.end_price); if (isNaN(endTime) || isNaN(endPrice)) { console.error('次周期未完成线段结束时间或价格转换错误:', seg.end_time, seg.end_price); return; } } else { // 没有结束时间和价格的未完成线段(最后一个) endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(endTime)) { console.error('次周期未完成线段结束时间转换错误'); return; } // 根据线段的方向确定终点价格 const latestKline = currentData.kline_data[currentData.kline_data.length-1]; if (seg.direction === 1) { // 向上线段,终点为最新K线的最高点 endPrice = parseFloat(latestKline.high); } else { // 向下线段,终点为最新K线的最低点 endPrice = parseFloat(latestKline.low); } } // 添加未完成线段(红色虚线) segLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: '#FF0000', // 红色 lineWidth: 2, lineStyle: 2 // 虚线 }); // 添加到图表对象 tvWidget.series.elementUncompletedSegSeries.push({ time: startTime, value: startPrice, color: '#FF0000', lineWidth: 2, lineStyle: 2 }); } catch (e) { console.error('次周期未完成线段处理出错:', e); } }); } // 次次周期线段 if ($('#showSubSubSeg').is(':checked') && currentData.sub_sub_seg_list && currentData.sub_sub_seg_list.length > 0) { tvWidget.series.subSubSegSeries = []; tvWidget.series.subSubUncompletedSegSeries = []; currentData.sub_sub_seg_list.forEach(function(seg) { try { const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000); const endTime = seg.end_time ? Math.floor(new Date(seg.end_time).getTime() / 1000) : 0; if (isNaN(startTime) || !endTime) return; const startPrice = parseFloat(seg.start_price); const endPrice = parseFloat(seg.end_price); if (isNaN(startPrice) || isNaN(endPrice)) return; segLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: seg.direction === 1 ? '#00897b' : '#26a69a', lineWidth: 2, lineStyle: 0 }); tvWidget.series.subSubSegSeries.push({ time: startTime, value: startPrice, color: '#00897b', lineWidth: 2 }); } catch (e) { console.error('次次周期线段处理出错:', e); } }); } // 次次周期未完成线段 if ($('#showSubSubSeg').is(':checked') && currentData.sub_sub_uncompleted_seg_list && currentData.sub_sub_uncompleted_seg_list.length > 0) { if (!tvWidget.series.subSubUncompletedSegSeries) tvWidget.series.subSubUncompletedSegSeries = []; const klineDataSeg = currentData.kline_data || []; const lastTimeSeg = klineDataSeg.length ? Math.floor(new Date(klineDataSeg[klineDataSeg.length-1].date).getTime() / 1000) : 0; currentData.sub_sub_uncompleted_seg_list.forEach(function(seg) { try { const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000); if (isNaN(startTime) || !lastTimeSeg) return; const startPrice = parseFloat(seg.start_price); if (isNaN(startPrice)) return; let endTime = lastTimeSeg, endPrice; if (seg.end_time && seg.end_price) { endTime = Math.floor(new Date(seg.end_time).getTime() / 1000); endPrice = parseFloat(seg.end_price); } else { const lastK = klineDataSeg[klineDataSeg.length-1]; endPrice = seg.direction === 1 ? parseFloat(lastK.high) : parseFloat(lastK.low); } segLines.push({ startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, color: '#00695c', lineWidth: 2, lineStyle: 2 }); tvWidget.series.subSubUncompletedSegSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 2 }); } catch (e) { console.error('次次周期未完成线段处理出错:', e); } }); } // 添加所有线段到图表 segLines.forEach(line => { const lineSeries = mainChart.addLineSeries({ color: line.color, lineWidth: line.lineWidth, lineStyle: line.lineStyle || 0, // 支持虚线样式 lastValueVisible: false, priceLineVisible: false, }); lineSeries.setData([ { time: line.startTime, value: line.startPrice }, { time: line.endTime, value: line.endPrice } ]); }); } else { console.log('绘制线段 - 已禁用'); } // 显示中枢的绘制 - 分别处理主周期、次周期和次次周期(包含BI中枢,沿用同样样式与开关) if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked')) { console.log('绘制中枢 - 已启用'); // 主周期中枢 if ($('#showMainZs').is(':checked') && currentData.zs_list && currentData.zs_list.length > 0) { console.log(`绘制主周期中枢数据,共${currentData.zs_list.length}条`); currentData.zs_list.forEach(function(zs) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = Math.floor(new Date(zs.end_time).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('主周期中枢时间转换错误:', zs.start_time, zs.end_time); return; } const zg = parseFloat(zs.zg); // 中枢上沿 const zd = parseFloat(zs.zd); // 中枢下沿 const gg = parseFloat(zs.gg); // 中枢高高 const dd = parseFloat(zs.dd); // 中枢低低 if (isNaN(zg) || isNaN(zd)) { console.error('主周期中枢价格转换错误:', zs.zg, zs.zd); return; } // 创建中枢上边界 const topSeries = mainChart.addLineSeries({ color: '#F1C40F', // 主周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); topSeries.setData([ { time: startTime, value: zg }, { time: endTime, value: zg } ]); // 为下边界创建另一条线 const bottomSeries = mainChart.addLineSeries({ color: '#F1C40F', // 主周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); bottomSeries.setData([ { time: startTime, value: zd }, { time: endTime, value: zd } ]); // 添加左边界 const leftSeries = mainChart.addLineSeries({ color: '#F1C40F', // 主周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); leftSeries.setData([ { time: startTime, value: zd }, { time: startTime, value: zg } ]); // 添加右边界 const rightSeries = mainChart.addLineSeries({ color: '#F1C40F', // 主周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); rightSeries.setData([ { time: endTime, value: zd }, { time: endTime, value: zg } ]); // 绘制gg线(中枢高高) if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color: '#F1C40F', // 使用中枢自己的颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); ggSeries.setData([ { time: startTime, value: gg }, { time: endTime, value: gg } ]); } // 绘制dd线(中枢低低) if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color: '#F1C40F', // 使用中枢自己的颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); ddSeries.setData([ { time: startTime, value: dd }, { time: endTime, value: dd } ]); } // 添加到图表对象 tvWidget.series.mainZsSeries.push({ time: startTime, value: zg, color: '#F1C40F', lineWidth: 1 }); tvWidget.series.mainZsSeries.push({ time: endTime, value: zg, color: '#F1C40F', lineWidth: 1 }); tvWidget.series.mainZsSeries.push({ time: startTime, value: zd, color: '#F1C40F', lineWidth: 1 }); tvWidget.series.mainZsSeries.push({ time: endTime, value: zd, color: '#F1C40F', lineWidth: 1 }); } catch (e) { console.error('主周期中枢处理出错:', e); } }); } // 次周期中枢 if ($('#showElementZs').is(':checked') && currentData.element_zs_list && currentData.element_zs_list.length > 0) { console.log(`绘制次周期中枢数据,共${currentData.element_zs_list.length}条`); currentData.element_zs_list.forEach(function(zs) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = Math.floor(new Date(zs.end_time).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('次周期中枢时间转换错误:', zs.start_time, zs.end_time); return; } const zg = parseFloat(zs.zg); // 中枢上沿 const zd = parseFloat(zs.zd); // 中枢下沿 const gg = parseFloat(zs.gg); // 中枢高高 const dd = parseFloat(zs.dd); // 中枢低低 if (isNaN(zg) || isNaN(zd)) { console.error('次周期中枢价格转换错误:', zs.zg, zs.zd); return; } // 创建中枢上边界 const topSeries = mainChart.addLineSeries({ color: '#3f51b5', // 次周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); topSeries.setData([ { time: startTime, value: zg }, { time: endTime, value: zg } ]); // 为下边界创建另一条线 const bottomSeries = mainChart.addLineSeries({ color: '#3f51b5', // 次周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); bottomSeries.setData([ { time: startTime, value: zd }, { time: endTime, value: zd } ]); // 添加左边界 const leftSeries = mainChart.addLineSeries({ color: '#3f51b5', // 次周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); leftSeries.setData([ { time: startTime, value: zd }, { time: startTime, value: zg } ]); // 添加右边界 const rightSeries = mainChart.addLineSeries({ color: '#3f51b5', // 次周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); rightSeries.setData([ { time: endTime, value: zd }, { time: endTime, value: zg } ]); // 绘制gg线(中枢高高) if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color: '#3f51b5', // 使用次周期中枢自己的颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); ggSeries.setData([ { time: startTime, value: gg }, { time: endTime, value: gg } ]); } // 绘制dd线(中枢低低) if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color: '#3f51b5', // 使用次周期中枢自己的颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); ddSeries.setData([ { time: startTime, value: dd }, { time: endTime, value: dd } ]); } // 添加到图表对象 tvWidget.series.elementZsSeries.push({ time: startTime, value: zg, color: '#3f51b5', lineWidth: 1 }); tvWidget.series.elementZsSeries.push({ time: endTime, value: zg, color: '#3f51b5', lineWidth: 1 }); tvWidget.series.elementZsSeries.push({ time: startTime, value: zd, color: '#3f51b5', lineWidth: 1 }); tvWidget.series.elementZsSeries.push({ time: endTime, value: zd, color: '#3f51b5', lineWidth: 1 }); } catch (e) { console.error('次周期中枢处理出错:', e); } }); } // 次次周期SEG中枢 if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_zs_list && currentData.sub_sub_zs_list.length > 0) { const subSubZsColor = '#00897b'; currentData.sub_sub_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : 0; if (isNaN(startTime) || !endTime) return; const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) return; mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } catch (e) { console.error('次次周期中枢处理出错:', e); } }); } } else { console.log('绘制中枢 - 已禁用'); } // BI中枢(已完成)- 使用独立的BI开关 if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) { try { console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`); } catch (e) {} currentData.bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { return; } const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) { return; } const color = '#F1C40F'; const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); } if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } } catch (e) { console.error('主周期BI中枢处理出错:', e); } }); } if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_bi_zs_list && currentData.sub_sub_bi_zs_list.length > 0) { currentData.sub_sub_bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const kd = currentData.kline_data || []; const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : (kd.length ? Math.floor(new Date(kd[kd.length-1].date).getTime() / 1000) : 0); if (isNaN(startTime) || !endTime) return; const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) return; const color = '#00897b'; mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } catch (e) { console.error('次次周期BI中枢处理出错:', e); } }); } if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) { try { console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`); } catch (e) {} currentData.element_bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { return; } const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) { return; } const color = '#3f51b5'; const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); } if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } } catch (e) { console.error('次周期BI中枢处理出错:', e); } }); } // 结构价值区绘制(半透明填充区 + 边框) if ($('#showMainStructureZone').is(':checked') && currentData.structure_zones && currentData.structure_zones.length > 0) { try { const kd = currentData.kline_data || []; if (kd.length > 0) { const chartStart = Math.floor(new Date(kd[0].date).getTime() / 1000); const chartEnd = Math.floor(new Date(kd[kd.length-1].date).getTime() / 1000); // 计算可见价格范围,过滤超出范围的区间 let priceMin = Infinity, priceMax = -Infinity; kd.forEach(function(k) { const hi = parseFloat(k.high), lo = parseFloat(k.low); if (!isNaN(hi) && hi > priceMax) priceMax = hi; if (!isNaN(lo) && lo < priceMin) priceMin = lo; }); const priceMargin = (priceMax - priceMin) * 0.05; priceMin -= priceMargin; priceMax += priceMargin; let drawnCount = 0; currentData.structure_zones.forEach(function(zone) { try { // 跳过完全超出可视价格范围的区间 if (zone.upper < priceMin || zone.lower > priceMax) return; const fillColor = zone.zone_type === 'support' ? 'rgba(46, 204, 113, 0.08)' : zone.zone_type === 'resistance' ? 'rgba(231, 76, 60, 0.08)' : 'rgba(149, 165, 166, 0.06)'; const borderColor = zone.zone_type === 'support' ? 'rgba(46, 204, 113, 0.7)' : zone.zone_type === 'resistance' ? 'rgba(231, 76, 60, 0.7)' : 'rgba(149, 165, 166, 0.6)'; // 填充区:在上下边界之间画多条半透明线模拟填充 const fillLines = 8; const step = (zone.upper - zone.lower) / (fillLines + 1); for (let fi = 1; fi <= fillLines; fi++) { const fy = zone.lower + step * fi; mainChart.addLineSeries({ color: fillColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false }) .setData([{ time: chartStart, value: fy }, { time: chartEnd, value: fy }]); } // 上边界(粗线) mainChart.addLineSeries({ color: borderColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false }) .setData([{ time: chartStart, value: zone.upper }, { time: chartEnd, value: zone.upper }]); // 下边界(粗线) mainChart.addLineSeries({ color: borderColor, lineWidth: 2, lineStyle: 0, lastValueVisible: false, priceLineVisible: false }) .setData([{ time: chartStart, value: zone.lower }, { time: chartEnd, value: zone.lower }]); // 中心线(虚线) mainChart.addLineSeries({ color: borderColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false }) .setData([{ time: chartStart, value: zone.center }, { time: chartEnd, value: zone.center }]); drawnCount++; } catch (e) { console.error('结构区绘制出错:', e); } }); console.log(`结构区: 共${currentData.structure_zones.length}个, 绘制${drawnCount}个 (可见价格范围: ${priceMin.toFixed(0)}-${priceMax.toFixed(0)})`); } } catch (e) { console.error('结构区整体绘制出错:', e); } } // 显示未完成中枢 - 分别处理主周期、次周期和次次周期 if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) { console.log('绘制未完成中枢 - 已启用'); // 显示BI中枢绘制(沿用中枢样式) if ($('#showMainBiZs').is(':checked') || $('#showElementBiZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) { console.log('绘制BI中枢 - 已启用'); // 主周期 BI 中枢 console.log('主BI开关:', $('#showMainBiZs').is(':checked'), '数据长度:', currentData.bi_zs_list ? currentData.bi_zs_list.length : 0); if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) { console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`); currentData.bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { return; } const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) { return; } const color = '#9C27B0'; // 主周期BI中枢颜色(紫色) const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); } if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } } catch (e) { console.error('主周期BI中枢处理出错:', e); } }); } // 主周期 未完成 BI 中枢 console.log('主未完成BI长度:', currentData.uncompleted_bi_zs_list ? currentData.uncompleted_bi_zs_list.length : 0); if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) { console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`); currentData.uncompleted_bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { return; } const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) { return; } const color = '#9C27B0'; const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); } if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } } catch (e) { console.error('主周期未完成BI中枢处理出错:', e); } }); } // 次周期 BI 中枢 console.log('次BI开关:', $('#showElementBiZs').is(':checked'), '数据长度:', currentData.element_bi_zs_list ? currentData.element_bi_zs_list.length : 0); if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) { console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`); currentData.element_bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { return; } const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) { return; } const color = '#8BC34A'; // 次周期BI中枢颜色(绿) const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); } if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } } catch (e) { console.error('次周期BI中枢处理出错:', e); } }); } // 次周期 未完成 BI 中枢 console.log('次未完成BI长度:', currentData.element_uncompleted_bi_zs_list ? currentData.element_uncompleted_bi_zs_list.length : 0); if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) { console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`); currentData.element_uncompleted_bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { return; } const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) { return; } const color = '#8BC34A'; const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); } if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); } }); } // 次次周期 未完成 BI 中枢 if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_uncompleted_bi_zs_list && currentData.sub_sub_uncompleted_bi_zs_list.length > 0) { const kdBi = currentData.kline_data || []; const endTimeBi = kdBi.length ? Math.floor(new Date(kdBi[kdBi.length-1].date).getTime() / 1000) : 0; currentData.sub_sub_uncompleted_bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); if (isNaN(startTime) || !endTimeBi) return; const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) return; const color = '#00897b'; mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeBi, value: zg }]); mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeBi, value: zd }]); mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeBi, value: gg }]); if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeBi, value: dd }]); } catch (e) { console.error('次次周期未完成BI中枢处理出错:', e); } }); } } // 主周期未完成中枢 if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) { console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`); currentData.uncompleted_zs_list.forEach(function(zs) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); // 未完成中枢的结束时间设为当前K线的最后时间 const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('主周期未完成中枢时间转换错误:', zs.start_time); return; } const zg = parseFloat(zs.zg); // 中枢上沿 const zd = parseFloat(zs.zd); // 中枢下沿 const gg = parseFloat(zs.gg); // 中枢高高 const dd = parseFloat(zs.dd); // 中枢低低 if (isNaN(zg) || isNaN(zd)) { console.error('主周期未完成中枢价格转换错误:', zs.zg, zs.zd); return; } // 创建未完成中枢上边界 const topSeries = mainChart.addLineSeries({ color: '#F1C40F', // 主周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); topSeries.setData([ { time: startTime, value: zg }, { time: endTime, value: zg } ]); // 为下边界创建另一条线 const bottomSeries = mainChart.addLineSeries({ color: '#F1C40F', // 主周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); bottomSeries.setData([ { time: startTime, value: zd }, { time: endTime, value: zd } ]); // 添加左边界 const leftSeries = mainChart.addLineSeries({ color: '#F1C40F', // 主周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); leftSeries.setData([ { time: startTime, value: zd }, { time: startTime, value: zg } ]); // 添加一个标记,标识这是未完成中枢 const markerSeries = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false, }); markerSeries.setMarkers([ { time: startTime, position: 'aboveBar', color: '#F1C40F', shape: 'circle', text: '未完', size: 1 } ]); // 绘制gg线(中枢高高) if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color: '#F1C40F', // 使用中枢自己的颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); ggSeries.setData([ { time: startTime, value: gg }, { time: endTime, value: gg } ]); } // 绘制dd线(中枢低低) if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color: '#F1C40F', // 使用中枢自己的颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); ddSeries.setData([ { time: startTime, value: dd }, { time: endTime, value: dd } ]); } // 添加到图表对象 tvWidget.series.mainUncompletedZsSeries.push({ time: startTime, value: zg, color: '#F1C40F', lineWidth: 1 }); tvWidget.series.mainUncompletedZsSeries.push({ time: endTime, value: zg, color: '#F1C40F', lineWidth: 1 }); tvWidget.series.mainUncompletedZsSeries.push({ time: startTime, value: zd, color: '#F1C40F', lineWidth: 1 }); tvWidget.series.mainUncompletedZsSeries.push({ time: endTime, value: zd, color: '#F1C40F', lineWidth: 1 }); } catch (e) { console.error('主周期未完成中枢处理出错:', e); } }); } // 次周期未完成中枢 if ($('#showElementZs').is(':checked') && currentData.element_uncompleted_zs_list && currentData.element_uncompleted_zs_list.length > 0) { console.log(`绘制次周期未完成中枢数据,共${currentData.element_uncompleted_zs_list.length}条`); currentData.element_uncompleted_zs_list.forEach(function(zs) { try { // 直接使用UTC时间戳(秒) const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); // 未完成中枢的结束时间设为当前K线的最后时间 const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { console.error('次周期未完成中枢时间转换错误:', zs.start_time); return; } const zg = parseFloat(zs.zg); // 中枢上沿 const zd = parseFloat(zs.zd); // 中枢下沿 const gg = parseFloat(zs.gg); // 中枢高高 const dd = parseFloat(zs.dd); // 中枢低低 if (isNaN(zg) || isNaN(zd)) { console.error('次周期未完成中枢价格转换错误:', zs.zg, zs.zd); return; } // 创建未完成中枢上边界 const topSeries = mainChart.addLineSeries({ color: '#3f51b5', // 次周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); topSeries.setData([ { time: startTime, value: zg }, { time: endTime, value: zg } ]); // 为下边界创建另一条线 const bottomSeries = mainChart.addLineSeries({ color: '#3f51b5', // 次周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); bottomSeries.setData([ { time: startTime, value: zd }, { time: endTime, value: zd } ]); // 添加左边界 const leftSeries = mainChart.addLineSeries({ color: '#3f51b5', // 次周期中枢颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); leftSeries.setData([ { time: startTime, value: zd }, { time: startTime, value: zg } ]); // 添加一个标记,标识这是未完成中枢 const markerSeries = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false, }); markerSeries.setMarkers([ { time: startTime, position: 'aboveBar', color: '#3f51b5', shape: 'circle', text: '未完', size: 1 } ]); // 绘制gg线(中枢高高) if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color: '#3f51b5', // 使用次周期中枢自己的颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); ggSeries.setData([ { time: startTime, value: gg }, { time: endTime, value: gg } ]); } // 绘制dd线(中枢低低) if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color: '#3f51b5', // 使用次周期中枢自己的颜色 lineWidth: 1, lastValueVisible: false, priceLineVisible: false, }); ddSeries.setData([ { time: startTime, value: dd }, { time: endTime, value: dd } ]); } // 添加到图表对象 tvWidget.series.elementUncompletedZsSeries.push({ time: startTime, value: zg, color: '#3f51b5', lineWidth: 1 }); tvWidget.series.elementUncompletedZsSeries.push({ time: endTime, value: zg, color: '#3f51b5', lineWidth: 1 }); tvWidget.series.elementUncompletedZsSeries.push({ time: startTime, value: zd, color: '#3f51b5', lineWidth: 1 }); tvWidget.series.elementUncompletedZsSeries.push({ time: endTime, value: zd, color: '#3f51b5', lineWidth: 1 }); } catch (e) { console.error('次周期未完成中枢处理出错:', e); } }); } // 次次周期未完成SEG中枢 if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_uncompleted_zs_list && currentData.sub_sub_uncompleted_zs_list.length > 0) { const kdZs = currentData.kline_data || []; const endTimeZs = kdZs.length ? Math.floor(new Date(kdZs[kdZs.length-1].date).getTime() / 1000) : 0; const subSubUZsColor = '#00897b'; currentData.sub_sub_uncompleted_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); if (isNaN(startTime) || !endTimeZs) return; const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) return; mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeZs, value: zg }]); mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeZs, value: zd }]); mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeZs, value: gg }]); if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeZs, value: dd }]); } catch (e) { console.error('次次周期未完成中枢处理出错:', e); } }); } } else { console.log('绘制未完成中枢 - 已禁用'); } // 未完成BI中枢 - 使用独立的BI开关 if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) { try { console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`); } catch (e) {} currentData.uncompleted_bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { return; } const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) { return; } const color = '#F1C40F'; const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); } if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } } catch (e) { console.error('主周期未完成BI中枢处理出错:', e); } }); } // 次周期 未完成 BI 中枢 if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) { try { console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`); } catch (e) {} currentData.element_uncompleted_bi_zs_list.forEach(function(zs) { try { const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); if (isNaN(startTime) || isNaN(endTime)) { return; } const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); if (isNaN(zg) || isNaN(zd)) { return; } const color = '#3f51b5'; const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); if (!isNaN(gg) && gg > 0) { const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); } if (!isNaN(dd) && dd > 0) { const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); } } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); } }); } // 添加买卖点标记(新版:基于 bsp_list / element_bsp_list / sub_sub_bsp_list,按与 KLC 分型相同方式合并到主图标记) if ($('#showMainBsp').is(':checked') || $('#showElementBsp').is(':checked') || $('#showSubSubBsp').is(':checked')) { console.log('绘制买卖点(BSP) - 已启用'); // BSP 样式定义 const BSP_STYLE = { 'BSP1_BUY': { color: '#FF1744', text: 'B1', position: 'belowBar', size: 0.5 }, 'BSP2_BUY': { color: '#F50057', text: 'B2', position: 'belowBar', size: 0.5 }, 'BSP3_BUY': { color: '#D500F9', text: 'B3', position: 'belowBar', size: 0.5 }, 'BSP1_SELL': { color: '#00E676', text: 'S1', position: 'aboveBar', size: 0.5 }, 'BSP2_SELL': { color: '#00B0FF', text: 'S2', position: 'aboveBar', size: 0.5 }, 'BSP3_SELL': { color: '#8B4513', text: 'S3', position: 'aboveBar', size: 0.5 }, }; const getBspStyleKey = (bsp) => { // 统一 BSP key: // - type 可能是 "BSP1"/"BSP2"/"BSP3", // - 也可能是后端给的 "B1"/"B2"/"B3" 或 "S1"/"S2"/"S3" // 最终都映射为 "BSP1_BUY" / "BSP1_SELL" 这类 key,方便复用现有样式定义 let type = (bsp.type || '').toUpperCase(); const dir = (bsp.dir || '').toUpperCase(); // 若是 "B1" / "B2" / "B3" 或 "S1" / "S2" / "S3" 形式,则提取数字并映射成 "BSP{n}" const simpleMatch = type.match(/^([BS])(\d)$/); if (simpleMatch) { const n = simpleMatch[2]; // "1" / "2" / "3" type = 'BSP' + n; } return type + '_' + dir; }; // 收集所有 BSP 标记 const allBspMarkers = []; // 主周期买卖点 // 兼容不同字段命名:优先使用 bsp_list,若不存在则尝试 bsp const mainBspList = currentData.bsp_list || currentData.bsp || []; // 调试:打印前几条主周期 BSP 的 key,方便排查样式不匹配问题 if (mainBspList.length > 0) { console.log( '主周期 BSP 示例 (前5条):', mainBspList.slice(0, 5).map(b => ({ raw_type: b.type, raw_dir: b.dir, key: getBspStyleKey(b) })) ); } if ($('#showMainBsp').is(':checked') && mainBspList.length > 0) { console.log(`绘制主周期买卖点,共${mainBspList.length}条`); mainBspList.forEach(function(bsp) { try { const ts = Math.floor(new Date(bsp.time).getTime() / 1000); if (isNaN(ts)) return; const key = getBspStyleKey(bsp); const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' }; const sureText = bsp.is_sure ? '' : '?'; allBspMarkers.push({ time: ts, position: style.position, color: style.color, shape: style.shape, text: style.text + sureText, size: 2 }); } catch (e) { console.error('主周期BSP处理出错:', e); } }); } // 次周期买卖点 // 兼容不同字段命名:优先使用 element_bsp_list,若不存在则尝试 element_bsp const elementBspList = currentData.element_bsp_list || currentData.element_bsp || []; // 调试:打印前几条次周期 BSP 的 key if (elementBspList.length > 0) { console.log( '次周期 BSP 示例 (前5条):', elementBspList.slice(0, 5).map(b => ({ raw_type: b.type, raw_dir: b.dir, key: getBspStyleKey(b) })) ); } if ($('#showElementBsp').is(':checked') && elementBspList.length > 0) { console.log(`绘制次周期买卖点,共${elementBspList.length}条`); elementBspList.forEach(function(bsp) { try { const ts = Math.floor(new Date(bsp.time).getTime() / 1000); if (isNaN(ts)) return; const key = getBspStyleKey(bsp); const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' }; const sureText = bsp.is_sure ? '' : '?'; // 次周期使用稍小的标记和不同前缀以区分 allBspMarkers.push({ time: ts, position: style.position, color: style.color, shape: style.shape, text: 'e' + style.text + sureText, size: 1 }); } catch (e) { console.error('次周期BSP处理出错:', e); } }); } // 次次周期买卖点 const subSubBspList = currentData.sub_sub_bsp_list || []; if ($('#showSubSubBsp').is(':checked') && subSubBspList.length > 0) { subSubBspList.forEach(function(bsp) { try { const ts = Math.floor(new Date(bsp.time).getTime() / 1000); if (isNaN(ts)) return; const key = getBspStyleKey(bsp); const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' }; const sureText = bsp.is_sure ? '' : '?'; allBspMarkers.push({ time: ts, position: style.position, color: '#00897b', shape: style.shape, text: 's' + (style.text || '?') + sureText, size: 1 }); } catch (e) { console.error('次次周期BSP处理出错:', e); } }); } // 将 BSP 标记挂到全局,后面与 KLC 分型等标记一起合并到主系列上 if (allBspMarkers.length > 0) { // 按时间排序(lightweight-charts 要求标记按时间升序) allBspMarkers.sort((a, b) => a.time - b.time); window.bspMarkers = allBspMarkers; console.log(`准备合并 ${allBspMarkers.length} 个BSP标记到主图标记中`); } else { window.bspMarkers = []; } } else { // 关闭 BSP 显示时,清空全局 BSP 标记 window.bspMarkers = []; } // 添加买卖点标记(旧版,保留兼容) // 这里为了与主面板上的「买卖点」开关保持一致, // 同时响应顶部的 `#showMainBsp` 复选框 if ($('#showTradePoints').is(':checked') || $('#showMainBsp').is(':checked')) { console.log('绘制买卖点 - 已启用(来源: showTradePoints / showMainBsp)'); // 优先使用小周期数据,如果不存在则使用主周期数据 const tradePointsData = currentData.element_trade_points || currentData.trade_points; console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`); // 调试信息 - 输出完整的买卖点数据 if (tradePointsData && tradePointsData.length > 0) { console.log("买卖点数据样例:", tradePointsData[0]); // 检查数据格式,如果time不是标准格式,进行格式化处理 const checkDataFormat = () => { for (let i = 0; i < tradePointsData.length; i++) { if (tradePointsData[i].time) { // 确保时间是标准格式 try { const timeValue = new Date(tradePointsData[i].time); if (isNaN(timeValue.getTime())) { console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time); } } catch (e) { console.error(`买卖点 #${i} 时间格式异常:`, e); } } else { console.error(`买卖点 #${i} 缺少时间属性`); } } }; // 执行格式检查 checkDataFormat(); // 对买卖点按时间排序,用于后续优化显示 const sortedPoints = [...tradePointsData].sort((a, b) => { return new Date(a.time) - new Date(b.time); }); // 记录已处理的时间点 - 按类型分开计数 const processedTimes = {}; // 创建买卖点标记系列 const buyMarkers = []; const sellMarkers = []; // 计数器,追踪成功和失败的处理次数 let successCount = 0; let errorCount = 0; sortedPoints.forEach(function(point, index) { try { // 检查所有必要的属性是否存在且有效 if (!point.time || !point.price || point.type === undefined) { console.error(`买卖点 #${index} 数据不完整:`, point); errorCount++; return; } const time = Math.floor(new Date(point.time).getTime() / 1000); const price = parseFloat(point.price); const type = parseInt(point.type); if (isNaN(time) || isNaN(price) || isNaN(type)) { console.error(`买卖点 #${index} 数据格式错误:`, { time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point); errorCount++; return; } // 获取买卖点样式 const style = TRADE_POINT_STYLE[type] || { color: '#999999', shape: 'circle', text: '?', size: 1 }; // 初始化该时间点的类型计数器 if (!processedTimes[time]) { processedTimes[time] = {}; } // 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移 let stackIndex = 0; if (processedTimes[time][type]) { // 已经有相同时间和类型的标记,记录堆叠索引 stackIndex = processedTimes[time][type]; processedTimes[time][type]++; } else { // 第一次出现这个时间点的这个类型 processedTimes[time][type] = 1; } // 为不同类型的买卖点获取基础垂直偏移系数 const baseOffset = TRADE_POINT_OFFSET[type] || 0; // 创建标记对象,包含额外的信息用于悬停提示 const marker = { time: time, position: 'inBar', // 改为在K线内部显示,不影响数据 color: style.color, shape: style.shape, text: style.text, size: style.size, // 记录堆叠索引 stackIndex: stackIndex, // 添加悬停提示的数据 tooltip: `${point.desc || (type > 0 ? '买点' : '卖点')}
时间: ${formatTime(point.time)}
价格: ${price.toFixed(2)}`, // 额外添加基础类型偏移 baseOffset: baseOffset, // 添加边框 borderColor: 'white', borderWidth: 1, // 添加价格偏移系数 pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0, // 保存实际价格用于计算 price: price, // 保存类型 type: type }; // 区分买卖点 if (type > 0) { buyMarkers.push(marker); } else { sellMarkers.push(marker); } successCount++; } catch (e) { console.error(`处理买卖点 #${index} 出错:`, e, point); errorCount++; } }); console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`); // 分别添加买卖点标记 if (buyMarkers.length > 0) { const buyMarkersSeries = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false, lineVisible: false, color: 'transparent', title: '买点' }); // 使用主K线的收盘价作为基准数据,保证买点标记与价格在同一纵轴范围 if (Array.isArray(candles) && candles.length > 0) { const baseData = candles.map(c => ({ time: c.time, value: c.close })); buyMarkersSeries.setData(baseData); } else { // 兜底:至少一个数据点,避免报错 buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: buyMarkers[0].price || 0 }]); } try { // 设置买点标记:文字在价格上方,仅显示文字不显示形状 buyMarkersSeries.setMarkers( buyMarkers.map(marker => { // 使用实际价格位置,买点显示在K线上方 return { ...marker, position: 'aboveBar', // 买点:价格上方 price: marker.price, // 隐藏形状,仅保留文字 size: 0, color: 'rgba(0, 0, 0, 0)' }; }) ); console.log(`成功添加 ${buyMarkers.length} 个买点标记`); } catch (e) { console.error("设置买点标记时出错:", e); } } if (sellMarkers.length > 0) { const sellMarkersSeries = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false, lineVisible: false, color: 'transparent', title: '卖点' }); // 使用主K线的收盘价作为基准数据,保证卖点标记与价格在同一纵轴范围 if (Array.isArray(candles) && candles.length > 0) { const baseData = candles.map(c => ({ time: c.time, value: c.close })); sellMarkersSeries.setData(baseData); } else { // 兜底:至少一个数据点,避免报错 sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: sellMarkers[0].price || 0 }]); } try { // 设置卖点标记:文字在价格下方,仅显示文字不显示形状 sellMarkersSeries.setMarkers( sellMarkers.map(marker => { // 使用实际价格位置,卖点显示在K线下方 return { ...marker, position: 'belowBar', // 卖点:价格下方 price: marker.price, // 隐藏形状,仅保留文字 size: 0, color: 'rgba(0, 0, 0, 0)' }; }) ); console.log(`成功添加 ${sellMarkers.length} 个卖点标记`); } catch (e) { console.error("设置卖点标记时出错:", e); } } // 添加鼠标悬停事件显示提示 mainChart.subscribeCrosshairMove(param => { // 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果 if (param.time && param.point && volumeChart) { try { // 清除之前的十字线标记 const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); existingVolumeLines.forEach(line => line.remove()); const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); existingAtrLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line'); existingChanMacdLines.forEach(line => line.remove()); // 获取时间对应的坐标位置 const mainTimeCoordinate = mainChart.timeScale().timeToCoordinate(param.time); if (mainTimeCoordinate !== null) { // 获取主图容器的位置 const mainChartRect = mainChartContainer.getBoundingClientRect(); // 在交易量图上绘制垂直线 const volumeTimeCoordinate = volumeChart.timeScale().timeToCoordinate(param.time); if (volumeTimeCoordinate !== null) { const volumeChartRect = volumeChartContainer.getBoundingClientRect(); const volumeLine = document.createElement('div'); volumeLine.className = 'volume-crosshair-line'; volumeLine.style.position = 'fixed'; // 改为fixed定位 volumeLine.style.left = (volumeChartRect.left + volumeTimeCoordinate) + 'px'; volumeLine.style.top = volumeChartRect.top + 'px'; volumeLine.style.width = '1px'; volumeLine.style.height = volumeChartRect.height + 'px'; volumeLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; volumeLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; volumeLine.style.pointerEvents = 'none'; volumeLine.style.zIndex = '1000'; document.body.appendChild(volumeLine); } // 在ATR图上绘制垂直线 if (atrChart && atrChartContainer) { const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time); if (atrTimeCoordinate !== null) { const atrChartRect = atrChartContainer.getBoundingClientRect(); const atrLine = document.createElement('div'); atrLine.className = 'atr-crosshair-line'; atrLine.style.position = 'fixed'; // 改为fixed定位 atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px'; atrLine.style.top = atrChartRect.top + 'px'; atrLine.style.width = '1px'; atrLine.style.height = atrChartRect.height + 'px'; atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; atrLine.style.pointerEvents = 'none'; atrLine.style.zIndex = '1000'; document.body.appendChild(atrLine); } } // 如果有MACD图,也在MACD图上绘制垂直线 if (showMacd && macdChart && macdChartContainer) { const macdTimeCoordinate = macdChart.timeScale().timeToCoordinate(param.time); if (macdTimeCoordinate !== null) { const macdChartRect = macdChartContainer.getBoundingClientRect(); const macdLine = document.createElement('div'); macdLine.className = 'macd-crosshair-line'; macdLine.style.position = 'fixed'; // 改为fixed定位 macdLine.style.left = (macdChartRect.left + macdTimeCoordinate) + 'px'; macdLine.style.top = macdChartRect.top + 'px'; macdLine.style.width = '1px'; macdLine.style.height = macdChartRect.height + 'px'; macdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; macdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; macdLine.style.pointerEvents = 'none'; macdLine.style.zIndex = '1000'; document.body.appendChild(macdLine); } } // 如果有ChanMACD图,也在ChanMACD图上绘制垂直线 if (showMacd && chanMacdChart && chanMacdChartContainer) { const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time); if (chanMacdTimeCoordinate !== null) { const chanMacdChartRect = chanMacdChartContainer.getBoundingClientRect(); console.log('ChanMACD图表位置:', { left: chanMacdChartRect.left, top: chanMacdChartRect.top, width: chanMacdChartRect.width, height: chanMacdChartRect.height, timeCoordinate: chanMacdTimeCoordinate }); const chanMacdLine = document.createElement('div'); chanMacdLine.className = 'chanmacd-crosshair-line'; chanMacdLine.style.position = 'fixed'; chanMacdLine.style.left = (chanMacdChartRect.left + chanMacdTimeCoordinate) + 'px'; chanMacdLine.style.top = chanMacdChartRect.top + 'px'; chanMacdLine.style.width = '1px'; chanMacdLine.style.height = chanMacdChartRect.height + 'px'; chanMacdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; chanMacdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; chanMacdLine.style.pointerEvents = 'none'; chanMacdLine.style.zIndex = '1000'; document.body.appendChild(chanMacdLine); console.log('ChanMACD垂直线已创建,位置:', chanMacdLine.style.left, chanMacdLine.style.top); } else { console.log('ChanMACD时间坐标为空'); } } else { console.log('ChanMACD图表条件不满足:', { showMacd: showMacd, hasChanMacdChart: !!chanMacdChart, hasChanMacdChartContainer: !!chanMacdChartContainer }); } } } catch (e) { console.debug('十字线同步出错:', e); } } else { // 当十字线离开时,清除垂直线 try { const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); existingVolumeLines.forEach(line => line.remove()); const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); existingAtrLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line'); existingChanMacdLines.forEach(line => line.remove()); } catch (e) { console.debug('清除十字线时出错:', e); } } if (param.time && param.point) { const timeStr = param.time; const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr); // 同时检查分型标记 const fxMarkers = (window.fxMarkers || []).filter(m => m.time === timeStr); const allMarkers = [...markers, ...fxMarkers]; // 显示时区调试信息 if (window.debugMode) { const timezone = $('#timezone').val(); const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone); // 获取当前价格 - 通过param.seriesPrices获取 let priceInfo = ''; if (param.seriesPrices && param.seriesPrices.size > 0) { // 依次从当前可能的主系列中获取价格 if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) { const price = param.seriesPrices.get(tvWidget.series.candleSeries); priceInfo = `价格: ${price.toFixed(2)}`; } else if (tvWidget.series.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) { const price = param.seriesPrices.get(tvWidget.series.renkoSeries); priceInfo = `价格: ${price.toFixed(2)}`; } else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) { const price = param.seriesPrices.get(tvWidget.series.heikinSeries); priceInfo = `价格: ${price.toFixed(2)}`; } else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) { const price = param.seriesPrices.get(tvWidget.series.barSeries); priceInfo = `价格: ${price.toFixed(2)}`; } else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) { const price = param.seriesPrices.get(tvWidget.series.lineSeries); priceInfo = `价格: ${price.toFixed(2)}`; } else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) { const price = param.seriesPrices.get(tvWidget.series.areaSeries); priceInfo = `价格: ${price.toFixed(2)}`; } else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) { const price = param.seriesPrices.get(tvWidget.series.baselineSeries); priceInfo = `价格: ${price.toFixed(2)}`; } // 如果没有蜡烛图系列价格,尝试从区域图系列获取 else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) { const price = param.seriesPrices.get(tvWidget.series.areaSeries); priceInfo = `价格: ${price.toFixed(2)}`; } // 如果没有蜡烛图系列价格,尝试从基线图系列获取 else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) { const price = param.seriesPrices.get(tvWidget.series.baselineSeries); priceInfo = `价格: ${price.toFixed(2)}`; } } // 仅记录最简短的调试信息 console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`); // 显示自定义时区工具提示,包含价格信息 crosshairTooltip.innerHTML = `
时间: ${formattedTime}
` + (priceInfo ? `
${priceInfo}
` : ''); crosshairTooltip.style.display = 'block'; crosshairTooltip.style.left = (param.point.x + 15) + 'px'; crosshairTooltip.style.top = (param.point.y - 30) + 'px'; } if (allMarkers.length > 0) { // 有买卖点或分型标记,显示自定义提示 const tooltips = allMarkers.map(m => m.tooltip).join('

'); tooltipElement.innerHTML = tooltips; tooltipElement.style.display = 'block'; tooltipElement.style.left = (param.point.x + 15) + 'px'; tooltipElement.style.top = (param.point.y + 15) + 'px'; } else { // 隐藏提示 tooltipElement.style.display = 'none'; } } else { // 隐藏提示 tooltipElement.style.display = 'none'; crosshairTooltip.style.display = 'none'; } }); // 处理图表缩放、平移等事件,隐藏提示 mainChart.timeScale().subscribeVisibleTimeRangeChange(() => { tooltipElement.style.display = 'none'; crosshairTooltip.style.display = 'none'; }); } } else { console.log('绘制买卖点 - 已禁用'); } // 绘制布林带 if ($('#showMainBollinger').is(':checked') || $('#showElementBollinger').is(':checked')) { console.log('绘制布林带 - 已启用'); // 主周期布林带 if ($('#showMainBollinger').is(':checked') && currentData.bollinger && currentData.bollinger.upper && currentData.bollinger.lower && currentData.bollinger.middle) { console.log(`绘制主周期布林带数据,共${currentData.bollinger.upper.length}条`); // 准备布林带数据 const upperBandData = []; const lowerBandData = []; const middleBandData = []; // 主周期布林带始终使用主周期K线数据作为时间源 const mainKlineData = currentData.kline_data; for (let i = 0; i < mainKlineData.length && i < currentData.bollinger.upper.length; i++) { const kline = mainKlineData[i]; const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); // 只添加非0的有效数据点 if (currentData.bollinger.upper[i] && currentData.bollinger.upper[i] !== 0) { upperBandData.push({ time: timestamp, value: currentData.bollinger.upper[i] }); } if (currentData.bollinger.lower[i] && currentData.bollinger.lower[i] !== 0) { lowerBandData.push({ time: timestamp, value: currentData.bollinger.lower[i] }); } if (currentData.bollinger.middle[i] && currentData.bollinger.middle[i] !== 0) { middleBandData.push({ time: timestamp, value: currentData.bollinger.middle[i] }); } } // 创建布林带上轨 const upperBandSeries = mainChart.addLineSeries({ color: '#2196F3', lineWidth: 1, lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, title: '布林上轨' }); upperBandSeries.setData(upperBandData); // 创建布林带下轨 const lowerBandSeries = mainChart.addLineSeries({ color: '#2196F3', lineWidth: 1, lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, title: '布林下轨' }); lowerBandSeries.setData(lowerBandData); // 创建布林带中轨(移动平均线) const middleBandSeries = mainChart.addLineSeries({ color: '#FF9800', lineWidth: 1, lastValueVisible: false, priceLineVisible: false, title: '布林中轨' }); middleBandSeries.setData(middleBandData); // 保存到tvWidget.series对象 tvWidget.series.mainBollingerSeries.push(upperBandSeries); tvWidget.series.mainBollingerSeries.push(lowerBandSeries); tvWidget.series.mainBollingerSeries.push(middleBandSeries); console.log('主周期布林带绘制完成'); } // 次周期布林带 if ($('#showElementBollinger').is(':checked') && currentData.element_bollinger && currentData.element_bollinger.upper && currentData.element_bollinger.lower && currentData.element_bollinger.middle) { console.log(`绘制次周期布林带数据,共${currentData.element_bollinger.upper.length}条`); // 准备次周期布林带数据 const elementUpperBandData = []; const elementLowerBandData = []; const elementMiddleBandData = []; // 使用次周期K线数据 const elementKlineData = currentData.element_kline_data || currentData.kline_data; for (let i = 0; i < elementKlineData.length && i < currentData.element_bollinger.upper.length; i++) { const kline = elementKlineData[i]; const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); // 只添加非0的有效数据点 if (currentData.element_bollinger.upper[i] && currentData.element_bollinger.upper[i] !== 0) { elementUpperBandData.push({ time: timestamp, value: currentData.element_bollinger.upper[i] }); } if (currentData.element_bollinger.lower[i] && currentData.element_bollinger.lower[i] !== 0) { elementLowerBandData.push({ time: timestamp, value: currentData.element_bollinger.lower[i] }); } if (currentData.element_bollinger.middle[i] && currentData.element_bollinger.middle[i] !== 0) { elementMiddleBandData.push({ time: timestamp, value: currentData.element_bollinger.middle[i] }); } } // 创建次周期布林带上轨 const elementUpperBandSeries = mainChart.addLineSeries({ color: '#9C27B0', lineWidth: 1, lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, title: '次周期布林上轨' }); elementUpperBandSeries.setData(elementUpperBandData); // 创建次周期布林带下轨 const elementLowerBandSeries = mainChart.addLineSeries({ color: '#9C27B0', lineWidth: 1, lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, title: '次周期布林下轨' }); elementLowerBandSeries.setData(elementLowerBandData); // 创建次周期布林带中轨 const elementMiddleBandSeries = mainChart.addLineSeries({ color: '#E91E63', lineWidth: 1, lastValueVisible: false, priceLineVisible: false, title: '次周期布林中轨' }); elementMiddleBandSeries.setData(elementMiddleBandData); // 保存到tvWidget.series对象 tvWidget.series.elementBollingerSeries.push(elementUpperBandSeries); tvWidget.series.elementBollingerSeries.push(elementLowerBandSeries); tvWidget.series.elementBollingerSeries.push(elementMiddleBandSeries); console.log('次周期布林带绘制完成'); } } else { console.log('绘制布林带 - 已禁用'); } // 绘制分型类型标签 console.log('=== 开始检查分型显示条件 ==='); console.log('showKlcFxType勾选状态:', $('#showKlcFxType').is(':checked')); console.log('showKluFxType勾选状态:', $('#showKluFxType').is(':checked')); console.log('currentData.klc_fx_info存在:', !!currentData.klc_fx_info); console.log('currentData.klu_fx_info存在:', !!currentData.klu_fx_info); console.log('currentData.klc_fx_info长度:', currentData.klc_fx_info ? currentData.klc_fx_info.length : 'undefined'); console.log('currentData.klu_fx_info长度:', currentData.klu_fx_info ? currentData.klu_fx_info.length : 'undefined'); if (currentData.klc_fx_info && currentData.klc_fx_info.length > 0) { console.log('前3个klc分型数据样本:', currentData.klc_fx_info.slice(0, 3)); } if (currentData.klu_fx_info && currentData.klu_fx_info.length > 0) { console.log('前3个klu分型数据样本:', currentData.klu_fx_info.slice(0, 3)); } // 收集所有主周期分型标记 const allMainFxMarkers = []; const mainFxMarkers = []; // 用于tooltip支持 // 处理主周期KLC分型 if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) { console.log(`绘制主周期K线合并分型标签,共${currentData.klc_fx_info.length}条`); currentData.klc_fx_info.forEach(function(fx) { try { // 直接使用UTC时间戳(秒) const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); const price = parseFloat(fx.price); if (isNaN(timestamp) || isNaN(price)) { console.error('主周期KLC分型时间或价格转换错误:', fx.time, fx.price); return; } // 主周期 KLC // 确定颜色和位置 const color = fx.is_bottom ? '#28a745' : '#dc3545'; // 底分型绿色,顶分型红色 // 根据强度等级调整颜色强度 let strengthColor = color; // 构建显示文本,包含分型类型和强度信息 let displayText = `${fx.fx_strength.toFixed(1)}`; if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 } displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "").replace("01", ""); // 添加标记配置 const markerConfig = { time: timestamp, position: fx.is_bottom ? 'belowBar' : 'aboveBar', color: strengthColor, shape: 'triangle', text: displayText, size: 2 // 调整尺寸,强分型稍大,普通分型更小 }; allMainFxMarkers.push(markerConfig); // 画虚线分型框(根据 start/end + high/low) if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); const high = parseFloat(fx.high); const low = parseFloat(fx.low); if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { const boxHigh = Math.max(high, low); const boxLow = Math.min(high, low); const boxColor = strengthColor; const topSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); const bottomSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); const leftSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); // 左边竖线:同一 time 上下两个点(和你已有ZS绘制写法保持一致) leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); const rightSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, // 虚线 lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = []; tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); } } // 创建分型标记对象,包含tooltip信息 const fxMarker = { time: timestamp, tooltip: `
主周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}
强度分数: ${fx.fx_strength}分
强度等级: ${fx.fx_strength_level}
是否强分型: ${fx.is_strong_fx ? '是' : '否'}
价格: ${price.toFixed(4)}
时间: ${fx.time}
` }; mainFxMarkers.push(fxMarker); } catch (e) { console.error('绘制主周期KLC分型标签出错:', e); } }); } // 处理主周期KLU分型 if ($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0) { console.log(`绘制主周期K线未合并分型标签,共${currentData.klu_fx_info.length}条`); currentData.klu_fx_info.forEach(function(fx) { try { // 直接使用UTC时间戳(秒) const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); const price = parseFloat(fx.price); if (isNaN(timestamp) || isNaN(price)) { console.error('主周期KLU分型时间或价格转换错误:', fx.time, fx.price); return; } // 主周期 KLU const color = fx.is_bottom ? '#17a2b8' : '#fd7e14'; // 底分型用青色,顶分型用橙色 // 根据强度等级调整颜色强度 let strengthColor = color; if (fx.is_strong_fx) { // 强分型使用更亮的颜色 strengthColor = fx.is_bottom ? '#20c997' : '#fd7e14'; } // 构建显示文本,包含分型类型和强度信息 let displayText = `${fx.fx_strength.toFixed(1)}`; if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 } displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", ""); // 添加标记配置 const markerConfig = { time: timestamp, position: fx.is_bottom ? 'belowBar' : 'aboveBar', color: strengthColor, // shape: 'triangle', // 使用三角形区分KLU分型 text: displayText, size: fx.is_strong_fx ? 0.8 : 0.5 // KLU分型稍小一些 }; allMainFxMarkers.push(markerConfig); // 创建分型标记对象,包含tooltip信息 const fxMarker = { time: timestamp, tooltip: `
主周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}
强度分数: ${fx.fx_strength}分
强度等级: ${fx.fx_strength_level}
是否强分型: ${fx.is_strong_fx ? '是' : '否'}
价格: ${price.toFixed(4)}
时间: ${fx.time}
` }; mainFxMarkers.push(fxMarker); } catch (e) { console.error('绘制主周期KLU分型标签出错:', e); } }); } // 暂存主周期分型标记 window.mainFxMarkers = allMainFxMarkers; // 将分型标记添加到全局markers中以支持tooltip功能 if (window.fxMarkers) { window.fxMarkers = [...window.fxMarkers, ...mainFxMarkers]; } else { window.fxMarkers = mainFxMarkers; } // 检查是否有任何主周期分型数据 const hasMainFxData = ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) || ($('#showKluFxType').is(':checked') && currentData.klu_fx_info && currentData.klu_fx_info.length > 0); if (!hasMainFxData) { console.log('绘制主周期分型标记 - 已禁用或无数据'); // 清空主周期分型标记 window.mainFxMarkers = []; window.fxMarkers = []; } // 绘制小周期分型标记(含次次周期) if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) || ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) || ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0)) { // 收集所有小周期分型标记 const allElementFxMarkers = []; const elementFxMarkers = []; // 用于tooltip支持 // 处理小周期KLC分型 if ($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) { console.log(`绘制小周期K线合并分型标记,共${currentData.element_klc_fx_info.length}条`); currentData.element_klc_fx_info.forEach(function(fx) { try { // 直接使用UTC时间戳(秒) const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); const price = parseFloat(fx.price); if (isNaN(timestamp) || isNaN(price)) { console.error('小周期KLC分型时间或价格转换错误:', fx.time, fx.price); return; } // 小周期 KLC let strengthColor = fx.is_bottom ? '#11116B' : '#222222'; // 底分型用珊瑚红,顶分型用薄荷绿 let displayText = `${fx.fx_strength.toFixed(1)}`; // 构建小周期分型显示文本 if (fx.fx_strength < 1.0){ // 调整小周期阈值 displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点 } displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", ""); // 小周期分型标记配置 const markerConfig = { time: timestamp, position: fx.is_bottom ? 'belowBar' : 'aboveBar', color: strengthColor, shape: 'triangle', text: displayText, size: fx.is_strong_fx ? 0.8 : 0.6 // 小周期标记整体更小一些 }; allElementFxMarkers.push(markerConfig); // 画虚线分型框(小周期) if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); const high = parseFloat(fx.high); const low = parseFloat(fx.low); if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { const boxHigh = Math.max(high, low); const boxLow = Math.min(high, low); const boxColor = strengthColor; const topSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); const bottomSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); const leftSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); const rightSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = []; tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); } } // 创建小周期分型标记对象,包含tooltip信息 const elementFxMarker = { time: timestamp, tooltip: `
小周期${fx.is_bottom ? '底分型' : '顶分型'}(合): ${fx.fx_type}
强度分数: ${fx.fx_strength}分
强度等级: ${fx.fx_strength_level}
是否强分型: ${fx.is_strong_fx ? '是' : '否'}
价格: ${price.toFixed(4)}
时间: ${fx.time}
` }; elementFxMarkers.push(elementFxMarker); } catch (e) { console.error('绘制小周期KLC分型标记出错:', e); } }); } // 处理小周期KLU分型 if ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) { console.log(`绘制小周期K线未合并分型标记,共${currentData.element_klu_fx_info.length}条`); currentData.element_klu_fx_info.forEach(function(fx) { try { // 直接使用UTC时间戳(秒) const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); const price = parseFloat(fx.price); if (isNaN(timestamp) || isNaN(price)) { console.error('小周期KLU分型时间或价格转换错误:', fx.time, fx.price); return; } // 小周期 KLU let strengthColor = fx.is_bottom ? '#9A8C98' : '#F2CC8F'; // 底分型用灰紫色,顶分型用浅黄色 let displayText = `${fx.fx_strength.toFixed(1)}`; // 构建小周期分型显示文本 if (fx.fx_strength < 2.0){ // 调整小周期阈值 displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.6以上显示点 } displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", ""); // 小周期KLU分型标记配置 const markerConfig = { time: timestamp, position: fx.is_bottom ? 'belowBar' : 'aboveBar', color: strengthColor, // shape: 'triangle', // 使用三角形区分KLU分型 text: displayText, size: fx.is_strong_fx ? 0.7 : 0.5 // 小周期KLU标记更小一些 }; allElementFxMarkers.push(markerConfig); // 创建小周期分型标记对象,包含tooltip信息 const elementFxMarker = { time: timestamp, tooltip: `
小周期${fx.is_bottom ? '底分型' : '顶分型'}(原): ${fx.fx_type}
强度分数: ${fx.fx_strength}分
强度等级: ${fx.fx_strength_level}
是否强分型: ${fx.is_strong_fx ? '是' : '否'}
价格: ${price.toFixed(4)}
时间: ${fx.time}
` }; elementFxMarkers.push(elementFxMarker); } catch (e) { console.error('绘制小周期KLU分型标记出错:', e); } }); } // 次次周期KLC分型 if ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0) { currentData.sub_sub_klc_fx_info.forEach(function(fx) { try { const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); const price = parseFloat(fx.price); if (isNaN(timestamp) || isNaN(price)) return; const strengthColor = '#00897b'; let displayText = (fx.fx_type || '').replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", ""); const markerConfig = { time: timestamp, position: fx.is_bottom ? 'belowBar' : 'aboveBar', color: strengthColor, shape: 'triangle', text: displayText, size: (fx.is_strong_fx ? 0.6 : 0.5) }; allElementFxMarkers.push(markerConfig); // 画虚线分型框(次次周期) if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); const high = parseFloat(fx.high); const low = parseFloat(fx.low); if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { const boxHigh = Math.max(high, low); const boxLow = Math.min(high, low); const boxColor = strengthColor; const topSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); const bottomSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); const leftSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); const rightSeries = mainChart.addLineSeries({ color: boxColor, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: false, }); rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = []; tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); } } } catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); } }); } // 将小周期分型标记添加到全局markers中以支持tooltip功能 if (window.fxMarkers) { window.fxMarkers = [...window.fxMarkers, ...elementFxMarkers]; } else { window.fxMarkers = elementFxMarkers; } // 基于后端提供的 KLC 趋势生成标记(不进行任何计算) let klcTrendMarkers = []; try { if (currentData.klc_trend && currentData.klc_trend.length > 0) { console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3)); // 当前图表的bar时间集合(秒)用于对齐标记到最近的K线 const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); const nearestTime = (target) => { if (!Array.isArray(candles) || candles.length === 0) return target; // 简单线性查找(数据量通常可接受),必要时可替换为二分 let best = candles[0].time; let bestDiff = Math.abs(best - target); for (let i = 1; i < candles.length; i++) { const t = candles[i].time; const d = Math.abs(t - target); if (d < bestDiff) { best = t; bestDiff = d; } } return best; }; klcTrendMarkers = currentData.klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); const trendRaw = (t.trend || '').toString().toUpperCase(); let timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); let marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'square', size: 0.8 }; if (trendRaw === 'UP') { marker = { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; } else if (trendRaw === 'DOWN') { marker = { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; } else if (trendRaw === 'FLAT') { marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; } else { // UNKNOWN 或其他 marker = { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; } return marker; }); console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); } } catch (e) { klcTrendMarkers = []; } // 暴露到全局以便调试或后续合并 window.klcTrendMarkers = klcTrendMarkers; // 无论当前显示主/小周期,只要勾选对应Trend,就叠加出来 let trendMarkersToUse = []; if ($('#showMainTrend').is(':checked')) { trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []); } if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) { const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); const nearestTime = (target) => { if (!Array.isArray(candles) || candles.length === 0) return target; let best = candles[0].time, bestDiff = Math.abs(best - target); for (let i = 1; i < candles.length; i++) { const t = candles[i].time, d = Math.abs(t - target); if (d < bestDiff) { best = t; bestDiff = d; } } return best; }; const elementMarkers = currentData.element_klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; }); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); } if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) { const candlesTimesSs = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); const nearestTimeSs = (target) => { if (!Array.isArray(candles) || candles.length === 0) return target; let best = candles[0].time, bestDiff = Math.abs(best - target); for (let i = 1; i < candles.length; i++) { const t = candles[i].time, d = Math.abs(t - target); if (d < bestDiff) { best = t; bestDiff = d; } } return best; }; const subSubColor = '#00897b'; const subSubMarkers = currentData.sub_sub_klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts); if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor, shape: 'arrowUp', size: 0.4 }; if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor, shape: 'arrowDown', size: 0.4 }; if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'circle', size: 0.5 }; return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'square', size: 0.5 }; }); trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers); } // 合并标记并设置 const combinedMarkers = [ ...(window.mainFxMarkers || []), ...allElementFxMarkers, ...(window.kluDivMarkersMain || []), ...(window.kluDivMarkersElement || []), ...(window.kluDivMarkersSubSub || []), ...trendMarkersToUse, ...(window.bspMarkers || []) ]; if (combinedMarkers.length > 0) { console.log( '合并设置', combinedMarkers.length, '个标记(主周期分型:', (window.mainFxMarkers || []).length, '个,小周期分型:', allElementFxMarkers.length, '个,UnitTF:', (window.unittfMarkers || []).length, '个,BSP标记:', (window.bspMarkers || []).length, '个)' ); // 根据当前主系列类型设置标记 const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); let targetSeries = null; if (klineType === 'candlestick') targetSeries = tvWidget.series.candleSeries; else if (klineType === 'renko') targetSeries = tvWidget.series.renkoSeries; else if (klineType === 'heikin') targetSeries = tvWidget.series.heikinSeries; else if (klineType === 'bar') targetSeries = tvWidget.series.barSeries; else if (klineType === 'line') targetSeries = tvWidget.series.lineSeries; else if (klineType === 'area') targetSeries = tvWidget.series.areaSeries; else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries; else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries; if (targetSeries) { try { targetSeries.setMarkers(combinedMarkers); } catch (e) { console.warn('设置主系列标记失败(可能series已释放):', e); } } else { console.log('未找到主数据系列,无法设置标记'); } } } else { console.log('绘制小周期分型标记 - 已禁用或无数据'); // 计算并缓存KLC趋势标记(即使未启用小周期分型,也应显示趋势) try { let klcTrendMarkers = []; if (currentData.klc_trend && currentData.klc_trend.length > 0) { console.log('KLC趋势点数量:', currentData.klc_trend.length, currentData.klc_trend.slice(0, 3)); const seriesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); const nearestTime = (target) => { if (!Array.isArray(candles) || candles.length === 0) return target; let best = candles[0].time; let bestDiff = Math.abs(best - target); for (let i = 1; i < candles.length; i++) { const t = candles[i].time; const d = Math.abs(t - target); if (d < bestDiff) { best = t; bestDiff = d; } } return best; }; klcTrendMarkers = currentData.klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); if (trendRaw === 'UP') { return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; } else if (trendRaw === 'DOWN') { return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; } else if (trendRaw === 'FLAT') { return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; } else { return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; } }); console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); } window.klcTrendMarkers = klcTrendMarkers; } catch (e) { window.klcTrendMarkers = []; } // 与上方一致:勾选哪个Trend就显示哪个 let trendMarkersToUse = []; if ($('#showMainTrend').is(':checked')) { trendMarkersToUse = trendMarkersToUse.concat(window.klcTrendMarkers || []); } if ($('#showElementTrend').is(':checked') && currentData.element_klc_trend) { const candlesTimes = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); const nearestTime = (target) => { if (!Array.isArray(candles) || candles.length === 0) return target; let best = candles[0].time, bestDiff = Math.abs(best - target); for (let i = 1; i < candles.length; i++) { const t = candles[i].time, d = Math.abs(t - target); if (d < bestDiff) { best = t; bestDiff = d; } } return best; }; const elementMarkers = currentData.element_klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; }); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); } if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) { const candlesTimesSs2 = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); const nearestTimeSs2 = (target) => { if (!Array.isArray(candles) || candles.length === 0) return target; let best = candles[0].time, bestDiff = Math.abs(best - target); for (let i = 1; i < candles.length; i++) { const t = candles[i].time, d = Math.abs(t - target); if (d < bestDiff) { best = t; bestDiff = d; } } return best; }; const subSubColor2 = '#00897b'; const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts); if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor2, shape: 'arrowUp', size: 0.4 }; if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor2, shape: 'arrowDown', size: 0.4 }; if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'circle', size: 0.5 }; return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'square', size: 0.5 }; }); trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2); } // 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记 // 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记, // 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。 // 修复:把 BSP 标记一并合并进来。 const onlyMainAndU = [ ...(window.mainFxMarkers || []), ...(window.kluDivMarkersMain || []), ...(window.kluDivMarkersElement || []), ...(window.kluDivMarkersSubSub || []), ...trendMarkersToUse, ...(window.bspMarkers || []) ]; if (onlyMainAndU.length > 0) { console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, ',UnitTF:', (window.unittfMarkers || []).length, ')'); // 根据当前主系列类型设置标记 const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); let targetSeries2 = null; if (klineType2 === 'candlestick') targetSeries2 = tvWidget.series.candleSeries; else if (klineType2 === 'renko') targetSeries2 = tvWidget.series.renkoSeries; else if (klineType2 === 'heikin') targetSeries2 = tvWidget.series.heikinSeries; else if (klineType2 === 'bar') targetSeries2 = tvWidget.series.barSeries; else if (klineType2 === 'line') targetSeries2 = tvWidget.series.lineSeries; else if (klineType2 === 'area') targetSeries2 = tvWidget.series.areaSeries; else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries; else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries; if (targetSeries2) { try { targetSeries2.setMarkers(onlyMainAndU); } catch (e) { console.warn('设置主系列标记失败(可能series已释放):', e); } } else { console.log('未找到主数据系列,无法设置标记'); } } else { console.log('没有分型标记需要显示,清空图表标记'); // 清空图表上的所有主系列标记 const klineType3 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); let targetSeries3 = null; if (klineType3 === 'candlestick') targetSeries3 = tvWidget.series.candleSeries; else if (klineType3 === 'renko') targetSeries3 = tvWidget.series.renkoSeries; else if (klineType3 === 'heikin') targetSeries3 = tvWidget.series.heikinSeries; else if (klineType3 === 'bar') targetSeries3 = tvWidget.series.barSeries; else if (klineType3 === 'line') targetSeries3 = tvWidget.series.lineSeries; else if (klineType3 === 'area') targetSeries3 = tvWidget.series.areaSeries; else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries; else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries; if (targetSeries3) { try { targetSeries3.setMarkers([]); } catch (e) { console.warn('清空主系列标记失败(可能series已释放):', e); } } } } // 同步所有图表的时间轴配置 const syncTimeScaleSettings = () => { // 获取主图表的时间轴设置 const mainTimeScale = mainChart.timeScale(); const baseOptions = { timeVisible: true, secondsVisible: false, borderColor: '#ddd', barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10, rightOffset: 12, lockVisibleTimeRangeOnResize: true, // 关键:确保所有图表边缘行为完全一致 fixLeftEdge: false, fixRightEdge: false, // 确保时间刻度行为一致 ticksVisible: true, minimumHeight: 0, }; console.log('🔧 同步时间轴设置:', baseOptions); // 应用相同的设置到所有图表 mainChart.timeScale().applyOptions(baseOptions); volumeChart.timeScale().applyOptions(baseOptions); atrChart.timeScale().applyOptions(baseOptions); if (showMacd && macdChart) { macdChart.timeScale().applyOptions(baseOptions); } }; // 首先同步时间轴设置 syncTimeScaleSettings(); // 仅在没有待恢复视图时,设置默认可见范围 const totalBars = candles ? candles.length : 0; const visibleBarsCount = 200; const hasPendingRestoreView = !!window._pendingRestoreView; if (!hasPendingRestoreView) { // 显示最近 200 根K线而非全部挤压(避免K线过多时重叠) if (totalBars > visibleBarsCount) { const rangeFrom = totalBars - visibleBarsCount; const rangeTo = totalBars + 12; mainChart.timeScale().setVisibleLogicalRange({ from: rangeFrom, to: rangeTo }); } else { mainChart.timeScale().fitContent(); } } // 立即同步其他图表到主图表的范围 setTimeout(() => { const logRange = mainChart.timeScale().getVisibleLogicalRange(); if (logRange) { console.log('🔧 同步可见范围:', logRange); volumeChart.timeScale().setVisibleLogicalRange(logRange); atrChart.timeScale().setVisibleLogicalRange(logRange); if (showMacd && macdChart) { macdChart.timeScale().setVisibleLogicalRange(logRange); } if (showMacd && chanMacdChart) { chanMacdChart.timeScale().setVisibleLogicalRange(logRange); } console.log('🔧 时间轴同步完成'); } }, 50); // 保存图表对象 tvWidget.mainChart = mainChart; tvWidget.volumeChart = volumeChart; tvWidget.atrChart = atrChart; tvWidget.macdChart = macdChart; tvWidget.chanMacdChart = chanMacdChart; tvWidget.state.isInitialized = true; // 注册窗口卸载时释放资源,避免GPU内存泄漏 window.onbeforeunload = function() { try { if (tvWidget && tvWidget.state && tvWidget.state.isInitialized) { if (tvWidget.mainChart && typeof tvWidget.mainChart.remove === 'function') tvWidget.mainChart.remove(); if (tvWidget.volumeChart && typeof tvWidget.volumeChart.remove === 'function') tvWidget.volumeChart.remove(); if (tvWidget.macdChart && typeof tvWidget.macdChart.remove === 'function') tvWidget.macdChart.remove(); if (tvWidget.chanMacdChart && typeof tvWidget.chanMacdChart.remove === 'function') tvWidget.chanMacdChart.remove(); if (tvWidget.atrChart && typeof tvWidget.atrChart.remove === 'function') tvWidget.atrChart.remove(); } } catch (e) {} }; // 初始化默认均线/布林带配置(仅在首次初始化时) if (!hasInitializedDefaultMAs && movingAverages.length === 0) { console.log('初始化默认均线与布林带指标'); if (typeof maIdCounter !== 'number' || !Number.isFinite(maIdCounter)) { maIdCounter = 0; } if (typeof bbIdCounter !== 'number' || !Number.isFinite(bbIdCounter)) { bbIdCounter = 0; } const defaultMAs = [ { type: 'EMA', length: 13, color: '#800080', name: 'EMA13', visible: true }, // 紫色 { type: 'EMA', length: 26, color: '#FF8C00', name: 'EMA26', visible: true }, // 橙色 { type: 'EMA', length: 52, color: '#000000', name: 'EMA52', visible: false }, // 黑色 { type: 'EMA', length: 104, color: '#1E90FF', name: 'EMA104', visible: false }, // 蓝色 { type: 'EMA', length: 156, color: '#F700FF', name: 'EMA156', visible: false } // 粉色 ]; defaultMAs.forEach(ma => { const config = { id: ++maIdCounter, type: ma.type, length: ma.length, source: 'close', smoothType: 'none', smoothLength: 3, lineWidth: 1, // 1px线宽 lineStyle: 0, // 实线 color: ma.color, visible: ma.visible }; movingAverages.push(config); console.log(`添加默认${ma.name}:`, ma.color); }); if (bollingerBands.length === 0) { const defaultBB = { id: ++bbIdCounter, type: 'Bollinger Bands', length: 20, upperMultiplier: 2, lowerMultiplier: 2, source: 'close', lineWidth: 1, lineStyle: 0, upperColor: '#ff6b6b', middleColor: '#ffffff', lowerColor: '#ff6b6b', visible: false }; bollingerBands.push(defaultBB); console.log('添加默认布林带: BB(20, 2, 2)'); } console.log('默认指标配置完成,当前均线数量', movingAverages.length, '布林带数量', bollingerBands.length); hasInitializedDefaultMAs = true; } // 添加均线到图表 addMovingAveragesToChart(candles); // 添加布林带到图表 addBollingerBandsToChart(candles); // 更新技术指标面板显示 updateIndicatorPanel(); // 绑定同步事件 bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd); // 最终确保所有图表时间轴对齐(同时恢复刷新前保存的缩放/位置) setTimeout(() => { const allCharts = [mainChart, volumeChart, atrChart]; if (showMacd && macdChart) allCharts.push(macdChart); if (showMacd && chanMacdChart) allCharts.push(chanMacdChart); // 检查是否有待恢复的视图(缩放 + 位置) const pending = window._pendingRestoreView; window._pendingRestoreView = null; if (pending) { // 恢复刷新前的缩放和位置(优先可见范围/逻辑范围,最后回退到滚动位置) console.log('📌 恢复图表视图:', JSON.stringify(pending)); restoreChartViewState(allCharts, pending); } else { // 无保存视图,正常同步主图到子图 const visibleRange = mainChart.timeScale().getVisibleRange(); if (visibleRange) { console.log('🔧 最终同步可见范围:', visibleRange); [volumeChart, atrChart].concat( showMacd && macdChart ? [macdChart] : [], showMacd && chanMacdChart ? [chanMacdChart] : [] ).forEach(c => { try { c.timeScale().setVisibleRange(visibleRange); } catch(e) {} }); } } console.log('🔧 最终时间轴对齐完成'); }, 150); // 只有在时间输入框都为空时才设置图表默认时间范围 if (!$('#start_time').val() && !$('#end_time').val()) { setDefaultTimeRange(); } // 添加买卖点提示 // 初始化 tooltip 与 U 显示状态 window.showUOnMain = $('#toggleUOnMain').is(':checked'); window.showUOnElement = $('#toggleUOnElement').is(':checked'); setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd); // 更新EMA52显示 if (currentData) { updateEMA52Display(currentData); } console.log('图表初始化完成'); } catch (e) { console.error('图表初始化错误:', e); } } // 增量更新图表数据