From aeced3c61893a7466d4b133b6d0bb4436cc320af Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Fri, 13 Mar 2026 01:21:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=89=BE=E5=88=B0klc=E7=94=9F=E6=88=90bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChanKLC.py | 3 +++ TF_DF.py | 1 - 缠论.txt | 11 +++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/ChanKLC.py b/ChanKLC.py index d6e91de..dc4c62d 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -326,6 +326,7 @@ class ChanKLC(): self.end_klu = klu self.end_time = klu.time self.close = klu.close + for klu in self.klu_list: if klu.exception: self.exception = True @@ -348,6 +349,8 @@ class ChanKLC(): self.close = self.low if self.close > self.high: self.close = self.high + #print(self.end_time, self.open, self.close, self.high, self.low) + #print(klu.time, klu.open, klu.close, klu.high, klu.low) def cal_fx(self): if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2: #print(self.end_time, self.fx, self.macd, self.macdhist, len(self.klu_list)) diff --git a/TF_DF.py b/TF_DF.py index e8f67ed..9267049 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -592,7 +592,6 @@ class TF_DF(): ema_down_list.append(ema_down_count) #print(last_klu.time, ema_down_count, "DOWN END") ema_down_count = 0 - last_klu = klu if len(klc_list) > 0: last_klc = klc_list[-1] if klu.exception: diff --git a/缠论.txt b/缠论.txt index 5d5c8b4..0f1f44b 100644 --- a/缠论.txt +++ b/缠论.txt @@ -41,3 +41,14 @@ - 多个笔/线段中枢若区间两两重叠,可合并为一个大级别中枢; - 重叠定义:两中枢 [zd,zg] 有交集,即 (zs_i.zg >= zs_j.zd and zs_i.zd <= zs_j.zg); - 大级别中枢的 zd/zg 取子中枢的并集(包住所有子中枢),用于显示更大级别震荡区间。 +线段高低点的判断 +注意,这里必须提醒一句,就是这在以前也曾说过,就是,如果线段中,最高或最低点不是线段的端点,那么,在任何以线段为基础的分析中,例如把线段为基础构成最小级别的中枢等,都可以把该线段标准化为最高低点都在端点。因为, 在以线段为基础的分析中,都把线段当成一个没有内部 结构的基本部件,所以,只需要关心这线段的实际区间就可以,这样就可以只看其高低点。 +经过标准化处理后,所有向上线段都是以最低点开始最高点结束,向下线段都是以最高点开始最低点结束,这样,所以线段的连接,就形成一条延续不断、首尾相连的折线,这样,复杂的图形,就会十分地标准化,也为后面的中枢、走势类型等分析提供了最标准且基础的部件。 + + + +概率引擎 + + +策略引擎 +分批建仓 \ No newline at end of file From 1c17cf1f1171797a028f4b317784b2e8bbc1bb5d Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sat, 14 Mar 2026 02:03:27 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=88=86=E5=9E=8B?= =?UTF-8?q?=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChanEnum.py | 25 +++++++++++++++++++++++++ TF_DF.py | 18 ++++++++++-------- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/ChanEnum.py b/ChanEnum.py index e1c5cb0..f940f26 100644 --- a/ChanEnum.py +++ b/ChanEnum.py @@ -355,5 +355,30 @@ class Chan_DATA_FIELD: FIELD_TURNOVER = "turnover" # 成交额 FIELD_TURNRATE = "turnover_rate" # 换手率 +class Chan_KLC_STATE: + """笔当下状态(缠论笔定理)。任意时刻必属其一。""" + FX = auto() # 分型构造中(未确认顶/底) + BI = auto() # 笔延伸中(分型已确认,笔在延伸) + UP = auto() # 顶分型构造中 (1,0):向上笔末端 + DOWN = auto() # 底分型构造中 (-1,0):向下笔末端 + + +# 笔定理四状态:(Chan_BI_DIR, Chan_KLC_STATE)。笔方向用 Chan_BI_DIR,阶段用 Chan_KLC_STATE。 +# (UP, BI) 向上笔延伸;(DOWN, BI) 向下笔延伸;(UP, UP) 向上笔顶分型构造;(DOWN, DOWN) 向下笔底分型构造 +def bi_theorem_state(direction: Chan_BI_DIR, phase: Literal[0, 1]) -> tuple[Chan_BI_DIR, int]: + """(direction, phase) -> (Chan_BI_DIR, Chan_KLC_STATE)。phase 0=分型构造中,1=笔延伸中。""" + if phase == 1: + return (direction, Chan_KLC_STATE.BI) + return (direction, Chan_KLC_STATE.UP if direction == Chan_BI_DIR.UP else Chan_KLC_STATE.DOWN) + + +# 笔定理状态转移:当前 (Chan_BI_DIR, Chan_KLC_STATE) 允许的下一状态列表 +# (UP,BI) 只能 -> (UP,UP);(DOWN,BI) 只能 -> (DOWN,DOWN);(UP,UP) 可 -> (UP,BI)|(DOWN,BI);(DOWN,DOWN) 可 -> (DOWN,BI)|(UP,BI) +Chan_BI_STATE_TRANSITIONS: dict[tuple[Chan_BI_DIR, int], list[tuple[Chan_BI_DIR, int]]] = { + (Chan_BI_DIR.UP, Chan_KLC_STATE.BI): [(Chan_BI_DIR.UP, Chan_KLC_STATE.UP)], + (Chan_BI_DIR.DOWN, Chan_KLC_STATE.BI): [(Chan_BI_DIR.DOWN, Chan_KLC_STATE.DOWN)], + (Chan_BI_DIR.UP, Chan_KLC_STATE.UP): [(Chan_BI_DIR.UP, Chan_KLC_STATE.BI), (Chan_BI_DIR.DOWN, Chan_KLC_STATE.BI)], + (Chan_BI_DIR.DOWN, Chan_KLC_STATE.DOWN): [(Chan_BI_DIR.DOWN, Chan_KLC_STATE.BI), (Chan_BI_DIR.UP, Chan_KLC_STATE.BI)], +} Chan_TRADE_INFO_LST = [Chan_DATA_FIELD.FIELD_VOLUME, Chan_DATA_FIELD.FIELD_TURNOVER, Chan_DATA_FIELD.FIELD_TURNRATE] diff --git a/TF_DF.py b/TF_DF.py index 9267049..d160503 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -919,11 +919,11 @@ class TF_DF(): bi_klc_min = 4 for klc in klc_list: fx = self.check_fx(klc) - if fx == Chan_FX_TYPE.TOP: + if fx == Chan_FX_TYPE.TOP and False: if last_bottom: if self.check_top_fx(last_bottom, klc) == False: fx = Chan_FX_TYPE.UNKNOWN - if fx == Chan_FX_TYPE.BOTTOM: + if fx == Chan_FX_TYPE.BOTTOM and False: if last_top: if self.check_bottom_fx(last_top, klc) == False: #print(klc.end_time, last_top.end_time, "---") @@ -986,7 +986,7 @@ class TF_DF(): if last_top.high > klc.high: bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) - #klc.set_klc_fx_type(Chan_KLC_FX.TOP3) + klc.set_klc_fx_type(Chan_KLC_FX.TOP3) #print(klc.end_time, klc.fx, "二类卖点Sell 1") else: # A new top found @@ -1000,11 +1000,12 @@ class TF_DF(): klc.set_bi(bi_list[-1]) # 不满足结合律的分型 else: - klc.set_klc_fx_type(Chan_KLC_FX.TOP0) + #klc.set_klc_fx_type(Chan_KLC_FX.TOP0) + print(klc.end_time, klc.klc_fx_type) if last_bottom.index + bi_klc_min > klc.index: if last_top.high > klc.high: #print(klc.start_time, klc.fx, "二类卖点Sell 1") - #klc.set_fx(Chan_FX_TYPE.PTOP) + klc.set_klc_fx_type(Chan_KLC_FX.TOP8) bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) # New TOP Found前面的UKNOWN可能出现TOP7,但是这里的也可能出现TOP8分型 @@ -1103,7 +1104,7 @@ class TF_DF(): if last_bottom.low < klc.low: bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) + klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) #print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1") #print(klc.end_time, klc.fx, "二类买点Buy 1") else: @@ -1117,12 +1118,14 @@ class TF_DF(): klc.set_bi(bi_list[-1]) # 不满足结合律的分型 else: - klc.set_klc_fx_type(Chan_KLC_FX.TOP0) + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM0) + #print(klc.end_time, klc.klc_fx_type) if last_top.index + bi_klc_min > klc.index: if last_bottom.low < klc.low: #print(klc.end_time, klc.fx, "中枢买点Buy 1") bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) + klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM8) # Found new bottom没有意义,上面UNKNOWN的时候已经是笔破坏了 else: #print(klc.end_time, last_bottom.end_time, "Found a new bottom") @@ -1155,7 +1158,6 @@ class TF_DF(): if not last_bi.is_sure: last_bi.set_end_klc(last_top, klc) bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN) - #klc.set_klc_fx_type(Chan_KLC_FX.TOP6) last_bi.set_next(bi) bi.set_pre(last_bi) bi.add_klc(klc) From e61af7b1a8210b5c2b7fc3f526fccfdd82a4fdda Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sun, 15 Mar 2026 17:07:15 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E6=AC=A1?= =?UTF-8?q?=E6=AC=A1=E5=91=A8=E6=9C=9F=E7=9A=84=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/app.py | 113 ++++++++++- web/templates/index.html | 427 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 524 insertions(+), 16 deletions(-) diff --git a/web/app.py b/web/app.py index a509f00..3e0d086 100644 --- a/web/app.py +++ b/web/app.py @@ -1180,6 +1180,16 @@ def index(): else: default_element = default_main + # 次次周期默认比次周期小一档 + if timeframe_keys: + try: + idx_el = timeframe_keys.index(default_element) + default_sub_sub = timeframe_keys[idx_el - 1] if idx_el > 0 else timeframe_keys[0] + except ValueError: + default_sub_sub = timeframe_keys[0] + else: + default_sub_sub = default_element + default_symbol = 'BTC/USDT:USDT' if 'BTC/USDT:USDT' in symbols else (symbols[0] if symbols else '') return render_template( @@ -1189,6 +1199,7 @@ def index(): a_stock_symbols=A_STOCK_SYMBOLS, default_main_timeframe=default_main, default_element_timeframe=default_element, + default_sub_sub_timeframe=default_sub_sub, default_symbol=default_symbol, timeframe_keys_json=json.dumps(timeframe_keys), data_service_available=DATA_SERVICE_AVAILABLE, @@ -1211,8 +1222,9 @@ def analyze(): # 获取客户端请求的时区 client_timezone = request.args.get('timezone', 'Asia/Shanghai') - # 获取分形元素时间周期 + # 获取分形元素时间周期与次次周期 element_timeframe = request.args.get('element_timeframe') + sub_sub_timeframe = request.args.get('sub_sub_timeframe') # 获取是否只需要分形元素数据的参数 elements_only_param = request.args.get('elements_only') @@ -1221,6 +1233,9 @@ def analyze(): # 验证小周期是否小于主周期 if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe): return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'}) + # 验证次次周期是否小于等于次周期 + if sub_sub_timeframe and element_timeframe and not is_smaller_or_equal_timeframe(sub_sub_timeframe, element_timeframe): + return jsonify({'error': '次次周期必须小于或等于次周期'}) # 获取数据 df = get_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time) @@ -1597,6 +1612,102 @@ def analyze(): 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 } for bsp in element_analysis.get('bsp_list', [])] + # 次次周期:仅当已指定次周期且次次周期有效时获取 + if sub_sub_timeframe and is_smaller_or_equal_timeframe(sub_sub_timeframe, element_timeframe): + sub_sub_df = get_kl_data(symbol, sub_sub_timeframe, start_time=start_time, end_time=end_time) + if sub_sub_df is not None and len(sub_sub_df) > 0: + sub_sub_df = add_indicators(sub_sub_df) + sub_sub_analysis = analyze_chan(sub_sub_df, symbol, sub_sub_timeframe) + result['sub_sub_timeframe'] = sub_sub_timeframe + result['sub_sub_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(), + 'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None, + 'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None, + '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 + } for bi in sub_sub_analysis['bi_list'] if bi.end_klc] + 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(), + 'end_time': None, + 'sure_time': format_time_safely(bi.sure_time, client_tz) if bi.sure_time else None, + 'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high, + 'end_price': None, + 'direction': convert_direction(bi.dir), + 'macd_div': float(bi.macd_div) if hasattr(bi, 'macd_div') else 0 + } for bi in sub_sub_analysis['bi_list'] if not bi.end_klc] + result['sub_sub_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(), + 'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None, + '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) + } 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'] = [{ + 'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(), + 'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None, + 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure + } for zs in sub_sub_analysis['zs_list'] if zs.end_klc] + result['sub_sub_uncompleted_zs_list'] = [{ + 'start_time': zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat(), + 'end_time': None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': zs.is_sure + } for zs in sub_sub_analysis['zs_list'] if not zs.is_sure] + result['sub_sub_bi_zs_list'] = [{ + 'start_time': ( + (zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat()) + if getattr(zs.start_klc, 'end_time', None) else + (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat()) + ), + 'end_time': (zs.end_time if isinstance(zs.end_time, str) else zs.end_time.astimezone(client_tz).isoformat()) if getattr(zs, 'end_time', None) else None, + 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) + } for zs in sub_sub_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)] + result['sub_sub_uncompleted_bi_zs_list'] = [{ + 'start_time': ( + (zs.start_klc.end_time if isinstance(zs.start_klc.end_time, str) else zs.start_klc.end_time.astimezone(client_tz).isoformat()) + if getattr(zs.start_klc, 'end_time', None) else + (zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat()) + ), + 'end_time': None, 'zg': zs.zg, 'zd': zs.zd, 'gg': zs.gg, 'dd': zs.dd, 'is_sure': bool(getattr(zs, 'is_sure', False)) + } for zs in sub_sub_analysis.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)] + result['sub_sub_klc_fx_info'] = [{ + 'time': format_time_safely(point['time'], client_tz), + 'price': float(point['price']), + 'fx_type': point['fx_type'], + 'is_bottom': bool(point['is_bottom']), + 'fx_strength': float(point['fx_strength']), + 'fx_strength_level': str(point['fx_strength_level']), + 'is_strong_fx': bool(point['is_strong_fx']) + } for point in sub_sub_analysis['klc_fx_info']] + result['sub_sub_bsp_list'] = [{ + 'time': format_time_safely(bsp.end_time, client_tz), + 'price': float(bsp.klc.low if str(bsp.dir) == 'Chan_BSP_DIR.BUY' else bsp.klc.high), + 'type': str(bsp.type).replace('Chan_BSP_TYPE.', ''), + 'dir': str(bsp.dir).replace('Chan_BSP_DIR.', ''), + 'is_sure': bool(bsp.is_sure), + 'sure_time': format_time_safely(bsp.sure_time, client_tz) if bsp.sure_time else None, + 'zs_count': int(bsp.zs_count) if hasattr(bsp, 'zs_count') else 0 + } for bsp in sub_sub_analysis.get('bsp_list', [])] + result['sub_sub_chan_macd'] = serialize_chan_macd_data(sub_sub_analysis.get('chan_macd', {}), client_tz) + try: + sub_sub_klc_trend = [] + for klc in sub_sub_analysis.get('klc_list', []): + trend_val = getattr(klc, 'trend', None) + t_obj = getattr(klc, 'end_time', None) or getattr(klc, 'start_time', None) + if trend_val is None or t_obj is None: + continue + trend_name = str(trend_val) + if '.' in trend_name: + trend_name = trend_name.split('.')[-1] + time_str = format_time_safely(t_obj, client_tz) + if time_str: + sub_sub_klc_trend.append({'time': time_str, 'trend': trend_name}) + result['sub_sub_klc_trend'] = sub_sub_klc_trend + except Exception: + result['sub_sub_klc_trend'] = [] + pass return jsonify(result) diff --git a/web/templates/index.html b/web/templates/index.html index 671b25f..8207afe 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -20,6 +20,7 @@ window.AVAILABLE_TIMEFRAMES = JSON.parse('{{ timeframe_keys_json | safe }}'); window.DEFAULT_MAIN_TIMEFRAME = "{{ default_main_timeframe }}"; window.DEFAULT_ELEMENT_TIMEFRAME = "{{ default_element_timeframe }}"; + window.DEFAULT_SUB_SUB_TIMEFRAME = "{{ default_sub_sub_timeframe }}"; window.timeframeToMs = function(tf) { if (!tf) return null; var unit = tf.slice(-1); @@ -959,6 +960,48 @@ +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -1509,6 +1552,10 @@ elementSegSeries: [], elementZsSeries: [], elementUncompletedZsSeries: [], + subSubBiSeries: [], + subSubSegSeries: [], + subSubZsSeries: [], + subSubUncompletedZsSeries: [], mainBollingerSeries: [], elementBollingerSeries: [], maSeries: [], // 添加均线系列 @@ -1763,6 +1810,14 @@ console.log('次BI中枢切换为:', $('#showElementBiZs').is(':checked')); updateChartDisplay(); }); + // 次次周期显示开关变更事件 + $('#showSubSubBi, #showSubSubSeg, #showSubSubZs, #showSubSubBiZs, #showSubSubKlcFxType, #showSubSubTrend, #showSubSubBsp').change(function() { + updateChartDisplay(); + }); + $(document).on('change', '#toggleUOnSubSub', function() { + window.showUOnSubSub = $('#toggleUOnSubSub').is(':checked'); + updateChartDisplay(); + }); // 买卖点复选框已移除 @@ -1790,9 +1845,30 @@ setSmallerOrEqualTimeframe(); // 重置为最大的小于等于时间周期 return; } - + // 次次周期必须小于等于次周期 + ensureSubSubLteElement(); console.log(`当前选择的元素时间周期: ${elementTimeframe},需要点击分析按钮来应用更改`); }); + // 次次周期变更时校验 <= 次周期 + $('#subSubTimeframe').change(function() { + const subSub = $(this).val(); + const elementTf = $('#elementTimeframe').val(); + if (compareTimeframes(subSub, elementTf) > 0) { + alert('次次周期必须小于或等于次周期。'); + ensureSubSubLteElement(); + return; + } + }); + function ensureSubSubLteElement() { + const timeframes = window.AVAILABLE_TIMEFRAMES || []; + const elementTf = $('#elementTimeframe').val(); + const subSubTf = $('#subSubTimeframe').val(); + if (compareTimeframes(subSubTf, elementTf) > 0) { + const idxEl = timeframes.indexOf(elementTf); + const validSubSub = idxEl > 0 ? timeframes[idxEl - 1] : timeframes[0]; + $('#subSubTimeframe').val(validSubSub || elementTf); + } + } // 比较两个时间周期的大小 function compareTimeframes(tf1, tf2) { @@ -1825,7 +1901,7 @@ $('#elementTimeframe').val(mainTimeframe); } - // 当主时间周期变更时,确保分形元素时间周期正确 + // 当主时间周期变更时,确保分形元素时间周期、次次周期正确 $('#timeframe').change(function() { const mainTimeframe = $(this).val(); const elementTimeframe = $('#elementTimeframe').val(); @@ -1833,6 +1909,7 @@ if (compareTimeframes(elementTimeframe, mainTimeframe) > 0) { setSmallerOrEqualTimeframe(mainTimeframe); } + ensureSubSubLteElement(); }); function updateChartDisplay() { if (currentData) { @@ -1982,6 +2059,7 @@ const timeframe = $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '5m'; const timezone = $('#timezone').val() || 'Asia/Shanghai'; const elementTimeframe = $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '1m'; + const subSubTimeframe = $('#subSubTimeframe').val() || ''; // 确保时区参数有效 console.log('更新图表使用时区:', timezone); @@ -2017,6 +2095,7 @@ timeframe: timeframe, timezone: timezone, element_timeframe: elementTimeframe, + sub_sub_timeframe: subSubTimeframe || undefined, start_time: startTimeMs, end_time: endTimeMs, elements_only: false @@ -2189,6 +2268,12 @@ elementUncompletedSegSeries: [], elementZsSeries: [], elementUncompletedZsSeries: [], + subSubBiSeries: [], + subSubUncompletedBiSeries: [], + subSubSegSeries: [], + subSubUncompletedSegSeries: [], + subSubZsSeries: [], + subSubUncompletedZsSeries: [], tradePointSeries: [], mainBollingerSeries: [], elementBollingerSeries: [], @@ -2751,6 +2836,9 @@ if (typeof window.showUOnElement === 'undefined') { window.showUOnElement = $('#toggleUOnElement').is(':checked'); } + if (typeof window.showUOnSubSub === 'undefined') { + window.showUOnSubSub = $('#toggleUOnSubSub').is(':checked'); + } if (showMacd && chanMacdChart && ((useElementPeriod && currentData.element_macd) || currentData.macd) && (useElementPeriod ? currentData.element_kline_data : currentData.kline_data)) { console.log('✅ 开始创建 ChanMACD 系列'); // 创建ChanMACD线系列 @@ -2973,6 +3061,31 @@ }); } + const subSubCm = currentData.sub_sub_chan_macd || {}; + const subSubMarkers = []; + const subSubMacdMap = buildMacdTimeMap(currentData.macd, currentData.kline_data); + if (window.showUOnSubSub && Array.isArray(subSubCm.klu_list)) { + subSubCm.klu_list.forEach((item) => { + if (!item || !item.time) return; + const ts = Math.floor(new Date(item.time).getTime() / 1000); + if (isNaN(ts)) return; + 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 }); + } + if (item.continue_div === true) { + const macdVal = subSubMacdMap.get(ts); + const posCd = (macdVal > 0) ? 'aboveBar' : (macdVal < 0) ? 'belowBar' : 'belowBar'; + subSubMarkers.push({ time: ts, position: posCd, color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 }); + } + if (item.near0_return && Number(item.near0_return) > 0) { + subSubMarkers.push({ time: ts, position: 'belowBar', color: '#00695c', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 }); + } + }); + } + window.kluDivMarkersSubSub = subSubMarkers; + // 保存到全局,供主图合并标记使用 window.kluDivMarkersMain = mainMarkers; window.kluDivMarkersElement = elementMarkers; @@ -2980,12 +3093,14 @@ console.warn('处理 KLU 背驰标记出错:', e); window.kluDivMarkersMain = []; window.kluDivMarkersElement = []; + window.kluDivMarkersSubSub = []; } } else { console.log('⚠️ 没有ChanMACD分析数据'); // 无数据时清空本次的 KLU 背驰标记 window.kluDivMarkersMain = []; window.kluDivMarkersElement = []; + window.kluDivMarkersSubSub = []; } } else { console.log('⚠️ ChanMACD图表创建条件不满足'); @@ -3057,10 +3172,30 @@ } window.kluDivMarkersMain = mainMarkersAll; window.kluDivMarkersElement = elementMarkersAll; + const subSubCmAll = currentData.sub_sub_chan_macd || {}; + const subSubMarkersAll = []; + if (window.showUOnSubSub && Array.isArray(subSubCmAll.klu_list)) { + subSubCmAll.klu_list.forEach((item) => { + if (!item || !item.time) return; + 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 }); + } + if (item.continue_div === true) { + subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#26a69a', shape: 'arrowDown', text: 'CD', size: 0.6 }); + } + if (item.near0_return && Number(item.near0_return) > 0) { + subSubMarkersAll.push({ time: ts, position: 'belowBar', color: '#00695c', shape: 'circle', text: `${Number(item.near0_return)}`, size: 0.6 }); + } + }); + } + window.kluDivMarkersSubSub = subSubMarkersAll; } catch (e) { console.warn('独立计算 KLU 背驰标记出错:', e); window.kluDivMarkersMain = []; window.kluDivMarkersElement = []; + window.kluDivMarkersSubSub = []; } // 实现三图联动滚动 @@ -3300,8 +3435,8 @@ } }, 200); }); - // 显示笔的绘制 - 分别处理主周期和次周期 - if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked')) { + // 显示笔的绘制 - 分别处理主周期、次周期和次次周期 + if ($('#showMainBi').is(':checked') || $('#showElementBi').is(':checked') || $('#showSubSubBi').is(':checked')) { console.log('绘制笔 - 已启用'); let biLines = []; @@ -3588,6 +3723,49 @@ }); } + // 次次周期笔 + if ($('#showSubSubBi').is(':checked') && currentData.sub_sub_bi_list && currentData.sub_sub_bi_list.length > 0) { + tvWidget.series.subSubBiSeries = []; + tvWidget.series.subSubUncompletedBiSeries = []; + currentData.sub_sub_bi_list.forEach(function(bi) { + try { + const startTime = Math.floor(new Date(bi.start_time).getTime() / 1000); + const endTime = bi.end_time ? Math.floor(new Date(bi.end_time).getTime() / 1000) : 0; + if (isNaN(startTime) || !endTime) return; + const startPrice = parseFloat(bi.start_price); + const endPrice = parseFloat(bi.end_price); + if (isNaN(startPrice) || isNaN(endPrice)) return; + biLines.push({ + startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, + color: bi.direction === 1 ? '#00897b' : '#26a69a', lineWidth: 1, lineStyle: 0 + }); + tvWidget.series.subSubBiSeries.push({ time: startTime, value: startPrice, color: '#00897b', lineWidth: 1 }); + } catch (e) { console.error('次次周期笔处理出错:', e); } + }); + } + // 次次周期未完成笔 + 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); + biLines.push({ + startTime: startTime, endTime: lastTime, startPrice: startPrice, endPrice: endPrice, + color: '#00695c', lineWidth: 1, lineStyle: 2 + }); + tvWidget.series.subSubUncompletedBiSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 1, lineStyle: 2 }); + } catch (e) { console.error('次次周期未完成笔处理出错:', e); } + }); + } + // 添加所有笔到图表 biLines.forEach(line => { const lineSeries = mainChart.addLineSeries({ @@ -3606,8 +3784,8 @@ } else { console.log('绘制笔 - 已禁用'); } - // 显示线段的绘制 - 分别处理主周期和次周期 - if ($('#showMainSeg').is(':checked') || $('#showElementSeg').is(':checked')) { + // 显示线段的绘制 - 分别处理主周期、次周期和次次周期 + if ($('#showMainSeg').is(':checked') || $('#showElementSeg').is(':checked') || $('#showSubSubSeg').is(':checked')) { console.log('绘制线段 - 已启用'); let segLines = []; @@ -3865,6 +4043,54 @@ }); } + // 次次周期线段 + if ($('#showSubSubSeg').is(':checked') && currentData.sub_sub_seg_list && currentData.sub_sub_seg_list.length > 0) { + tvWidget.series.subSubSegSeries = []; + tvWidget.series.subSubUncompletedSegSeries = []; + currentData.sub_sub_seg_list.forEach(function(seg) { + try { + const startTime = Math.floor(new Date(seg.start_time).getTime() / 1000); + const endTime = seg.end_time ? Math.floor(new Date(seg.end_time).getTime() / 1000) : 0; + if (isNaN(startTime) || !endTime) return; + const startPrice = parseFloat(seg.start_price); + const endPrice = parseFloat(seg.end_price); + if (isNaN(startPrice) || isNaN(endPrice)) return; + segLines.push({ + startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, + color: seg.direction === 1 ? '#00897b' : '#26a69a', lineWidth: 2, lineStyle: 0 + }); + tvWidget.series.subSubSegSeries.push({ time: startTime, value: startPrice, color: '#00897b', lineWidth: 2 }); + } catch (e) { console.error('次次周期线段处理出错:', e); } + }); + } + // 次次周期未完成线段 + 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); + } + segLines.push({ + startTime: startTime, endTime: endTime, startPrice: startPrice, endPrice: endPrice, + color: '#00695c', lineWidth: 2, lineStyle: 2 + }); + tvWidget.series.subSubUncompletedSegSeries.push({ time: startTime, value: startPrice, color: '#00695c', lineWidth: 2 }); + } catch (e) { console.error('次次周期未完成线段处理出错:', e); } + }); + } + // 添加所有线段到图表 segLines.forEach(line => { const lineSeries = mainChart.addLineSeries({ @@ -3883,8 +4109,8 @@ } else { console.log('绘制线段 - 已禁用'); } - // 显示中枢的绘制 - 分别处理主周期和次周期(包含BI中枢,沿用同样样式与开关) - if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) { + // 显示中枢的绘制 - 分别处理主周期、次周期和次次周期(包含BI中枢,沿用同样样式与开关) + if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked')) { console.log('绘制中枢 - 已启用'); // 主周期中枢 @@ -4162,6 +4388,25 @@ } }); } + // 次次周期SEG中枢 + if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_zs_list && currentData.sub_sub_zs_list.length > 0) { + const subSubZsColor = '#00897b'; + currentData.sub_sub_zs_list.forEach(function(zs) { + try { + const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); + const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : 0; + if (isNaN(startTime) || !endTime) return; + const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); + if (isNaN(zg) || isNaN(zd)) return; + mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); + if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } catch (e) { console.error('次次周期中枢处理出错:', e); } + }); + } } else { console.log('绘制中枢 - 已禁用'); } @@ -4197,6 +4442,25 @@ } catch (e) { console.error('主周期BI中枢处理出错:', e); } }); } + if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_bi_zs_list && currentData.sub_sub_bi_zs_list.length > 0) { + currentData.sub_sub_bi_zs_list.forEach(function(zs) { + try { + const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); + const kd = currentData.kline_data || []; + const endTime = zs.end_time ? Math.floor(new Date(zs.end_time).getTime() / 1000) : (kd.length ? Math.floor(new Date(kd[kd.length-1].date).getTime() / 1000) : 0); + if (isNaN(startTime) || !endTime) return; + const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); + if (isNaN(zg) || isNaN(zd)) return; + const color = '#00897b'; + mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); + if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } catch (e) { console.error('次次周期BI中枢处理出错:', e); } + }); + } if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) { try { console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`); @@ -4228,11 +4492,11 @@ } catch (e) { console.error('次周期BI中枢处理出错:', e); } }); } - // 显示未完成中枢 - 分别处理主周期和次周期 - if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) { + // 显示未完成中枢 - 分别处理主周期、次周期和次次周期 + if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked')) { console.log('绘制未完成中枢 - 已启用'); // 显示BI中枢绘制(沿用中枢样式) - if ($('#showMainBiZs').is(':checked') || $('#showElementBiZs').is(':checked')) { + if ($('#showMainBiZs').is(':checked') || $('#showElementBiZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) { console.log('绘制BI中枢 - 已启用'); // 主周期 BI 中枢 console.log('主BI开关:', $('#showMainBiZs').is(':checked'), '数据长度:', currentData.bi_zs_list ? currentData.bi_zs_list.length : 0); @@ -4354,6 +4618,25 @@ } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); } }); } + // 次次周期 未完成 BI 中枢 + if ($('#showSubSubBiZs').is(':checked') && currentData.sub_sub_uncompleted_bi_zs_list && currentData.sub_sub_uncompleted_bi_zs_list.length > 0) { + const kdBi = currentData.kline_data || []; + const endTimeBi = kdBi.length ? Math.floor(new Date(kdBi[kdBi.length-1].date).getTime() / 1000) : 0; + currentData.sub_sub_uncompleted_bi_zs_list.forEach(function(zs) { + try { + const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); + if (isNaN(startTime) || !endTimeBi) return; + const zg = parseFloat(zs.zg), zd = parseFloat(zs.zd), gg = parseFloat(zs.gg), dd = parseFloat(zs.dd); + if (isNaN(zg) || isNaN(zd)) return; + const color = '#00897b'; + mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeBi, value: zg }]); + mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeBi, value: zd }]); + mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeBi, value: gg }]); + if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeBi, value: dd }]); + } catch (e) { console.error('次次周期未完成BI中枢处理出错:', e); } + }); + } } // 主周期未完成中枢 if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) { @@ -4640,6 +4923,25 @@ } }); } + // 次次周期未完成SEG中枢 + if ($('#showSubSubZs').is(':checked') && currentData.sub_sub_uncompleted_zs_list && currentData.sub_sub_uncompleted_zs_list.length > 0) { + const kdZs = currentData.kline_data || []; + const endTimeZs = kdZs.length ? Math.floor(new Date(kdZs[kdZs.length-1].date).getTime() / 1000) : 0; + const subSubUZsColor = '#00897b'; + currentData.sub_sub_uncompleted_zs_list.forEach(function(zs) { + try { + const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); + if (isNaN(startTime) || !endTimeZs) return; + const zg = parseFloat(zs.zg); const zd = parseFloat(zs.zd); const gg = parseFloat(zs.gg); const dd = parseFloat(zs.dd); + if (isNaN(zg) || isNaN(zd)) return; + mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zg }, { time: endTimeZs, value: zg }]); + mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: endTimeZs, value: zd }]); + mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + if (!isNaN(gg) && gg > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: gg }, { time: endTimeZs, value: gg }]); + if (!isNaN(dd) && dd > 0) mainChart.addLineSeries({ color: subSubUZsColor, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }).setData([{ time: startTime, value: dd }, { time: endTimeZs, value: dd }]); + } catch (e) { console.error('次次周期未完成中枢处理出错:', e); } + }); + } } else { console.log('绘制未完成中枢 - 已禁用'); } @@ -4699,8 +5001,8 @@ } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); } }); } - // 添加买卖点标记(新版:基于 bsp_list / element_bsp_list,按与 KLC 分型相同方式合并到主图标记) - if ($('#showMainBsp').is(':checked') || $('#showElementBsp').is(':checked')) { + // 添加买卖点标记(新版:基于 bsp_list / element_bsp_list / sub_sub_bsp_list,按与 KLC 分型相同方式合并到主图标记) + if ($('#showMainBsp').is(':checked') || $('#showElementBsp').is(':checked') || $('#showSubSubBsp').is(':checked')) { console.log('绘制买卖点(BSP) - 已启用'); // BSP 样式定义 @@ -4809,6 +5111,30 @@ }); } + // 次次周期买卖点 + const subSubBspList = currentData.sub_sub_bsp_list || []; + if ($('#showSubSubBsp').is(':checked') && subSubBspList.length > 0) { + subSubBspList.forEach(function(bsp) { + try { + const ts = Math.floor(new Date(bsp.time).getTime() / 1000); + if (isNaN(ts)) return; + const key = getBspStyleKey(bsp); + const style = BSP_STYLE[key] || { color: '#999', shape: 'circle', text: '?', position: 'inBar' }; + const sureText = bsp.is_sure ? '' : '?'; + allBspMarkers.push({ + time: ts, + position: style.position, + color: '#00897b', + shape: style.shape, + text: 's' + (style.text || '?') + sureText, + size: 1 + }); + } catch (e) { + console.error('次次周期BSP处理出错:', e); + } + }); + } + // 将 BSP 标记挂到全局,后面与 KLC 分型等标记一起合并到主系列上 if (allBspMarkers.length > 0) { // 按时间排序(lightweight-charts 要求标记按时间升序) @@ -5595,9 +5921,10 @@ window.mainFxMarkers = []; window.fxMarkers = []; } - // 绘制小周期分型标记 + // 绘制小周期分型标记(含次次周期) 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)) { + ($('#showElementKluFxType').is(':checked') && currentData.element_klu_fx_info && currentData.element_klu_fx_info.length > 0) || + ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0)) { // 收集所有小周期分型标记 const allElementFxMarkers = []; @@ -5715,6 +6042,28 @@ }); } + // 次次周期KLC分型 + if ($('#showSubSubKlcFxType').is(':checked') && currentData.sub_sub_klc_fx_info && currentData.sub_sub_klc_fx_info.length > 0) { + currentData.sub_sub_klc_fx_info.forEach(function(fx) { + try { + const timestamp = Math.floor(new Date(fx.time).getTime() / 1000); + const price = parseFloat(fx.price); + if (isNaN(timestamp) || isNaN(price)) return; + const strengthColor = '#00897b'; + let displayText = (fx.fx_type || '').replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", ""); + const markerConfig = { + time: timestamp, + position: fx.is_bottom ? 'belowBar' : 'aboveBar', + color: strengthColor, + shape: 'triangle', + text: displayText || 's', + size: (fx.is_strong_fx ? 0.6 : 0.5) + }; + allElementFxMarkers.push(markerConfig); + } catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); } + }); + } + // 将小周期分型标记添加到全局markers中以支持tooltip功能 if (window.fxMarkers) { window.fxMarkers = [...window.fxMarkers, ...elementFxMarkers]; @@ -5794,6 +6143,29 @@ }); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); } + if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) { + const candlesTimesSs = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); + const nearestTimeSs = (target) => { + if (!Array.isArray(candles) || candles.length === 0) return target; + let best = candles[0].time, bestDiff = Math.abs(best - target); + for (let i = 1; i < candles.length; i++) { + const t = candles[i].time, d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + } + return best; + }; + const subSubColor = '#00897b'; + const subSubMarkers = currentData.sub_sub_klc_trend.map(t => { + const ts = Math.floor(new Date(t.time).getTime() / 1000); + const trendRaw = (t.trend || '').toString().toUpperCase(); + const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts); + if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor, shape: 'arrowUp', size: 0.4 }; + if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor, shape: 'arrowDown', size: 0.4 }; + if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'circle', size: 0.5 }; + return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'square', size: 0.5 }; + }); + trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers); + } // 合并标记并设置 const combinedMarkers = [ @@ -5801,6 +6173,7 @@ ...allElementFxMarkers, ...(window.kluDivMarkersMain || []), ...(window.kluDivMarkersElement || []), + ...(window.kluDivMarkersSubSub || []), ...trendMarkersToUse, ...(window.bspMarkers || []) ]; @@ -5900,6 +6273,29 @@ }); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); } + if ($('#showSubSubTrend').is(':checked') && currentData.sub_sub_klc_trend && currentData.sub_sub_klc_trend.length > 0) { + const candlesTimesSs2 = (typeof candles !== 'undefined' && Array.isArray(candles)) ? new Set(candles.map(c => c.time)) : new Set(); + const nearestTimeSs2 = (target) => { + if (!Array.isArray(candles) || candles.length === 0) return target; + let best = candles[0].time, bestDiff = Math.abs(best - target); + for (let i = 1; i < candles.length; i++) { + const t = candles[i].time, d = Math.abs(t - target); + if (d < bestDiff) { best = t; bestDiff = d; } + } + return best; + }; + const subSubColor2 = '#00897b'; + const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => { + const ts = Math.floor(new Date(t.time).getTime() / 1000); + const trendRaw = (t.trend || '').toString().toUpperCase(); + const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts); + if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor2, shape: 'arrowUp', size: 0.4 }; + if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor2, shape: 'arrowDown', size: 0.4 }; + if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'circle', size: 0.5 }; + return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'square', size: 0.5 }; + }); + trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2); + } // 这里的 onlyMainAndU 实际上是「最终要挂到主K线上」的一组标记 // 之前没有把 window.bspMarkers 合进去,导致上面已经合并了 BSP 标记, // 但在这里再次调用 setMarkers 时把 BSP 覆盖掉了,从而前端看不到买卖点。 @@ -5908,6 +6304,7 @@ ...(window.mainFxMarkers || []), ...(window.kluDivMarkersMain || []), ...(window.kluDivMarkersElement || []), + ...(window.kluDivMarkersSubSub || []), ...trendMarkersToUse, ...(window.bspMarkers || []) ];