/* chart_tv_indicators.js — volume / ATR / ChanMACD */ function formatSdMarkerText(separateDiv) { const n = Number(separateDiv); return `SD${n === 99999 ? 0 : n}`; } function chartTvRenderIndicators(ctx) { var symbol = ctx.symbol; var timeframe = ctx.timeframe; var symbolConfig = ctx.symbolConfig; var useSubSubPeriod = ctx.useSubSubPeriod; var useElementPeriod = ctx.useElementPeriod; var klinePeriodLabel = ctx.klinePeriodLabel; var candles = ctx.candles; var klineDataSource = ctx.klineDataSource; var container = ctx.container; var showMacd = ctx.showMacd; var showOriginalKline = ctx.showOriginalKline; var mainChartContainer = ctx.mainChartContainer; var volumeChartContainer = ctx.volumeChartContainer; var atrChartContainer = ctx.atrChartContainer; var macdChartContainer = ctx.macdChartContainer; var chanMacdChartContainer = ctx.chanMacdChartContainer; var mainChart = ctx.mainChart; var volumeChart = ctx.volumeChart; var atrChart = ctx.atrChart; var macdChart = ctx.macdChart; var chanMacdChart = ctx.chanMacdChart; var createChartOptions = ctx.createChartOptions; // 转换成交量数据 - 与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 = useSubSubPeriod ? (currentData.sub_sub_macd || currentData.macd) : (useElementPeriod ? (currentData.element_macd || currentData.macd) : currentData.macd); console.log('MACD数据源选择:', useSubSubPeriod ? '次次周期' : (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: formatSdMarkerText(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: formatSdMarkerText(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: formatSdMarkerText(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: formatSdMarkerText(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: formatSdMarkerText(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: formatSdMarkerText(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; if (typeof refreshUnittfOverlayFromData === 'function') { refreshUnittfOverlayFromData(currentData); } } catch (e) { console.warn('独立计算 KLU 背驰标记出错:', e); window.kluDivMarkersMain = []; window.kluDivMarkersElement = []; window.kluDivMarkersSubSub = []; } }