From dce2099ba1da52da3bcec59c97aad28af0775a6b Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Fri, 24 Oct 2025 23:31:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=AC=94=E4=B8=AD=E6=9E=A2?= =?UTF-8?q?=EF=BC=8C=E5=88=A0=E9=99=A4=E4=BA=86=E5=BE=88=E5=A4=9A=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E7=94=A8=E7=9A=84=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ChanBI.py | 13 +- ChanBIZS.py | 43 +-- ChanKLC.py | 1 + ChanKLU.py | 6 + ChanLun.py | 48 ++- ChanSEG.py | 113 +++++- web/app.py | 765 ++++--------------------------------- web/static/css/style.css | 2 +- web/templates/index.html | 794 ++++++++++++++------------------------- 9 files changed, 519 insertions(+), 1266 deletions(-) diff --git a/ChanBI.py b/ChanBI.py index 9426bb2..aaafaa3 100644 --- a/ChanBI.py +++ b/ChanBI.py @@ -19,6 +19,9 @@ class ChanBI(): self.start_time = klc.start_time self.macd_hist = 0 self.macd_div = 0 + self.seg = None + def set_seg(self, seg): + self.seg = seg def set_macdhist(self, macd_hist): self.macd_hist = macd_hist def set_macd_div(self, macd_div): @@ -39,8 +42,16 @@ class ChanBI(): self.macd_hist += klu.macdhist if self.dir == Chan_BI_DIR.DOWN and klu.macdhist < 0: self.macd_hist -= klu.macdhist - def check_overlap(self): + def check_bi_zs_overlap(self): if self.next and self.next.next: + if self.dir == Chan_BI_DIR.UP: + return self.low < self.next.next.high + else: + return self.high > self.next.next.low + else: + return False + def check_overlap(self): + if self.next and self.next.next and self.next.next.is_sure: if self.dir == Chan_BI_DIR.UP: return self.high > self.next.low and self.high < self.next.next.high else: diff --git a/ChanBIZS.py b/ChanBIZS.py index 4cd496a..c90c1e2 100644 --- a/ChanBIZS.py +++ b/ChanBIZS.py @@ -7,49 +7,34 @@ class ChanBIZS(): self.start_time = self.start_klc.start_time self.end_time = None self.index = index - self.next = None - self.pre = None self.start_bi = start_bi self.bi_list = [] self.bi_list.append(start_bi) self.end_bi = None - self.last_bi_in = None self.bi_out = None self.is_sure = False self.zg = 0 self.zd = 0 + self.gg = 0 + self.dd = 0 self.dir = ddir self.sure_time = None self.end_klc = None - self.bi_out_count = 0 - self.bi_out_list = [] - def set_last_bi_in(self, last_bi_in): - self.last_bi_in = last_bi_in - def set_bi_out(self, bi_out): - if bi_out: - #print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list)) - if len(self.bi_out_list) > 0: - last_bi = self.bi_out_list[-1] - if last_bi.index != bi_out.index: - self.bi_out_list.append(bi_out) - else: - self.bi_out_list.append(bi_out) - self.bi_out = bi_out - def set_end_klc(self, end_klc, sure_time, bi_out_count): - self.end_klc = end_klc - self.set_end_time(end_klc.end_time) + def set_end_bi(self, end_bi, sure_bi): + self.end_bi = end_bi + self.set_end_time(end_bi.end_klc.end_time) self.is_sure = True - self.sure_time = sure_time - self.bi_out_count = bi_out_count - def set_pre(self, pre): - self.pre = pre - def set_next(self, next): - self.next = next + self.sure_time = sure_bi.sure_time def set_end_time(self, end_time): self.end_time = end_time - def add_klc(self, klc): - self.klc_list.append(klc) def set_zg(self, zg): self.zg = zg def set_zd(self, zd): - self.zd = zd \ No newline at end of file + self.zd = zd + def set_gg(self, gg): + self.gg = gg + def set_dd(self, dd): + self.dd = dd + def add_bi(self, bi: ChanBI): + if bi: + self.bi_list.append(bi) \ No newline at end of file diff --git a/ChanKLC.py b/ChanKLC.py index 3d88e8d..8e8bc7d 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -48,6 +48,7 @@ class ChanKLC(): self.ema52 = klu.ema52 self.ema24 = klu.ema24 self.trend = Chan_PRICE_TREND.UNKNOWN + self.exception = klu.exception def set_trend(self, trend): self.trend = trend def to_string(self): diff --git a/ChanKLU.py b/ChanKLU.py index 3a15f81..552a1a0 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -36,6 +36,8 @@ class ChanKLU: self.body_ratio = self.body / self.range self.upper_shadow_ratio = self.upper_shadow / self.body self.lower_shadow_ratio = self.lower_shadow / self.body + self.exception = False + self.cal_exception() self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR self.continue_div = 0 @@ -65,6 +67,10 @@ class ChanKLU: #print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength) def set_macd_state(self, state): self.macd_state = state + def cal_exception(self): + if self.upper_shadow_ratio > 5 or self.lower_shadow_ratio > 5: + self.exception = True + #print(self.time, self.upper_shadow_ratio, self.lower_shadow_ratio, self.body, self.lower_shadow, self.upper_shadow, self.high, self.low, self.close, self.open) def set_trend(self, trend): self.trend = trend def set_next(self, next): diff --git a/ChanLun.py b/ChanLun.py index 29ad727..b97fd4d 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -246,7 +246,7 @@ class ChanLun(): if last_down_sbi.has_fx_gap: look_for_bottom = True last_seg.pre_set_end_bi(bi_list[last_down_sbi.start_bi.index - 1]) - seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN) + seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN, bi) seg_list.append(seg) last_seg.set_next(seg) seg.set_pre(last_seg) @@ -272,7 +272,7 @@ class ChanLun(): #print(bi.start_time, look_for_top, "UP 3") else: last_seg.set_end_bi(bi_list[last_down_sbi.start_bi.index - 1], bi) - seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN) + seg = ChanSEG(last_down_sbi.start_bi, len(seg_list), Chan_SEG_DIR.DOWN, bi) seg_list.append(seg) last_seg.set_next(seg) seg.set_pre(last_seg) @@ -340,7 +340,7 @@ class ChanLun(): if last_up_sbi.has_fx_gap: look_for_top = True last_seg.pre_set_end_bi(bi_list[last_up_sbi.start_bi.index - 1]) - seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP) + seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP, bi) seg_list.append(seg) last_seg.set_next(seg) seg.set_pre(last_seg) @@ -366,7 +366,7 @@ class ChanLun(): #print(bi.start_time, look_for_top, "DOWN 3") else: last_seg.set_end_bi(bi_list[last_up_sbi.start_bi.index - 1], bi) - seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP) + seg = ChanSEG(last_up_sbi.start_bi, len(seg_list), Chan_SEG_DIR.UP, bi) #print(last_up_sbi.start_bi.start_time) last_seg.set_next(seg) seg.set_pre(last_seg) @@ -414,14 +414,14 @@ class ChanLun(): else: if bi.check_overlap(): if bi.dir == Chan_BI_DIR.UP: - seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.UP) + seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.UP, bi) last_up_bi = bi last_up_sbi = ChanSBI(bi, len(up_sbi_list), bi.dir) seg_list.append(seg) last_seg = seg #print(bi.start_time, 'Create first UP SEG') else: - seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.DOWN) + seg = ChanSEG(bi, len(seg_list), Chan_SEG_DIR.DOWN, bi) last_down_bi = bi last_down_sbi = ChanSBI(bi, len(down_sbi_list), bi.dir) seg_list.append(seg) @@ -448,7 +448,7 @@ class ChanLun(): # The confirmed print("Last UP seg is broken, create a new seg. 1") seg.pre_set_end_bi(bi_list[i]) - seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.DOWN) + seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.DOWN, bi) seg_list.append(seg) last_seg = seg_list[-2] if len(last_seg.bi_list) > 3: @@ -460,7 +460,7 @@ class ChanLun(): if bi_list[i].low < last_seg_peak: print("Last DOWN seg is broken, create a new seg. 1") seg.pre_set_end_bi(bi_list[i]) - seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.UP) + seg = ChanSEG(bi_list[i+1], len(seg_list), Chan_SEG_DIR.UP, bi) seg_list.append(seg) last_seg = seg_list[-2] if len(last_seg.bi_list) > 3: @@ -478,12 +478,13 @@ class ChanLun(): if bi_list[i].high > last_seg_peak: print("Last seg is broken, create a new seg. 2") last_seg.pre_set_end_bi(bi_list[i-1]) - seg = ChanSEG(bi_list[i], len(seg_list), Chan_SEG_DIR.UP) + seg = ChanSEG(bi_list[i], len(seg_list), Chan_SEG_DIR.UP, bi) seg_list.append(seg) last_seg = seg last_seg_bi = bi_list[i] break """ + self.cal_bi_zs(seg_list) return seg_list def cal_trend(self, klc_list): """ @@ -1170,7 +1171,7 @@ class ChanLun(): # SEG is not in ZS if seg.is_sure: if ((seg.low > last_zs.zg and seg.high > last_zs.zg) or (seg.high < last_zs.zd and seg.low < last_zs.zd)): - last_zs.set_end_klc(seg.pre.pre.end_bi.end_klc, seg.sure_time, bi_out_count, seg) + last_zs.set_end_klc(seg.pre.end_bi.end_klc, seg.sure_time, bi_out_count, seg.pre) bi_out_count = 0 #print(seg.start_bi.start_klc.start_time) first_bi_out = None @@ -1285,6 +1286,13 @@ class ChanLun(): bsp_list.append(bsp) #self.print_zs(zs_list) return zs_list + def cal_bi_zs(self, seg_list): + bi_zs_list = [] + for seg in seg_list: + zs_list = seg.cal_bi_zs() + if len(zs_list) > 0: + bi_zs_list.append(zs_list) + return bi_zs_list def get_decimal(self, value): return Decimal("{:.2f}".format(value)) @@ -1297,19 +1305,33 @@ class ChanLun(): for klu in klu_list: if len(klc_list) > 0: last_klc = klc_list[-1] - included = last_klc.check_klu_included(klu) - if not included: + if klu.exception: ddir = Chan_KLINE_DIR.DOWN if last_klc.high < klu.high: ddir = Chan_KLINE_DIR.UP klc = ChanKLC(klu, index=len(klc_list), ddir=ddir) + klc.high = klu.close if klu.close > klu.open else klu.open + klc.low = klu.open if klu.close > klu.open else klu.close klc_list.append(klc) last_klc.set_next(klc) klc.set_pre(last_klc) last_klc.set_end_klu(last_klu) klc.set_pre_fx() + #print(klu.time, klu.high, klu.low, klu.close, klu.open, klu.exception) else: - last_klc.add_klu(klu) + included = last_klc.check_klu_included(klu) + if not included: + ddir = Chan_KLINE_DIR.DOWN + if last_klc.high < klu.high: + ddir = Chan_KLINE_DIR.UP + klc = ChanKLC(klu, index=len(klc_list), ddir=ddir) + klc_list.append(klc) + last_klc.set_next(klc) + klc.set_pre(last_klc) + last_klc.set_end_klu(last_klu) + klc.set_pre_fx() + else: + last_klc.add_klu(klu) else: ddir = Chan_KLINE_DIR.UP if klu.open > klu.close: diff --git a/ChanSEG.py b/ChanSEG.py index e102875..ba0adfc 100644 --- a/ChanSEG.py +++ b/ChanSEG.py @@ -1,12 +1,12 @@ import copy from typing import Dict, Optional -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR -import ChanKLU +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR, Chan_BI_DIR, Chan_ZS_DIR import ChanCTime from ChanBI import ChanBI +from ChanBIZS import ChanBIZS class ChanSEG(): - def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP): + def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP, pre_end_bi: ChanBI = None): self.start_bi = start_bi self.start_time = start_bi.start_time self.end_time = None @@ -28,6 +28,17 @@ class ChanSEG(): self.sure_time = None self.macd_hist = 0 self.macd_div = 0 + self.start_bi.set_seg(self) + self.pre_end_bi = pre_end_bi + if self.pre_end_bi: + self.ini_seg() + def ini_seg(self): + next_bi = self.start_bi.next + for index in range(self.start_bi.index+1, self.pre_end_bi.index): + if next_bi: + self.bi_list.append(next_bi) + next_bi.set_seg(self) + next_bi = next_bi.next def set_macdhist(self, macd_hist): self.macd_hist = macd_hist def set_macd_div(self, macd_div): @@ -63,14 +74,104 @@ class ChanSEG(): self.is_sure = True self.format_bi_list() def format_bi_list(self): - bi_list = [] - bi_list.append(self.start_bi) + self.bi_list = [] + self.bi_list.append(self.start_bi) if self.end_bi: next_bi = self.start_bi.next for i in range(self.start_bi.index, self.end_bi.index): if next_bi: self.bi_list.append(next_bi) + next_bi.set_seg(self) next_bi = next_bi.next def add_bi(self, bi: ChanBI): if len(self.bi_list) > 0: - self.bi_list.append(bi) \ No newline at end of file + self.bi_list.append(bi) + bi.set_seg(self) + def cal_bi_zs(self): + zs_list = [] + if len(self.bi_list) > 3: + last_zs = None + zs_count = 0 + if self.dir == Chan_SEG_DIR.UP: + for index in range(1, len(self.bi_list)): + bi = self.bi_list[index] + if len(zs_list) == 0 or (last_zs and last_zs.is_sure): + if bi.next and bi.next.next and bi.next.next.is_sure and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: + zg = min(bi.high, bi.next.high, bi.next.next.high) + zd = max(bi.low, bi.next.low, bi.next.next.low) + gg = max(bi.high, bi.next.high, bi.next.next.high) + dd = min(bi.low, bi.next.low, bi.next.next.low) + zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.add_bi(bi.next) + zs.add_bi(bi.next.next) + zs_list.append(zs) + last_zs = zs + else: + if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.DOWN and bi.is_sure: + if bi.low < last_zs.zg: + last_zs.add_bi(bi.pre) + last_zs.add_bi(bi) + else: + last_zs.set_end_bi(last_zs.bi_list[-1], bi) + if bi.next and bi.next.next and bi.next.next.is_sure and bi.next.next.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: + zg = min(bi.high, bi.next.high, bi.next.next.high) + zd = max(bi.low, bi.next.low, bi.next.next.low) + gg = max(bi.high, bi.next.high, bi.next.next.high) + dd = min(bi.low, bi.next.low, bi.next.next.low) + zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.UP) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.add_bi(bi.next) + zs.add_bi(bi.next.next) + zs_list.append(zs) + last_zs = zs + else: + for index in range(1, len(self.bi_list)): + bi = self.bi_list[index] + if len(zs_list) == 0 or (last_zs and last_zs.is_sure): + if bi.next and bi.next.next and bi.next.next.is_sure and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: + zg = min(bi.high, bi.next.high, bi.next.next.high) + zd = max(bi.low, bi.next.low, bi.next.next.low) + gg = max(bi.high, bi.next.high, bi.next.next.high) + dd = min(bi.low, bi.next.low, bi.next.next.low) + zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.add_bi(bi.next) + zs.add_bi(bi.next.next) + zs_list.append(zs) + last_zs = zs + else: + if bi.index > last_zs.bi_list[-1].index and bi.dir == Chan_BI_DIR.UP and bi.is_sure: + if bi.high > last_zs.zd: + last_zs.add_bi(bi.pre) + last_zs.add_bi(bi) + else: + last_zs.set_end_bi(last_zs.bi_list[-1], bi) + if bi.next and bi.next.next and bi.next.next.is_sure and bi.next.next.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: + zg = min(bi.high, bi.next.high, bi.next.next.high) + zd = max(bi.low, bi.next.low, bi.next.next.low) + gg = max(bi.high, bi.next.high, bi.next.next.high) + dd = min(bi.low, bi.next.low, bi.next.next.low) + zs = ChanBIZS(bi, len(zs_list), Chan_ZS_DIR.DOWN) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.add_bi(bi.next) + zs.add_bi(bi.next.next) + zs_list.append(zs) + last_zs = zs + if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1]) + #print(self.start_time, len(zs_list)) + print(self.bi_list[-1].end_time, "end_bi") + return zs_list \ No newline at end of file diff --git a/web/app.py b/web/app.py index 56bf248..b813cf7 100644 --- a/web/app.py +++ b/web/app.py @@ -125,7 +125,7 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time= # 添加请求计数和最大限制 request_count = 0 - max_requests = 50 # 最大请求次数,防止无限循环 + max_requests = 300 # 最大请求次数,防止无限循环 # 分页加载数据 while request_count < max_requests: @@ -188,8 +188,8 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time= # 限制数据条数的逻辑 - 优先考虑时间范围 if start_time and end_time: # 如果指定了明确的时间范围,返回该时间范围内的所有数据 - if len(df) > 10000: # 防止数据量过大,设置一个合理的上限 - df = df.tail(10000).reset_index(drop=True) + if len(df) > 100000: # 防止数据量过大,设置一个合理的上限 + df = df.tail(100000).reset_index(drop=True) elif limit and len(df) > limit: # 如果没有指定明确时间范围,使用默认的limit限制 df = df.tail(limit).reset_index(drop=True) @@ -391,8 +391,13 @@ def analyze_chan(df, symbol=None, timeframe=None): #print(bi_list[index].start_time, bi_list[index].start_klc.end_time, bi_list[index].dir) seg_list = chan.get_seg_list(bi_list) zs_list = chan.calculate_zs(bi_list, seg_list) + # 计算笔中枢(BI中枢)并拍平成列表 + try: + bi_zs_nested = chan.cal_bi_zs(seg_list) + bi_zs_list = [zs for group in bi_zs_nested for zs in (group or [])] if bi_zs_nested else [] + except Exception: + bi_zs_list = [] # 添加买卖点识别 - buy_sell_points = identify_trade_points(bi_list, seg_list, zs_list) for bi in bi_list: bi.cal_macdhist() for bi in bi_list: @@ -513,63 +518,6 @@ def analyze_chan(df, symbol=None, timeframe=None): 'is_strong_fx': False }) - # 提取KLU分型信息 - klu_fx_info = [] - for klu in klu_list: - if hasattr(klu, 'fx_type') and klu.fx_type != Chan_FX_TYPE.UNKNOWN: - try: - # 计算分型强度 - fx_strength = 0 - fx_strength_level = "" - is_strong_fx = False - - # 尝试调用分型强度计算方法 - if hasattr(klu, 'calculate_realtime_fx_strength'): - fx_strength = klu.calculate_realtime_fx_strength() - elif hasattr(klu, 'fx_strength'): - fx_strength = klu.fx_strength - - # 尝试获取分型强度等级 - 基于强度值生成等级 - if fx_strength >= 2: - fx_strength_level = "强" - is_strong_fx = True - elif fx_strength >= 1: - fx_strength_level = "中" - is_strong_fx = False - elif fx_strength >= 0: - fx_strength_level = "弱" - is_strong_fx = False - else: - fx_strength_level = "极弱" - is_strong_fx = False - - # 确保分型确认状态 - is_confirmed = getattr(klu, 'fx_confirmed', True) - - klu_fx_info.append({ - 'time': klu.time, - 'price': klu.low if klu.fx_type == Chan_FX_TYPE.BOTTOM else klu.high, - 'fx_type': str(klu.fx_type).replace("Chan_FX_TYPE.", ""), - 'is_bottom': klu.fx_type == Chan_FX_TYPE.BOTTOM, - 'fx_strength': fx_strength, # 分型强度分数 - 'fx_strength_level': fx_strength_level, # 分型强度等级 - 'is_strong_fx': is_strong_fx, # 是否为强分型 - 'fx_confirmed': is_confirmed # 分型是否确认 - }) - except Exception as e: - # 如果出错,仍然添加基本信息,但分型强度为0 - klu_fx_info.append({ - 'time': klu.time, - 'price': klu.low if klu.fx_type == Chan_FX_TYPE.BOTTOM else klu.high, - 'fx_type': str(klu.fx_type).replace("Chan_FX_TYPE.", ""), - 'is_bottom': klu.fx_type == Chan_FX_TYPE.BOTTOM, - 'fx_strength': 0, - 'fx_strength_level': "", - 'is_strong_fx': False, - 'fx_confirmed': False - }) - - return { 'klc_list': klc_list, @@ -577,474 +525,12 @@ def analyze_chan(df, symbol=None, timeframe=None): 'bi_list': bi_list, 'seg_list': seg_list, 'zs_list': zs_list, - 'trade_points': buy_sell_points, + 'bi_zs_list': bi_zs_list, # 添加BI中枢列表 'klc_fx_info': klc_fx_info, # KLC分型信息 - 'klu_fx_info': klu_fx_info, # 添加KLU分型信息 'chan_macd': chan_macd_data, # 添加ChanMACD分析数据 'ema52_dict': ema52_dict # 添加多时间周期EMA52数据 } -def generate_replay_data(df, client_tz, symbol=None, element_timeframe=None, start_time=None, end_time=None): - """生成逐步计算的回放数据""" - replay_data = {} - - # 预先获取完整的次周期数据(避免重复数据获取) - element_full_data = None - if element_timeframe and symbol: - # 一次性获取完整的次周期数据 - element_full_data = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time) - if element_full_data is not None and len(element_full_data) > 0: - # 一次性添加技术指标 - element_full_data = add_indicators(element_full_data) - - # 为每个K线索引计算分析结果 - for i in range(1, len(df) + 1): # 从1开始,至少需要1根K线 - try: - # 截取到当前索引的数据 - current_df = df.iloc[:i].copy() - - # 添加技术指标 - current_df = add_indicators(current_df) - - # 进行缠论分析 - analysis_result = analyze_chan(current_df, symbol, timeframe) - - # 计算MACD - macd_data = calculate_macd(current_df) - - # 如果有次周期数据,筛选对应时间范围的数据 - element_step_data = {} - if element_full_data is not None: - # 获取当前主周期时间范围 - current_end_time = current_df['timestamp'].iloc[-1] if len(current_df) > 0 else None - - if current_end_time: - # 筛选次周期数据:只取时间戳小于等于当前主周期结束时间的数据 - element_current_df = element_full_data[element_full_data['timestamp'] <= current_end_time].copy() - - if len(element_current_df) > 0: - # 重新对当前时间范围的次周期数据进行缠论分析 - # 这样可以确保数据的准确性,避免时间筛选的复杂性 - element_current_analysis = analyze_chan(element_current_df, symbol, element_timeframe) - - # 直接使用分析结果,无需复杂的时间筛选 - filtered_bi_list = element_current_analysis['bi_list'] - filtered_seg_list = element_current_analysis['seg_list'] - filtered_zs_list = element_current_analysis['zs_list'] - filtered_trade_points = element_current_analysis['trade_points'] - filtered_klc_fx = element_current_analysis['klc_fx_info'] - filtered_klu_fx = element_current_analysis['klu_fx_info'] - - # 计算当前时间范围的MACD - element_macd_data = calculate_macd(element_current_df) - - element_step_data = { - 'element_kline_data': clean_dataframe_for_json(element_current_df).to_dict('records'), - 'element_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 filtered_bi_list if bi.end_klc], - 'element_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 filtered_bi_list if not bi.end_klc], - 'element_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 filtered_seg_list if seg.is_sure], - 'element_uncompleted_seg_list': get_uncompleted_seg_list(filtered_seg_list, client_tz), - 'element_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 filtered_zs_list if zs.end_klc], - 'element_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 filtered_zs_list if not zs.is_sure], - 'element_trade_points': [{ - 'type': point['type'], - 'time': format_time_safely(point['time'], client_tz), - 'price': point['price'], - 'desc': point['desc'] - } for point in filtered_trade_points], - 'element_macd': element_macd_data, - 'element_bollinger': { - 'upper': element_current_df['bb_upper'].tolist(), - 'middle': element_current_df['bb_middle'].tolist(), - 'lower': element_current_df['bb_lower'].tolist() - }, - 'element_element_bollinger': { - 'upper': element_current_df['element_bb_upper'].tolist(), - 'middle': element_current_df['element_bb_middle'].tolist(), - 'lower': element_current_df['element_bb_lower'].tolist() - }, - # 添加次周期ATR数据 - 'element_atr': element_current_df['atr'].tolist(), - 'element_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 filtered_klc_fx], - 'element_klu_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']), - 'fx_confirmed': bool(point['fx_confirmed']) - } for point in filtered_klu_fx] - } - - # 构建该索引对应的分析结果 - step_data = { - 'step_index': i-1, # 当前步骤索引 - 'total_steps': len(df), # 总步骤数 - 'has_element_data': element_timeframe is not None and len(element_step_data) > 0, # 是否包含次周期数据 - 'element_timeframe': element_timeframe, # 次周期时间框架 - 'kline_data': clean_dataframe_for_json(current_df).to_dict('records'), - '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 analysis_result['bi_list'] if bi.end_klc], - '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 analysis_result['bi_list'] if not bi.end_klc], - '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 analysis_result['seg_list'] if seg.is_sure], - 'uncompleted_seg_list': get_uncompleted_seg_list(analysis_result['seg_list'], client_tz), - '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 analysis_result['zs_list'] if zs.end_klc], - '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 analysis_result['zs_list'] if not zs.is_sure], - 'trade_points': [{ - 'type': point['type'], - 'time': format_time_safely(point['time'], client_tz), - 'price': point['price'], - 'desc': point['desc'] - } for point in analysis_result['trade_points']], - 'macd': macd_data, - 'bollinger': { - 'upper': current_df['bb_upper'].tolist(), - 'middle': current_df['bb_middle'].tolist(), - 'lower': current_df['bb_lower'].tolist() - }, - 'element_bollinger': { - 'upper': current_df['element_bb_upper'].tolist(), - 'middle': current_df['element_bb_middle'].tolist(), - 'lower': current_df['element_bb_lower'].tolist() - }, - # 添加ATR数据 - 'atr': current_df['atr'].tolist(), - '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 analysis_result['klc_fx_info']], - 'klu_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']), # 是否为强分型 - 'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认 - } for point in analysis_result['klu_fx_info']] - } - - # 合并次周期数据到step_data中,如果没有次周期数据则提供空的占位符 - if element_step_data: - step_data.update(element_step_data) - else: - # 提供空的次周期数据结构,确保前端可以统一处理 - step_data.update({ - 'element_kline_data': [], - 'element_bi_list': [], - 'element_uncompleted_bi_list': [], - 'element_seg_list': [], - 'element_uncompleted_seg_list': [], - 'element_zs_list': [], - 'element_uncompleted_zs_list': [], - 'element_trade_points': [], - 'element_macd': {'macd': [], 'signal': [], 'histogram': []}, - 'element_bollinger': {'upper': [], 'middle': [], 'lower': []}, - 'element_element_bollinger': {'upper': [], 'middle': [], 'lower': []}, - 'element_atr': [], - 'element_klc_fx_info': [], - 'element_klu_fx_info': [] - }) - - replay_data[i-1] = step_data # 使用0-based索引 - - - except Exception as e: - continue - return replay_data - -def identify_trade_points(bi_list, seg_list, zs_list): - """识别缠论买卖点 - 多级别识别,减少滞后性""" - trade_points = [] - - - - # 1. 基于笔的二三类买卖点识别(更及时) - trade_points.extend(identify_bi_trade_points(bi_list, zs_list)) - - # 2. 基于线段的一类买卖点识别(传统方法) - trade_points.extend(identify_seg_trade_points(seg_list)) - - # 3. 基于分型强度的预警点识别(最及时) - trade_points.extend(identify_fx_warning_points(bi_list)) - - # 4. 基于MACD背驰的买卖点识别 - trade_points.extend(identify_macd_divergence_points(bi_list)) - - # 按时间排序 - trade_points.sort(key=lambda x: x['time']) - - return trade_points - -def identify_bi_trade_points(bi_list, zs_list): - """基于笔识别二三类买卖点 - 更及时的信号""" - trade_points = [] - - if len(bi_list) < 3: - return trade_points - - # 构建中枢映射,便于快速查找 - zs_map = {} - for zs in zs_list: - if zs.is_sure: # 只考虑已确认的中枢 - zs_map[zs.start_klc.end_time] = zs - - for i in range(2, len(bi_list)): - current_bi = bi_list[i] - prev_bi = bi_list[i-1] - prev_prev_bi = bi_list[i-2] - - # 确保笔已完成 - if not current_bi.end_klc or not prev_bi.end_klc or not prev_prev_bi.end_klc: - continue - - # 二类买点:向下笔后的向上笔,且不创新低 - if (convert_direction(prev_bi.dir) == -1 and - convert_direction(current_bi.dir) == 1): - - prev_low = prev_bi.end_klc.low - current_end_price = current_bi.end_klc.high - - # 检查是否不创新低(相对于前面的低点) - if i >= 4: # 至少需要5个笔来判断 - earlier_lows = [bi.end_klc.low for bi in bi_list[max(0, i-4):i-1] - if convert_direction(bi.dir) == -1 and bi.end_klc] - if earlier_lows and prev_low > min(earlier_lows): - trade_points.append({ - 'type': TRADE_POINT_TYPE.BUY2, - 'time': current_bi.end_klc.end_time, - 'price': current_end_price, - 'desc': '二类买点(笔)' - }) - - # 二类卖点:向上笔后的向下笔,且不创新高 - if (convert_direction(prev_bi.dir) == 1 and - convert_direction(current_bi.dir) == -1): - - prev_high = prev_bi.end_klc.high - current_end_price = current_bi.end_klc.low - - # 检查是否不创新高(相对于前面的高点) - if i >= 4: # 至少需要5个笔来判断 - earlier_highs = [bi.end_klc.high for bi in bi_list[max(0, i-4):i-1] - if convert_direction(bi.dir) == 1 and bi.end_klc] - if earlier_highs and prev_high < max(earlier_highs): - trade_points.append({ - 'type': TRADE_POINT_TYPE.SELL2, - 'time': current_bi.end_klc.end_time, - 'price': current_end_price, - 'desc': '二类卖点(笔)' - }) - - return trade_points - -def identify_seg_trade_points(seg_list): - """基于线段识别一类买卖点 - 传统方法""" - trade_points = [] - - if len(seg_list) >= 3: - for i in range(2, len(seg_list)): - # 确保线段已完成 - if seg_list[i].end_bi and seg_list[i-1].end_bi and seg_list[i-2].end_bi: - # 一类买点:向下-向上-向下的底分型 - if (convert_direction(seg_list[i-2].dir) == -1 and - convert_direction(seg_list[i-1].dir) == 1 and - convert_direction(seg_list[i].dir) == -1): - trade_points.append({ - 'type': TRADE_POINT_TYPE.BUY1, - 'time': seg_list[i].end_bi.end_klc.end_time, - 'price': seg_list[i].end_bi.end_klc.low, - 'desc': '一类买点(线段)' - }) - - # 一类卖点:向上-向下-向上的顶分型 - if (convert_direction(seg_list[i-2].dir) == 1 and - convert_direction(seg_list[i-1].dir) == -1 and - convert_direction(seg_list[i].dir) == 1): - trade_points.append({ - 'type': TRADE_POINT_TYPE.SELL1, - 'time': seg_list[i].end_bi.end_klc.end_time, - 'price': seg_list[i].end_bi.end_klc.high, - 'desc': '一类卖点(线段)' - }) - - return trade_points - -def identify_fx_warning_points(bi_list): - """基于分型强度识别预警点 - 最及时的信号""" - trade_points = [] - - if len(bi_list) < 2: - return trade_points - - # 检查最近的几个笔 - recent_bis = bi_list[-3:] if len(bi_list) >= 3 else bi_list - - for bi in recent_bis: - if not bi.end_klc: - continue - - # 获取分型强度(如果有的话) - fx_strength = 0 - if hasattr(bi.end_klc, 'cal_fx_strength'): - try: - fx_strength = bi.end_klc.cal_fx_strength(5) - except: - fx_strength = 0 - - # 强分型预警(分型强度>=2) - if fx_strength >= 2: - if convert_direction(bi.dir) == -1: # 向下笔结束,可能的底部 - trade_points.append({ - 'type': TRADE_POINT_TYPE.BUY3, - 'time': bi.end_klc.end_time, - 'price': bi.end_klc.low, - 'desc': f'强分型预警-买点(强度:{fx_strength})' - }) - elif convert_direction(bi.dir) == 1: # 向上笔结束,可能的顶部 - trade_points.append({ - 'type': TRADE_POINT_TYPE.SELL3, - 'time': bi.end_klc.end_time, - 'price': bi.end_klc.high, - 'desc': f'强分型预警-卖点(强度:{fx_strength})' - }) - - return trade_points - -def identify_macd_divergence_points(bi_list): - """基于MACD背驰识别买卖点""" - trade_points = [] - - if len(bi_list) < 4: - return trade_points - - # 检查最近的笔是否有背驰 - for i in range(2, len(bi_list)): - current_bi = bi_list[i] - - if not current_bi.end_klc or not hasattr(current_bi, 'macd_div'): - continue - - # MACD背驰阈值 - divergence_threshold = 0.3 - - # 向下笔的底背驰 -> 买点 - if (convert_direction(current_bi.dir) == -1 and - hasattr(current_bi, 'macd_div') and - current_bi.macd_div > divergence_threshold): - trade_points.append({ - 'type': TRADE_POINT_TYPE.BUY2, - 'time': current_bi.end_klc.end_time, - 'price': current_bi.end_klc.low, - 'desc': f'MACD底背驰买点(背驰度:{current_bi.macd_div:.2f})' - }) - - # 向上笔的顶背驰 -> 卖点 - elif (convert_direction(current_bi.dir) == 1 and - hasattr(current_bi, 'macd_div') and - current_bi.macd_div > divergence_threshold): - trade_points.append({ - 'type': TRADE_POINT_TYPE.SELL2, - 'time': current_bi.end_klc.end_time, - 'price': current_bi.end_klc.high, - 'desc': f'MACD顶背驰卖点(背驰度:{current_bi.macd_div:.2f})' - }) - - return trade_points - # 辅助函数,转换缠论方向枚举为整数 def convert_direction(direction): """转换方向枚举为数字""" @@ -1719,6 +1205,20 @@ def analyze(): 'dd': zs.dd, 'is_sure': zs.is_sure # 添加中枢是否完成的标志 } for zs in analysis_result['zs_list'] if zs.end_klc], + # 添加主周期BI中枢列表(已完成) + '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 analysis_result.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)], # 添加未完成中枢列表 '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(), @@ -1729,12 +1229,21 @@ def analyze(): 'dd': zs.dd, 'is_sure': zs.is_sure # 未完成中枢的is_sure为False } for zs in analysis_result['zs_list'] if not zs.is_sure], - 'trade_points': [{ - 'type': point['type'], - 'time': format_time_safely(point['time'], client_tz), - 'price': point['price'], - 'desc': point['desc'] - } for point in analysis_result['trade_points']], + # 添加未完成BI中枢列表 + '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 analysis_result.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)], + 'macd': macd_data, # 添加布林带数据 'bollinger': { @@ -1759,16 +1268,6 @@ def analyze(): 'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级 'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型 } for point in analysis_result['klc_fx_info']], - 'klu_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']), # 是否为强分型 - 'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认 - } for point in analysis_result['klu_fx_info']], # 添加ChanMACD分析数据 'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz), # 添加多时间周期EMA52数据 @@ -1885,13 +1384,35 @@ def analyze(): 'dd': zs.dd, 'is_sure': zs.is_sure # 未完成中枢的is_sure为False } for zs in element_analysis['zs_list'] if not zs.is_sure] + + # 添加次周期 BI 中枢(已完成/未完成) + result['element_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 element_analysis.get('bi_zs_list', []) if getattr(zs, 'is_sure', False)] + result['element_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 element_analysis.get('bi_zs_list', []) if not getattr(zs, 'is_sure', False)] - result['element_trade_points'] = [{ - 'type': point['type'], - 'time': format_time_safely(point['time'], client_tz), - 'price': point['price'], - 'desc': point['desc'] - } for point in element_analysis['trade_points']] # 添加小周期分型信息 result['element_klc_fx_info'] = [{ @@ -1903,18 +1424,7 @@ def analyze(): 'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级 'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型 } for point in element_analysis['klc_fx_info']] - - result['element_klu_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']), # 是否为强分型 - 'fx_confirmed': bool(point['fx_confirmed']) # 分型是否确认 - } for point in element_analysis['klu_fx_info']] - + # 添加次周期ChanMACD分析数据 result['element_chan_macd'] = serialize_chan_macd_data(element_analysis.get('chan_macd', {}), client_tz) @@ -1990,145 +1500,6 @@ def search_stock(): except Exception as e: return jsonify({'error': str(e)}) -@app.route('/api/test_element_data') -def test_element_data(): - """测试次周期数据是否正确生成""" - try: - symbol = request.args.get('symbol', 'SOL/USDT:USDT') - timeframe = request.args.get('timeframe', '1h') - element_timeframe = request.args.get('element_timeframe', '15m') - - # 获取主周期数据 - main_df = get_kl_data(symbol, timeframe, limit=3) - if main_df is None or len(main_df) == 0: - return jsonify({'error': '无法获取主周期数据'}) - - # 获取次周期数据 - element_df = get_kl_data(symbol, element_timeframe, - start_time=main_df['timestamp'].iloc[0], - end_time=main_df['timestamp'].iloc[-1]) - - if element_df is None or len(element_df) == 0: - return jsonify({'error': '无法获取次周期数据'}) - - # 分析次周期数据 - element_df = add_indicators(element_df) - element_analysis = analyze_chan(element_df, symbol, element_timeframe) - - return jsonify({ - 'main_data_count': len(main_df), - 'element_data_count': len(element_df), - 'element_analysis': { - 'bi_count': len(element_analysis['bi_list']), - 'seg_count': len(element_analysis['seg_list']), - 'zs_count': len(element_analysis['zs_list']), - 'klc_fx_count': len(element_analysis['klc_fx_info']), - 'klu_fx_count': len(element_analysis['klu_fx_info']), - 'trade_points_count': len(element_analysis['trade_points']) - }, - 'sample_bi': [{'has_end_klc': bi.end_klc is not None, - 'direction': convert_direction(bi.dir)} - for bi in element_analysis['bi_list'][:2]] if len(element_analysis['bi_list']) > 0 else [], - 'sample_klc_fx': element_analysis['klc_fx_info'][:3] if len(element_analysis['klc_fx_info']) > 0 else [], - 'sample_klu_fx': element_analysis['klu_fx_info'][:3] if len(element_analysis['klu_fx_info']) > 0 else [] - }) - - except Exception as e: - import traceback - return jsonify({'error': str(e), 'traceback': traceback.format_exc()}) - -@app.route('/api/debug_replay_sample') -def debug_replay_sample(): - """调试接口:返回回放数据样本,方便前端调试""" - try: - symbol = request.args.get('symbol', 'SOL/USDT:USDT') - timeframe = request.args.get('timeframe', '1h') - element_timeframe = request.args.get('element_timeframe', '15m') - step = int(request.args.get('step', 2)) # 返回第几步的数据 - - # 获取少量数据进行测试 - df = get_kl_data(symbol, timeframe, limit=5) - if df is None or len(df) == 0: - return jsonify({'error': '无法获取测试数据'}) - - # 生成回放数据 - client_tz = timezone('Asia/Shanghai') - replay_data = generate_replay_data( - df, client_tz, symbol, element_timeframe, - start_time=None, end_time=None - ) - - if step not in replay_data: - return jsonify({'error': f'步骤 {step} 不存在,可用步骤:{list(replay_data.keys())}'}) - - # 返回指定步骤的完整数据 - step_data = replay_data[step] - - return jsonify({ - 'step': step, - 'data': step_data, - 'summary': { - 'has_element_data': step_data.get('has_element_data', False), - 'element_timeframe': step_data.get('element_timeframe'), - 'main_bi_count': len(step_data.get('bi_list', [])), - 'main_klc_fx_count': len(step_data.get('klc_fx_info', [])), - 'main_klu_fx_count': len(step_data.get('klu_fx_info', [])), - 'element_bi_count': len(step_data.get('element_bi_list', [])), - 'element_klc_fx_count': len(step_data.get('element_klc_fx_info', [])), - 'element_klu_fx_count': len(step_data.get('element_klu_fx_info', [])), - 'element_kline_count': len(step_data.get('element_kline_data', [])) - } - }) - - except Exception as e: - return jsonify({'error': str(e)}) - -@app.route('/api/debug_replay_structure') -def debug_replay_structure(): - """调试接口:检查回放数据结构""" - try: - # 获取一个简单的测试案例 - symbol = request.args.get('symbol', 'SOL/USDT:USDT') - timeframe = request.args.get('timeframe', '1h') - element_timeframe = request.args.get('element_timeframe', '15m') - - # 获取少量数据进行测试 - df = get_kl_data(symbol, timeframe, limit=5) # 只取5根K线 - if df is None or len(df) == 0: - return jsonify({'error': '无法获取测试数据'}) - - # 生成回放数据 - client_tz = timezone('Asia/Shanghai') - replay_data = generate_replay_data( - df, client_tz, symbol, element_timeframe, - start_time=None, end_time=None - ) - - # 返回结构信息 - result = { - 'total_steps': len(replay_data), - 'sample_step_keys': list(replay_data[0].keys()) if len(replay_data) > 0 else [], - 'has_element_data_in_steps': [], - 'element_data_counts': {} - } - - # 检查每个步骤的次周期数据 - for step_idx, step_data in replay_data.items(): - has_element = step_data.get('has_element_data', False) - result['has_element_data_in_steps'].append({ - 'step': step_idx, - 'has_element_data': has_element, - 'element_bi_count': len(step_data.get('element_bi_list', [])), - 'element_klc_fx_count': len(step_data.get('element_klc_fx_info', [])), - 'element_klu_fx_count': len(step_data.get('element_klu_fx_info', [])) - }) - - return jsonify(result) - - except Exception as e: - return jsonify({'error': str(e)}) - -# 已移除:/api/filter_stocks 路由 def get_uncompleted_seg_list(seg_list, client_tz): """获取未完成线段列表,正确处理倒数第二个和最后一个未完成线段""" diff --git a/web/static/css/style.css b/web/static/css/style.css index 596cf3f..a1ff895 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -122,7 +122,7 @@ table.dataTable tbody tr:hover { } .chart-container { - height: 400px; + height: 800px; } .controls .row { diff --git a/web/templates/index.html b/web/templates/index.html index 40f163b..c3ce2bd 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -48,9 +48,10 @@ margin-bottom: 6px; color: #495057; } + /*主图大小*/ .chart-container { width: 100%; - height: 900px; + height: 1200px; margin-top: 10px; border: 1px solid #e9ecef; border-radius: 8px; @@ -635,9 +636,6 @@ margin-right: 4px; display: inline-block; } - - - .bb-config-modal { position: fixed; top: 0; @@ -877,22 +875,22 @@
- +
- - + +
- +
- +
@@ -915,11 +913,11 @@
- +
- - + +
@@ -1712,9 +1710,10 @@ $('#showMainZs').change(function() { updateChartDisplay(); }); - - // 添加未完成中枢复选框变更事件 - $('#showMainUncompletedZs').change(function() { + // 添加主周期BI中枢复选框变更事件(委托绑定,避免DOM更新后失效) + console.log('初始化BI中枢事件绑定'); + $(document).on('change', '#showMainBiZs', function() { + console.log('主BI中枢切换为:', $('#showMainBiZs').is(':checked')); updateChartDisplay(); }); @@ -1740,9 +1739,9 @@ $('#showElementZs').change(function() { updateChartDisplay(); }); - - // 添加未完成中枢复选框变更事件 - $('#showElementUncompletedZs').change(function() { + // 添加次周期BI中枢复选框变更事件(委托绑定,避免DOM更新后失效) + $(document).on('change', '#showElementBiZs', function() { + console.log('次BI中枢切换为:', $('#showElementBiZs').is(':checked')); updateChartDisplay(); }); @@ -1843,11 +1842,16 @@ } console.log('更新图表显示'); + console.log('BI中枢输入状态:', { + main: $('#showMainBiZs').is(':checked'), + element: $('#showElementBiZs').is(':checked') + }); console.log('- 显示K线:', $('#showOriginalKline').is(':checked')); console.log('- 显示笔:', $('#showMainBi').is(':checked')); console.log('- 显示线段:', $('#showMainSeg').is(':checked')); console.log('- 显示中枢:', $('#showMainZs').is(':checked')); - console.log('- 显示未完成中枢:', $('#showMainUncompletedZs').is(':checked')); + console.log('- 显示BI中枢(主):', $('#showMainBiZs').is(':checked')); + console.log('- 显示BI中枢(次):', $('#showElementBiZs').is(':checked')); console.log('- 显示MACD:', true); console.log('- 显示成交量:', false); console.log('- 显示分型类型:', $('#showKlcFxType').is(':checked')); @@ -2033,6 +2037,11 @@ // 检查和记录服务器返回的时区 console.log('服务器返回的时区:', data.timezone || '未指定'); + // BI中枢调试输出 + console.log('主周期BI中枢(完成):', Array.isArray(data.bi_zs_list) ? data.bi_zs_list.length : 0); + console.log('主周期BI中枢(未完成):', Array.isArray(data.uncompleted_bi_zs_list) ? data.uncompleted_bi_zs_list.length : 0); + console.log('次周期BI中枢(完成):', Array.isArray(data.element_bi_zs_list) ? data.element_bi_zs_list.length : 0); + console.log('次周期BI中枢(未完成):', Array.isArray(data.element_uncompleted_bi_zs_list) ? data.element_uncompleted_bi_zs_list.length : 0); // 刷新图表 refreshChart(data); @@ -2077,7 +2086,6 @@ } catch (e) { console.warn('释放旧图表资源失败(可忽略):', e); } - console.log('初始化TradingView图表:', symbol, timeframe); // 获取当前交易对的配置 @@ -2624,7 +2632,6 @@ console.log('处理后的ATR数据点数:', atrData.length); console.log('ATR数据样本:', atrData.slice(0, 5)); } - console.log('处理后的ATR数据点数:', atrData.length); atrLineSeries.setData(atrData); tvWidget.series.atrLineSeries = atrLineSeries; @@ -3851,7 +3858,7 @@ } else { console.log('绘制线段 - 已禁用'); } - // 显示中枢的绘制 - 分别处理主周期和次周期 + // 显示中枢的绘制 - 分别处理主周期和次周期(包含BI中枢,沿用同样样式与开关) if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) { console.log('绘制中枢 - 已启用'); @@ -4133,12 +4140,199 @@ } else { console.log('绘制中枢 - 已禁用'); } + // BI中枢(已完成)- 使用独立的BI开关 + if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) { + try { + console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`); + } catch (e) {} + currentData.bi_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) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); + if (isNaN(startTime) || isNaN(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 = '#F1C40F'; + const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); + if (!isNaN(gg) && gg > 0) { + const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + } + if (!isNaN(dd) && dd > 0) { + const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ddSeries.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}条`); + } catch (e) {} + currentData.element_bi_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) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); + if (isNaN(startTime) || isNaN(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 = '#3f51b5'; + const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); + if (!isNaN(gg) && gg > 0) { + const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + } + if (!isNaN(dd) && dd > 0) { + const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } + } catch (e) { console.error('次周期BI中枢处理出错:', e); } + }); + } // 显示未完成中枢 - 分别处理主周期和次周期 - if ($('#showMainUncompletedZs').is(':checked') || $('#showElementUncompletedZs').is(':checked')) { + if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked')) { console.log('绘制未完成中枢 - 已启用'); + // 显示BI中枢绘制(沿用中枢样式) + if ($('#showMainBiZs').is(':checked') || $('#showElementBiZs').is(':checked')) { + console.log('绘制BI中枢 - 已启用'); + // 主周期 BI 中枢 + console.log('主BI开关:', $('#showMainBiZs').is(':checked'), '数据长度:', currentData.bi_zs_list ? currentData.bi_zs_list.length : 0); + if ($('#showMainBiZs').is(':checked') && currentData.bi_zs_list && currentData.bi_zs_list.length > 0) { + console.log(`绘制主周期BI中枢数据,共${currentData.bi_zs_list.length}条`); + currentData.bi_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) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); + if (isNaN(startTime) || isNaN(endTime)) { 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 = '#9C27B0'; // 主周期BI中枢颜色(紫色) + const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); + if (!isNaN(gg) && gg > 0) { + const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + } + if (!isNaN(dd) && dd > 0) { + const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } + } catch (e) { console.error('主周期BI中枢处理出错:', e); } + }); + } + // 主周期 未完成 BI 中枢 + console.log('主未完成BI长度:', currentData.uncompleted_bi_zs_list ? currentData.uncompleted_bi_zs_list.length : 0); + if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) { + console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`); + currentData.uncompleted_bi_zs_list.forEach(function(zs) { + try { + const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); + const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); + if (isNaN(startTime) || isNaN(endTime)) { 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 = '#9C27B0'; + const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + if (!isNaN(gg) && gg > 0) { + const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + } + if (!isNaN(dd) && dd > 0) { + const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } + } catch (e) { console.error('主周期未完成BI中枢处理出错:', e); } + }); + } + // 次周期 BI 中枢 + console.log('次BI开关:', $('#showElementBiZs').is(':checked'), '数据长度:', currentData.element_bi_zs_list ? currentData.element_bi_zs_list.length : 0); + if ($('#showElementBiZs').is(':checked') && currentData.element_bi_zs_list && currentData.element_bi_zs_list.length > 0) { + console.log(`绘制次周期BI中枢数据,共${currentData.element_bi_zs_list.length}条`); + currentData.element_bi_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) : Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); + if (isNaN(startTime) || isNaN(endTime)) { 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 = '#8BC34A'; // 次周期BI中枢颜色(绿) + const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + const rightSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + rightSeries.setData([{ time: endTime, value: zd }, { time: endTime, value: zg }]); + if (!isNaN(gg) && gg > 0) { + const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + } + if (!isNaN(dd) && dd > 0) { + const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } + } catch (e) { console.error('次周期BI中枢处理出错:', e); } + }); + } + // 次周期 未完成 BI 中枢 + console.log('次未完成BI长度:', currentData.element_uncompleted_bi_zs_list ? currentData.element_uncompleted_bi_zs_list.length : 0); + if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) { + console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`); + currentData.element_uncompleted_bi_zs_list.forEach(function(zs) { + try { + const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); + const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); + if (isNaN(startTime) || isNaN(endTime)) { 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 = '#8BC34A'; + const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + if (!isNaN(gg) && gg > 0) { + const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + } + if (!isNaN(dd) && dd > 0) { + const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } + } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); } + }); + } + } // 主周期未完成中枢 - if ($('#showMainUncompletedZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) { + if ($('#showMainZs').is(':checked') && currentData.uncompleted_zs_list && currentData.uncompleted_zs_list.length > 0) { console.log(`绘制主周期未完成中枢数据,共${currentData.uncompleted_zs_list.length}条`); currentData.uncompleted_zs_list.forEach(function(zs) { @@ -4281,7 +4475,7 @@ } // 次周期未完成中枢 - if ($('#showElementUncompletedZs').is(':checked') && currentData.element_uncompleted_zs_list && currentData.element_uncompleted_zs_list.length > 0) { + if ($('#showElementZs').is(':checked') && currentData.element_uncompleted_zs_list && currentData.element_uncompleted_zs_list.length > 0) { console.log(`绘制次周期未完成中枢数据,共${currentData.element_uncompleted_zs_list.length}条`); currentData.element_uncompleted_zs_list.forEach(function(zs) { @@ -4425,6 +4619,62 @@ } else { console.log('绘制未完成中枢 - 已禁用'); } + // 未完成BI中枢 - 使用独立的BI开关 + if ($('#showMainBiZs').is(':checked') && currentData.uncompleted_bi_zs_list && currentData.uncompleted_bi_zs_list.length > 0) { + try { console.log(`绘制主周期未完成BI中枢数据,共${currentData.uncompleted_bi_zs_list.length}条`); } catch (e) {} + currentData.uncompleted_bi_zs_list.forEach(function(zs) { + try { + const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); + const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); + if (isNaN(startTime) || isNaN(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 = '#F1C40F'; + const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + if (!isNaN(gg) && gg > 0) { + const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + } + if (!isNaN(dd) && dd > 0) { + const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } + } catch (e) { console.error('主周期未完成BI中枢处理出错:', e); } + }); + } + // 次周期 未完成 BI 中枢 + if ($('#showElementBiZs').is(':checked') && currentData.element_uncompleted_bi_zs_list && currentData.element_uncompleted_bi_zs_list.length > 0) { + try { console.log(`绘制次周期未完成BI中枢数据,共${currentData.element_uncompleted_bi_zs_list.length}条`); } catch (e) {} + currentData.element_uncompleted_bi_zs_list.forEach(function(zs) { + try { + const startTime = Math.floor(new Date(zs.start_time).getTime() / 1000); + const endTime = Math.floor(new Date(currentData.kline_data[currentData.kline_data.length-1].date).getTime() / 1000); + if (isNaN(startTime) || isNaN(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 = '#3f51b5'; + const topSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + topSeries.setData([{ time: startTime, value: zg }, { time: endTime, value: zg }]); + const bottomSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + bottomSeries.setData([{ time: startTime, value: zd }, { time: endTime, value: zd }]); + const leftSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + leftSeries.setData([{ time: startTime, value: zd }, { time: startTime, value: zg }]); + if (!isNaN(gg) && gg > 0) { + const ggSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ggSeries.setData([{ time: startTime, value: gg }, { time: endTime, value: gg }]); + } + if (!isNaN(dd) && dd > 0) { + const ddSeries = mainChart.addLineSeries({ color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false }); + ddSeries.setData([{ time: startTime, value: dd }, { time: endTime, value: dd }]); + } + } catch (e) { console.error('次周期未完成BI中枢处理出错:', e); } + }); + } // 添加买卖点标记 if ($('#showTradePoints').is(':checked')) { console.log('绘制买卖点 - 已启用'); @@ -5044,7 +5294,6 @@ if (currentData.klu_fx_info && currentData.klu_fx_info.length > 0) { console.log('前3个klu分型数据样本:', currentData.klu_fx_info.slice(0, 3)); } - // 收集所有主周期分型标记 const allMainFxMarkers = []; const mainFxMarkers = []; // 用于tooltip支持 @@ -5674,17 +5923,6 @@ setDefaultTimeRange(); } - // 添加买卖点提示 - // 初始化 tooltip 与 U 显示状态 - window.showUOnMain = $('#toggleUOnMain').is(':checked'); - window.showUOnElement = $('#toggleUOnElement').is(':checked'); - setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd); - - // 显示买卖点 - if ($('#showTradePoints').is(':checked')) { - displayTradePoints(); - } - // 更新EMA52显示 if (currentData) { updateEMA52Display(currentData); @@ -5962,7 +6200,6 @@ initTradingView($('#symbol').val(), $('#timeframe').val()); } } - function bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) { // 防止同步过程中的无限循环 let syncInProgress = false; @@ -6210,266 +6447,7 @@ }, 200); }); } - function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, volumeChart, atrChart, macdChart, chanMacdChart, showMacd) { - // 调试变量 - window.debugMode = true; - // 初始化 U 显示状态(主/次周期分开控制) - const isShowUMain = $('#toggleUOnMain').is(':checked'); - const isShowUElement = $('#toggleUOnElement').is(':checked'); - window.showUOnMain = isShowUMain; - window.showUOnElement = isShowUElement; - if (!isShowUMain && !isShowUElement) { - // 隐藏时清空子图上的 U 标记 - if (tvWidget.series && tvWidget.series.chanMacdLineSeries) { - try { tvWidget.series.chanMacdLineSeries.setMarkers([]); } catch (e) {} - } - if (tvWidget.series && tvWidget.series.chanMacdSignalSeries) { - try { tvWidget.series.chanMacdSignalSeries.setMarkers([]); } catch (e) {} - } - } - - // 添加买卖点悬浮提示元素 - const tooltipElement = document.createElement('div'); - tooltipElement.className = 'point-tooltip'; - // document.body.appendChild(tooltipElement); - - // 添加自定义十字线信息显示 - const crosshairTooltip = document.createElement('div'); - crosshairTooltip.className = 'crosshair-tooltip'; - crosshairTooltip.style.position = 'absolute'; - crosshairTooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; - crosshairTooltip.style.color = 'white'; - crosshairTooltip.style.padding = '5px 10px'; - crosshairTooltip.style.borderRadius = '4px'; - crosshairTooltip.style.fontSize = '12px'; - crosshairTooltip.style.zIndex = '1000'; - crosshairTooltip.style.pointerEvents = 'none'; - crosshairTooltip.style.display = 'none'; - // document.body.appendChild(crosshairTooltip); - - // 添加鼠标悬停事件显示提示 - if (mainChart) { - mainChart.subscribeCrosshairMove(param => { - // 十字线同步到其他图表 - 通过DOM元素绘制垂直线实现虚线延长效果 - if (param.time && param.point && volumeChart) { - try { - // 清除之前的十字线标记 - const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); - existingVolumeLines.forEach(line => line.remove()); - const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); - existingAtrLines.forEach(line => line.remove()); - const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); - existingMacdLines.forEach(line => line.remove()); - const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line'); - existingChanMacdLines.forEach(line => line.remove()); - - // 获取时间对应的坐标位置 - const mainTimeCoordinate = mainChart.timeScale().timeToCoordinate(param.time); - if (mainTimeCoordinate !== null) { - // 获取主图容器的位置 - const mainChartRect = mainChartContainer.getBoundingClientRect(); - - // 在交易量图上绘制垂直线 - const volumeTimeCoordinate = volumeChart.timeScale().timeToCoordinate(param.time); - if (volumeTimeCoordinate !== null) { - const volumeChartRect = volumeChartContainer.getBoundingClientRect(); - const volumeLine = document.createElement('div'); - volumeLine.className = 'volume-crosshair-line'; - volumeLine.style.position = 'fixed'; // 改为fixed定位 - volumeLine.style.left = (volumeChartRect.left + volumeTimeCoordinate) + 'px'; - volumeLine.style.top = volumeChartRect.top + 'px'; - volumeLine.style.width = '1px'; - volumeLine.style.height = volumeChartRect.height + 'px'; - volumeLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; - volumeLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; - volumeLine.style.pointerEvents = 'none'; - volumeLine.style.zIndex = '1000'; - document.body.appendChild(volumeLine); - } - - // 在ATR图上绘制垂直线 - if (atrChart && atrChartContainer) { - const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time); - if (atrTimeCoordinate !== null) { - const atrChartRect = atrChartContainer.getBoundingClientRect(); - const atrLine = document.createElement('div'); - atrLine.className = 'atr-crosshair-line'; - atrLine.style.position = 'fixed'; // 改为fixed定位 - atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px'; - atrLine.style.top = atrChartRect.top + 'px'; - atrLine.style.width = '1px'; - atrLine.style.height = atrChartRect.height + 'px'; - atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; - atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; - atrLine.style.pointerEvents = 'none'; - atrLine.style.zIndex = '1000'; - document.body.appendChild(atrLine); - } - } - - // 如果有MACD图,也在MACD图上绘制垂直线 - if (showMacd && macdChart && macdChartContainer) { - const macdTimeCoordinate = macdChart.timeScale().timeToCoordinate(param.time); - if (macdTimeCoordinate !== null) { - const macdChartRect = macdChartContainer.getBoundingClientRect(); - const macdLine = document.createElement('div'); - macdLine.className = 'macd-crosshair-line'; - macdLine.style.position = 'fixed'; // 改为fixed定位 - macdLine.style.left = (macdChartRect.left + macdTimeCoordinate) + 'px'; - macdLine.style.top = macdChartRect.top + 'px'; - macdLine.style.width = '1px'; - macdLine.style.height = macdChartRect.height + 'px'; - macdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; - macdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; - macdLine.style.pointerEvents = 'none'; - macdLine.style.zIndex = '1000'; - document.body.appendChild(macdLine); - } - } - - // 如果有ChanMACD图,也在ChanMACD图上绘制垂直线 - if (showMacd && chanMacdChart && chanMacdChartContainer) { - const chanMacdTimeCoordinate = chanMacdChart.timeScale().timeToCoordinate(param.time); - if (chanMacdTimeCoordinate !== null) { - const chanMacdChartRect = chanMacdChartContainer.getBoundingClientRect(); - console.log('ChanMACD图表位置(第二个位置):', { - left: chanMacdChartRect.left, - top: chanMacdChartRect.top, - width: chanMacdChartRect.width, - height: chanMacdChartRect.height, - timeCoordinate: chanMacdTimeCoordinate - }); - const chanMacdLine = document.createElement('div'); - chanMacdLine.className = 'chanmacd-crosshair-line'; - chanMacdLine.style.position = 'fixed'; - chanMacdLine.style.left = (chanMacdChartRect.left + chanMacdTimeCoordinate) + 'px'; - chanMacdLine.style.top = chanMacdChartRect.top + 'px'; - chanMacdLine.style.width = '1px'; - chanMacdLine.style.height = chanMacdChartRect.height + 'px'; - chanMacdLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; - chanMacdLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; - chanMacdLine.style.pointerEvents = 'none'; - chanMacdLine.style.zIndex = '1000'; - document.body.appendChild(chanMacdLine); - console.log('ChanMACD垂直线已创建(第二个位置),位置:', chanMacdLine.style.left, chanMacdLine.style.top); - } else { - console.log('ChanMACD时间坐标为空(第二个位置)'); - } - } else { - console.log('ChanMACD图表条件不满足(第二个位置):', { - showMacd: showMacd, - hasChanMacdChart: !!chanMacdChart, - hasChanMacdChartContainer: !!chanMacdChartContainer - }); - } - } - } catch (e) { - console.debug('十字线同步出错:', e); - } - } else { - // 当十字线离开时,清除垂直线 - try { - const existingVolumeLines = document.querySelectorAll('.volume-crosshair-line'); - existingVolumeLines.forEach(line => line.remove()); - const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); - existingAtrLines.forEach(line => line.remove()); - const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); - existingMacdLines.forEach(line => line.remove()); - const existingChanMacdLines = document.querySelectorAll('.chanmacd-crosshair-line'); - existingChanMacdLines.forEach(line => line.remove()); - } catch (e) { - console.debug('清除十字线时出错:', e); - } - } - - if (param.time && param.point) { - const timeStr = param.time; - const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr); - - // 同时检查分型标记 - const fxMarkers = (window.fxMarkers || []).filter(m => m.time === timeStr); - const allMarkers = [...markers, ...fxMarkers]; - - // 显示时区调试信息 - if (window.debugMode) { - const timezone = $('#timezone').val(); - const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone); - - // 获取当前价格 - 通过param.seriesPrices获取 - let priceInfo = ''; - if (param.seriesPrices && param.seriesPrices.size > 0) { - // 依次从当前可能的主系列中获取价格 - if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) { - const price = param.seriesPrices.get(tvWidget.series.candleSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } else if (tvWidget.series.renkoSeries && param.seriesPrices.get(tvWidget.series.renkoSeries)) { - const price = param.seriesPrices.get(tvWidget.series.renkoSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } else if (tvWidget.series.heikinSeries && param.seriesPrices.get(tvWidget.series.heikinSeries)) { - const price = param.seriesPrices.get(tvWidget.series.heikinSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } else if (tvWidget.series.barSeries && param.seriesPrices.get(tvWidget.series.barSeries)) { - const price = param.seriesPrices.get(tvWidget.series.barSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) { - const price = param.seriesPrices.get(tvWidget.series.lineSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) { - const price = param.seriesPrices.get(tvWidget.series.areaSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) { - const price = param.seriesPrices.get(tvWidget.series.baselineSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } - // 如果没有蜡烛图系列价格,尝试从区域图系列获取 - else if (tvWidget.series.areaSeries && param.seriesPrices.get(tvWidget.series.areaSeries)) { - const price = param.seriesPrices.get(tvWidget.series.areaSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } - // 如果没有蜡烛图系列价格,尝试从基线图系列获取 - else if (tvWidget.series.baselineSeries && param.seriesPrices.get(tvWidget.series.baselineSeries)) { - const price = param.seriesPrices.get(tvWidget.series.baselineSeries); - priceInfo = `价格: ${price.toFixed(2)}`; - } - } - - // 仅记录最简短的调试信息 - console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`); - - // 显示自定义时区工具提示,包含价格信息 - crosshairTooltip.innerHTML = `
时间: ${formattedTime}
` + - (priceInfo ? `
${priceInfo}
` : ''); - crosshairTooltip.style.display = 'block'; - crosshairTooltip.style.left = (param.point.x + 15) + 'px'; - crosshairTooltip.style.top = (param.point.y - 30) + 'px'; - } - - if (allMarkers.length > 0) { - // 有买卖点或分型标记,显示自定义提示 - const tooltips = allMarkers.map(m => m.tooltip).join('

'); - tooltipElement.innerHTML = tooltips; - tooltipElement.style.display = 'block'; - tooltipElement.style.left = (param.point.x + 15) + 'px'; - tooltipElement.style.top = (param.point.y + 15) + 'px'; - } else { - // 隐藏提示 - tooltipElement.style.display = 'none'; - } - } else { - // 隐藏提示 - tooltipElement.style.display = 'none'; - crosshairTooltip.style.display = 'none'; - } - }); - // 处理图表缩放、平移等事件,隐藏提示 - mainChart.timeScale().subscribeVisibleTimeRangeChange(() => { - tooltipElement.style.display = 'none'; - crosshairTooltip.style.display = 'none'; - }); - } - } - // 辅助函数:使用指定时区格式化时间戳 function formatTimeWithTimezone(timestamp, timezone) { try { @@ -6487,220 +6465,6 @@ return new Date(timestamp).toLocaleString(); } } - function displayTradePoints() { - // 优先使用小周期数据,如果不存在则使用主周期数据 - const tradePointsData = currentData.element_trade_points || currentData.trade_points; - console.log(`绘制${currentData.element_trade_points ? '元素周期' : '主周期'}买卖点数据,共${tradePointsData ? tradePointsData.length : 0}条`); - - // 调试信息 - 输出完整的买卖点数据 - if (tradePointsData && tradePointsData.length > 0) { - console.log("买卖点数据样例:", tradePointsData[0]); - - // 检查数据格式,如果time不是标准格式,进行格式化处理 - const checkDataFormat = () => { - for (let i = 0; i < tradePointsData.length; i++) { - if (tradePointsData[i].time) { - // 确保时间是标准格式 - try { - const timeValue = new Date(tradePointsData[i].time); - if (isNaN(timeValue.getTime())) { - console.error(`买卖点 #${i} 时间格式无效:`, tradePointsData[i].time); - } - } catch (e) { - console.error(`买卖点 #${i} 时间格式异常:`, e); - } - } else { - console.error(`买卖点 #${i} 缺少时间属性`); - } - } - }; - - // 执行格式检查 - checkDataFormat(); - - // 对买卖点按时间排序,用于后续优化显示 - const sortedPoints = [...tradePointsData].sort((a, b) => { - return new Date(a.time) - new Date(b.time); - }); - - // 记录已处理的时间点 - 按类型分开计数 - const processedTimes = {}; - - // 创建买卖点标记系列 - const buyMarkers = []; - const sellMarkers = []; - - // 计数器,追踪成功和失败的处理次数 - let successCount = 0; - let errorCount = 0; - - sortedPoints.forEach(function(point, index) { - try { - // 检查所有必要的属性是否存在且有效 - if (!point.time || !point.price || point.type === undefined) { - console.error(`买卖点 #${index} 数据不完整:`, point); - errorCount++; - return; - } - - const time = Math.floor(new Date(point.time).getTime() / 1000); - const price = parseFloat(point.price); - const type = parseInt(point.type); - - if (isNaN(time) || isNaN(price) || isNaN(type)) { - console.error(`买卖点 #${index} 数据格式错误:`, - { time: isNaN(time), price: isNaN(price), type: isNaN(type) }, point); - errorCount++; - return; - } - - // 获取买卖点样式 - const style = TRADE_POINT_STYLE[type] || { - color: '#999999', - shape: 'circle', - text: '?', - size: 1 - }; - - // 初始化该时间点的类型计数器 - if (!processedTimes[time]) { - processedTimes[time] = {}; - } - - // 优化:检查是否有相同时间点和相同类型的标记,如果有,进行类型内的偏移 - let stackIndex = 0; - if (processedTimes[time][type]) { - // 已经有相同时间和类型的标记,记录堆叠索引 - stackIndex = processedTimes[time][type]; - processedTimes[time][type]++; - } else { - // 第一次出现这个时间点的这个类型 - processedTimes[time][type] = 1; - } - - // 为不同类型的买卖点获取基础垂直偏移系数 - const baseOffset = TRADE_POINT_OFFSET[type] || 0; - - // 创建标记对象,包含额外的信息用于悬停提示 - const marker = { - time: time, - position: 'inBar', // 改为在K线内部显示,不影响数据 - color: style.color, - shape: style.shape, - text: style.text, - size: style.size, - // 记录堆叠索引 - stackIndex: stackIndex, - // 添加悬停提示的数据 - tooltip: `${point.desc || (type > 0 ? '买点' : '卖点')}
- 时间: ${formatTime(point.time)}
- 价格: ${price.toFixed(2)}`, - // 额外添加基础类型偏移 - baseOffset: baseOffset, - // 添加边框 - borderColor: 'white', - borderWidth: 1, - // 添加价格偏移系数 - pricePercentOffset: PRICE_PERCENT_OFFSET[type] || 0, - // 保存实际价格用于计算 - price: price, - // 保存类型 - type: type - }; - - // 区分买卖点 - if (type > 0) { - buyMarkers.push(marker); - } else { - sellMarkers.push(marker); - } - - successCount++; - } catch (e) { - console.error(`处理买卖点 #${index} 出错:`, e, point); - errorCount++; - } - }); - - console.log(`买卖点处理完成: 成功=${successCount}, 失败=${errorCount}, 买点=${buyMarkers.length}, 卖点=${sellMarkers.length}`); - - // 分别添加买卖点标记 - if (buyMarkers.length > 0) { - const buyMarkersSeries = mainChart.addLineSeries({ - lastValueVisible: false, - priceLineVisible: false, - lineVisible: false, - title: '买点' - }); - - // 设置临时的数据点 - buyMarkersSeries.setData([{ time: buyMarkers[0].time, value: 0 }]); - - try { - // 设置买点标记,并添加偏移 - buyMarkersSeries.setMarkers( - buyMarkers.map(marker => { - // 使用固定偏移而非百分比 - const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20; - - // 为同一时间点的多个买点额外增加堆叠偏移 - const stackOffset = marker.stackIndex > 0 ? - 10 * marker.stackIndex : 0; - - // 使用实际价格位置添加标记,不使用偏移 - return { - ...marker, - position: 'aboveBar', // 显示在K线上方 - // 使用原始价格 - price: marker.price - }; - }) - ); - console.log(`成功添加 ${buyMarkers.length} 个买点标记`); - } catch (e) { - console.error("设置买点标记时出错:", e); - } - } - - if (sellMarkers.length > 0) { - const sellMarkersSeries = mainChart.addLineSeries({ - lastValueVisible: false, - priceLineVisible: false, - lineVisible: false, - title: '卖点' - }); - - // 设置临时的数据点 - sellMarkersSeries.setData([{ time: sellMarkers[0].time, value: 0 }]); - - try { - // 设置卖点标记,并添加偏移 - sellMarkersSeries.setMarkers( - sellMarkers.map(marker => { - // 使用固定偏移而非百分比 - const fixedOffset = PRICE_FIXED_OFFSET[marker.type] || 20; - - // 为同一时间点的多个卖点额外增加堆叠偏移 - const stackOffset = marker.stackIndex > 0 ? - 10 * marker.stackIndex : 0; - - // 使用实际价格位置添加标记,不使用偏移 - return { - ...marker, - position: 'belowBar', // 显示在K线下方 - // 使用原始价格 - price: marker.price - }; - }) - ); - console.log(`成功添加 ${sellMarkers.length} 个卖点标记`); - } catch (e) { - console.error("设置卖点标记时出错:", e); - } - } - } - } - function updateTables(currentData) { // 检查数据有效性 if (!currentData) { @@ -7334,7 +7098,6 @@ const timeStr = nextRefreshTime.toLocaleTimeString(); $('#nextRefreshTime').text(`下次刷新: ${timeStr}`); } - // 倒计时定时器 let countdownTimer = null; @@ -7475,7 +7238,6 @@ console.log('重绘分形元素 - 完成'); } - // 只更新分形元素(笔、线段、中枢)的表格数据 function updateFractalTables() { if (!currentData) return; @@ -7682,7 +7444,6 @@ 'showMainBi': $('#showMainBi').is(':checked'), 'showMainSeg': $('#showMainSeg').is(':checked'), 'showMainZs': $('#showMainZs').is(':checked'), - 'showMainUncompletedZs': $('#showMainUncompletedZs').is(':checked'), 'showVolume': false, 'showMacd': $('#showMacd').is(':checked'), 'showKlcFxType': $('#showKlcFxType').is(':checked'), @@ -7908,7 +7669,6 @@ console.log('✅ EMA52系列已清理'); } - // 存储上次的EMA52数据,用于比较 let lastEMA52Data = null; @@ -8264,7 +8024,6 @@ return result; } - // 更新技术指标面板(统一面板) function updateIndicatorPanel() { const panel = $('#indicatorPanel'); @@ -8547,7 +8306,6 @@ try { updateIndicatorPanel(); } catch(e) {} try { if ($('#maConfigModal').is(':visible')) { hideMAConfig(); } } catch(e) {} } - // 获取当前K线数据的辅助函数 function getCurrentCandleData() { if (!currentData || !currentData.kline_data) { @@ -9063,7 +8821,6 @@ // 监听配置变化以更新预览 $(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview); $(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview); - // 点击弹窗外部关闭 $(document).on('click', '#bbConfigModal', function(e) { if (e.target === this) { @@ -9159,7 +8916,6 @@ // 清空全局UnitTF标记,避免旧数据残留影响主图合并 window.unittfMarkers = []; } - // 添加所有ChanMACD标记 function addAllChanMacdMarkers(segList, unittfList, histsetList, stateMarkers) { const macdMarkers = [];