fix(web): 未完成笔/线段终点对齐图表最新 K 线

各周期使用对应 kline 数据,终点时间 snap 到 candles,优先使用分析 end_price。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-26 00:40:41 +08:00
co-authored by Cursor
parent 97e77847d0
commit 8c165f11cd
2 changed files with 164 additions and 187 deletions
+163 -186
View File
@@ -1,5 +1,108 @@
/* 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;
@@ -201,40 +304,18 @@ function chartTvRenderChan(ctx) {
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);
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;
}
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,
startTime: ep.startTime,
endTime: ep.endTime,
startPrice: ep.startPrice,
endPrice: ep.endPrice,
color: '#FF0000', // 红色
lineWidth: 1,
lineStyle: 2 // 虚线
@@ -242,8 +323,8 @@ function chartTvRenderChan(ctx) {
// 添加到图表对象
tvWidget.series.mainUncompletedBiSeries.push({
time: startTime,
value: startPrice,
time: ep.startTime,
value: ep.startPrice,
color: '#FF0000',
lineWidth: 1,
lineStyle: 2
@@ -260,40 +341,22 @@ function chartTvRenderChan(ctx) {
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);
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;
}
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,
startTime: ep.startTime,
endTime: ep.endTime,
startPrice: ep.startPrice,
endPrice: ep.endPrice,
color: '#FF0000', // 红色
lineWidth: 1,
lineStyle: 2 // 虚线
@@ -301,8 +364,8 @@ function chartTvRenderChan(ctx) {
// 添加到图表对象
tvWidget.series.elementUncompletedBiSeries.push({
time: startTime,
value: startPrice,
time: ep.startTime,
value: ep.startPrice,
color: '#FF0000',
lineWidth: 1,
lineStyle: 2
@@ -337,21 +400,19 @@ function chartTvRenderChan(ctx) {
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);
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: startTime, endTime: lastTime, startPrice: startPrice, endPrice: endPrice,
startTime: ep.startTime, endTime: ep.endTime, startPrice: ep.startPrice, endPrice: ep.endPrice,
color: '#00695c', lineWidth: 1, lineStyle: 2
});
tvWidget.series.subSubUncompletedBiSeries.push({ time: startTime, value: startPrice, 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); }
});
}
@@ -503,58 +564,18 @@ function chartTvRenderChan(ctx) {
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);
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;
}
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,
startTime: ep.startTime,
endTime: ep.endTime,
startPrice: ep.startPrice,
endPrice: ep.endPrice,
color: '#FF0000', // 红色
lineWidth: 2,
lineStyle: 2 // 虚线
@@ -562,8 +583,8 @@ function chartTvRenderChan(ctx) {
// 添加到图表对象
tvWidget.series.mainUncompletedSegSeries.push({
time: startTime,
value: startPrice,
time: ep.startTime,
value: ep.startPrice,
color: '#FF0000',
lineWidth: 2,
lineStyle: 2
@@ -580,58 +601,22 @@ function chartTvRenderChan(ctx) {
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);
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;
}
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,
startTime: ep.startTime,
endTime: ep.endTime,
startPrice: ep.startPrice,
endPrice: ep.endPrice,
color: '#FF0000', // 红色
lineWidth: 2,
lineStyle: 2 // 虚线
@@ -639,8 +624,8 @@ function chartTvRenderChan(ctx) {
// 添加到图表对象
tvWidget.series.elementUncompletedSegSeries.push({
time: startTime,
value: startPrice,
time: ep.startTime,
value: ep.startPrice,
color: '#FF0000',
lineWidth: 2,
lineStyle: 2
@@ -674,27 +659,19 @@ function chartTvRenderChan(ctx) {
// 次次周期未完成线段
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);
}
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: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice,
startTime: ep.startTime, endTime: ep.endTime, startPrice: ep.startPrice, endPrice: ep.endPrice,
color: '#00695c', lineWidth: 2, lineStyle: 2
});
tvWidget.series.subSubUncompletedSegSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 2 });
tvWidget.series.subSubUncompletedSegSeries.push({ time: ep.startTime, value: ep.startPrice, color: '#00695c', lineWidth: 2, lineStyle: 2 });
} catch (e) { console.error('次次周期未完成线段处理出错:', e); }
});
}
+1 -1
View File
@@ -1392,7 +1392,7 @@
<script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260808i"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260809j"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260808i"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260808i"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260810d"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260810c"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260810a"></script>
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260808i"></script>