fix(web): 未完成笔/线段终点对齐图表最新 K 线
各周期使用对应 kline 数据,终点时间 snap 到 candles,优先使用分析 end_price。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+163
-186
@@ -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); }
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user