diff --git a/web/static/js/app/chart_tv_chan.js b/web/static/js/app/chart_tv_chan.js
index 35dc2e0..020faa2 100644
--- a/web/static/js/app/chart_tv_chan.js
+++ b/web/static/js/app/chart_tv_chan.js
@@ -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); }
});
}
diff --git a/web/templates/index.html b/web/templates/index.html
index b48eca0..e9d0b69 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -1392,7 +1392,7 @@
-
+