Files
Chan/web/static/js/app/chart_tv_chan.js
T
jackyu66gitandCursor 8c165f11cd fix(web): 未完成笔/线段终点对齐图表最新 K 线
各周期使用对应 kline 数据,终点时间 snap 到 candles,优先使用分析 end_price。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-26 00:40:41 +08:00

1081 lines
58 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_chan.js — BI / SEG / ZS */
/** 按界面结束时间裁掉被 recent 合并进来的更晚 K 线 */
function klinesUpToSelectedEnd(klineData) {
if (!Array.isArray(klineData) || !klineData.length) return [];
const endVal = $('#end_time').val();
if (!endVal) return klineData;
const endMs = new Date(endVal).getTime();
if (isNaN(endMs)) return klineData;
const out = [];
for (let i = 0; i < klineData.length; i++) {
const k = klineData[i];
let t;
if (k.timestamp != null && k.timestamp !== '') {
t = Number(k.timestamp);
} else {
t = new Date(k.date).getTime();
}
if (!isNaN(t) && t <= endMs) out.push(k);
}
return out;
}
function alignTimeToChartCandles(targetSec, chartCandles) {
if (targetSec == null || isNaN(targetSec)) return targetSec;
if (!Array.isArray(chartCandles) || !chartCandles.length) return targetSec;
const t = Math.floor(Number(targetSec));
let best = Math.floor(Number(chartCandles[0].time));
let bestDiff = Math.abs(best - t);
for (let i = 1; i < chartCandles.length; i++) {
const ct = Math.floor(Number(chartCandles[i].time));
if (!isFinite(ct)) continue;
const d = Math.abs(ct - t);
if (d < bestDiff) {
best = ct;
bestDiff = d;
}
}
return best;
}
function getLastBarFromChartCandles(chartCandles) {
if (!Array.isArray(chartCandles) || !chartCandles.length) return null;
const c = chartCandles[chartCandles.length - 1];
if (!c || c.time == null) return null;
return {
time: Math.floor(Number(c.time)),
high: parseFloat(c.high),
low: parseFloat(c.low),
close: parseFloat(c.close)
};
}
/**
* 未完成笔终点:优先分析 end_price;时间接到该周期末根并 snap 到图表 candles。
*/
function resolveUncompletedBiEndpoint(bi, klineData, chartCandles) {
const clipped = klinesUpToSelectedEnd(klineData);
const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000);
const startPrice = parseFloat(bi.start_price);
let endPrice = parseFloat(bi.end_price);
let endTime = bi.end_time ? Math.floor(new Date(bi.end_time).getTime() / 1000) : NaN;
const lastFromChart = getLastBarFromChartCandles(chartCandles);
const lastK = clipped.length ? clipped[clipped.length - 1] : null;
if (lastFromChart) {
endTime = lastFromChart.time;
if (isNaN(endPrice)) {
endPrice = bi.direction === 1 ? lastFromChart.high : lastFromChart.low;
}
} else if (lastK) {
const lastTime = Math.floor(new Date(lastK.date).getTime() / 1000);
if (!isNaN(lastTime)) endTime = lastTime;
if (isNaN(endPrice)) {
endPrice = bi.direction === 1 ? parseFloat(lastK.high) : parseFloat(lastK.low);
}
}
endTime = alignTimeToChartCandles(endTime, chartCandles);
return { startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice };
}
function resolveUncompletedSegEndpoint(seg, klineData, chartCandles) {
const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000);
const startPrice = parseFloat(seg.start_price);
let endTime;
let endPrice;
if (seg.end_time != null && seg.end_price != null && seg.end_price !== '' && !isNaN(parseFloat(seg.end_price))) {
endTime = Math.floor(new Date(seg.end_time).getTime() / 1000);
endPrice = parseFloat(seg.end_price);
endTime = alignTimeToChartCandles(endTime, chartCandles);
} else {
const ep = resolveUncompletedBiEndpoint({
start_time: seg.start_time,
start_price: seg.start_price,
end_price: null,
end_time: null,
direction: seg.direction
}, klineData, chartCandles);
endTime = ep.endTime;
endPrice = ep.endPrice;
}
return { startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice };
}
function chartTvRenderChan(ctx) {
var symbol = ctx.symbol;
var timeframe = ctx.timeframe;
var symbolConfig = ctx.symbolConfig;
var useSubSubPeriod = ctx.useSubSubPeriod;
var useElementPeriod = ctx.useElementPeriod;
var klinePeriodLabel = ctx.klinePeriodLabel;
var candles = ctx.candles;
var klineDataSource = ctx.klineDataSource;
var container = ctx.container;
var showMacd = ctx.showMacd;
var showOriginalKline = ctx.showOriginalKline;
var mainChartContainer = ctx.mainChartContainer;
var volumeChartContainer = ctx.volumeChartContainer;
var atrChartContainer = ctx.atrChartContainer;
var macdChartContainer = ctx.macdChartContainer;
var chanMacdChartContainer = ctx.chanMacdChartContainer;
var mainChart = ctx.mainChart;
var volumeChart = ctx.volumeChart;
var atrChart = ctx.atrChart;
var macdChart = ctx.macdChart;
var chanMacdChart = ctx.chanMacdChart;
var createChartOptions = ctx.createChartOptions;
// 图表同步事件统一由文末 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 {
const ep = resolveUncompletedBiEndpoint(bi, currentData.kline_data, candles);
if (isNaN(ep.startTime) || isNaN(ep.endTime) || isNaN(ep.startPrice) || isNaN(ep.endPrice)) {
console.error('主周期未完成笔时间/价格转换错误:', bi.start_time, bi.end_price);
return;
}
// 添加未完成笔(红色虚线)
biLines.push({
startTime: ep.startTime,
endTime: ep.endTime,
startPrice: ep.startPrice,
endPrice: ep.endPrice,
color: '#FF0000', // 红色
lineWidth: 1,
lineStyle: 2 // 虚线
});
// 添加到图表对象
tvWidget.series.mainUncompletedBiSeries.push({
time: ep.startTime,
value: ep.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 {
const ep = resolveUncompletedBiEndpoint(
bi,
currentData.element_kline_data || currentData.kline_data,
candles
);
if (isNaN(ep.startTime) || isNaN(ep.endTime) || isNaN(ep.startPrice) || isNaN(ep.endPrice)) {
console.error('次周期未完成笔时间/价格转换错误:', bi.start_time, bi.end_price);
return;
}
// 添加未完成笔(红色虚线)
biLines.push({
startTime: ep.startTime,
endTime: ep.endTime,
startPrice: ep.startPrice,
endPrice: ep.endPrice,
color: '#FF0000', // 红色
lineWidth: 1,
lineStyle: 2 // 虚线
});
// 添加到图表对象
tvWidget.series.elementUncompletedBiSeries.push({
time: ep.startTime,
value: ep.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 = [];
currentData.sub_sub_uncompleted_bi_list.forEach(function(bi) {
try {
const ep = resolveUncompletedBiEndpoint(
bi,
currentData.sub_sub_kline_data || currentData.kline_data,
candles
);
if (isNaN(ep.startTime) || isNaN(ep.endTime) || isNaN(ep.startPrice) || isNaN(ep.endPrice)) return;
biLines.push({
startTime: ep.startTime, endTime: ep.endTime, startPrice: ep.startPrice, endPrice: ep.endPrice,
color: '#00695c', lineWidth: 1, lineStyle: 2
});
tvWidget.series.subSubUncompletedBiSeries.push({ time: ep.startTime, value: ep.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 {
const ep = resolveUncompletedSegEndpoint(seg, currentData.kline_data, candles);
if (isNaN(ep.startTime) || isNaN(ep.endTime) || isNaN(ep.startPrice) || isNaN(ep.endPrice)) {
console.error('主周期未完成线段时间/价格转换错误:', seg.start_time, seg.end_price);
return;
}
// 添加未完成线段(红色虚线)
segLines.push({
startTime: ep.startTime,
endTime: ep.endTime,
startPrice: ep.startPrice,
endPrice: ep.endPrice,
color: '#FF0000', // 红色
lineWidth: 2,
lineStyle: 2 // 虚线
});
// 添加到图表对象
tvWidget.series.mainUncompletedSegSeries.push({
time: ep.startTime,
value: ep.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 {
const ep = resolveUncompletedSegEndpoint(
seg,
currentData.element_kline_data || currentData.kline_data,
candles
);
if (isNaN(ep.startTime) || isNaN(ep.endTime) || isNaN(ep.startPrice) || isNaN(ep.endPrice)) {
console.error('次周期未完成线段时间/价格转换错误:', seg.start_time, seg.end_price);
return;
}
// 添加未完成线段(红色虚线)
segLines.push({
startTime: ep.startTime,
endTime: ep.endTime,
startPrice: ep.startPrice,
endPrice: ep.endPrice,
color: '#FF0000', // 红色
lineWidth: 2,
lineStyle: 2 // 虚线
});
// 添加到图表对象
tvWidget.series.elementUncompletedSegSeries.push({
time: ep.startTime,
value: ep.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 = [];
currentData.sub_sub_uncompleted_seg_list.forEach(function(seg) {
try {
const ep = resolveUncompletedSegEndpoint(
seg,
currentData.sub_sub_kline_data || currentData.kline_data,
candles
);
if (isNaN(ep.startTime) || isNaN(ep.endTime) || isNaN(ep.startPrice) || isNaN(ep.endPrice)) return;
segLines.push({
startTime: ep.startTime, endTime: ep.endTime, startPrice: ep.startPrice, endPrice: ep.endPrice,
color: '#00695c', lineWidth: 2, lineStyle: 2
});
tvWidget.series.subSubUncompletedSegSeries.push({ time: ep.startTime, value: ep.startPrice, color: '#00695c', lineWidth: 2, lineStyle: 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); }
});
}
}