/* chart_tv.js — split from chart.js */
/** 释放 Lightweight Charts 实例、DOM 与全局事件,避免自动刷新内存泄漏 */
function disposeTradingViewCharts() {
try {
if (window._tvInitCleanups && Array.isArray(window._tvInitCleanups)) {
window._tvInitCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
}
window._tvInitCleanups = [];
if (window._bindSyncCleanups && Array.isArray(window._bindSyncCleanups)) {
window._bindSyncCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
}
window._bindSyncCleanups = [];
if (window._tooltipCleanups && Array.isArray(window._tooltipCleanups)) {
window._tooltipCleanups.forEach(function (fn) { try { fn(); } catch (e) {} });
}
window._tooltipCleanups = [];
document.querySelectorAll(
'.volume-crosshair-line, .atr-crosshair-line, .macd-crosshair-line, .chanmacd-crosshair-line'
).forEach(function (el) { try { el.remove(); } catch (e) {} });
if (typeof clearEMA52Series === 'function') {
try { clearEMA52Series(); } catch (e) {}
}
if (tvWidget) {
['mainChart', 'volumeChart', 'macdChart', 'chanMacdChart', 'atrChart'].forEach(function (key) {
try {
if (tvWidget[key] && typeof tvWidget[key].remove === 'function') {
tvWidget[key].remove();
}
} catch (e) {}
tvWidget[key] = null;
});
if (tvWidget.state) {
tvWidget.state.isInitialized = false;
}
}
var chartRoot = document.getElementById('tradingview_chart');
if (chartRoot) {
chartRoot.innerHTML = '';
}
} catch (e) {
console.warn('disposeTradingViewCharts 失败(可忽略):', e);
}
}
function initTradingView(symbol, timeframe) {
try {
// 每次重建前完整释放,防止自动刷新导致 GPU/监听器泄漏
disposeTradingViewCharts();
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} 条记录`);
}
// 重置图表对象(容器已在 disposeTradingViewCharts 清空)
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);
}
// 防止同步过程中的无限循环(实际同步由 bindSyncEvents 负责)
// 创建统一的图表选项
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 = [];
}
// 图表同步事件统一由文末 bindSyncEvents 注册(带 cleanup),此处不再重复 addEventListener,
// 否则每次自动刷新/重建都会在 document/window 上堆积监听导致内存泄漏。
// 显示笔的绘制 - 分别处理主周期、次周期和次次周期
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); }
}
// 威科夫叠层:区间 / 阶段 / 事件 / VP
if ($('#showWyckoff').is(':checked') && currentData.wyckoff) {
try {
const w = currentData.wyckoff;
const tr = w.trading_range;
const parseTs = function(t) {
if (t == null) return NaN;
if (typeof t === 'number') return Math.floor(t > 1e12 ? t / 1000 : t);
const ms = new Date(t).getTime();
return isNaN(ms) ? NaN : Math.floor(ms / 1000);
};
const kd = currentData.kline_data || [];
const chartEnd = kd.length
? Math.floor(new Date(kd[kd.length - 1].date).getTime() / 1000)
: NaN;
if ($('#showWyckoffRange').is(':checked') && tr) {
const t0 = parseTs(tr.start_time);
const t1 = tr.end_time ? parseTs(tr.end_time) : chartEnd;
const hi = parseFloat(tr.high), lo = parseFloat(tr.low), mid = parseFloat(tr.mid);
if (!isNaN(t0) && !isNaN(t1) && !isNaN(hi) && !isNaN(lo)) {
const fill = 'rgba(52, 152, 219, 0.07)';
const border = 'rgba(52, 152, 219, 0.75)';
// ECR-004:填充线 6→3,减 series
const fillLines = 3;
const step = (hi - lo) / (fillLines + 1);
for (let fi = 1; fi <= fillLines; fi++) {
const fy = lo + step * fi;
mainChart.addLineSeries({ color: fill, lineWidth: 2, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: fy }, { time: t1, value: fy }]);
}
mainChart.addLineSeries({ color: border, lineWidth: 2, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: hi }, { time: t1, value: hi }]);
mainChart.addLineSeries({ color: border, lineWidth: 2, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: lo }, { time: t1, value: lo }]);
if (!isNaN(mid)) {
mainChart.addLineSeries({ color: border, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: mid }, { time: t1, value: mid }]);
}
mainChart.addLineSeries({ color: border, lineWidth: 1, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: lo }, { time: t0, value: hi }]);
mainChart.addLineSeries({ color: border, lineWidth: 1, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t1, value: lo }, { time: t1, value: hi }]);
}
}
if ($('#showWyckoffPhases').is(':checked') && w.phases && w.phases.length) {
const phaseColors = {
A: 'rgba(241, 196, 15, 0.85)',
B: 'rgba(155, 89, 182, 0.85)',
C: 'rgba(230, 126, 34, 0.85)',
D: 'rgba(46, 204, 113, 0.85)',
E: 'rgba(52, 152, 219, 0.85)'
};
const phaseMarkers = [];
w.phases.forEach(function(ph) {
const t0 = parseTs(ph.start_time);
const t1 = ph.end_time ? parseTs(ph.end_time) : chartEnd;
if (isNaN(t0) || isNaN(t1) || !tr) return;
const hi = parseFloat(tr.high);
if (isNaN(hi)) return;
const col = phaseColors[ph.phase] || 'rgba(149,165,166,0.85)';
// 阶段顶部分段色带(略高于区间高)
const y = hi * 1.002;
mainChart.addLineSeries({ color: col, lineWidth: 3, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: y }, { time: t1, value: y }]);
phaseMarkers.push({
time: t0,
position: 'aboveBar',
color: col,
shape: 'square',
text: String(ph.phase || ph.label || ''),
size: 1
});
});
if (phaseMarkers.length) {
const phSeries = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false });
phSeries.setMarkers(phaseMarkers);
}
}
if ($('#showWyckoffEvents').is(':checked') && w.events && w.events.length) {
const eventColors = {
Spring: '#27ae60',
SOS: '#2ecc71',
LPS: '#16a085',
UTAD: '#e74c3c',
SOW: '#c0392b',
LPSY: '#d35400'
};
const checks = (w.volume_confirm && w.volume_confirm.event_checks) || {};
const markers = [];
w.events.forEach(function(ev) {
const t = parseTs(ev.time);
if (isNaN(t)) return;
const typ = ev.type || '';
const chk = checks[typ] || {};
const volOk = (chk.volume_ok != null) ? chk.volume_ok : ev.volume_ok;
const ratioVal = (chk.volume_ratio != null) ? chk.volume_ratio : ev.volume_ratio;
const ok = volOk === true ? '✓' : (volOk === false ? '✗' : '');
const note = ev.note || '';
const ratio = (ratioVal != null) ? (' vol×' + Number(ratioVal).toFixed(2)) : '';
markers.push({
time: t,
position: (typ === 'Spring' || typ === 'LPS' || typ === 'SOW') ? 'belowBar' : 'aboveBar',
color: eventColors[typ] || '#7f8c8d',
shape: 'arrowUp',
text: typ + (ok ? ' ' + ok : '') + (note ? ' ' + note : '') + ratio,
size: 1
});
});
if (markers.length) {
const evSeries = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false });
evSeries.setMarkers(markers);
}
}
if ($('#showWyckoffVP').is(':checked') && w.volume_profile && tr) {
const vp = w.volume_profile;
const t1 = tr.end_time ? parseTs(tr.end_time) : chartEnd;
if (!isNaN(t1)) {
const bins = vp.bins || [];
// ECR-004 A+C:只画有量 Top-N,避免每 bin 一条 series
const TOP_N = 8;
const ranked = bins
.filter(function(b) { return b && b.volume > 0; })
.slice()
.sort(function(a, b) { return b.volume - a.volume; })
.slice(0, TOP_N);
let maxVol = 0;
ranked.forEach(function(b) { if (b.volume > maxVol) maxVol = b.volume; });
const tStart = parseTs(tr.start_time);
const maxWidthSec = Math.max(60, Math.floor((t1 - (isNaN(tStart) ? t1 : tStart)) * 0.15));
ranked.forEach(function(b) {
if (!b.volume || maxVol <= 0) return;
const wSec = Math.max(1, Math.floor(maxWidthSec * (b.volume / maxVol)));
const alpha = 0.2 + 0.55 * (b.volume / maxVol);
const leftT = Math.max(isNaN(tStart) ? (t1 - wSec) : tStart, t1 - wSec);
mainChart.addLineSeries({
color: 'rgba(142, 68, 173, ' + alpha.toFixed(2) + ')',
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false
}).setData([
{ time: leftT, value: b.price },
{ time: t1, value: b.price }
]);
});
const levels = [
{ p: vp.poc, c: 'rgba(142, 68, 173, 0.95)', w: 2, style: 0 },
{ p: vp.vah, c: 'rgba(155, 89, 182, 0.7)', w: 1, style: 2 },
{ p: vp.val, c: 'rgba(155, 89, 182, 0.7)', w: 1, style: 2 }
];
const t0 = parseTs(tr.start_time);
levels.forEach(function(lv) {
const p = parseFloat(lv.p);
if (isNaN(p) || isNaN(t0)) return;
mainChart.addLineSeries({
color: lv.c,
lineWidth: lv.w,
lineStyle: lv.style,
lastValueVisible: false,
priceLineVisible: false
}).setData([{ time: t0, value: p }, { time: t1, value: p }]);
});
}
}
} 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 = `