diff --git a/ChanBI.py b/ChanBI.py index 99c0fc7..f5ef16d 100644 --- a/ChanBI.py +++ b/ChanBI.py @@ -20,8 +20,19 @@ class ChanBI(): self.macd_hist = 0 self.macd_div = 0 self.seg = None + self.height = 0 + self.width = 0 + self.slop = 0 + self.fib_list = [] + self.seg_index = 0 + self.bi_zs = None + self.seg_zs = None + def set_bi_zs(self, bi_zs): + for klc in self.klc_list: + klc.set_bi_zs(bi_zs) def set_seg(self, seg): self.seg = seg + self.seg_index = len(seg.bi_list)-1 def set_macdhist(self, macd_hist): self.macd_hist = macd_hist def set_macd_div(self, macd_div): @@ -66,7 +77,17 @@ class ChanBI(): self.end_klc = klc self.set_is_sure(True, sure_klc.end_time) self.end_time = klc.end_time + self.cal_properties() #print(self.start_time, klc.fx, "This bi is ended", len(self.klc_list), klc.index - self.start_klc.index) + def cal_properties(self): + if self.is_sure: + self.height = float(format(self.high - self.low, ".2f")) + self.width = self.end_klc.index - self.start_klc.index + self.slop = float(format(self.height / self.width, ".2f")) + fib_list = [0.0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0] + for fib in fib_list: + self.fib_list.append(float(format(self.height * fib + self.low, ".2f"))) + #print(self.end_time, self.height, self.width, self.slop, self.fib_list) def set_is_sure(self, is_sure, time): self.is_sure = is_sure self.sure_time = time diff --git a/ChanBIZS.py b/ChanBIZS.py index cffdf87..17c6b85 100644 --- a/ChanBIZS.py +++ b/ChanBIZS.py @@ -21,11 +21,13 @@ class ChanBIZS(): self.sure_time = None self.end_klc = None self.zs_type = Chan_ZS_TYPE.NORMAL - def set_end_bi(self, end_bi, sure_bi): + start_bi.set_bi_zs(self) + def set_end_bi(self, end_bi, sure_time): self.end_bi = end_bi self.set_end_time(end_bi.end_klc.end_time) self.is_sure = True - self.sure_time = sure_bi.sure_time + self.sure_time = sure_time + end_bi.set_bi_zs(self) #print(self.start_time, self.is_sure, len(self.bi_list), self.dir, self.zs_type) def set_end_time(self, end_time): self.end_time = end_time @@ -40,8 +42,12 @@ class ChanBIZS(): def add_bi(self, bi: ChanBI): if bi: self.bi_list.append(bi) + bi.set_bi_zs(self) self.classify_zs() - + def set_pre(self, pre): + self.pre = pre + def set_next(self, next): + self.next = next def classify_zs(self): """ 根据中枢内笔的高低点变化趋势,对中枢进行分类 diff --git a/ChanEnum.py b/ChanEnum.py index f940f26..9164374 100644 --- a/ChanEnum.py +++ b/ChanEnum.py @@ -167,6 +167,10 @@ class Chan_FX_TYPE(Enum): BB = auto() PTOP = auto() PBOTTOM = auto() +class Chan_FX(Enum): + CONTINUATION = auto() + REVERSAL = auto() + UNKNOWN = auto() class Chan_PRICE_TREND(Enum): UP = auto() DOWN = auto() @@ -357,28 +361,8 @@ class Chan_DATA_FIELD: class Chan_KLC_STATE: """笔当下状态(缠论笔定理)。任意时刻必属其一。""" - FX = auto() # 分型构造中(未确认顶/底) - BI = auto() # 笔延伸中(分型已确认,笔在延伸) - UP = auto() # 顶分型构造中 (1,0):向上笔末端 - DOWN = auto() # 底分型构造中 (-1,0):向下笔末端 - - -# 笔定理四状态:(Chan_BI_DIR, Chan_KLC_STATE)。笔方向用 Chan_BI_DIR,阶段用 Chan_KLC_STATE。 -# (UP, BI) 向上笔延伸;(DOWN, BI) 向下笔延伸;(UP, UP) 向上笔顶分型构造;(DOWN, DOWN) 向下笔底分型构造 -def bi_theorem_state(direction: Chan_BI_DIR, phase: Literal[0, 1]) -> tuple[Chan_BI_DIR, int]: - """(direction, phase) -> (Chan_BI_DIR, Chan_KLC_STATE)。phase 0=分型构造中,1=笔延伸中。""" - if phase == 1: - return (direction, Chan_KLC_STATE.BI) - return (direction, Chan_KLC_STATE.UP if direction == Chan_BI_DIR.UP else Chan_KLC_STATE.DOWN) - - -# 笔定理状态转移:当前 (Chan_BI_DIR, Chan_KLC_STATE) 允许的下一状态列表 -# (UP,BI) 只能 -> (UP,UP);(DOWN,BI) 只能 -> (DOWN,DOWN);(UP,UP) 可 -> (UP,BI)|(DOWN,BI);(DOWN,DOWN) 可 -> (DOWN,BI)|(UP,BI) -Chan_BI_STATE_TRANSITIONS: dict[tuple[Chan_BI_DIR, int], list[tuple[Chan_BI_DIR, int]]] = { - (Chan_BI_DIR.UP, Chan_KLC_STATE.BI): [(Chan_BI_DIR.UP, Chan_KLC_STATE.UP)], - (Chan_BI_DIR.DOWN, Chan_KLC_STATE.BI): [(Chan_BI_DIR.DOWN, Chan_KLC_STATE.DOWN)], - (Chan_BI_DIR.UP, Chan_KLC_STATE.UP): [(Chan_BI_DIR.UP, Chan_KLC_STATE.BI), (Chan_BI_DIR.DOWN, Chan_KLC_STATE.BI)], - (Chan_BI_DIR.DOWN, Chan_KLC_STATE.DOWN): [(Chan_BI_DIR.DOWN, Chan_KLC_STATE.BI), (Chan_BI_DIR.UP, Chan_KLC_STATE.BI)], -} - -Chan_TRADE_INFO_LST = [Chan_DATA_FIELD.FIELD_VOLUME, Chan_DATA_FIELD.FIELD_TURNOVER, Chan_DATA_FIELD.FIELD_TURNRATE] + S10 = "(1, 0)" # 顶分型构造中 (1,0) + S_10 = "(-1, 0)" # 底分型构造中 (-1,0) + S11 = "(1,1)" # 向上笔延续中 + S_11 = "(-1,1)" # 向下笔延续中 + UNKNOWN = "Unknown" # 初始状态 diff --git a/ChanKLC.py b/ChanKLC.py index 91680d1..a140f1d 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -1,7 +1,9 @@ import copy 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 +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_KLC_FX +from ChanEnum import Chan_K_DIR, Chan_MACD_STATE, Chan_PRICE_TREND, Chan_EMA_POS +from ChanEnum import Chan_EMA_SEMANTIC, Chan_BSP_TYPE, Chan_KLC_STATE, Chan_FX import ChanKLU import ChanCTime import Chan_FX_Box @@ -22,6 +24,7 @@ class ChanKLC(): self.start_klu = klu self.end_klu = None self.state = "00" + self.klc_state = Chan_KLC_STATE.UNKNOWN self.open = klu.open self.close = klu.close self.volume = klu.volume @@ -40,8 +43,8 @@ class ChanKLC(): self.candle_dir = klu.candle_dir self.range = klu.range self.bb_out = True - self.macd = 0 - self.signal = 0 + self.macd = klu.macd + self.signal = klu.signal self.state = Chan_MACD_STATE.UNKNOWN self.continue_div = False self.separate_div = False @@ -72,6 +75,14 @@ class ChanKLC(): self.fx_box = None self.in_fx = False self.fx_confirmed = False + self.ema52_dis = klu.high - klu.ema52 if klu.close > klu.ema52 else klu.ema52 - klu.low + self.ema26_dis = klu.high - klu.ema26 if klu.close > klu.ema26 else klu.ema26 - klu.low + self.macd_signal_dis = abs(klu.macd - klu.signal) + self.ema52_ema26_dis = abs(klu.ema52 - klu.ema26) + self.fx_type = Chan_FX.UNKNOWN + self.bi_zs = None + self.seg_zs = None + self.last_bi_zs = None # ==================== EMA 通用计算方法 ==================== @staticmethod @@ -314,6 +325,9 @@ class ChanKLC(): price_diff = getattr(self, 'price_diff', None) out += str(start) + " " + str(end) + " " + str(self.close) + " " + str(self.ema24) + " " + str(self.ema52) + " " + str(self.trend) + " " + str(self.close - self.ema52) return out + def set_bi_zs(self, bi_zs): + if bi_zs: + self.bi_zs = bi_zs def set_klc_fx_type(self, klc_fx_type): #print(self.start_time, klc_fx_type, self.get_feature_data()['klu_macd'], self.get_feature_data()['klu_macdhist'], self.get_feature_data()['klu_rsi']) self.klc_fx_type = klc_fx_type @@ -327,6 +341,22 @@ class ChanKLC(): #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() + self.cal_fx_type() + def cal_fx_type(self): + if self.fx == Chan_FX_TYPE.TOP and self.next: + if self.ema52_dis > self.ema26_dis: + if self.pre.macd < self.macd and self.macd < self.next.macd: + self.fx_type = Chan_FX.CONTINUATION + else: + self.fx_type = Chan_FX.REVERSAL + elif self.fx == Chan_FX_TYPE.BOTTOM and self.next: + if self.ema52_dis < self.ema26_dis: + if self.pre.macd > self.macd and self.macd > self.next.macd: + self.fx_type = Chan_FX.CONTINUATION + else: + self.fx_type = Chan_FX.REVERSAL + #if self.fx_type != Chan_FX.UNKNOWN and self.fx_type != Chan_FX.CONTINUATION: + #print(self.end_time, self.fx_type) def cal_fx_box(self): # 每次重算前先清空,避免旧box残留 self.fx_box = None @@ -357,7 +387,7 @@ class ChanKLC(): self.fx_confirmed = True self.fx_box = Chan_FX_Box.Chan_FX_Box(start_time, end_time, high, low) def check_fx_confirmed(self, last_top, last_bottom): - if last_top and last_bottom: + if last_top and last_bottom and False: if last_top.index > last_bottom.index: if self.in_fx == False and last_top.fx_confirmed == False: pre = last_top.pre @@ -369,7 +399,7 @@ class ChanKLC(): else: high = last_top.high low = self.low - last_top.fx_box = Chan_FX_Box.Chan_FX_Box(last_top.pre.end_time, self.end_time, high, low) + last_top.fx_box = Chan_FX_Box.Chan_FX_Box(last_top.pre.start_time, self.end_time, high, low) #print(self.end_time, "fx_confirmed new box top") elif self.in_fx == False and last_bottom.fx_confirmed == False: pre = last_bottom.pre @@ -381,15 +411,30 @@ class ChanKLC(): else: high = self.high low = last_bottom.low - last_bottom.fx_box = Chan_FX_Box.Chan_FX_Box(last_bottom.pre.end_time, self.end_time, high, low) + last_bottom.fx_box = Chan_FX_Box.Chan_FX_Box(last_bottom.pre.start_time, self.end_time, high, low) #print(self.end_time, "fx_confirmed new box bottom") def add_klu(self, klu): self.klu_list.append(klu) + def check_klc_state(self, last_fx_klc): + if last_fx_klc and last_fx_klc.fx == Chan_FX_TYPE.TOP: + if self.high > last_fx_klc.high: + self.klc_state = Chan_KLC_STATE.S11 + else: + self.klc_state = Chan_KLC_STATE.S_11 + elif last_fx_klc and last_fx_klc.fx == Chan_FX_TYPE.BOTTOM: + if self.low < last_fx_klc.low: + self.klc_state = Chan_KLC_STATE.S_11 + else: + self.klc_state = Chan_KLC_STATE.S11 + if self.pre and self.pre.fx == Chan_FX_TYPE.TOP: + self.klc_state = Chan_KLC_STATE.S10 + elif self.pre and self.pre.fx == Chan_FX_TYPE.BOTTOM: + self.klc_state = Chan_KLC_STATE.S_10 + #print(self.end_time, self.klc_state) def set_end_klu(self, klu): self.end_klu = klu self.end_time = klu.time self.close = klu.close - for klu in self.klu_list: if klu.exception: self.exception = True diff --git a/ChanKLU.py b/ChanKLU.py index 4be0cda..eeb32b3 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -233,14 +233,18 @@ class ChanKLU: self.near0_return = 0 elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52: self.near0_return = 0 + # 向上穿越EMA52 7 if self.close > self.ema52 and self.open < self.ema52: - self.near0_return = 7 + self.near0_return = 0 + # 向下穿越EMA52 8 elif self.close < self.ema52 and self.open > self.ema52: - self.near0_return = 8 + self.near0_return = 0 if self.pre.near0_return == 7: + # 向上穿越后的一根价格再EMA52上方 9 if self.low > self.ema52 and self.close > self.open: self.near0_return = 9 if self.pre.near0_return == 8: + # 向下穿越后的一根价格再EMA52下方 10 if self.high < self.ema52 and self.close < self.open: self.near0_return = 10 # CROSS0 仅以 Signal 穿越零轴判定 diff --git a/ChanLun.py b/ChanLun.py index 1e12ba9..2eeee78 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -141,8 +141,8 @@ class ChanLun(): return self.tf_df.cal_kl_data(dataframe) def cal_volume_ratio(self, dataframe, window=10): return self.tf_df.cal_volume_ratio(dataframe, window) - def calculate_zs(self, bi_list, seg_list): - return self.get_zs_list(bi_list, seg_list) + def calculate_seg_zs(self, bi_list, seg_list): + return self.get_seg_zs_list(bi_list, seg_list) def get_seg_list(self, bi_list): return self.tf_df.get_seg_list(bi_list) def cal_trend(self, klc_list): @@ -163,6 +163,9 @@ class ChanLun(): return self.tf_df.get_zs_list(bi_list, seg_list) def cal_bi_zs(self, seg_list): return self.tf_df.cal_bi_zs(seg_list) + def cal_bi_zs_list(self, bi_list): + #return self.tf_df.cal_bi_zs(bi_list) + return self.tf_df.cal_bi_zs_list(bi_list) def get_decimal(self, value): return Decimal("{:.2f}".format(value)) def get_klc_list(self, klu_list): diff --git a/ChanSEG.py b/ChanSEG.py index 4412085..67eda1c 100644 --- a/ChanSEG.py +++ b/ChanSEG.py @@ -91,41 +91,44 @@ class ChanSEG(): 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 bi.next == None or bi.next.next == None: + continue + bi2 = bi.next + bi3 = bi.next.next 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.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) + if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: + zg = min(bi.high, bi2.high, bi3.high) + zd = max(bi.low, bi2.low, bi3.low) + gg = max(bi.high, bi2.high, bi3.high) + dd = min(bi.low, bi2.low, bi3.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.add_bi(bi2) + zs.add_bi(bi3) 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 or bi.high < last_zs.zd: - 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) + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) + if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.DOWN: + zg = min(bi.high, bi2.high, bi3.high) + zd = max(bi.low, bi2.low, bi3.low) + gg = max(bi.high, bi2.high, bi3.high) + dd = min(bi.low, bi2.low, bi3.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.add_bi(bi2) + zs.add_bi(bi3) zs_list.append(zs) last_zs = zs else: @@ -133,41 +136,45 @@ class ChanSEG(): last_zs.add_bi(bi) if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: #print(bi.start_time, "BI", last_zs.is_sure) - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1]) + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) else: for index in range(1, len(self.bi_list)): bi = self.bi_list[index] + if bi.next == None or bi.next.next == None: + continue + bi2 = bi.next + bi3 = bi.next.next 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.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) + if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: + zg = min(bi.high, bi2.high, bi3.high) + zd = max(bi.low, bi2.low, bi3.low) + gg = max(bi.high, bi2.high, bi3.high) + dd = min(bi.low, bi2.low, bi3.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.add_bi(bi2) + zs.add_bi(bi3) 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.low > last_zs.zg or bi.high < last_zs.zd: - 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) + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) + if bi3.is_sure and bi3.index <= self.bi_list[-1].index and bi.check_bi_zs_overlap() and bi.dir == Chan_BI_DIR.UP: + zg = min(bi.high, bi2.high, bi3.high) + zd = max(bi.low, bi2.low, bi3.low) + gg = max(bi.high, bi2.high, bi3.high) + dd = min(bi.low, bi2.low, bi3.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.add_bi(bi2) + zs.add_bi(bi3) zs_list.append(zs) last_zs = zs else: @@ -175,7 +182,7 @@ class ChanSEG(): last_zs.add_bi(bi) if index == len(self.bi_list) - 1 and last_zs and not last_zs.is_sure: #print(bi.start_time, "BI", last_zs.is_sure) - last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1]) + last_zs.set_end_bi(last_zs.bi_list[-1], last_zs.bi_list[-1].sure_time) #print(self.start_time, len(zs_list)) #print(self.bi_list[-1].end_time, "end_bi") diff --git a/ChanZS.py b/ChanZS.py index dbc7ae8..0643c41 100644 --- a/ChanZS.py +++ b/ChanZS.py @@ -30,6 +30,7 @@ class ChanZS(): self.bi_out_list = [] self.bi_out_seg_list = [] self.bi_out_seg = None + self.is_extended = False def set_last_bi_in(self, last_bi_in): self.last_bi_in = last_bi_in def set_bi_out(self, bi_out, bi_out_seg): @@ -70,7 +71,19 @@ class ChanZS(): self.gg = gg def set_dd(self, dd): self.dd = dd - + def extend_zs(self, seg_list): + self.is_sure = False + self.end_seg = None + self.end_klc = None + self.sure_time = None + for seg in seg_list: + if seg.end_bi.high > self.gg: + self.set_gg(seg.end_bi.high) + if seg.end_bi.low < self.dd: + self.set_dd(seg.end_bi.low) + self.seg_list.append(seg) + self.is_extended = True + #print(self.start_time, "extend zs", seg_list[-1].end_time) # 大级别中枢:由多个区间重叠(扩张)的笔/线段中枢合并而成,用于显示更大级别的震荡区间 class ChanZS_Big(): diff --git a/TF_DF.py b/TF_DF.py index e733516..1bb8c78 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -1,12 +1,13 @@ from datetime import timedelta from pandas import DataFrame -from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_PRICE_TREND, Chan_KLU_PATTERN, Chan_K_DIR +from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_PRICE_TREND, Chan_KLU_PATTERN, Chan_K_DIR, Chan_KLC_STATE from ChanKLU import ChanKLU from ChanKLC import ChanKLC from ChanBI import ChanBI from ChanSBI import ChanSBI from ChanSEG import ChanSEG from ChanZS import ChanZS, ChanZS_Big +from ChanBIZS import ChanBIZS from ChanBSP import ChanBSP import talib.abstract as ta import pandas as pd @@ -72,8 +73,8 @@ class TF_DF(): return self.klc_list[-2] return None def add_indicators(self, df): - fast = 12 - slow = 26 + fast = 26 + slow = 52 period = 9 macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) @@ -137,32 +138,24 @@ class TF_DF(): klc_index = len(klc_list) - 1 klc = klc_list[klc_index] if klc.end_klu and klc.end_klu.idx == index: - if klc.klc_fx_type == Chan_KLC_FX.TOP1 or klc.klc_fx_type == Chan_KLC_FX.TOP2: + if klc.klc_state == Chan_KLC_STATE.S10: klu_state_list.append("10") #print(klc.end_time, klc.klc_fx_type) - elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM1 or klc.klc_fx_type == Chan_KLC_FX.BOTTOM2: + elif klc.klc_state == Chan_KLC_STATE.S_10: klu_state_list.append("-10") #print(klc.end_time, klc.klc_fx_type) + elif klc.klc_state == Chan_KLC_STATE.S11: + klu_state_list.append("11") + #print(klc.end_time, klc.klc_fx_type) + elif klc.klc_state == Chan_KLC_STATE.S_11: + klu_state_list.append("-11") + #print(klc.end_time, klc.klc_fx_type) else: - if klc.continue_div: - if klc.macdhist > 0: - klu_state_list.append("20") - #print(klc.end_time, "CD", 20) - else: - klu_state_list.append("-20") - #print(klc.end_time, "CD", -20) - elif klc.separate_div: - if klc.macdhist > 0: - klu_state_list.append("30") - #print(klc.end_time, "SD", 30) - else: - klu_state_list.append("-30") - #print(klc.end_time, "SD", -30) - else: - klu_state_list.append("00") + klu_state_list.append("00") klc_index += 1 else: klu_state_list.append("00") + print(klu_state_list[:20]) return klu_state_list def get_ema_state(self, dataframe): @@ -183,6 +176,23 @@ class TF_DF(): klu_state_list.append("0") return klu_state_list def check_fx(self, klc): + if klc.pre and klc.next: + if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low: + if klc.pre.pre and klc.next.next: + if klc.high > klc.pre.pre.high and klc.high > klc.next.next.high: + #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: + klc.set_fx(Chan_FX_TYPE.TOP) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "TOP") + return Chan_FX_TYPE.TOP + elif klc.low < klc.pre.low and klc.low < klc.next.low and klc.high < klc.pre.high and klc.high < klc.next.high: + #if (klc.close < klc.ema52 or klc.next.close < klc.next.ema52) and klc.macd < 0: + if klc.pre.pre and klc.next.next: + if klc.low < klc.pre.pre.low and klc.low < klc.next.next.low: + klc.set_fx(Chan_FX_TYPE.BOTTOM) + #print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time, klc.macd, klc.state, klc.fx, "BOTTOM") + return Chan_FX_TYPE.BOTTOM + return Chan_FX_TYPE.UNKNOWN + def check_fx1(self, klc): if klc.pre and klc.next: if klc.high > klc.pre.high and klc.high > klc.next.high and klc.low > klc.pre.low and klc.low > klc.next.low: #if (klc.close > klc.ema52 or klc.next.close > klc.next.ema52) and klc.macd > 0: @@ -911,22 +921,31 @@ class TF_DF(): last_seg_bi = bi_list[i] break """ - self.cal_bi_zs(seg_list) + #self.cal_bi_zs(seg_list) return seg_list - + def get_zs_state(self, df): + bi_list = self.cal_bi_list(self.get_klc_list(self.get_kl_data(df))) + seg_list = self.get_seg_list(bi_list) + zs_list = self.calculate_zs(seg_list) + for zs in zs_list: + last_zs = zs + return zs_list def cal_bi_list(self, klc_list): bi_list = [] last_top = None last_bottom = None bi_klc_min = 4 + last_fx_klc = None for klc in klc_list: + if last_fx_klc: + klc.check_klc_state(last_fx_klc) klc.check_fx_confirmed(last_top, last_bottom) fx = self.check_fx(klc) - if fx == Chan_FX_TYPE.TOP and False: + 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 and False: + 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, "---") @@ -979,6 +998,7 @@ class TF_DF(): #print(klc.start_time, bi.start_time, bi.end_time, bi.dir, bi.high, bi.low, bi.is_sure) """ else: + last_fx_klc = klc if fx == Chan_FX_TYPE.TOP: #print(klc.end_time, fx, klc.pre.high, klc.high, klc.pre.start_time, klc.pre.end_time) if last_top: @@ -1069,7 +1089,7 @@ class TF_DF(): klc.set_bi(bi_list[-1]) # 初始化的时候用,其他时间不用 else: - klc.set_fx(Chan_FX_TYPE.TT) + #klc.set_fx(Chan_FX_TYPE.TT) #print(klc.start_time, klc.fx, "二类卖点Sell 2") bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) @@ -1184,7 +1204,7 @@ class TF_DF(): klc.set_bi(bi_list[-1]) #print(klc.start_time, klc.fx, "笔买点Buy 3") else: - klc.set_fx(Chan_FX_TYPE.BB) + #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) @@ -1248,13 +1268,144 @@ class TF_DF(): if (last_top.low < klc.pre.high or last_top.low < klc.next.high) and (klc.index - last_top.index < 10): return False return True - + # 建议用这种方式生成笔中枢 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) + bi_zs_list = list(bi_zs_list) + list(zs_list) + return bi_zs_list + # 这个种方式不是很好,会有很多重叠的 + def cal_bi_zs_list(self, bi_list): + """ + 根据缠论笔中枢定义计算中枢(参照 get_zs_list 线段中枢判断规则) + 从第4根笔开始(索引3),每3根笔为一组检查 + 上涨中枢:后中枢 zd > 前中枢 zg(不重叠上移) + 下跌中枢:后中枢 zg < 前中枢 zd(不重叠下移) + 中枢可按两笔一组继续扩展到5根、7根... + """ + bi_zs_list = [] + if len(bi_list) < 3: + return bi_zs_list + + last_zs = None + start_idx = 3 + + while start_idx < len(bi_list): + if start_idx + 2 >= len(bi_list): + break + + bi1 = bi_list[start_idx] + bi2 = bi_list[start_idx + 1] + bi3 = bi_list[start_idx + 2] + + if not (bi1.is_sure and bi2.is_sure and bi3.is_sure): + start_idx += 1 + continue + + zg = min(bi1.high, bi2.high, bi3.high) + zd = max(bi1.low, bi2.low, bi3.low) + + if zg <= zd: + start_idx += 1 + continue + + valid = False + if last_zs is None: + if bi1.dir == Chan_BI_DIR.DOWN: + zs_dir = Chan_ZS_DIR.UP + valid = (bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) + else: + zs_dir = Chan_ZS_DIR.DOWN + valid = (bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) + else: + is_up_zs = zg > last_zs.zg + is_down_zs = zd < last_zs.zd + + if is_up_zs: + zs_dir = Chan_ZS_DIR.UP + valid = (bi1.dir == Chan_BI_DIR.DOWN and bi2.dir == Chan_BI_DIR.UP and bi3.dir == Chan_BI_DIR.DOWN) + elif is_down_zs: + zs_dir = Chan_ZS_DIR.DOWN + valid = (bi1.dir == Chan_BI_DIR.UP and bi2.dir == Chan_BI_DIR.DOWN and bi3.dir == Chan_BI_DIR.UP) + + if not valid: + start_idx += 1 + continue + gg = max(bi1.high, bi2.high, bi3.high) + dd = min(bi1.low, bi2.low, bi3.low) + zs = ChanBIZS(bi1, len(bi_zs_list), zs_dir) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.is_sure = False + zs.bi_list = [bi1, bi2, bi3] + + added_after_leave = [] + leave_index = start_idx + 4 + while leave_index < len(bi_list): + b = bi_list[leave_index] + if not b.is_sure: + break + if b.high >= zs.zd and b.low <= zs.zg: + added_after_leave.append(b.pre) + added_after_leave.append(b) + else: + break + leave_index += 2 + + if added_after_leave: + bis_for_zs = list(zs.bi_list) + list(added_after_leave) + bi_highs = [bi.high for bi in bis_for_zs] + bi_lows = [bi.low for bi in bis_for_zs] + zs.set_gg(max(bi_highs)) + zs.set_dd(min(bi_lows)) + zs.bi_list = bis_for_zs + bi = bis_for_zs[-1] + if bi.end_klc: + zs.set_end_bi(bi, bi.sure_time) + + start_idx = start_idx + len(added_after_leave) + else: + zs.set_end_bi(bi3, bi3.sure_time) + + if last_zs: + last_zs.set_next(zs) + zs.set_pre(last_zs) + + bi_zs_list.append(zs) + last_zs = zs + + start_idx += 4 + + if last_zs: + last_zs.is_sure = bi_list[-1].is_sure + + if last_zs and not last_zs.is_sure: + if last_zs.bi_list and len(last_zs.bi_list) > 0: + last_bi_of_zs = last_zs.bi_list[-1] + last_bi_idx = -1 + for i, bi in enumerate(bi_list): + if bi == last_bi_of_zs: + last_bi_idx = i + break + + has_leave = False + if last_bi_idx >= 0 and last_bi_idx + 1 < len(bi_list): + for i in range(last_bi_idx + 1, len(bi_list)): + bi = bi_list[i] + if bi.is_sure: + leave = (bi.low > last_zs.zg and bi.high > last_zs.zg) or \ + (bi.high < last_zs.zd and bi.low < last_zs.zd) + if leave: + has_leave = True + break + + if has_leave: + if last_bi_of_zs.end_klc: + last_zs.set_end_bi(last_bi_of_zs, last_bi_of_zs.sure_time) return bi_zs_list def find_all_bsp(self, bi_list, bi_zs_list): """ @@ -1570,9 +1721,9 @@ class TF_DF(): bsp_list.append(bsp) return bsp_list - def calculate_zs(self, bi_list, seg_list): - return self.get_zs_list(bi_list, seg_list) - def get_zs_list(self, bi_list, seg_list): + def calculate_seg_zs(self, seg_list): + return self.get_seg_zs_list(seg_list) + def get_seg_zs_list(self, seg_list): """ 根据缠论线段中枢定义计算中枢 从第4根线段开始(索引3),每3根线段为一组检查 @@ -1639,26 +1790,44 @@ class TF_DF(): # 不重叠下移 zs_dir = Chan_ZS_DIR.DOWN valid = (seg1.dir == Chan_SEG_DIR.UP and seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP) - + create_new_zs = False # 验证是否有效 if not valid: - start_idx += 1 - continue + # 如果新中枢和前一个中枢的中枢区间有重叠,不行成新中枢需要合并两个中枢 + is_in_last_zs = (zd > last_zs.zd and zd < last_zs.zg) or (zg < last_zs.zg and zg > last_zs.zd) or (zg > last_zs.zg and zd < last_zs.zd) or (zg < last_zs.zg and zd > last_zs.zd) + if is_in_last_zs: + #print(seg1.start_time, "New zs is in last zs, not valid") + last_zs.extend_zs(seg_list[last_zs.seg_list[-1].index:(seg3.index + 1)]) + create_new_zs = False + else: + start_idx += 1 + continue + else: + create_new_zs = True + if last_zs and create_new_zs: + last_seg = last_zs.seg_list[-1] + last_bi = last_seg.end_bi + if last_bi: + last_zs.is_sure = True + last_zs.set_end_klc(last_bi.end_klc, last_bi.sure_time, 0, last_seg) + last_zs.set_end_seg(last_seg) + zs = last_zs + if create_new_zs: + # 创建新中枢 + gg = max(seg1.high, seg2.high, seg3.high) + dd = min(seg1.low, seg2.low, seg3.low) - # 创建新中枢 - gg = max(seg1.high, seg2.high, seg3.high) - dd = min(seg1.low, seg2.low, seg3.low) - - zs = ChanZS(seg1, len(zs_list), zs_dir) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - zs.is_sure = False - zs.seg_list = [seg1, seg2, seg3] + zs = ChanZS(seg1, len(zs_list), zs_dir) + zs.set_zg(zg) + zs.set_zd(zd) + zs.set_gg(gg) + zs.set_dd(dd) + zs.is_sure = False + zs.seg_list = [seg1, seg2, seg3] # 若第二线段与 [zd,zg] 重叠(如离开后回抽回到前中枢)则并入扩展 added_after_leave = [] leave_index = start_idx + 4 + is_break = False while leave_index < len(seg_list): s = seg_list[leave_index] if not s.is_sure: @@ -1668,9 +1837,24 @@ class TF_DF(): if sh >= zs.zd and sl <= zs.zg: added_after_leave.append(s.pre) added_after_leave.append(s) + leave_index += 2 else: + next_seg = s.next + if next_seg and next_seg.is_sure: + if next_seg.dir == Chan_SEG_DIR.UP: + if next_seg.high <= zs.zg and next_seg.low >= zs.zd: + leave_index += 2 + continue + else: + is_break = True + else: + if next_seg.low >= zs.zd and next_seg.low <= zs.zg: + leave_index += 2 + continue + else: + is_break = True + if is_break: break - leave_index += 2 if added_after_leave: #print(len(added_after_leave)) segs_for_zs = list(zs.seg_list) + list(added_after_leave) @@ -1680,16 +1864,12 @@ class TF_DF(): zs.set_dd(min(seg_lows)) zs.seg_list = segs_for_zs seg = segs_for_zs[-1] - if seg.end_bi: - zs.set_end_klc(seg.end_bi.end_klc, seg.sure_time, 0, seg) - zs.set_end_seg(seg) - zs.is_sure = True + #if seg.end_bi: + #zs.set_end_klc(seg.end_bi.end_klc, seg.sure_time, 0, seg) + #zs.set_end_seg(seg) + #zs.is_sure = True start_idx = start_idx + len(added_after_leave) - else: - zs.set_end_klc(seg3.end_bi.end_klc, seg3.sure_time, 0, seg3) - zs.set_end_seg(seg3) - zs.is_sure = True - if last_zs: + if last_zs and last_zs.index != zs.index: last_zs.set_next(zs) zs.set_pre(last_zs) @@ -1700,6 +1880,7 @@ class TF_DF(): start_idx += 4 if last_zs: last_zs.is_sure = seg_list[-1].is_sure + """ # 处理最后一个未确认的中枢 - 不自动扩展,保持未完成状态 if last_zs and not last_zs.is_sure: # 获取中枢最后一个线段的索引 @@ -1731,10 +1912,11 @@ class TF_DF(): else: # 有离开,确认中枢 if last_seg_of_zs.end_bi: + #print(last_seg_of_zs.start_time, "last_seg_of_zs.end_time", last_seg_of_zs.end_time) last_zs.set_end_klc(last_seg_of_zs.end_bi.end_klc, last_seg_of_zs.sure_time, 0, last_seg_of_zs) last_zs.set_end_seg(last_seg_of_zs) last_zs.is_sure = True - + """ return zs_list def get_big_zs_list(self, zs_list): diff --git a/config/ElliottWaveBTC.json b/config/ElliottWaveBTC.json new file mode 100644 index 0000000..28fd29e --- /dev/null +++ b/config/ElliottWaveBTC.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://schema.freqtrade.io/schema.json", + "max_open_trades": 2, + "stake_currency": "USDT", + "stake_amount": "unlimited", + "tradable_balance_ratio": 0.99, + "fiat_display_currency": "USD", + "dry_run": true, + "db_url": "sqlite:///tradesv3.elliottwave_btc.sqlite", + "dry_run_wallet": 10000, + "cancel_open_orders_on_exit": true, + "trading_mode": "futures", + "margin_mode": "isolated", + "unfilledtimeout": { + "entry": 5, + "exit": 5, + "exit_timeout_count": 3, + "unit": "minutes" + }, + "entry_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1, + "price_last_balance": 0.0, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "exit_pricing": { + "price_side": "same", + "use_order_book": true, + "order_book_top": 1 + }, + "exchange": { + "name": "binance", + "key": "", + "secret": "", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + "BTC/USDT:USDT" + ] + }, + "pairlists": [ + { + "method": "StaticPairList", + "number_assets": 1, + "sort_key": "quoteVolume", + "min_value": 0 + } + ], + "telegram": { + "enabled": false, + "token": "", + "chat_id": "" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "freqtrade_secret", + "ws_token": "freqtrade_ws", + "username": "freqtrade", + "password": "freqtrade" + }, + "bot_name": "ElliottWaveBTC" +} diff --git a/strategies/ChanLun_BTC_30.py b/strategies/ChanLun_BTC_30.py index 1b777b8..5a6efe6 100644 --- a/strategies/ChanLun_BTC_30.py +++ b/strategies/ChanLun_BTC_30.py @@ -121,6 +121,7 @@ class ChanLun_BTC_30(IStrategy): #dataframe_5['chanpy_state'] = chanpy_state #state_list = self.chan.get_klu_state(dataframe_3) #dataframe_3['state'] = state_list + dataframe['state'] = self.chan.get_klu_state(dataframe) state_list = self.chan.get_klu_state(dataframe_5) dataframe_5['state'] = state_list #state_list = self.chan.get_klu_state(dataframe_15) @@ -329,36 +330,34 @@ class ChanLun_BTC_30(IStrategy): def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: shift30 = self.time30 state30 = 'resample_{}_state'.format(self.get_ticker_indicator()*shift30) + shift5 = self.time5 + state5 = 'resample_{}_state'.format(self.get_ticker_indicator()*shift5) dataframe.loc[ ( - (dataframe[state30].shift(shift30) == "-10") | - (dataframe[state30].shift(shift30) == "-20") | - (dataframe[state30].shift(shift30) == "-30") + (dataframe[state30].shift(shift30) == "11") & + (dataframe[state5].shift(shift5) == "-10") & + (dataframe['state'].shift(1) == "11") ), - ['enter_long', 'enter_tag']] = (1, 'long_10') + ['enter_long', 'enter_tag']] = (1, 'long') dataframe.loc[ ( - (dataframe[state30].shift(shift30) == "10") | - (dataframe[state30].shift(shift30) == "20") | - (dataframe[state30].shift(shift30) == "30") + (dataframe[state30].shift(shift30) == "-11") & + (dataframe[state5].shift(shift5) == "10") & + (dataframe['state'].shift(1) == "-11") ), - ['enter_short', 'enter_tag']] = (1, 'short_10') + ['enter_short', 'enter_tag']] = (1, 'short') return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - shift30 = self.time30 + shift30 = self.time5 state30 = 'resample_{}_state'.format(self.get_ticker_indicator()*shift30) dataframe.loc[ ( - (dataframe[state30].shift(shift30) == "10") | - (dataframe[state30].shift(shift30) == "20") | - (dataframe[state30].shift(shift30) == "30") + (dataframe[state30].shift(shift30) == "10") ), ['exit_long', 'exit_tag']] = (1, 'long_close_30') dataframe.loc[ ( - (dataframe[state30].shift(shift30) == "-10") | - (dataframe[state30].shift(shift30) == "-20") | - (dataframe[state30].shift(shift30) == "-30") + (dataframe[state30].shift(shift30) == "-10") ), ['exit_short', 'exit_tag']] = (1, 'short_close_30') return dataframe diff --git a/strategies/ElliottWaveBTCStrategy.py b/strategies/ElliottWaveBTCStrategy.py new file mode 100644 index 0000000..c2fefb4 --- /dev/null +++ b/strategies/ElliottWaveBTCStrategy.py @@ -0,0 +1,240 @@ +""" + ChanLun Wave Strategy for BTC Perpetual Futures + 基于缠论波浪策略 V8 + + 核心逻辑: + - 使用Chan库计算KLC-based缠论分型 + - 只做空头(在下跌趋势中做空反弹) + - 顶分型确认 + RSI > 55 + 趋势确认 → 做空 + - 空头出场:底分型 + RSI < 40 + + 策略设计: + - 短周期(5m)为主,长周期(1h/1d)确认趋势 + - 使用Chan库KLC分型确认入场 + - RSI > 55 做空条件,RSI < 40 出场条件 + - 不做多头(下跌趋势中做多风险太大) + + 作者: AI Assistant +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from ChanLun import ChanLun + +from freqtrade.strategy import IStrategy +from pandas import DataFrame +import pandas as pd +import numpy as np +import talib.abstract as ta +import logging +from datetime import datetime +from typing import Optional + +logger = logging.getLogger(__name__) + + +class ElliottWaveBTCStrategy(IStrategy): + + INTERFACE_VERSION = 3 + can_short = True + + stoploss = -0.02 + minimal_roi = { + "0": 0.06, + "120": 0.03, + "360": 0.01 + } + + trailing_stop = True + trailing_stop_positive = 0.02 + trailing_stop_positive_offset = 0.08 + trailing_only_offset_is_reached = True + + startup_candle_count = 500 + position_adjustment_enable = False + + pair = 'BTC/USDT:USDT' + timeframe = '5m' + chan = ChanLun() + + def informative_pairs(self): + return [ + (self.pair, '5m'), + (self.pair, '1h'), + (self.pair, '1d'), + ] + + def _add_indicators(self, df: DataFrame) -> DataFrame: + df['ema20'] = ta.EMA(df, timeperiod=20) + df['ema50'] = ta.EMA(df, timeperiod=50) + df['ema200'] = ta.EMA(df, timeperiod=200) + df['rsi'] = ta.RSI(df, timeperiod=14) + df['atr'] = ta.ATR(df, timeperiod=14) + + macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + + # Chan库指标 + df['ema52'] = ta.EMA(df, timeperiod=52) + df['ema104'] = ta.EMA(df, timeperiod=104) + df['ema24'] = ta.EMA(df, timeperiod=24) + df['ema26'] = ta.EMA(df, timeperiod=26) + df['volume_sma'] = ta.SMA(df, timeperiod=20) + df['volume_ratio'] = df['volume'] / df['volume_sma'] + + bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) + df['bb2633upper'] = bb['upperband'] + df['bb2633lower'] = bb['lowerband'] + df['bb2633middle'] = bb['middleband'] + + return df + + def _get_dataframe(self, timeframe: str) -> DataFrame: + return self.dp.get_pair_dataframe(pair=self.pair, timeframe=timeframe) + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe = self._add_indicators(dataframe) + + df_1h = self._get_dataframe('1h') + df_1d = self._get_dataframe('1d') + + if len(df_1h) > 0: + df_1h = self._add_indicators(df_1h) + df_1h['chan_state'] = self.chan.get_klu_state(df_1h) + dataframe['1h_ema200'] = df_1h['ema200'].reindex(dataframe.index, method='ffill') + dataframe['1h_trend_up'] = (df_1h['close'] > df_1h['ema200']).reindex(dataframe.index, method='ffill') + dataframe['1h_trend_down'] = (df_1h['close'] < df_1h['ema200']).reindex(dataframe.index, method='ffill') + dataframe['1h_chan_state'] = df_1h['chan_state'].reindex(dataframe.index, method='ffill') + else: + dataframe['1h_ema200'] = dataframe['ema200'] + dataframe['1h_trend_up'] = True + dataframe['1h_trend_down'] = True + dataframe['1h_chan_state'] = '00' + + if len(df_1d) > 0: + df_1d = self._add_indicators(df_1d) + df_1d['chan_state'] = self.chan.get_klu_state(df_1d) + dataframe['1d_ema200'] = df_1d['ema200'].reindex(dataframe.index, method='ffill') + dataframe['1d_trend_up'] = (df_1d['close'] > df_1d['ema200']).reindex(dataframe.index, method='ffill') + dataframe['1d_trend_down'] = (df_1d['close'] < df_1d['ema200']).reindex(dataframe.index, method='ffill') + dataframe['1d_rsi'] = df_1d['rsi'].reindex(dataframe.index, method='ffill') + dataframe['1d_chan_state'] = df_1d['chan_state'].reindex(dataframe.index, method='ffill') + else: + dataframe['1d_ema200'] = dataframe['ema200'] + dataframe['1d_trend_up'] = True + dataframe['1d_trend_down'] = True + dataframe['1d_rsi'] = 50 + dataframe['1d_chan_state'] = '00' + + # 缠论分型(使用Chan库) + dataframe['chan_state'] = self.chan.get_klu_state(dataframe) + dataframe = self._generate_signals(dataframe) + + return dataframe + + def _generate_signals(self, df: DataFrame) -> DataFrame: + """缠论分型 + 趋势确认 - 做空为主""" + n = len(df) + if n < 10: + return df + + # 延迟分型状态(避免未来数据) + df['_fx'] = df['chan_state'].shift(1).fillna('00') + + # 1h趋势 + hourly_down = df['1h_trend_down'].fillna(False) + hourly_up = df['1h_trend_up'].fillna(False) + + # MACD方向 + macd_cross_down = (df['macd'] < df['macdsignal']) & (df['macd'].shift(1) >= df['macdsignal'].shift(1)) + + # === 空头信号(下跌趋势中做空)=== + # 条件1: 顶分型 + RSI > 55 + 1h下跌趋势 + short_cond1 = ( + (df['_fx'] == '10') & + (df['rsi'] > 55) & + hourly_down + ) + + # 条件2: 1h共振顶分型 + RSI > 55 + short_cond2 = ( + (df['_fx'] == '10') & + (df['1h_chan_state'].fillna('00') == '10') & + (df['rsi'] > 55) + ) + + # 条件3: 顶分型 + MACD死叉 + RSI > 60 + short_cond3 = ( + (df['_fx'] == '10') & + macd_cross_down & + (df['rsi'] > 60) + ) + + df['chan_short'] = (short_cond1 | short_cond2 | short_cond3).astype(bool) + + # === 多头信号(仅在1h上涨趋势中做多,且很少)=== + # 只在1d和1h同时上涨时才做多,且需要强确认 + daily_up = df['1d_trend_up'].fillna(False) + long_cond = ( + (df['_fx'] == '-10') & + (df['rsi'] < 30) & # 极低RSI才做多 + hourly_up & + daily_up + ) + + # 1h和1d共振底分型 + long_cond2 = ( + (df['_fx'] == '-10') & + (df['rsi'] < 30) & + (df['1h_chan_state'].fillna('00') == '-10') & + (df['1d_chan_state'].fillna('00') == '-10') + ) + + df['chan_long'] = (long_cond | long_cond2).astype(bool) + + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['enter_long'] = 0 + dataframe['enter_short'] = 0 + dataframe['enter_tag'] = '' + + if 'chan_long' not in dataframe.columns: + return dataframe + + dataframe.loc[dataframe['chan_long'], 'enter_long'] = 1 + dataframe.loc[dataframe['chan_long'], 'enter_tag'] = 'chan_long' + + dataframe.loc[dataframe['chan_short'], 'enter_short'] = 1 + dataframe.loc[dataframe['chan_short'], 'enter_tag'] = 'chan_short' + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['exit_long'] = 0 + dataframe['exit_short'] = 0 + + if len(dataframe) < 2: + return dataframe + + if 'chan_state' not in dataframe.columns: + return dataframe + + df = dataframe.copy() + df['_fx'] = df['chan_state'].shift(1).fillna('00') + + # 空头出场:底分型 + RSI < 40(仅在明显反弹时出场) + dataframe['exit_short'] = ((df['_fx'] == '-10') & (df['rsi'] < 40)).astype(int) + + # 多头出场:顶分型 + RSI > 60 + dataframe['exit_long'] = ((df['_fx'] == '10') & (df['rsi'] > 60)).astype(int) + + return dataframe + + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, + **kwargs) -> float: + return 2.0 diff --git a/strategies/ElliottWaveBTCStrategyV2.py b/strategies/ElliottWaveBTCStrategyV2.py new file mode 100644 index 0000000..59d89dc --- /dev/null +++ b/strategies/ElliottWaveBTCStrategyV2.py @@ -0,0 +1,692 @@ +""" +Elliott Wave Strategy for BTC Perpetual Futures V2 +基于真正的艾略特波浪理论 + 缠论分型确认 + +核心逻辑: +- 艾略特波浪识别: 自动识别1-5浪上涨和A-C浪下跌 +- 多时间框架确认: 5m入场,1h确认趋势方向,1d确认大周期浪型 +- 双向交易: 根据波浪位置决定做多或做空 +- 动态风险管理: 根据波动率调整仓位和止损 + +改进点: +1. 实现真正的波浪计数器 (Wave Counter) +2. 斐波那契回撤/扩展用于止盈止损 +3. 波浪完成度评估 +4. 多周期共振确认 +5. 市场情绪过滤 + +作者: AI Assistant (Optimized) +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from ChanLun import ChanLun + +from freqtrade.strategy import IStrategy +from pandas import DataFrame +import pandas as pd +import numpy as np +import talib.abstract as ta +import logging +from datetime import datetime +from typing import Optional, Tuple, List +from dataclasses import dataclass +from enum import Enum + +logger = logging.getLogger(__name__) + + +class WaveType(Enum): + """波浪类型""" + IMPULSE = "impulse" # 推动浪 (1,2,3,4,5) + CORRECTIVE = "corrective" # 调整浪 (A,B,C) + UNKNOWN = "unknown" + + +class WavePosition(Enum): + """当前在波浪中的位置""" + WAVE_1 = 1 + WAVE_2 = 2 + WAVE_3 = 3 + WAVE_4 = 4 + WAVE_5 = 5 + WAVE_A = 6 + WAVE_B = 7 + WAVE_C = 8 + UNKNOWN = 0 + + +@dataclass +class Wave: + """波浪数据结构""" + start_idx: int + end_idx: int + start_price: float + end_price: float + wave_num: int # 1-5 or 6-8 (A-C) + wave_type: WaveType + is_complete: bool = False + + +class ElliottWaveBTCStrategyV2(IStrategy): + """ + 艾略特波浪理论策略 V2 + 结合缠论分型进行波浪确认 + """ + + INTERFACE_VERSION = 3 + can_short = True + + # 基础止损止盈 (会根据波动率动态调整) + stoploss = -0.015 + minimal_roi = { + "0": 0.08, + "60": 0.05, + "120": 0.03, + "240": 0.015 + } + + # 追踪止损 + trailing_stop = True + trailing_stop_positive = 0.015 + trailing_stop_positive_offset = 0.06 + trailing_only_offset_is_reached = True + + startup_candle_count = 1000 + position_adjustment_enable = True + + pair = 'BTC/USDT:USDT' + timeframe = '5m' + chan = ChanLun() + + # ========== 策略参数 (可优化) ========== + # 波浪检测参数 + wave_pivot_lookback = 5 # 波浪枢轴点回看周期 + min_wave_bars = 8 # 最小波浪K线数 + max_wave_bars = 200 # 最大波浪K线数 + + # 斐波那契参数 + fib_entry_threshold = 0.618 # 入场回撤位 + fib_target_1 = 1.272 # 第一目标位 + fib_target_2 = 1.618 # 第二目标位 + fib_stop_loss = 0.5 # 止损位 (低于/高于0.5) + + # RSI参数 + rsi_oversold = 35 + rsi_overbought = 65 + rsi_period = 14 + + # 波动率参数 + atr_period = 14 + atr_multiplier_entry = 1.5 # 入场ATR倍数 + atr_multiplier_stop = 2.0 # 止损ATR倍数 + + # 趋势过滤参数 + ema_trend_period = 200 + trend_filter_strict = True # 严格趋势过滤 + + # 波浪完成度阈值 + wave_completion_threshold = 0.8 + + def informative_pairs(self): + return [ + (self.pair, '5m'), + (self.pair, '1h'), + (self.pair, '4h'), + (self.pair, '1d'), + ] + + def _add_indicators(self, df: DataFrame) -> DataFrame: + """添加技术指标""" + # 基础EMA + df['ema20'] = ta.EMA(df, timeperiod=20) + df['ema50'] = ta.EMA(df, timeperiod=50) + df['ema200'] = ta.EMA(df, timeperiod=self.ema_trend_period) + + # RSI + df['rsi'] = ta.RSI(df, timeperiod=self.rsi_period) + df['rsi_ma'] = df['rsi'].rolling(window=9).mean() + + # ATR + df['atr'] = ta.ATR(df, timeperiod=self.atr_period) + df['atr_percent'] = df['atr'] / df['close'] * 100 + + # MACD + macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9) + df['macd'] = macd['macd'] + df['macdsignal'] = macd['macdsignal'] + df['macdhist'] = macd['macdhist'] + + # 布林带 + bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) + df['bb_upper'] = bb['upper'] + df['bb_lower'] = bb['lower'] + df['bb_middle'] = bb['middle'] + df['bb_percent'] = (df['close'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower']) + + # 成交量 + df['volume_sma'] = ta.SMA(df, timeperiod=20) + df['volume_ratio'] = df['volume'] / df['volume_sma'] + + # 波动率 + df['volatility'] = df['close'].pct_change().rolling(20).std() * np.sqrt(365 * 24 * 12) + + return df + + def _detect_pivots(self, df: DataFrame, left_bars: int = 5) -> Tuple[List[int], List[int]]: + """ + 检测价格枢轴点 (用于波浪识别) - 无未来数据版本 + 只使用左侧已确认的数据,避免lookahead bias + 返回: (高点索引列表, 低点索引列表) + """ + highs = [] + lows = [] + + # 只使用左侧数据确认枢轴点,不使用right_bars避免未来数据 + for i in range(left_bars, len(df)): + # 检测高点: 当前点比之前left_bars个bar都高 + is_high = True + for j in range(1, left_bars + 1): + if df['high'].iloc[i] <= df['high'].iloc[i - j]: + is_high = False + break + if is_high: + highs.append(i) + + # 检测低点: 当前点比之前left_bars个bar都低 + is_low = True + for j in range(1, left_bars + 1): + if df['low'].iloc[i] >= df['low'].iloc[i - j]: + is_low = False + break + if is_low: + lows.append(i) + + return highs, lows + + def _calculate_wave(self, pivots: List[int], df: DataFrame, is_up: bool) -> Optional[Wave]: + """ + 计算单个波浪的属性 + """ + if len(pivots) < 2: + return None + + start_idx = pivots[0] + end_idx = pivots[-1] + start_price = df['low'].iloc[start_idx] if is_up else df['high'].iloc[start_idx] + end_price = df['high'].iloc[end_idx] if is_up else df['low'].iloc[end_idx] + + wave_height = abs(end_price - start_price) + wave_bars = end_idx - start_idx + + if wave_bars < self.min_wave_bars or wave_bars > self.max_wave_bars: + return None + + return Wave( + start_idx=start_idx, + end_idx=end_idx, + start_price=start_price, + end_price=end_price, + wave_num=0, # 稍后分配 + wave_type=WaveType.UNKNOWN + ) + + def _identify_elliott_waves(self, df: DataFrame) -> List[Wave]: + """ + 识别艾略特波浪结构 + 简化版:基于枢轴点识别5浪上涨或3浪下跌 + """ + highs, lows = self._detect_pivots(df, self.wave_pivot_lookback) + + waves = [] + all_pivots = sorted(highs + lows) + + if len(all_pivots) < 4: + return waves + + # 简化波浪识别:基于价格走势判断当前处于哪个浪 + recent_pivots = all_pivots[-8:] # 取最近8个枢轴点 + + for i in range(0, len(recent_pivots) - 1, 2): + if i + 1 >= len(recent_pivots): + break + + start_idx = recent_pivots[i] + end_idx = recent_pivots[i + 1] + + # 确定是上涨还是下跌浪 + price_change = df['close'].iloc[end_idx] - df['close'].iloc[start_idx] + is_up = price_change > 0 + + wave = Wave( + start_idx=start_idx, + end_idx=end_idx, + start_price=df['close'].iloc[start_idx], + end_price=df['close'].iloc[end_idx], + wave_num=(i // 2) + 1, + wave_type=WaveType.IMPULSE if is_up else WaveType.CORRECTIVE, + is_complete=True + ) + waves.append(wave) + + return waves + + def _get_current_wave_position(self, df: DataFrame, waves: List[Wave]) -> WavePosition: + """ + 判断当前处于波浪的哪个位置 + """ + if not waves: + return WavePosition.UNKNOWN + + last_wave = waves[-1] + current_price = df['close'].iloc[-1] + + # 基于最后一浪的特征判断位置 + if last_wave.wave_num == 1: + return WavePosition.WAVE_2 if current_price < last_wave.end_price else WavePosition.WAVE_1 + elif last_wave.wave_num == 2: + return WavePosition.WAVE_3 if current_price > last_wave.end_price else WavePosition.WAVE_2 + elif last_wave.wave_num == 3: + return WavePosition.WAVE_4 if current_price < last_wave.end_price else WavePosition.WAVE_3 + elif last_wave.wave_num == 4: + return WavePosition.WAVE_5 if current_price > last_wave.end_price else WavePosition.WAVE_4 + elif last_wave.wave_num >= 5: + return WavePosition.WAVE_A + + return WavePosition.UNKNOWN + + def _calculate_fibonacci_levels(self, wave: Wave) -> dict: + """ + 计算斐波那契回撤和扩展位 + """ + if wave is None: + return {} + + price_range = abs(wave.end_price - wave.start_price) + is_up = wave.end_price > wave.start_price + + if is_up: + levels = { + '0.0': wave.end_price, + '0.236': wave.end_price - price_range * 0.236, + '0.382': wave.end_price - price_range * 0.382, + '0.5': wave.end_price - price_range * 0.5, + '0.618': wave.end_price - price_range * 0.618, + '0.786': wave.end_price - price_range * 0.786, + '1.0': wave.start_price, + '1.272': wave.end_price + price_range * 0.272, + '1.618': wave.end_price + price_range * 0.618, + } + else: + levels = { + '0.0': wave.end_price, + '0.236': wave.end_price + price_range * 0.236, + '0.382': wave.end_price + price_range * 0.382, + '0.5': wave.end_price + price_range * 0.5, + '0.618': wave.end_price + price_range * 0.618, + '0.786': wave.end_price + price_range * 0.786, + '1.0': wave.start_price, + '1.272': wave.end_price - price_range * 0.272, + '1.618': wave.end_price - price_range * 0.618, + } + + return levels + + def _get_dataframe(self, timeframe: str) -> DataFrame: + return self.dp.get_pair_dataframe(pair=self.pair, timeframe=timeframe) + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """计算所有指标""" + dataframe = self._add_indicators(dataframe) + + # 获取多时间框架数据 + df_1h = self._get_dataframe('1h') + df_4h = self._get_dataframe('4h') + df_1d = self._get_dataframe('1d') + + # 1小时指标 + if len(df_1h) > 50: + df_1h = self._add_indicators(df_1h) + dataframe['1h_ema200'] = df_1h['ema200'].reindex(dataframe.index, method='ffill') + dataframe['1h_trend'] = np.where(dataframe['close'] > dataframe['1h_ema200'], 1, -1) + dataframe['1h_rsi'] = df_1h['rsi'].reindex(dataframe.index, method='ffill') + dataframe['1h_atr'] = df_1h['atr'].reindex(dataframe.index, method='ffill') + + # 1h波浪识别 + waves_1h = self._identify_elliott_waves(df_1h) + dataframe['1h_wave_position'] = self._get_current_wave_position(df_1h, waves_1h).value + else: + dataframe['1h_trend'] = 0 + dataframe['1h_rsi'] = 50 + dataframe['1h_wave_position'] = 0 + + # 4小时指标 + if len(df_4h) > 50: + df_4h = self._add_indicators(df_4h) + dataframe['4h_ema200'] = df_4h['ema200'].reindex(dataframe.index, method='ffill') + dataframe['4h_trend'] = np.where(dataframe['close'] > dataframe['4h_ema200'], 1, -1) + else: + dataframe['4h_trend'] = 0 + + # 日线指标 + if len(df_1d) > 50: + df_1d = self._add_indicators(df_1d) + dataframe['1d_ema200'] = df_1d['ema200'].reindex(dataframe.index, method='ffill') + dataframe['1d_trend'] = np.where(dataframe['close'] > dataframe['1d_ema200'], 1, -1) + dataframe['1d_rsi'] = df_1d['rsi'].reindex(dataframe.index, method='ffill') + + # 日线波浪 (大趋势) + waves_1d = self._identify_elliott_waves(df_1d) + dataframe['1d_wave_position'] = self._get_current_wave_position(df_1d, waves_1d).value + else: + dataframe['1d_trend'] = 0 + dataframe['1d_rsi'] = 50 + dataframe['1d_wave_position'] = 0 + + # 当前时间框架波浪识别 + waves = self._identify_elliott_waves(dataframe) + dataframe['wave_position'] = self._get_current_wave_position(dataframe, waves).value + + # 缠论分型 + dataframe['chan_state'] = self.chan.get_klu_state(dataframe) + + # 生成交易信号 + dataframe = self._generate_signals(dataframe, waves) + + return dataframe + + def _generate_signals(self, df: DataFrame, waves: List[Wave]) -> DataFrame: + """ + 基于艾略特波浪理论生成交易信号 + """ + n = len(df) + if n < 50: + return df + + # 获取当前波浪位置 + current_wave = self._get_current_wave_position(df, waves) + + # 延迟分型 (避免未来数据) + df['_fx'] = df['chan_state'].shift(1).fillna('00') + + # 趋势方向 + trend_up = df['1h_trend'] > 0 + trend_down = df['1h_trend'] < 0 + trend_aligned_daily = df['1d_trend'] == df['1h_trend'] + + # RSI条件 + rsi_oversold = df['rsi'] < self.rsi_oversold + rsi_overbought = df['rsi'] > self.rsi_overbought + rsi_divergence_long = (df['rsi'] > df['rsi'].shift(5)) & (df['close'] < df['close'].shift(5)) + rsi_divergence_short = (df['rsi'] < df['rsi'].shift(5)) & (df['close'] > df['close'].shift(5)) + + # 波动率过滤 + low_volatility = df['atr_percent'] < df['atr_percent'].rolling(50).mean() * 0.8 + high_volatility = df['atr_percent'] > df['atr_percent'].rolling(50).mean() * 1.5 + + # ========== 多头信号 ========== + long_conditions = [] + + # 浪2回调做多 (最佳入场点) + # 条件: 浪2位置 + 底分型 + RSI超卖 + 趋势向上 + long_cond_wave2 = ( + (df['wave_position'] == WavePosition.WAVE_2.value) | + (df['1h_wave_position'] == WavePosition.WAVE_2.value) + ) & ( + (df['_fx'] == '-10') | + ((df['close'] > df['ema20']) & (df['ema20'] > df['ema50'])) + ) & rsi_oversold & trend_up + + long_conditions.append(('wave2', long_cond_wave2)) + + # 浪4回调做多 (谨慎入场) + long_cond_wave4 = ( + (df['wave_position'] == WavePosition.WAVE_4.value) | + (df['1h_wave_position'] == WavePosition.WAVE_4.value) + ) & (df['_fx'] == '-10') & rsi_oversold & trend_up & ( + df['rsi_divergence_long'] if 'rsi_divergence_long' in df.columns else True + ) + + long_conditions.append(('wave4', long_cond_wave4)) + + # C浪结束做多 (趋势反转) + long_cond_wave_c = ( + (df['wave_position'] == WavePosition.WAVE_C.value) | + (df['1h_wave_position'] == WavePosition.WAVE_C.value) + ) & (df['_fx'] == '-10') & rsi_oversold & ( + df['volume_ratio'] > 1.5 # 放量确认 + ) + + long_conditions.append(('wave_c', long_cond_wave_c)) + + # 强势突破做多 + long_cond_breakout = ( + (df['close'] > df['bb_upper']) & + (df['volume_ratio'] > 2.0) & + trend_up & + (df['macdhist'] > 0) & + (df['1h_wave_position'].isin([WavePosition.WAVE_3.value, WavePosition.WAVE_5.value])) + ) + + long_conditions.append(('breakout', long_cond_breakout)) + + # 合并多头信号 + df['elliott_long'] = False + for name, cond in long_conditions: + df[f'long_{name}'] = cond & ~low_volatility # 避免低波动时入场 + df['elliott_long'] |= df[f'long_{name}'] + + # ========== 空头信号 ========== + short_conditions = [] + + # 浪2回调做空 (下跌趋势) + short_cond_wave2 = ( + (df['wave_position'] == WavePosition.WAVE_2.value) | + (df['1h_wave_position'] == WavePosition.WAVE_2.value) + ) & ( + (df['_fx'] == '10') | + ((df['close'] < df['ema20']) & (df['ema20'] < df['ema50'])) + ) & rsi_overbought & trend_down + + short_conditions.append(('wave2', short_cond_wave2)) + + # 浪4回调做空 (谨慎) + short_cond_wave4 = ( + (df['wave_position'] == WavePosition.WAVE_4.value) | + (df['1h_wave_position'] == WavePosition.WAVE_4.value) + ) & (df['_fx'] == '10') & rsi_overbought & trend_down + + short_conditions.append(('wave4', short_cond_wave4)) + + # 浪5结束做空 (趋势反转) + short_cond_wave5 = ( + (df['wave_position'] == WavePosition.WAVE_5.value) | + (df['1h_wave_position'] == WavePosition.WAVE_5.value) + ) & (df['_fx'] == '10') & rsi_overbought & ( + df['volume_ratio'] > 1.5 + ) + + short_conditions.append(('wave5', short_cond_wave5)) + + # B浪反弹做空 (继续下跌) + short_cond_wave_b = ( + (df['wave_position'] == WavePosition.WAVE_B.value) | + (df['1h_wave_position'] == WavePosition.WAVE_B.value) + ) & (df['_fx'] == '10') & rsi_overbought & trend_down + + short_conditions.append(('wave_b', short_cond_wave_b)) + + # 强势跌破做空 + short_cond_breakdown = ( + (df['close'] < df['bb_lower']) & + (df['volume_ratio'] > 2.0) & + trend_down & + (df['macdhist'] < 0) & + (df['1h_wave_position'].isin([WavePosition.WAVE_3.value, WavePosition.WAVE_C.value])) + ) + + short_conditions.append(('breakdown', short_cond_breakdown)) + + # 合并空头信号 + df['elliott_short'] = False + for name, cond in short_conditions: + df[f'short_{name}'] = cond & ~low_volatility + df['elliott_short'] |= df[f'short_{name}'] + + # 强趋势过滤 + if self.trend_filter_strict: + df['elliott_long'] &= trend_up | (df['1d_trend'] > 0) + df['elliott_short'] &= trend_down | (df['1d_trend'] < 0) + + # 避免高波动时期入场 + df['elliott_long'] &= ~high_volatility + df['elliott_short'] &= ~high_volatility + + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """入场信号""" + dataframe['enter_long'] = 0 + dataframe['enter_short'] = 0 + dataframe['enter_tag'] = '' + + if 'elliott_long' not in dataframe.columns: + return dataframe + + # 多头入场 + long_mask = dataframe['elliott_long'].fillna(False) + dataframe.loc[long_mask, 'enter_long'] = 1 + + # 标记入场类型 + for col in dataframe.columns: + if col.startswith('long_') and col != 'elliott_long': + mask = dataframe[col].fillna(False) & (dataframe['enter_long'] == 1) + dataframe.loc[mask, 'enter_tag'] = col.replace('long_', 'elliott_') + + # 空头入场 + short_mask = dataframe['elliott_short'].fillna(False) + dataframe.loc[short_mask, 'enter_short'] = 1 + + for col in dataframe.columns: + if col.startswith('short_') and col != 'elliott_short': + mask = dataframe[col].fillna(False) & (dataframe['enter_short'] == 1) + dataframe.loc[mask, 'enter_tag'] = col.replace('short_', 'elliott_') + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """出场信号""" + dataframe['exit_long'] = 0 + dataframe['exit_short'] = 0 + + if len(dataframe) < 2: + return dataframe + + df = dataframe.copy() + df['_fx'] = df['chan_state'].shift(1).fillna('00') + + # 多头出场条件 + exit_long_cond = ( + # 顶分型出场 + (df['_fx'] == '10') | + # RSI超买 + (df['rsi'] > 75) | + # 跌破EMA20 + (df['close'] < df['ema20']) & (df['close'].shift(1) > df['ema20'].shift(1)) | + # MACD死叉 + (df['macd'] < df['macdsignal']) & (df['macd'].shift(1) > df['macdsignal'].shift(1)) + ) + + # 波浪位置出场 + exit_long_wave = df['wave_position'].isin([ + WavePosition.WAVE_5.value, + WavePosition.WAVE_C.value + ]) + + dataframe['exit_long'] = (exit_long_cond | exit_long_wave).astype(int) + + # 空头出场条件 + exit_short_cond = ( + # 底分型出场 + (df['_fx'] == '-10') | + # RSI超卖 + (df['rsi'] < 25) | + # 突破EMA20 + (df['close'] > df['ema20']) & (df['close'].shift(1) < df['ema20'].shift(1)) | + # MACD金叉 + (df['macd'] > df['macdsignal']) & (df['macd'].shift(1) < df['macdsignal'].shift(1)) + ) + + # 波浪位置出场 + exit_short_wave = df['wave_position'].isin([ + WavePosition.WAVE_C.value, + WavePosition.WAVE_5.value + ]) + + dataframe['exit_short'] = (exit_short_cond | exit_short_wave).astype(int) + + return dataframe + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + """ + 动态止损:基于ATR和波浪位置调整 + """ + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if len(dataframe) < 2: + return self.stoploss + + current_candle = dataframe.iloc[-1] + atr = current_candle['atr_percent'] + + # 基于ATR的动态止损 + dynamic_stop = -atr * self.atr_multiplier_stop / 100 + + # 根据盈利情况收紧止损 + if current_profit > 0.03: # 盈利3%后收紧止损 + return max(dynamic_stop, -0.01) + elif current_profit > 0.05: # 盈利5%后更紧 + return max(dynamic_stop, -0.005) + + return max(dynamic_stop, self.stoploss) + + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, + **kwargs) -> float: + """ + 动态杠杆:根据波动率调整 + """ + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if len(dataframe) < 2: + return 2.0 + + current_candle = dataframe.iloc[-1] + volatility = current_candle['atr_percent'] + + # 低波动时提高杠杆,高波动时降低杠杆 + if volatility < 0.5: + return 3.0 + elif volatility < 1.0: + return 2.0 + elif volatility < 2.0: + return 1.5 + else: + return 1.0 + + def adjust_trade_position(self, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs) -> Optional[float]: + """ + 仓位调整:金字塔加仓 + """ + if current_profit < -0.01: # 亏损时不加仓 + return None + + if current_profit > 0.02 and current_profit < 0.03: # 盈利2-3%时加仓 + return min_stake * 0.5 if min_stake else None + + return None diff --git a/web/app.py b/web/app.py index 887680d..4d8918a 100644 --- a/web/app.py +++ b/web/app.py @@ -33,7 +33,8 @@ class TRADE_POINT_TYPE: SELL3 = -3 # 三类卖点 app = Flask(__name__) - +macd_factor = 2 *6 +smooth_factor = 1 # 初始化交易所 exchange = ccxt.binance({ 'enableRateLimit': True, @@ -421,9 +422,9 @@ def get_a_stock_kl_data(symbol, timeframe, limit=100000, start_time=None, end_ti return None def add_indicators(df): - fast = 12*1 - slow = 26*1 - period = 9*1 + fast = 12 * macd_factor + slow = 26 * macd_factor + period = 9 * smooth_factor macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) df['macd'] = macd['macd'] @@ -509,10 +510,10 @@ def add_indicators(df): def calculate_macd(df): """计算MACD指标""" - exp1 = df['close'].ewm(span=12, adjust=False).mean() - exp2 = df['close'].ewm(span=26, adjust=False).mean() + exp1 = df['close'].ewm(span=12*macd_factor, adjust=False).mean() + exp2 = df['close'].ewm(span=26*macd_factor, adjust=False).mean() macd = exp1 - exp2 - signal = macd.ewm(span=9, adjust=False).mean() + signal = macd.ewm(span=9 * smooth_factor, adjust=False).mean() histogram = macd - signal return { @@ -534,13 +535,11 @@ def analyze_chan(df, symbol=None, timeframe=None): #for index in range(0, 10): #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) + zs_list = chan.calculate_seg_zs(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 = [] + + #bi_zs_list = chan.cal_bi_zs_list(bi_list) + bi_zs_list = chan.cal_bi_zs(seg_list) bsp_list = [] if len(bi_zs_list) > 0: bsp_list = chan.find_all_bsp(bi_list, bi_zs_list) diff --git a/web/templates/index.html b/web/templates/index.html index 9940e5c..4646ee0 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -767,11 +767,7 @@ -
-
-
缠论分析系统
-
- +
diff --git a/缠论.txt b/缠论.txt index a446705..bd92f5e 100644 --- a/缠论.txt +++ b/缠论.txt @@ -81,3 +81,4 @@ 注意,这里必须提醒一句,就是这在以前也曾说过,就是,如果线段中,最高或最低点不是线段的端点,那么,在任何以线段为基础的分析中,例如把线段为基础构成最小级别的中枢等,都可以把该线段标准化为最高低点都在端点。因为, 在以线段为基础的分析中,都把线段当成一个没有内部 结构的基本部件,所以,只需要关心这线段的实际区间就可以,这样就可以只看其高低点。 经过标准化处理后,所有向上线段都是以最低点开始最高点结束,向下线段都是以最高点开始最低点结束,这样,所以线段的连接,就形成一条延续不断、首尾相连的折线,这样,复杂的图形,就会十分地标准化,也为后面的中枢、走势类型等分析提供了最标准且基础的部件。 +本级别没有背驰的,次级别背驰下跌后形成第三类卖点进而形成本级别的V型反转,小转大