diff --git a/ChanKLC.py b/ChanKLC.py index dc4c62d..1999dd3 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -4,7 +4,7 @@ from typing import Dict, Optional from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX, Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS, Chan_EMA_SEMANTIC, Chan_BSP_TYPE import ChanKLU import ChanCTime - +import Chan_FX_Box # 根据结合律合并K线后的K线 class ChanKLC(): def __init__(self, klu: ChanKLU, index, ddir=Chan_KLINE_DIR.UP): @@ -51,6 +51,8 @@ class ChanKLC(): self.ema104 = klu.ema104 self.ema156 = klu.ema156 self.ema208 = klu.ema208 + self.ema13 = klu.ema13 + self.ema7 = klu.ema7 self.trend = Chan_PRICE_TREND.UNKNOWN self.exception = klu.exception self.klc_dir = Chan_KLINE_DIR.UP if klu.close > klu.open else Chan_KLINE_DIR.DOWN @@ -67,6 +69,7 @@ class ChanKLC(): self.bb2633middle = klu.bb2633middle self.ema5 = klu.ema5 self.ma5 = klu.ma5 + self.fx_box = None # ==================== EMA 通用计算方法 ==================== @staticmethod @@ -320,6 +323,35 @@ class ChanKLC(): #print(self.end_time, ema_name, self.ema_status[ema_name]['semantic'], hist_div) #self.cal_bb_out() #print(self.pre.start_time, self.next.end_time, self.klc_fx_type) + if klc_fx_type == Chan_KLC_FX.TOP1 or klc_fx_type == Chan_KLC_FX.TOP2 or klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc_fx_type == Chan_KLC_FX.BOTTOM2: + self.cal_fx_box() + def cal_fx_box(self): + # 每次重算前先清空,避免旧box残留 + self.fx_box = None + start_time = None + end_time = None + high = 0 + low = 0 + display = False + if self.pre and self.next: + if self.fx == Chan_FX_TYPE.TOP: + start_time = self.pre.start_time + end_time = self.next.end_time + high = self.high + low = self.pre.low if self.pre.low < self.next.low else self.next.low + if self.next.close < self.pre.open: + display = True + elif self.fx == Chan_FX_TYPE.BOTTOM: + start_time = self.pre.start_time + end_time = self.next.end_time + high = self.pre.high if self.pre.high > self.next.high else self.next.high + low = self.low + if self.next.close > self.pre.open: + display = True + if high > 0 and display: + print(start_time, end_time, high, low) + # Chan_FX_BOX 这里导入的是模块,类名在模块内部为 Chan_FX_Box + self.fx_box = Chan_FX_Box.Chan_FX_Box(start_time, end_time, high, low) def add_klu(self, klu): self.klu_list.append(klu) def set_end_klu(self, klu): @@ -397,6 +429,8 @@ class ChanKLC(): self.ema104 += self.klu_list[index].ema104 self.ema156 += self.klu_list[index].ema156 self.ema208 += self.klu_list[index].ema208 + self.ema13 += self.klu_list[index].ema13 + self.ema7 += self.klu_list[index].ema7 self.bb2633upper += self.klu_list[index].bb2633upper self.bb2633lower += self.klu_list[index].bb2633lower self.bb2633middle += self.klu_list[index].bb2633middle @@ -415,6 +449,8 @@ class ChanKLC(): self.ema104 = self.ema104 / n self.ema156 = self.ema156 / n self.ema208 = self.ema208 / n + self.ema13 = self.ema13 / n + self.ema7 = self.ema7 / n self.ma5 = self.ma5 / n self.ema5 = self.ema5 / n self.bb2633upper = self.bb2633upper / n diff --git a/ChanKLU.py b/ChanKLU.py index be7e1b6..4be0cda 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -182,6 +182,8 @@ class ChanKLU: self.ema104 = float(item['ema104']) if 'ema104' in item and item['ema104'] else 0 self.ema156 = float(item['ema156']) if 'ema156' in item and item['ema156'] else 0 self.ema208 = float(item['ema208']) if 'ema208' in item and item['ema208'] else 0 + self.ema13 = float(item['ema13']) if 'ema13' in item and item['ema13'] else 0 + self.ema7 = float(item['ema7']) if 'ema7' in item and item['ema7'] else 0 self.rsi = float(item['rsi']) if 'rsi' in item and item['rsi'] else 0 self.volume_ratio = float(item['volume_ratio']) if 'volume_ratio' in item and item['volume_ratio'] else 0 self.bb52upper = float(item['bb52upper']) if 'bb52upper' in item and item['bb52upper'] else 0 diff --git a/ChanMACDHistSet.py b/ChanMACDHistSet.py index fa990bc..484fc94 100644 --- a/ChanMACDHistSet.py +++ b/ChanMACDHistSet.py @@ -19,6 +19,7 @@ class ChanMACDHistSet(): self.start_klu = start_klu self.peak_div_list = [] self.middle_area = 0 + self.total_macdhist = 0 def set_next(self, next_histset): self.next = next_histset def set_pre(self, pre_histset): @@ -102,6 +103,15 @@ class ChanMACDHistSet(): for peak_div in self.peak_div_list: peak_str += f"{peak_div.time}, " state_str += f"{peak_div.macd_state}, " + total_macdhist = 0 + first_klu = self.klu_list[0] + last_klu = self.klu_list[-1] + if (first_klu.macd > 0 and last_klu.macd > 0 and first_klu.macdhist > 0) or (first_klu.macd < 0 and last_klu.macd < 0 and first_klu.macdhist < 0): + for klu in self.klu_list: + self.total_macdhist += klu.macdhist + if abs(self.total_macdhist) < 150: + #print(self.end_time, "Total MACDHist: ", self.total_macdhist) + last_klu.separate_div = 99999 #if self.peak_klu and len(self.peak_div_list) > 0: #print("Continue Div: ",self.start_time, "Peak:", self.peak_klu.time, "Div: ", peak_str, state_str) \ No newline at end of file diff --git a/Chan_FX_Box.py b/Chan_FX_Box.py new file mode 100644 index 0000000..8a9d9ec --- /dev/null +++ b/Chan_FX_Box.py @@ -0,0 +1,7 @@ +class Chan_FX_Box(): + def __init__(self, start_time, end_time, high, low): + self.start_time = start_time + self.end_time = end_time + self.high = high + self.low = low + \ No newline at end of file diff --git a/TF_DF.py b/TF_DF.py index 9a052cf..a5f0e9b 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -122,6 +122,8 @@ class TF_DF(): df['ema156'] = ta.EMA(df, timeperiod=156) df['ema208'] = ta.EMA(df, timeperiod=208) df['ema26'] = ta.EMA(df, timeperiod=26) + df['ema13'] = ta.EMA(df, timeperiod=13) + df['ema7'] = ta.EMA(df, timeperiod=7) df['rsi'] = ta.RSI(df, timeperiod=14) df['volume_ratio'] = self.cal_volume_ratio(df) return df @@ -1001,7 +1003,7 @@ class TF_DF(): # 不满足结合律的分型 else: #klc.set_klc_fx_type(Chan_KLC_FX.TOP0) - print(klc.end_time, klc.klc_fx_type) + #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") @@ -1213,331 +1215,29 @@ class TF_DF(): klc.set_bi(bi_list[-1]) #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 5") #print(klc.start_time, klc.fx, "笔买点Buy 4") + self.get_above_zero_bsp(klc_list) return bi_list - def cal_bi_list1(self, klc_list): - bi_list = [] - last_top = None - last_bottom = None + def get_above_zero_bsp(self, klc_list): + buy_bsp_list = [] + sell_bsp_list = [] + above_zero = False + buy_bsp = None + sell_bsp = None for klc in klc_list: - fx = self.check_fx(klc) - if fx == Chan_FX_TYPE.TOP: - if last_bottom: - if self.check_top_fx(last_bottom, klc) == False: - fx = Chan_FX_TYPE.UNKNOWN - if fx == Chan_FX_TYPE.BOTTOM: - if last_top: - if self.check_bottom_fx(last_top, klc) == False: - #print(klc.end_time, last_top.end_time, "---") - fx = Chan_FX_TYPE.UNKNOWN - # Do nothing - if fx == Chan_FX_TYPE.UNKNOWN: - if len(bi_list) > 0: - bi_list[-1].add_klc(klc) - continue - if len(bi_list) > 0 and klc.end_klu: - last_bi = bi_list[-1] - #print(klc.start_time, last_bi.start_time, last_bi.end_time, last_bi.dir, last_bi.high, last_bi.low, last_bottom.end_time, "last bi") - if last_top and last_bi.dir == Chan_BI_DIR.DOWN: - print("fx=unknown, 1") - if last_bottom and klc.high > last_bi.high: - last_bi.set_end_klc(last_bottom, klc) - bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7) - #klc.bb_out = True - last_bi.set_next(bi) - bi.set_pre(last_bi) - for klc_index in range(last_bi.end_klc.index, len(klc_list)): - bi.add_klc(klc_list[klc_index]) - bi_list.append(bi) - last_top = klc - klc.set_bi(bi) - #print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure) - else: - print("fx=unknown, 2") - if last_bottom and last_bi.dir == Chan_BI_DIR.UP: - if last_top and klc.low < last_bi.low: - 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) - #klc.bb_out = True - last_bi.set_next(bi) - bi.set_pre(last_bi) - for klc_index in range(last_bi.end_klc.index, len(klc_list)): - bi.add_klc(klc_list[klc_index]) - bi_list.append(bi) - last_bottom = klc - klc.set_bi(bi) - #print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure) - else: - if fx == Chan_FX_TYPE.TOP: - if last_top: - if last_bottom: - #print(klc.start_time, last_bottom.start_time, last_top.start_time) - if last_bottom.index < last_top.index: - # Second top lower to be second sell point - if last_top.high > klc.high: - #klc.set_fx(Chan_FX_TYPE.TT) - #klc.set_state("20") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #klc.cal_invisible() - #klc.set_klc_fx_type(Chan_KLC_FX.TOP3) - #print(klc.start_time, klc.fx, "二类卖点Sell 1") - else: - # A new top found - #last_top.set_fx(Chan_FX_TYPE.UNKNOWN) - last_top = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 1") - klc.set_klc_fx_type(Chan_KLC_FX.TOP1) - self.check_fx_pattern(klc) - #print(klc.end_time, klc.fx, "一类卖点Sell 1") - #klc.set_fx(fx) - #klc.set_state("10") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - # 不满足结合律的分型 - if last_bottom.index + 4 > klc.index: - if last_top.high > klc.high: - #print(klc.start_time, klc.fx, "二类卖点Sell 1") - #klc.set_fx(Chan_FX_TYPE.PTOP) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # New TOP Found replace last top - else: - if last_top.index + 4 < klc.index and len(bi_list) > 1: - pre_last_bi = bi_list[-2] - last_bi = bi_list[-1] - if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.UP and False: - pre_last_bi.update_bi(klc) - bi_list.remove(last_bi) - pre_last_bi.set_next(None) - #last_top.set_fx(Chan_FX_TYPE.PTOP) - last_top = klc - last_bottom = pre_last_bi.start_klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 1") - klc.set_klc_fx_type(Chan_KLC_FX.TOP2) - #print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi") - #klc.set_state("10") - #print(klc.start_time, klc.fx, "笔卖点Sell 1") - ###klc.set_klc_fx_type(Chan_KLC_FX.TOP2) # when bi is down but the fx is top - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - klc.set_fx(Chan_FX_TYPE.PTOP) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "无效分型") - # 满足结合律 - else: - # New Temp TOP and last bottom confirmed ***** confirm last down bi(last bottom and last top) - last_bi = bi_list[-1] - if not last_bi.is_sure: - last_bi.set_end_klc(last_bottom, klc) - bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7) - #klc.bb_out = True - last_bi.set_next(bi) - bi.set_pre(last_bi) - bi.add_klc(klc) - bi_list.append(bi) - last_top = klc - #print(klc.end_time, klc.fx, bi_list[-1].dir, "Last Top Change 2") - klc.set_klc_fx_type(Chan_KLC_FX.TOP2) - self.check_fx_pattern(klc) - #klc.set_state('30') - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, last_bottom.start_time, "Normal TOP Found, Confirm down bi 4") - #print(klc.start_time, klc.fx, "笔卖点Sell 2") - # last bottom = None - else: - if last_top.high < klc.high: - last_bi = bi_list[-1] - last_bi.set_start_klc(klc, Chan_BI_DIR.DOWN) - #last_top.set_fx(Chan_FX_TYPE.UNKNOWN) - last_top = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 3") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "笔卖点Sell 3") - else: - klc.set_fx(Chan_FX_TYPE.TT) - #klc.set_state('20') - #print(klc.start_time, klc.fx, "二类卖点Sell 2") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - if last_bottom: - # 不满足结合律的分型 - if last_bottom.index + 4 > klc.index: - #klc.set_fx(Chan_FX_TYPE.PTOP) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "中枢卖点Sell 1") - else: - # First temp top and last bottom confirmed - last_top = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 4") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "一类卖点Sell 1") - # Last top = None, last bottom = None, create first down bi - else: - # First temp top - last_top = klc - bi = ChanBI(klc, len(bi_list), Chan_BI_DIR.DOWN) - #klc.set_klc_fx_type(Chan_KLC_FX.TOP6) - #klc.bb_out = True - bi_list.append(bi) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 5") - #print(klc.start_time, 'Create first top') - #print(klc.start_time, klc.fx, "笔卖点Sell 1") - #klc.fx = Bottom ======================== - else: - if last_bottom: - if last_top: - # Bottom after top and find a new bottom - if last_top.index < last_bottom.index: - # Second bottom uppper to be second buy point and confirm last bi - if last_bottom.low < klc.low: - #klc.set_fx(Chan_FX_TYPE.BB) - #klc.set_state("-20") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #klc.cal_invisible() - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) - #print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1") - #print(klc.start_time, klc.fx, "二类买点Buy 1") - else: - # A new bottom found - #last_bottom.set_fx(Chan_FX_TYPE.UNKNOWN) - last_bottom = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 1") - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM1) - self.check_fx_pattern(klc) - #print(klc.start_time, klc.fx, "一类买点Buy 1") - #klc.set_state("-10") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - # 不满足结合律的分型 - if last_top.index + 4 > klc.index: - if last_bottom.low < klc.low: - #klc.set_fx(Chan_FX_TYPE.PBOTTOM) - #klc.set_fx(Chan_FX_TYPE.BB) - #klc.set_state("-100") - #print(klc.start_time, klc.fx, "中枢买点Buy 1") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # Found new bottom - else: - if last_bottom.index + 4 < klc.index and len(bi_list) > 1: - pre_last_bi = bi_list[-2] - last_bi = bi_list[-1] - if pre_last_bi.is_sure and not last_bi.is_sure and pre_last_bi.dir == Chan_BI_DIR.DOWN and False: - pre_last_bi.update_bi(klc) - bi_list.remove(last_bi) - pre_last_bi.set_next(None) - #last_bottom.set_fx(Chan_FX_TYPE.PBOTTOM) - last_bottom = klc - last_top = pre_last_bi.start_klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Bottom Change 2") - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) - #print(klc.start_time, last_bi.start_klc.start_time, "New BOTTOM Found reset last bi") - #klc.set_state("-10") - #print(klc.start_time, klc.fx, "笔买点Buy 1") - ###klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) # when bi is up but the fx is bottom - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - else: - #klc.set_fx(Chan_FX_TYPE.UNKNOWN) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "无效分型") - # 满足结合律的分型 - else: - # New Temp Bottom and last top confirmed ***** confirm last up bi(last bottom and last top) - last_bi = bi_list[-1] - 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) - #klc.bb_out = True - last_bi.set_next(bi) - bi.set_pre(last_bi) - bi.add_klc(klc) - bi_list.append(bi) - last_bottom = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 2") - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) - self.check_fx_pattern(klc) - #klc.set_state('-30') - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "笔买点Buy 2") - #print(klc.start_time, last_top.start_time, "Normal Bottom Found, Confirm up bi 6") - # last_top = None - else: - if last_bottom.low > klc.low: - last_bi = bi_list[-1] - last_bi.set_start_klc(klc, Chan_BI_DIR.UP) - #last_bottom.set_fx(Chan_FX_TYPE.UNKNOWN) - last_bottom = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 3") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "笔买点Buy 3") - else: - klc.set_fx(Chan_FX_TYPE.BB) - #klc.set_state('-20') - #print(klc.start_time, klc.fx, "二类买点Buy 2") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - # last_bottom = None - else: - if last_top: - # 不满足结合律的分型 - if last_top.index + 4 > klc.index: - #klc.set_fx(Chan_FX_TYPE.PBOTTOM) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "中枢买点Buy 1") - else: - # First temp bottom and last top confirmed - last_bottom = klc - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 4") - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "一类买点Buy 1") - # Last top = None, last bottom = None, create first up bi - else: - # First temp bottom and no top yet - last_bottom = klc - bi = ChanBI(klc, len(bi_list), Chan_BI_DIR.UP) - #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM7) - #klc.bb_out = True - bi_list.append(bi) - bi_list[-1].add_klc(klc) - klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Bottom Change 5") - #print(klc.start_time, klc.fx, "笔买点Buy 4") - #if klc.fx != Chan_FX_TYPE.UNKNOWN: - #print(klc.start_time, klc.fx, klc.index) - """ - for klc in klc_list: - if klc.fx == Chan_FX_TYPE.TOP: - klc.state = "10" - #print(klc.time, klc.state) - if klc.fx == Chan_FX_TYPE.BOTTOM: - klc.state = "-10" - #print(klc.time, klc.state) - """ - #for index in range(0, 10): - #print(bi_list[index].start_time, bi_list[index].start_klc.start_time, bi_list[index].dir) - return bi_list + if klc.pre and klc.pre.signal < 0 and klc.signal > 0: + above_zero = True + if klc.pre and klc.pre.signal > 0 and klc.signal < 0: + above_zero = False + if above_zero and klc.klc_fx_type == Chan_KLC_FX.BOTTOM2 and klc.macd > 0: + buy_bsp = klc + buy_bsp_list.append(klc) + #print(klc.end_time, "MACD 0轴上穿,回调笔底分型做多") + if buy_bsp and klc.pre and klc.pre.macdhist > 0 and klc.macdhist < 0: + sell_bsp = klc + sell_bsp_list.append(klc) + buy_bsp = None + #print(klc.end_time, "Sell BSP Found") + return buy_bsp_list def check_top_fx(self, last_bottom, klc): if (last_bottom.high > klc.pre.low or last_bottom.high > klc.next.low) and (klc.index - last_bottom.index < 10): return False diff --git a/web/app.py b/web/app.py index 090ba7d..887680d 100644 --- a/web/app.py +++ b/web/app.py @@ -439,6 +439,11 @@ def add_indicators(df): df['ema24'] = (ta.EMA(df, timeperiod=24)).fillna(0) df['ema52'] = (ta.EMA(df, timeperiod=52)).fillna(0) df['ema26'] = (ta.EMA(df, timeperiod=26)).fillna(0) + df['ema13'] = (ta.EMA(df, timeperiod=13)).fillna(0) + df['ema7'] = (ta.EMA(df, timeperiod=7)).fillna(0) + df['ema104'] = (ta.EMA(df, timeperiod=104)).fillna(0) + df['ema156'] = (ta.EMA(df, timeperiod=156)).fillna(0) + df['ema208'] = (ta.EMA(df, timeperiod=208)).fillna(0) # 常用SMA 24/52 try: df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0) @@ -624,6 +629,16 @@ def analyze_chan(df, symbol=None, timeframe=None): # 如果分型强度小于1,设为0 if fx_strength < 1: fx_strength = 0 + + # KLC 分型框(起止时间+高低价): + # 仅使用 cal_fx_box 通过 display 条件后生成的 klc.fx_box。 + # 若无 fx_box,则前端不应绘制分型框。 + fx_box = getattr(klc, 'fx_box', None) + box_start_time = getattr(fx_box, 'start_time', None) if fx_box else None + box_end_time = getattr(fx_box, 'end_time', None) if fx_box else None + box_high = getattr(fx_box, 'high', None) if fx_box else None + box_low = getattr(fx_box, 'low', None) if fx_box else None + if klc.bb_out: klc_fx_info.append({ 'time': klc.end_time, @@ -632,10 +647,22 @@ def analyze_chan(df, symbol=None, timeframe=None): 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, 'fx_strength': fx_strength, # 分型强度分数 (0-100) 'fx_strength_level': fx_strength_level, # 分型强度等级 (极强/强/中等/弱/极弱) - 'is_strong_fx': is_strong_fx # 是否为强分型 + 'is_strong_fx': is_strong_fx, # 是否为强分型 + + # 虚线分型框信息(给前端画框用) + 'start_time': box_start_time, + 'end_time': box_end_time, + 'high': float(box_high) if box_high is not None else None, + 'low': float(box_low) if box_low is not None else None, }) except Exception as e: # 如果出错,仍然添加基本信息,但分型强度为0 + fx_box = getattr(klc, 'fx_box', None) + box_start_time = getattr(fx_box, 'start_time', None) if fx_box else None + box_end_time = getattr(fx_box, 'end_time', None) if fx_box else None + box_high = getattr(fx_box, 'high', None) if fx_box else None + box_low = getattr(fx_box, 'low', None) if fx_box else None + klc_fx_info.append({ 'time': klc.end_time, 'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high, @@ -643,7 +670,13 @@ def analyze_chan(df, symbol=None, timeframe=None): 'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM, 'fx_strength': 0, 'fx_strength_level': "", - 'is_strong_fx': False + 'is_strong_fx': False, + + # 虚线分型框信息(给前端画框用) + 'start_time': box_start_time, + 'end_time': box_end_time, + 'high': float(box_high) if box_high is not None else None, + 'low': float(box_low) if box_low is not None else None, }) @@ -1391,12 +1424,17 @@ def analyze(): # 添加K线分型信息 'klc_fx_info': [{ 'time': format_time_safely(point['time'], client_tz), + 'start_time': format_time_safely(point['start_time'], client_tz), + 'end_time': format_time_safely(point['end_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']) # 是否为强分型 + 'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型 + # 分型框(虚线矩形)用到的高低价 + 'high': float(point['high']) if point.get('high') is not None else None, + 'low': float(point['low']) if point.get('low') is not None else None } for point in analysis_result['klc_fx_info']], # 添加ChanMACD分析数据 'chan_macd': serialize_chan_macd_data(analysis_result.get('chan_macd', {}), client_tz), @@ -1587,12 +1625,17 @@ def analyze(): # 添加小周期分型信息 result['element_klc_fx_info'] = [{ 'time': format_time_safely(point['time'], client_tz), + 'start_time': format_time_safely(point['start_time'], client_tz), + 'end_time': format_time_safely(point['end_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']) # 是否为强分型 + 'is_strong_fx': bool(point['is_strong_fx']), # 是否为强分型 + # 分型框(虚线矩形)用到的高低价 + 'high': float(point['high']) if point.get('high') is not None else None, + 'low': float(point['low']) if point.get('low') is not None else None } for point in element_analysis['klc_fx_info']] # 添加次周期ChanMACD分析数据 @@ -1640,6 +1683,20 @@ def analyze(): '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] + # 次次周期 KLC 列表 + result['sub_sub_klc_list'] = [{ + 'date': klc.end_time if isinstance(klc.end_time, str) else klc.end_time.astimezone(client_tz).isoformat(), + 'open': float(klc.open), + 'high': float(klc.high), + 'low': float(klc.low), + 'close': float(klc.close), + 'volume': float(klc.volume) if hasattr(klc, 'volume') else 0, + 'direction': str(klc.dir).replace('Chan_KLINE_DIR.', ''), + 'fx_type': str(klc.fx).replace('Chan_FX_TYPE.', ''), + 'klc_fx_type': str(klc.klc_fx_type).replace('Chan_KLC_FX.', ''), + 'trend': str(klc.trend).replace('Chan_PRICE_TREND.', '') + } for klc in sub_sub_analysis.get('klc_list', []) if hasattr(klc, 'end_time') and klc.end_time] + 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, @@ -1677,12 +1734,17 @@ def analyze(): } 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), + 'start_time': format_time_safely(point['start_time'], client_tz), + 'end_time': format_time_safely(point['end_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']) + 'is_strong_fx': bool(point['is_strong_fx']), + # 分型框(虚线矩形)用到的高低价 + 'high': float(point['high']) if point.get('high') is not None else None, + 'low': float(point['low']) if point.get('low') is not None else None } for point in sub_sub_analysis['klc_fx_info']] result['sub_sub_bsp_list'] = [{ 'time': format_time_safely(bsp.end_time, client_tz), diff --git a/web/templates/index.html b/web/templates/index.html index 7cdbd12..9940e5c 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -5817,6 +5817,64 @@ allMainFxMarkers.push(markerConfig); + // 画虚线分型框(根据 start/end + high/low) + if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { + const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); + const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); + const high = parseFloat(fx.high); + const low = parseFloat(fx.low); + + if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { + const boxHigh = Math.max(high, low); + const boxLow = Math.min(high, low); + const boxColor = strengthColor; + + const topSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + + const bottomSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + + const leftSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + // 左边竖线:同一 time 上下两个点(和你已有ZS绘制写法保持一致) + leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + + const rightSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, // 虚线 + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + + if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = []; + tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + } + } + // 创建分型标记对象,包含tooltip信息 const fxMarker = { time: timestamp, @@ -5966,6 +6024,63 @@ allElementFxMarkers.push(markerConfig); + // 画虚线分型框(小周期) + if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { + const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); + const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); + const high = parseFloat(fx.high); + const low = parseFloat(fx.low); + + if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { + const boxHigh = Math.max(high, low); + const boxLow = Math.min(high, low); + const boxColor = strengthColor; + + const topSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + + const bottomSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + + const leftSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + + const rightSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + + if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = []; + tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + } + } + // 创建小周期分型标记对象,包含tooltip信息 const elementFxMarker = { time: timestamp, @@ -6061,6 +6176,63 @@ size: (fx.is_strong_fx ? 0.6 : 0.5) }; allElementFxMarkers.push(markerConfig); + + // 画虚线分型框(次次周期) + if (fx.start_time && fx.end_time && fx.high !== null && fx.high !== undefined && fx.low !== null && fx.low !== undefined) { + const startTs = Math.floor(new Date(fx.start_time).getTime() / 1000); + const endTs = Math.floor(new Date(fx.end_time).getTime() / 1000); + const high = parseFloat(fx.high); + const low = parseFloat(fx.low); + + if (!isNaN(startTs) && !isNaN(endTs) && !isNaN(high) && !isNaN(low)) { + const boxHigh = Math.max(high, low); + const boxLow = Math.min(high, low); + const boxColor = strengthColor; + + const topSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]); + + const bottomSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]); + + const leftSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]); + + const rightSeries = mainChart.addLineSeries({ + color: boxColor, + lineWidth: 1, + lineStyle: 2, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: false, + }); + rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]); + + if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = []; + tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries); + } + } } catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); } }); } @@ -6200,7 +6372,11 @@ else if (klineType === 'baseline') targetSeries = tvWidget.series.baselineSeries; else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries; if (targetSeries) { - targetSeries.setMarkers(combinedMarkers); + try { + targetSeries.setMarkers(combinedMarkers); + } catch (e) { + console.warn('设置主系列标记失败(可能series已释放):', e); + } } else { console.log('未找到主数据系列,无法设置标记'); } @@ -6324,7 +6500,11 @@ else if (klineType2 === 'baseline') targetSeries2 = tvWidget.series.baselineSeries; else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries; if (targetSeries2) { - targetSeries2.setMarkers(onlyMainAndU); + try { + targetSeries2.setMarkers(onlyMainAndU); + } catch (e) { + console.warn('设置主系列标记失败(可能series已释放):', e); + } } else { console.log('未找到主数据系列,无法设置标记'); } @@ -6342,7 +6522,11 @@ else if (klineType3 === 'baseline') targetSeries3 = tvWidget.series.baselineSeries; else if (klineType3 === 'klc') targetSeries3 = tvWidget.series.klcSeries; if (targetSeries3) { - targetSeries3.setMarkers([]); + try { + targetSeries3.setMarkers([]); + } catch (e) { + console.warn('清空主系列标记失败(可能series已释放):', e); + } } } } @@ -6552,15 +6736,33 @@ // 检查是否显示原始K线 const showOriginalKline = $('#showOriginalKline').is(':checked'); - // 检查是否使用小周期数据 - const useElementPeriod = $('#elementPeriodKline').is(':checked') && + // 检查是否使用次次周期 / 小周期数据 + const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') && + currentData.sub_sub_timeframe && + currentData.sub_sub_kline_data && + Array.isArray(currentData.sub_sub_kline_data); + const useElementPeriod = !useSubSubPeriod && + $('#elementPeriodKline').is(':checked') && currentData.element_timeframe && currentData.element_kline_data && Array.isArray(currentData.element_kline_data); // 转换K线数据 let candles = []; - if (useElementPeriod) { + if (useSubSubPeriod) { + console.log('使用次次周期K线数据'); + candles = currentData.sub_sub_kline_data.map((kline) => { + const date = new Date(kline.date); + const timestamp = date.getTime() / 1000; + return { + time: timestamp, + open: parseFloat(kline.open), + high: parseFloat(kline.high), + low: parseFloat(kline.low), + close: parseFloat(kline.close), + }; + }); + } else if (useElementPeriod) { console.log('使用小周期K线数据'); candles = currentData.element_kline_data.map((kline) => { const date = new Date(kline.date); @@ -6622,7 +6824,16 @@ // 更新成交量数据 let volumes = []; - if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) { + if (useSubSubPeriod && currentData.sub_sub_kline_data && Array.isArray(currentData.sub_sub_kline_data)) { + volumes = currentData.sub_sub_kline_data.map(kline => { + const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); + return { + time: timestamp, + value: parseFloat(kline.volume), + color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(40, 167, 69, 0.5)' : 'rgba(220, 53, 69, 0.5)', + }; + }); + } else if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) { volumes = currentData.element_kline_data.map(kline => { const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); return { @@ -6649,9 +6860,9 @@ // 更新ATR数据 if (tvWidget.series.atrLineSeries) { const atrData = []; - const atrDataSource = useElementPeriod ? - (currentData.element_atr || currentData.atr) : - currentData.atr; + const atrDataSource = useSubSubPeriod ? + (currentData.sub_sub_atr || currentData.atr) : + (useElementPeriod ? (currentData.element_atr || currentData.atr) : currentData.atr); if (atrDataSource && Array.isArray(atrDataSource)) { const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? currentData.element_kline_data : currentData.kline_data); @@ -9187,44 +9398,39 @@ try { updateIndicatorPanel(); } catch(e) {} try { if ($('#maConfigModal').is(':visible')) { hideMAConfig(); } } catch(e) {} } - // 获取当前K线数据的辅助函数 + // 获取当前K线数据的辅助函数(与基础显示的主/小/次次周期保持一致) function getCurrentCandleData() { if (!currentData || !currentData.kline_data) { return []; } - // 检查是否使用小周期数据 - const useElementPeriod = $('#elementPeriodKline').is(':checked') && + const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') && + currentData.sub_sub_kline_data && + Array.isArray(currentData.sub_sub_kline_data); + const useElementPeriod = !useSubSubPeriod && + $('#elementPeriodKline').is(':checked') && currentData.element_kline_data && Array.isArray(currentData.element_kline_data); - let candles = []; - if (useElementPeriod) { - candles = currentData.element_kline_data.map((kline) => { - const date = new Date(kline.date); - const timestamp = date.getTime() / 1000; - return { - time: timestamp, - open: parseFloat(kline.open), - high: parseFloat(kline.high), - low: parseFloat(kline.low), - close: parseFloat(kline.close), - }; - }); - } else { - candles = currentData.kline_data.map((kline) => { - const date = new Date(kline.date); - const timestamp = date.getTime() / 1000; - return { - time: timestamp, - open: parseFloat(kline.open), - high: parseFloat(kline.high), - low: parseFloat(kline.low), - close: parseFloat(kline.close), - }; - }); + let source = currentData.kline_data; + if (useSubSubPeriod) { + source = currentData.sub_sub_kline_data; + } else if (useElementPeriod) { + source = currentData.element_kline_data; } + const candles = source.map((kline) => { + const date = new Date(kline.date); + const timestamp = date.getTime() / 1000; + return { + time: timestamp, + open: parseFloat(kline.open), + high: parseFloat(kline.high), + low: parseFloat(kline.low), + close: parseFloat(kline.close), + }; + }); + return candles; } // 从蜡烛数据生成 Heikin-Ashi(平均K) @@ -9253,14 +9459,20 @@ function buildKLCFromAnalysis(data) { if (!data) return []; - // 检查是否使用小周期数据 - const useElementPeriod = $('#elementPeriodKline').is(':checked') && - data.element_klc_list && + // 根据基础显示的K线周期选择:次次周期 / 小周期 / 主周期 + const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') && + data.sub_sub_klc_list && + Array.isArray(data.sub_sub_klc_list); + const useElementPeriod = !useSubSubPeriod && + $('#elementPeriodKline').is(':checked') && + data.element_klc_list && Array.isArray(data.element_klc_list); - const klcList = useElementPeriod ? data.element_klc_list : data.klc_list; + const klcList = useSubSubPeriod + ? data.sub_sub_klc_list + : (useElementPeriod ? data.element_klc_list : data.klc_list); - if (!klcList) return []; + if (!klcList || !Array.isArray(klcList)) return []; const klcCandles = []; @@ -9268,11 +9480,9 @@ klcList.forEach(klc => { if (!klc || !klc.date) return; - // 使用KLC的date字段,转换为时间戳格式 const date = new Date(klc.date); const timestamp = date.getTime() / 1000; - // 创建KLC蜡烛数据 const candle = { time: timestamp, open: klc.open || 0,