diff --git a/.gitignore b/.gitignore index 2aebbcf..f0ec8a7 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ research/out/run_meta_*.json # Telegram 凭据。**不要提交** research/live/deploy/tg.env +research/.tg.env # 生产状态与信号总线。刻意放在仓库外(LIVE_HOME / BUS_DIR),这几条只防 # 有人把它们指回仓库里:里面是日亏损累计与已处理信号键,被 git clean diff --git a/chanlun/core/ChanSEG.py b/chanlun/core/ChanSEG.py index c6e257c..f3409ce 100644 --- a/chanlun/core/ChanSEG.py +++ b/chanlun/core/ChanSEG.py @@ -43,6 +43,29 @@ class ChanSEG(): self.macd_hist = macd_hist def set_macd_div(self, macd_div): self.macd_div = macd_div + def cal_macdhist(self): + # 线段面积 = 同向笔 MACD 柱面积之和(与笔面积口径一致) + acc = 0.0 + seg_dir_name = getattr(self.dir, 'name', None) + for bi in self.bi_list: + if bi is None: + continue + if getattr(getattr(bi, 'dir', None), 'name', None) != seg_dir_name: + continue + acc += float(bi.macd_hist or 0) + self.macd_hist = acc + return acc + def cal_macd_div(self): + # 与前一个同向线段比面积:seg.pre 是反向邻段,pre.pre 才是同向 + self.macd_div = 0.0 + prev = self.pre.pre if self.pre and self.pre.pre else None + if prev is None: + return 0.0 + prev_hist = float(prev.macd_hist or 0) + if prev_hist == 0: + return 0.0 + self.macd_div = float(self.macd_hist or 0) / prev_hist + return self.macd_div def set_end_bi(self, bi: ChanBI, sure_bi: ChanBI): self.end_bi = bi if bi and bi.is_sure: diff --git a/web/api/analyze.py b/web/api/analyze.py index a4a808e..2471f6e 100644 --- a/web/api/analyze.py +++ b/web/api/analyze.py @@ -108,7 +108,8 @@ def analyze(): 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, 'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None, 'direction': convert_direction(bi.dir), - 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 + 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0, + 'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0 } for bi in analysis_result['bi_list'] if bi.is_sure], # 添加未完成笔列表 'uncompleted_bi_list': [{ @@ -118,7 +119,8 @@ def analyze(): 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, 'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格 'direction': convert_direction(bi.dir), - 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 + 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0, + 'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0 } for bi in analysis_result['bi_list'] if not bi.is_sure], 'seg_list': [{ 'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(), @@ -126,7 +128,9 @@ def analyze(): 'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None, 'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high, 'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None, - 'direction': convert_direction(seg.dir) + 'direction': convert_direction(seg.dir), + 'macd_div': float(getattr(seg, 'macd_div', 0) or 0), + 'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0) } for seg in analysis_result['seg_list'] if seg.is_sure], # 添加未完成线段列表 'uncompleted_seg_list': get_uncompleted_seg_list(analysis_result['seg_list'], client_tz), @@ -305,7 +309,8 @@ def analyze(): 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, 'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None, 'direction': convert_direction(bi.dir), - 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 + 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0, + 'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0 } for bi in element_analysis['bi_list'] if bi.is_sure] # 添加次周期未完成笔列表 result['element_uncompleted_bi_list'] = [{ @@ -315,7 +320,8 @@ def analyze(): 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, 'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, # 未完成笔没有结束价格 'direction': convert_direction(bi.dir), - 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 + 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0, + 'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0 } for bi in element_analysis['bi_list'] if not bi.is_sure] # 添加小周期K线数据 @@ -341,7 +347,9 @@ def analyze(): 'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None, 'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high, 'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None, - 'direction': convert_direction(seg.dir) + 'direction': convert_direction(seg.dir), + 'macd_div': float(getattr(seg, 'macd_div', 0) or 0), + 'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0) } for seg in element_analysis['seg_list'] if seg.is_sure] # 添加次周期未完成线段列表 result['element_uncompleted_seg_list'] = get_uncompleted_seg_list(element_analysis['seg_list'], client_tz) @@ -446,7 +454,8 @@ def analyze(): 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, 'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None, 'direction': convert_direction(bi.dir), - 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 + 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0, + 'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0 } for bi in sub_sub_analysis['bi_list'] if bi.is_sure] result['sub_sub_uncompleted_bi_list'] = [{ 'start_time': bi.start_klc.end_time if isinstance(bi.start_klc.end_time, str) else bi.start_klc.end_time.astimezone(client_tz).isoformat(), @@ -455,7 +464,8 @@ def analyze(): 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, 'end_price': bi.end_klc.low if convert_direction(bi.dir) == 1 else bi.end_klc.high, 'direction': convert_direction(bi.dir), - 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 + 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0, + 'macd_hist': float(bi.macd_hist) if getattr(bi, 'macd_hist', None) is not None else 0 } for bi in sub_sub_analysis['bi_list'] if not bi.is_sure] # 次次周期 KLC 列表 result['sub_sub_klc_list'] = [{ @@ -477,7 +487,9 @@ def analyze(): 'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None, 'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high, 'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None, - 'direction': convert_direction(seg.dir) + 'direction': convert_direction(seg.dir), + 'macd_div': float(getattr(seg, 'macd_div', 0) or 0), + 'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0) } for seg in sub_sub_analysis['seg_list'] if seg.is_sure] result['sub_sub_uncompleted_seg_list'] = get_uncompleted_seg_list(sub_sub_analysis['seg_list'], client_tz) result['sub_sub_zs_list'] = [{ diff --git a/web/services/runtime/analyze.py b/web/services/runtime/analyze.py index b9f10c3..8dda472 100644 --- a/web/services/runtime/analyze.py +++ b/web/services/runtime/analyze.py @@ -48,6 +48,10 @@ def analyze_chan(df, symbol=None, timeframe=None): for bi in bi_list: bi.cal_macd_div() #print(bi.start_time, bi.macd_hist, bi.macd_div) + for seg in seg_list: + seg.cal_macdhist() + for seg in seg_list: + seg.cal_macd_div() # 添加ChanMACD分析(复用 get_klc_list 内已算好的结果,避免同周期二次全量分析) chan_macd = None diff --git a/web/services/runtime/serialize.py b/web/services/runtime/serialize.py index de75046..63530b9 100644 --- a/web/services/runtime/serialize.py +++ b/web/services/runtime/serialize.py @@ -98,8 +98,14 @@ def serialize_chan_macd_data(chan_macd_data, client_tz): # 序列化unittf_list(兼容新结构与枚举类型) for unittf in chan_macd_data.get('unittf_list', []): try: - dir_value = getattr(unittf, 'uinttf_dir', None) + dir_value = getattr(unittf, 'unittf_dir', None) dir_name = getattr(dir_value, 'name', dir_value if isinstance(dir_value, str) else None) + if dir_name == 'ABOVE': + dir_num = 1 + elif dir_name == 'UNDER': + dir_num = -1 + else: + dir_num = 0 start_t = getattr(unittf, 'start_type', None) start_type = getattr(start_t, 'name', start_t) end_t = getattr(unittf, 'end_type', None) @@ -114,7 +120,7 @@ def serialize_chan_macd_data(chan_macd_data, client_tz): unittf_data = { 'start_time': format_time_safely(getattr(unittf, 'start_time', None), client_tz), 'end_time': format_time_safely(getattr(unittf, 'end_time', None), client_tz) if getattr(unittf, 'end_time', None) else None, - 'dir': dir_name, # 'ABOVE' | 'UNDER' | None + 'dir': dir_num, 'start_type': start_type, # e.g. 'START' | 'CROSS0' | 'NEAR0_UP' | 'NEAR0_DOWN' 'end_type': end_type, 'invalid': getattr(unittf, 'invalid', False), @@ -307,7 +313,9 @@ def get_uncompleted_seg_list(seg_list, client_tz): 'start_time': seg.start_bi.start_klc.end_time if isinstance(seg.start_bi.start_klc.end_time, str) else seg.start_bi.start_klc.end_time.astimezone(client_tz).isoformat(), 'sure_time': format_time_safely(seg.sure_time, client_tz) if seg.sure_time else None, 'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high, - 'direction': convert_direction(seg.dir) + 'direction': convert_direction(seg.dir), + 'macd_div': float(getattr(seg, 'macd_div', 0) or 0), + 'macd_hist': float(getattr(seg, 'macd_hist', 0) or 0) } if is_last: diff --git a/web/static/js/app/chart_tv_chan.js b/web/static/js/app/chart_tv_chan.js index 020faa2..7dcc881 100644 --- a/web/static/js/app/chart_tv_chan.js +++ b/web/static/js/app/chart_tv_chan.js @@ -178,38 +178,6 @@ function chartTvRenderChan(ctx) { 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); } @@ -260,38 +228,6 @@ function chartTvRenderChan(ctx) { 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); } diff --git a/web/static/js/app/chart_tv_indicators.js b/web/static/js/app/chart_tv_indicators.js index eb6f5b2..d33d210 100644 --- a/web/static/js/app/chart_tv_indicators.js +++ b/web/static/js/app/chart_tv_indicators.js @@ -1,5 +1,10 @@ /* chart_tv_indicators.js — volume / ATR / ChanMACD */ +function formatSdMarkerText(separateDiv) { + const n = Number(separateDiv); + return `SD${n === 99999 ? 0 : n}`; +} + function chartTvRenderIndicators(ctx) { var symbol = ctx.symbol; var timeframe = ctx.timeframe; @@ -141,11 +146,11 @@ function chartTvRenderIndicators(ctx) { // 使用与K线数据相同的数据源来确保时间对齐 const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data); - const macdDataSource = useElementPeriod ? - (currentData.element_macd || currentData.macd) : // 如果有次周期MACD数据则使用,否则使用主周期 - currentData.macd; // 主周期使用主周期MACD数据 + const macdDataSource = useSubSubPeriod + ? (currentData.sub_sub_macd || currentData.macd) + : (useElementPeriod ? (currentData.element_macd || currentData.macd) : currentData.macd); - console.log('MACD数据源选择:', useElementPeriod ? '次周期' : '主周期'); + console.log('MACD数据源选择:', useSubSubPeriod ? '次次周期' : (useElementPeriod ? '次周期' : '主周期')); console.log('K线数据长度:', klineDataSource.length); console.log('MACD数据:', macdDataSource); @@ -390,7 +395,7 @@ function chartTvRenderIndicators(ctx) { if (Number(item.separate_div) > 0) { const macdVal = mainMacdMap.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; - mainMarkers.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); + mainMarkers.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 }); } if (item.continue_div === true) { const macdVal = mainMacdMap.get(ts); @@ -412,7 +417,7 @@ function chartTvRenderIndicators(ctx) { if (Number(item.separate_div) > 0) { const macdVal = elementMacdMap.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; - elementMarkers.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); + elementMarkers.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 }); } if (item.continue_div === true) { const macdVal = elementMacdMap.get(ts); @@ -436,7 +441,7 @@ function chartTvRenderIndicators(ctx) { if (Number(item.separate_div) > 0) { const macdVal = subSubMacdMap.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; - subSubMarkers.push({ time: ts, position: posSd, color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); + subSubMarkers.push({ time: ts, position: posSd, color: '#00897b', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 }); } if (item.continue_div === true) { const macdVal = subSubMacdMap.get(ts); @@ -502,7 +507,7 @@ function chartTvRenderIndicators(ctx) { if (Number(item.separate_div) > 0) { const macdVal = mainMacdMapAll.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; - mainMarkersAll.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); + mainMarkersAll.push({ time: ts, position: posSd, color: '#03a9f4', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 }); } if (item.continue_div === true) { const macdVal = mainMacdMapAll.get(ts); @@ -522,7 +527,7 @@ function chartTvRenderIndicators(ctx) { if (Number(item.separate_div) > 0) { const macdVal = elementMacdMapAll.get(ts); const posSd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'aboveBar'; - elementMarkersAll.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); + elementMarkersAll.push({ time: ts, position: posSd, color: '#9c27b0', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 }); } if (item.continue_div === true) { const macdVal = elementMacdMapAll.get(ts); @@ -544,7 +549,7 @@ function chartTvRenderIndicators(ctx) { const ts = Math.floor(new Date(item.time).getTime() / 1000); if (isNaN(ts)) return; if (Number(item.separate_div) > 0) { - subSubMarkersAll.push({ time: ts, position: 'aboveBar', color: '#00897b', shape: 'arrowUp', text: `SD${Number(item.separate_div)}`, size: 0.6 }); + subSubMarkersAll.push({ time: ts, position: 'aboveBar', color: '#00897b', shape: 'arrowUp', text: formatSdMarkerText(item.separate_div), size: 0.6 }); } if (item.continue_div === true) { subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 }); @@ -555,6 +560,9 @@ function chartTvRenderIndicators(ctx) { }); } window.kluDivMarkersSubSub = subSubMarkersAll; + if (typeof refreshUnittfOverlayFromData === 'function') { + refreshUnittfOverlayFromData(currentData); + } } catch (e) { console.warn('独立计算 KLU 背驰标记出错:', e); window.kluDivMarkersMain = []; diff --git a/web/static/js/app/chart_tv_overlays.js b/web/static/js/app/chart_tv_overlays.js index 0936d5e..ffc4be8 100644 --- a/web/static/js/app/chart_tv_overlays.js +++ b/web/static/js/app/chart_tv_overlays.js @@ -82,6 +82,169 @@ var SUB_SUB_KLC_TREND_STYLE = { UNKNOWN: { position: 'inBar', color: '#004d40', shape: 'square', size: 0.5 } }; +function parseAreaDivValue(v) { + var n = Number(v); + return (isFinite(n) && n !== 0) ? n : 0; +} + +function pushAreaTextLabel(out, item, style, text) { + if (!item || !item.end_time || !text) return; + var ts = Math.floor(new Date(item.end_time).getTime() / 1000); + if (isNaN(ts)) return; + var price = Number(item.end_price); + if (!isFinite(price)) price = Number(item.start_price); + if (!isFinite(price)) return; + var up = Number(item.direction) === 1; + out.push({ + time: ts, + price: price, + text: text, + color: style.color, + above: up + }); +} + +function pushAreaDivMarker(out, item, style) { + var div = parseAreaDivValue(item && item.macd_div); + if (!div) return; + pushAreaTextLabel(out, item, style, div.toFixed(2)); +} + +function collectAreaDivMarkers(biList, uncompletedBi, segList, uncompletedSeg, biStyle, segStyle, showBi, showSeg) { + var out = []; + if (showBi) { + (biList || []).forEach(function (bi) { pushAreaDivMarker(out, bi, biStyle); }); + (uncompletedBi || []).forEach(function (bi) { pushAreaDivMarker(out, bi, biStyle); }); + } + if (showSeg) { + (segList || []).forEach(function (seg) { pushAreaDivMarker(out, seg, segStyle); }); + (uncompletedSeg || []).forEach(function (seg) { pushAreaDivMarker(out, seg, segStyle); }); + } + return out; +} + +function formatMacdAreaText(v) { + var n = Number(v); + if (!isFinite(n) || n === 0) return ''; + var abs = Math.abs(n); + if (abs >= 100) return n.toFixed(0); + if (abs >= 10) return n.toFixed(1); + return n.toFixed(2); +} + +function pushAreaHistMarker(out, item, style) { + pushAreaTextLabel(out, item, style, formatMacdAreaText(item && item.macd_hist)); +} + +function collectAreaHistMarkers(biList, uncompletedBi, segList, uncompletedSeg, biStyle, segStyle, showBi, showSeg) { + var out = []; + if (showBi) { + (biList || []).forEach(function (bi) { pushAreaHistMarker(out, bi, biStyle); }); + (uncompletedBi || []).forEach(function (bi) { pushAreaHistMarker(out, bi, biStyle); }); + } + if (showSeg) { + (segList || []).forEach(function (seg) { pushAreaHistMarker(out, seg, segStyle); }); + (uncompletedSeg || []).forEach(function (seg) { pushAreaHistMarker(out, seg, segStyle); }); + } + return out; +} + +function buildAreaHistMarkersFromData(data) { + var markers = []; + if (!data) return markers; + var showMainBi = $('#showMainBiArea').is(':checked'); + var showMainSeg = $('#showMainSegArea').is(':checked'); + if (showMainBi || showMainSeg) { + markers = markers.concat(collectAreaHistMarkers( + data.bi_list, + data.uncompleted_bi_list, + data.seg_list, + data.uncompleted_seg_list, + { color: '#1565c0', size: 0.55 }, + { color: '#00838f', size: 0.65 }, + showMainBi, + showMainSeg + )); + } + var showElementBi = $('#showElementBiArea').is(':checked'); + var showElementSeg = $('#showElementSegArea').is(':checked'); + if (showElementBi || showElementSeg) { + markers = markers.concat(collectAreaHistMarkers( + data.element_bi_list, + data.element_uncompleted_bi_list, + data.element_seg_list, + data.element_uncompleted_seg_list, + { color: '#3949ab', size: 0.5 }, + { color: '#5c6bc0', size: 0.6 }, + showElementBi, + showElementSeg + )); + } + var showSubSubBi = $('#showSubSubBiArea').is(':checked'); + var showSubSubSeg = $('#showSubSubSegArea').is(':checked'); + if (showSubSubBi || showSubSubSeg) { + markers = markers.concat(collectAreaHistMarkers( + data.sub_sub_bi_list, + data.sub_sub_uncompleted_bi_list, + data.sub_sub_seg_list, + data.sub_sub_uncompleted_seg_list, + { color: '#2e7d32', size: 0.45 }, + { color: '#558b2f', size: 0.55 }, + showSubSubBi, + showSubSubSeg + )); + } + return markers; +} + +function buildAreaDivMarkersFromData(data) { + var markers = []; + if (!data) return markers; + var showMainBi = $('#showMainMacdDiv').is(':checked'); + var showMainSeg = $('#showMainSegMacdDiv').is(':checked'); + if (showMainBi || showMainSeg) { + markers = markers.concat(collectAreaDivMarkers( + data.bi_list, + data.uncompleted_bi_list, + data.seg_list, + data.uncompleted_seg_list, + { color: '#e53935', size: 0.6 }, + { color: '#fb8c00', size: 0.7 }, + showMainBi, + showMainSeg + )); + } + var showElementBi = $('#showElementMacdDiv').is(':checked'); + var showElementSeg = $('#showElementSegMacdDiv').is(':checked'); + if (showElementBi || showElementSeg) { + markers = markers.concat(collectAreaDivMarkers( + data.element_bi_list, + data.element_uncompleted_bi_list, + data.element_seg_list, + data.element_uncompleted_seg_list, + { color: '#8e24aa', size: 0.55 }, + { color: '#5e35b1', size: 0.65 }, + showElementBi, + showElementSeg + )); + } + var showSubSubBi = $('#showSubSubMacdDiv').is(':checked'); + var showSubSubSeg = $('#showSubSubSegMacdDiv').is(':checked'); + if (showSubSubBi || showSubSubSeg) { + markers = markers.concat(collectAreaDivMarkers( + data.sub_sub_bi_list, + data.sub_sub_uncompleted_bi_list, + data.sub_sub_seg_list, + data.sub_sub_uncompleted_seg_list, + { color: '#00897b', size: 0.5 }, + { color: '#00695c', size: 0.6 }, + showSubSubBi, + showSubSubSeg + )); + } + return markers; +} + function buildKlcTrendMarker(timeAligned, trendRaw, palette) { var kind = normalizeKlcTrendRaw(trendRaw); var style = palette[kind] || palette.UNKNOWN || palette.FLAT; @@ -163,23 +326,38 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { // LWC 4 无 priceScale 订阅:采样坐标变化(含增量 setData 后自动缩放) var sampleSig = function () { var boxes = window._fxBoxVerticals || []; + var labels = window._areaTextLabels || []; var series = getMainPriceSeries(); - if (!series || !boxes.length) return '0'; + if (!series || (!boxes.length && !labels.length)) return '0'; var ts = mainChart.timeScale(); - var a = boxes[0]; - var b = boxes[boxes.length - 1]; - return [ - boxes.length, - quant(ts.timeToCoordinate(a.time)), - quant(series.priceToCoordinate(a.hi)), - quant(series.priceToCoordinate(a.lo)), - quant(ts.timeToCoordinate(b.time)), - quant(series.priceToCoordinate(b.hi)), - quant(series.priceToCoordinate(b.lo)) - ].join('|'); + var parts = [boxes.length, labels.length]; + if (boxes.length) { + var a = boxes[0]; + var b = boxes[boxes.length - 1]; + parts.push( + quant(ts.timeToCoordinate(a.time)), + quant(series.priceToCoordinate(a.hi)), + quant(series.priceToCoordinate(a.lo)), + quant(ts.timeToCoordinate(b.time)), + quant(series.priceToCoordinate(b.hi)), + quant(series.priceToCoordinate(b.lo)) + ); + } + if (labels.length) { + var la = labels[0]; + var lb = labels[labels.length - 1]; + parts.push( + quant(ts.timeToCoordinate(la.time)), + quant(series.priceToCoordinate(la.price)), + quant(ts.timeToCoordinate(lb.time)), + quant(series.priceToCoordinate(lb.price)) + ); + } + return parts.join('|'); }; var redraw = function () { var boxes = window._fxBoxVerticals || []; + var labels = window._areaTextLabels || []; var series = getMainPriceSeries(); var rect = mainChartContainer.getBoundingClientRect(); var dpr = window.devicePixelRatio || 1; @@ -191,7 +369,7 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { if (!ctx2) return; ctx2.setTransform(dpr, 0, 0, dpr, 0, 0); ctx2.clearRect(0, 0, rect.width, rect.height); - if (!series || !boxes.length) { + if (!series || (!boxes.length && !labels.length)) { lastSig = sampleSig(); return; } @@ -211,6 +389,19 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { ctx2.stroke(); } ctx2.setLineDash([]); + if (labels.length) { + ctx2.font = '11px sans-serif'; + ctx2.textAlign = 'center'; + for (var li = 0; li < labels.length; li++) { + var lab = labels[li]; + var lx = ts.timeToCoordinate(lab.time); + var ly = series.priceToCoordinate(lab.price); + if (lx == null || ly == null) continue; + ctx2.fillStyle = lab.color; + ctx2.textBaseline = lab.above ? 'bottom' : 'top'; + ctx2.fillText(lab.text, Math.round(lx), lab.above ? ly - 3 : ly + 3); + } + } lastSig = sampleSig(); }; var scheduleRedraw = function () { @@ -257,6 +448,7 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) { function chartTvRenderOverlays(ctx) { window._fxBoxVerticals = []; + window._areaTextLabels = []; var symbol = ctx.symbol; var timeframe = ctx.timeframe; var symbolConfig = ctx.symbolConfig; @@ -1852,6 +2044,14 @@ function chartTvRenderOverlays(ctx) { window.mainFxMarkers = []; window.fxMarkers = []; } + window._areaTextLabels = alignMarkersToCandles( + buildAreaDivMarkersFromData(currentData).concat(buildAreaHistMarkersFromData(currentData)), + candles + ); + if (typeof window._redrawFxBoxVerticalOverlay === 'function') { + window._redrawFxBoxVerticalOverlay(); + } + // 绘制小周期分型标记(含次次周期) if (($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) || ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) || @@ -2157,7 +2357,7 @@ function chartTvRenderOverlays(ctx) { trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers); } - // 合并标记并设置 + // 合并标记并设置(主图不画 U/穿零轴,只留背驰 SD/CD) const combinedMarkers = [ ...(window.mainFxMarkers || []), ...allElementFxMarkers, @@ -2173,7 +2373,7 @@ function chartTvRenderOverlays(ctx) { '合并设置', combinedMarkers.length, '个标记(主周期分型:', (window.mainFxMarkers || []).length, '个,小周期分型:', allElementFxMarkers.length, - '个,UnitTF:', (window.unittfMarkers || []).length, + '个,背驰:', (window.kluDivMarkersMain || []).length, '个,BSP标记:', (window.bspMarkers || []).length, '个,第四类标记:', (window.fastBspMarkers || []).length, '个)' @@ -2288,7 +2488,7 @@ function chartTvRenderOverlays(ctx) { ...(window.fastBspMarkers || []) ]; if (onlyMainAndU.length > 0) { - console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, ',UnitTF:', (window.unittfMarkers || []).length, ')'); + console.log('仅设置', onlyMainAndU.length, '个主图标记(主周期分型:', (window.mainFxMarkers || []).length, ',背驰:', (window.kluDivMarkersMain || []).length, ')'); // 根据当前主系列类型设置标记 const klineType2 = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line')); diff --git a/web/static/js/app/ui.js b/web/static/js/app/ui.js index a41e90a..cb4bf0b 100644 --- a/web/static/js/app/ui.js +++ b/web/static/js/app/ui.js @@ -597,14 +597,9 @@ function refreshChartOnly() { } } -// 绑定主周期MACD背离显示开关 -$('#showMainMacdDiv').change(function() { - refreshChartOnly(); -}); - -// 绑定次周期MACD背离显示开关 -$('#showElementMacdDiv').change(function() { - refreshChartOnly(); +// 绑定主/次/次次周期笔背驰、线段背驰、笔面积、线段面积显示开关 +$('#showMainMacdDiv, #showMainSegMacdDiv, #showElementMacdDiv, #showElementSegMacdDiv, #showSubSubMacdDiv, #showSubSubSegMacdDiv, #showMainBiArea, #showMainSegArea, #showElementBiArea, #showElementSegArea, #showSubSubBiArea, #showSubSubSegArea').change(function() { + updateChartDisplay(); }); // 绑定分型类型显示开关(与笔一致:全量重建,避免增量路径标记未对齐) diff --git a/web/templates/index.html b/web/templates/index.html index af2e883..326c0b8 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1013,6 +1013,22 @@ +