Files
Chan/web/static/js/app/chart_tv.js
T
jackyu66gitandCursor 9f1e7361b6 fix: 修复主站自动刷新内存泄漏,并完善 chan_tv 图表体验
主站重建前完整 dispose、去掉重复 sync 监听,自动刷新默认增量更新;顺带消除首屏重复 analyze、复用 ChanMACD,以及全版 TV 指标/未完成中枢/布局本地缓存。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 16:09:48 +08:00

4448 lines
251 KiB
JavaScript
Raw Blame History

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