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 @@ - +