diff --git a/.DS_Store b/.DS_Store index b539a4d..5a943f3 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/ChanKLC.py b/ChanKLC.py index edb6fd8..d6e91de 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -46,6 +46,7 @@ class ChanKLC(): self.continue_div = False self.separate_div = False self.ema24 = klu.ema24 + self.ema26 = klu.ema26 self.ema52 = klu.ema52 self.ema104 = klu.ema104 self.ema156 = klu.ema156 @@ -387,6 +388,7 @@ class ChanKLC(): self.rsi += self.klu_list[index].rsi self.volume_ratio += self.klu_list[index].volume_ratio self.macdhist += self.klu_list[index].macdhist + self.ema26 += self.klu_list[index].ema26 self.ema24 += self.klu_list[index].ema24 self.ema52 += self.klu_list[index].ema52 self.ema104 += self.klu_list[index].ema104 @@ -404,6 +406,7 @@ class ChanKLC(): self.volume_ratio = self.volume_ratio / n self.volume = self.volume / n self.macdhist = self.macdhist / n + self.ema26 = self.ema26 / n self.ema24 = self.ema24 / n self.ema52 = self.ema52 / n self.ema104 = self.ema104 / n diff --git a/ChanKLU.py b/ChanKLU.py index 7c869c0..be7e1b6 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -46,6 +46,7 @@ class ChanKLU: self.near0_return = 0 self.ema52 = 0 self.ema24 = 0 + self.ema26 = 0 self.ema104 = 0 self.ema156 = 0 self.ema208 = 0 @@ -175,6 +176,7 @@ class ChanKLU: self.macd = float(item['macd']) if 'macd' in item and item['macd'] else 0 self.signal = float(item['macdsignal']) if 'macdsignal' in item and item['macdsignal'] else 0 self.macdhist = float(item['macdhist']) if 'macdhist' in item and item['macdhist'] else 0 + self.ema26 = float(item['ema26']) if 'ema26' in item and item['ema26'] else 0 self.ema52 = float(item['ema52']) if 'ema52' in item and item['ema52'] else 0 self.ema24 = float(item['ema24']) if 'ema24' in item and item['ema24'] else 0 self.ema104 = float(item['ema104']) if 'ema104' in item and item['ema104'] else 0 @@ -230,15 +232,15 @@ class ChanKLU: elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52: self.near0_return = 0 if self.close > self.ema52 and self.open < self.ema52: - if self.pre.near0_return == 0: - self.near0_return = 7 - elif self.pre.near0_return == 8: - self.pre.near0_return = 0 + self.near0_return = 7 elif self.close < self.ema52 and self.open > self.ema52: - if self.pre.near0_return == 0: - self.near0_return = 8 - elif self.pre.near0_return == 7: - self.pre.near0_return = 0 + self.near0_return = 8 + if self.pre.near0_return == 7: + if self.low > self.ema52 and self.close > self.open: + self.near0_return = 9 + if self.pre.near0_return == 8: + if self.high < self.ema52 and self.close < self.open: + self.near0_return = 10 # CROSS0 仅以 Signal 穿越零轴判定 if self.pre.signal >= 0 and self.signal < 0: self.macd_state = Chan_MACD_STATE.CROSS0_DOWN diff --git a/ChanZS.py b/ChanZS.py index 367368a..dbc7ae8 100644 --- a/ChanZS.py +++ b/ChanZS.py @@ -69,4 +69,23 @@ class ChanZS(): def set_gg(self, gg): self.gg = gg def set_dd(self, dd): - self.dd = dd \ No newline at end of file + self.dd = dd + + +# 大级别中枢:由多个区间重叠(扩张)的笔/线段中枢合并而成,用于显示更大级别的震荡区间 +class ChanZS_Big(): + def __init__(self, zs_list): + assert len(zs_list) >= 1 + self.zs_list = list(zs_list) + first = self.zs_list[0] + last = self.zs_list[-1] + self.start_time = first.start_time + self.end_time = last.end_time if last.end_time else None + self.start_klc = first.start_klc + self.end_klc = last.end_klc + # 大级别区间取并集:包住所有子中枢 + self.zd = min(zs.zd for zs in self.zs_list) + self.zg = max(zs.zg for zs in self.zs_list) + self.dd = min(zs.dd for zs in self.zs_list) + self.gg = max(zs.gg for zs in self.zs_list) + self.index = 0 # 由外部设置 \ No newline at end of file diff --git a/TF_DF.py b/TF_DF.py index 1ba436f..7ae8313 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -6,7 +6,7 @@ from ChanKLC import ChanKLC from ChanBI import ChanBI from ChanSBI import ChanSBI from ChanSEG import ChanSEG -from ChanZS import ChanZS +from ChanZS import ChanZS, ChanZS_Big from ChanBSP import ChanBSP import talib.abstract as ta import pandas as pd @@ -46,6 +46,7 @@ class TF_DF(): self.bi_list = self.cal_bi_list(self.klc_list) self.seg_list = self.get_seg_list(self.bi_list) self.zs_list = self.get_zs_list(self.bi_list, self.seg_list) + self.big_zs_list = self.get_big_zs_list(self.zs_list) self.chanmacd = ChanMACD(self.klu_list) self.klu_list = self.chanmacd.cal_macd_state() @@ -571,7 +572,27 @@ class TF_DF(): last_klu = None macd = ChanMACD(klu_list) klu_list = macd.cal_macd_state() + ema_up_list = [] + ema_down_list = [] + ema_up_count = 0 + ema_down_count = 0 + last_klu = None for klu in klu_list: + ema = klu.ema52 + last_ema = last_klu.ema52 if last_klu else 0 + if klu.close >= ema: + ema_up_count += 1 + elif klu.close < ema: + ema_down_count += 1 + if last_klu and last_klu.close >= last_ema and klu.close < ema: + ema_up_list.append(ema_up_count) + #print(last_klu.time, ema_up_count, "UP END") + ema_up_count = 0 + elif last_klu and last_klu.close < last_ema and klu.close >= ema: + ema_down_list.append(ema_down_count) + #print(last_klu.time, ema_down_count, "DOWN END") + ema_down_count = 0 + last_klu = klu if len(klc_list) > 0: last_klc = klc_list[-1] if klu.exception: @@ -609,6 +630,7 @@ class TF_DF(): klc_list.append(klc) last_klu = klu klc_list = self.cal_trend(klc_list) + #print(ema52_up_list, ema52_down_list) return klc_list def get_seg_list(self, bi_list): @@ -965,7 +987,7 @@ class TF_DF(): if last_top.high > klc.high: bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) - klc.set_klc_fx_type(Chan_KLC_FX.TOP3) + #klc.set_klc_fx_type(Chan_KLC_FX.TOP3) #print(klc.end_time, klc.fx, "二类卖点Sell 1") else: # A new top found @@ -979,7 +1001,7 @@ class TF_DF(): klc.set_bi(bi_list[-1]) # 不满足结合律的分型 else: - #klc.set_klc_fx_type(Chan_KLC_FX.TOP0) + klc.set_klc_fx_type(Chan_KLC_FX.TOP0) if last_bottom.index + bi_klc_min > klc.index: if last_top.high > klc.high: #print(klc.start_time, klc.fx, "二类卖点Sell 1") @@ -1082,7 +1104,7 @@ class TF_DF(): if last_bottom.low < klc.low: bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) - klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) #print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1") #print(klc.end_time, klc.fx, "二类买点Buy 1") else: @@ -1096,7 +1118,7 @@ class TF_DF(): klc.set_bi(bi_list[-1]) # 不满足结合律的分型 else: - #klc.set_klc_fx_type(Chan_KLC_FX.TOP0) + klc.set_klc_fx_type(Chan_KLC_FX.TOP0) if last_top.index + bi_klc_min > klc.index: if last_bottom.low < klc.low: #print(klc.end_time, klc.fx, "中枢买点Buy 1") @@ -1849,180 +1871,225 @@ class TF_DF(): 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): + """ + 根据缠论线段中枢定义计算中枢 + 从第4根线段开始(索引3),每3根线段为一组检查 + 后一个中枢比前一个高 -> 上涨中枢,以下跌开始、以下跌结束 + 后一个中枢比前一个低 -> 下跌中枢,以上涨开始、以上涨结束 + 中枢可以扩展到5根、7根... + """ zs_list = [] - bsp_list = [] - if len(seg_list) > 3: - last_zs = None - first_bi_out = None - in_again = False - bi_out_count = 0 - zs_count = 0 - for seg in seg_list: - # No zs or Last ZS is completed - if len(zs_list) == 0 or (last_zs and last_zs.is_sure): - # Has three completed segments - if seg.next and seg.next.next: - if seg.next.next.is_sure: - zg = min(seg.high, seg.next.high, seg.next.next.high) - zd = max(seg.low, seg.next.low, seg.next.next.low) - gg = max(seg.high, seg.next.high, seg.next.next.high) - dd = min(seg.low, seg.next.low, seg.next.next.low) - ddir = None - if last_zs: - if zg < last_zs.zd: - ddir = Chan_ZS_DIR.DOWN - else: - if zd > last_zs.zg: - ddir = Chan_ZS_DIR.UP - else: - ddir = None - else: - if seg.dir == Chan_SEG_DIR.UP: - ddir = Chan_ZS_DIR.DOWN - else: - ddir = Chan_ZS_DIR.UP - if (seg.dir == Chan_SEG_DIR.DOWN and ddir == Chan_ZS_DIR.DOWN) or (seg.dir == Chan_SEG_DIR.UP and ddir == Chan_ZS_DIR.UP): - ddir = None - if ddir and zg > zd: - # New ZS - zs = ChanZS(seg, len(zs_list), ddir) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - if last_zs: - last_zs.set_next(zs) - zs.set_pre(last_zs) - zs_list.append(zs) - if last_zs and last_zs.dir == zs.dir: - zs_count += 1 - else: - zs_count = 1 - last_zs = zs - # Last ZS is not completed + if len(seg_list) < 3: + return zs_list + + last_zs = None + + # 从第4根线段开始(索引3),每3根为一组 + start_idx = 3 + + while start_idx < len(seg_list): + # 取连续3个线段 + if start_idx + 2 >= len(seg_list): + break + + seg1 = seg_list[start_idx] + seg2 = seg_list[start_idx + 1] + seg3 = seg_list[start_idx + 2] + + # 三个线段都必须是已确认的 + if not (seg1.is_sure and seg2.is_sure and seg3.is_sure): + start_idx += 1 + continue + + # 计算这3个线段的中枢区间 + zg = min(seg1.high, seg2.high, seg3.high) + zd = max(seg1.low, seg2.low, seg3.low) + + if zg <= zd: + start_idx += 1 + continue + + # 判断中枢类型 + # 上涨中枢:以下跌开始、以下跌结束(下跌+上涨+下跌) + # 下跌中枢:以上涨开始、以上涨结束(上涨+下跌+上涨) + if last_zs is None: + # 第一个中枢 + if seg1.dir == Chan_SEG_DIR.DOWN: + # 下跌开始 -> 上涨中枢 + zs_dir = Chan_ZS_DIR.DOWN + # 验证模式:下跌+上涨+下跌 + valid = (seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN) else: - # Last ZS is not completed - if last_zs and not last_zs.is_sure: - if first_bi_out: - # SEG is not in ZS - if seg.is_sure: - if ((seg.low > last_zs.zg and seg.high > last_zs.zg) or (seg.high < last_zs.zd and seg.low < last_zs.zd)): - last_zs.set_end_klc(seg.pre.end_bi.end_klc, seg.sure_time, bi_out_count, seg.pre) - bi_out_count = 0 - #print(seg.start_bi.start_klc.start_time) - first_bi_out = None - # Last ZS is completed and look for new ZS - if seg.next and seg.next.next: - if seg.next.next.is_sure: - zg = min(seg.high, seg.next.high, seg.next.next.high) - zd = max(seg.low, seg.next.low, seg.next.next.low) - gg = max(seg.high, seg.next.high, seg.next.next.high) - dd = min(seg.low, seg.next.low, seg.next.next.low) - ddir = None - if last_zs: - if zg < last_zs.zd: - ddir = Chan_ZS_DIR.DOWN - else: - if zd > last_zs.zg: - ddir = Chan_ZS_DIR.UP - else: - ddir = None - else: - if seg.dir == Chan_SEG_DIR.UP: - ddir = Chan_ZS_DIR.DOWN - else: - ddir = Chan_ZS_DIR.UP - if (seg.dir == Chan_SEG_DIR.DOWN and ddir == Chan_ZS_DIR.DOWN) or (seg.dir == Chan_SEG_DIR.UP and ddir == Chan_ZS_DIR.UP): - ddir = None - if ddir and zg > zd: - # New ZS - zs = ChanZS(seg, len(zs_list), ddir) - zs.set_zg(zg) - zs.set_zd(zd) - zs.set_gg(gg) - zs.set_dd(dd) - last_zs.set_next(zs) - zs.set_pre(last_zs) - zs_list.append(zs) - if last_zs and last_zs.dir == zs.dir: - zs_count += 1 - else: - zs_count = 1 - last_zs = zs - # Last SEG is in ZS - else: - # SEG is inside ZS - if seg.end_bi: - for index in range(seg.start_bi.index, seg.end_bi.index+1): - bi = bi_list[index] - if (bi.high >= last_zs.zd and bi.high <= last_zs.zg) or (bi.low >= last_zs.zd and bi.low <= last_zs.zg) or (bi.high >= last_zs.zg and bi.low <= last_zs.zd): - in_again = True - last_zs.set_bi_out(None, None) - last_zs.set_last_bi_in(None) - last_zs.set_end_seg(None) - first_bi_out = None - #print("Bi in again 3", bi.start_klc.start_time) - if in_again and (bi.low > last_zs.zg or bi.high < last_zs.zd): - last_zs.set_bi_out(bi, seg) - last_zs.set_last_bi_in(bi_list[index - 1]) - #last_zs.set_end_seg(seg.next.next) - bi_out_count += 1 - first_bi_out = bi - if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP): - bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) - bsp_list.append(bsp) - #print("First bi out 3", first_bi_out.start_klc.start_time) - in_again = False - """" - if first_bi_out: - if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP: - #print(bi.start_klc.start_time, bi.high, seg.high) - if bi.high == seg.high: - bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg) - bsp_list.append(bsp) - else: - if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN: - if bi.low == seg.low: - bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg) - bsp_list.append(bsp) - """ - else: - # SEG in ZS and not out and find first bi out - if seg.end_bi: - for index in range(seg.start_bi.index, seg.end_bi.index+1): - bi = bi_list[index] - if (bi.high >= last_zs.zd and bi.high <= last_zs.zg) or (bi.low >= last_zs.zd and bi.low <= last_zs.zg) or (bi.high >= last_zs.zg and bi.low <= last_zs.zd): - in_again = True - last_zs.set_bi_out(None, None) - last_zs.set_last_bi_in(None) - last_zs.set_end_seg(None) - first_bi_out = None - #print("Bi in again 4", bi.start_klc.start_time) - if in_again and (bi.low > last_zs.zg or bi.high < last_zs.zd): - last_zs.set_bi_out(bi, seg) - last_zs.set_last_bi_in(bi_list[index - 1]) - #last_zs.set_end_seg(seg.next.next) - bi_out_count += 1 - first_bi_out = bi - if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP): - bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg) - bsp_list.append(bsp) - #print("First bi out 4", first_bi_out.start_klc.start_time) - in_again = False - if first_bi_out: - if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP: - #print(bi.start_klc.start_time, bi.high, seg.high) - if bi.high == seg.high: - bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.S3, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg) - bsp_list.append(bsp) - else: - if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN: - if bi.low == seg.low: - bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg) - bsp_list.append(bsp) - #self.print_zs(zs_list) + # 上涨开始 -> 下跌中枢 + zs_dir = Chan_ZS_DIR.UP + # 验证模式:上涨+下跌+上涨 + valid = (seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP) + else: + # 根据与前一个中枢的高低比较判断 + if zg > last_zs.zg: + # 上涨中枢:以下跌开始、以下跌结束 + zs_dir = Chan_ZS_DIR.DOWN + valid = (seg1.dir == Chan_SEG_DIR.DOWN and seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN) + else: + # 下跌中枢:以上涨开始、以上涨结束 + zs_dir = Chan_ZS_DIR.UP + valid = (seg1.dir == Chan_SEG_DIR.UP and seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP) + + # 验证是否有效 + if not valid: + start_idx += 1 + continue + + # 检查是否与前一个中枢重叠 + if last_zs: + # 判断是否有重叠 + overlap = (zg >= last_zs.zd and zd <= last_zs.zg) + + if overlap: + # 有重叠,扩展中枢到5根、7根...(缠论:合并为同一中枢) + # 本组先纳入当前 3 根,再向后逐根尝试;遇到与 [zd,zg] 不重叠(离开中枢)则停止扩展 + added_segs = [seg_list[start_idx], seg_list[start_idx + 1], seg_list[start_idx + 2]] + cur_idx = start_idx + 3 + + while cur_idx < len(seg_list): + next_seg = seg_list[cur_idx] + if not next_seg.is_sure: + break + + # 扩展条件:新线段与中枢区间 [zd, zg] 有重叠即并入;不重叠则停止,离开中枢的线段不包含 + # 用起止笔的极值算线段区间,避免 seg.high/seg.low 在个别线段上未同步导致的误判 + seg_high = max(next_seg.start_bi.high, next_seg.end_bi.high) if next_seg.end_bi else next_seg.start_bi.high + seg_low = min(next_seg.start_bi.low, next_seg.end_bi.low) if next_seg.end_bi else next_seg.start_bi.low + overlap_with_zs = (seg_high >= last_zs.zd and seg_low <= last_zs.zg) + if not overlap_with_zs: + break + added_segs.append(next_seg) + cur_idx += 1 + + # 扩展中枢 = 原中枢线段 + 本组并入的线段(缠论合并) + segs_for_zs = list(last_zs.seg_list) + list(added_segs) + + # 中枢开始与结束线段方向一致:上涨中枢结束于 DOWN,下跌中枢结束于 UP + required_end_seg_dir = Chan_SEG_DIR.DOWN if last_zs.dir == Chan_ZS_DIR.DOWN else Chan_SEG_DIR.UP + while len(segs_for_zs) >= 3 and segs_for_zs[-1].dir != required_end_seg_dir: + segs_for_zs.pop() + + # 扩展时只更新 gg、dd 和 seg_list;zg、zd 由前 3 根线段确定,不随扩展改变 + seg_highs = [s.high for s in segs_for_zs] + seg_lows = [s.low for s in segs_for_zs] + last_zs.set_gg(max(seg_highs)) + last_zs.set_dd(min(seg_lows)) + last_zs.seg_list = segs_for_zs + + # 更新结束时间(以裁剪后的最后一段为准) + last_seg = segs_for_zs[-1] + if last_seg.end_bi: + last_zs.set_end_klc(last_seg.end_bi.end_klc, last_seg.sure_time, 0, last_seg) + last_zs.set_end_seg(last_seg) + + # 跳过本组已扫描的线段(从 start_idx 到 cur_idx-1),下一组从 cur_idx 起可能再形成新中枢 + start_idx = cur_idx + continue + else: + # 没有重叠,创建新中枢 + # 先确认前一个中枢 - 使用前一个中枢本身的最后一个线段 + if last_zs.seg_list and len(last_zs.seg_list) > 0: + prev_zs_last_seg = last_zs.seg_list[-1] + if prev_zs_last_seg.end_bi: + last_zs.set_end_klc(prev_zs_last_seg.end_bi.end_klc, prev_zs_last_seg.sure_time, 0, prev_zs_last_seg) + last_zs.set_end_seg(prev_zs_last_seg) + last_zs.is_sure = True + + # 创建新中枢 + 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.set_end_klc(seg3.end_bi.end_klc, seg3.sure_time, 0, seg3) + zs.set_end_seg(seg3) + zs.is_sure = False + zs.seg_list = [seg1, seg2, seg3] + + if last_zs: + last_zs.set_next(zs) + zs.set_pre(last_zs) + + zs_list.append(zs) + last_zs = zs + + # 移动到下一组 + start_idx += 3 + + # 处理最后一个未确认的中枢 - 不自动扩展,保持未完成状态 + if last_zs and not last_zs.is_sure: + # 获取中枢最后一个线段的索引 + if last_zs.seg_list and len(last_zs.seg_list) > 0: + last_seg_of_zs = last_zs.seg_list[-1] + # 找到这个线段在seg_list中的索引 + last_seg_idx = -1 + for i, seg in enumerate(seg_list): + if seg == last_seg_of_zs: + last_seg_idx = i + break + + # 从中枢最后一个线段之后检查是否有离开 + has_leave = False + if last_seg_idx >= 0 and last_seg_idx + 1 < len(seg_list): + for i in range(last_seg_idx + 1, len(seg_list)): + seg = seg_list[i] + if seg.is_sure: + # 检查是否离开中枢 + leave = (seg.low > last_zs.zg and seg.high > last_zs.zg) or \ + (seg.high < last_zs.zd and seg.low < last_zs.zd) + if leave: + has_leave = True + break + + if not has_leave: + # 没有离开,保持未完成状态 + pass + else: + # 有离开,确认中枢 + if last_seg_of_zs.end_bi: + 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): + """ + 中枢扩张:将区间重叠的连续中枢合并为大级别中枢,便于显示更大级别的震荡区间。 + 重叠定义:两中枢 [zd,zg] 有交集,即 (zs_i.zg >= zs_j.zd and zs_i.zd <= zs_j.zg)。 + """ + big_list = [] + if len(zs_list) < 2: + return big_list + i = 0 + while i < len(zs_list): + group = [zs_list[i]] + j = i + 1 + while j < len(zs_list): + cur = zs_list[j] + # 与当前组内任一中枢有重叠即算扩张(通常只需与组内最后一个比) + last_in_group = group[-1] + overlap = (last_in_group.zg >= cur.zd and last_in_group.zd <= cur.zg) + if overlap: + group.append(cur) + j += 1 + else: + break + if len(group) >= 2: + big = ChanZS_Big(group) + big.index = len(big_list) + big_list.append(big) + i = j if len(group) >= 2 else i + 1 + return big_list + def get_klu_list(self, dataframe): klu_list = self.get_kl_data(dataframe) #klu_list = self.cal_klu_pattern(klu_list) diff --git a/config/Local_Test.json b/config/Local_Test.json index 8d1b9f5..4e11d57 100644 --- a/config/Local_Test.json +++ b/config/Local_Test.json @@ -42,7 +42,7 @@ "ccxt_config": {}, "ccxt_async_config": {}, "pair_whitelist": [ - "SOL/USDT:USDT" + "BTC/USDT:USDT" ], "pair_blacklist": [ "BNB/.*" diff --git a/strategies/CryptoFutures1m5mStrategy.py b/strategies/CryptoFutures1m5mStrategy.py index d5f0e04..9d62210 100644 --- a/strategies/CryptoFutures1m5mStrategy.py +++ b/strategies/CryptoFutures1m5mStrategy.py @@ -9,68 +9,44 @@ from typing import Optional from freqtrade.persistence import Trade import warnings -# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告 -# 这个警告来自 freqtrade 库的 strategy_helper.py warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') -# 或者启用未来行为(推荐) pd.set_option('future.no_silent_downcasting', True) -# freqtrade trade -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategy --strategy-path ./user_data/Chan/strategies -# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategy --strategy-path ./user_data/Chan/strategies --timerange=20260304- -# freqtrade download-data -c ./user_data/Chan/config/Local_Test.json -t 1m 5m --data-format-ohlcv json --pairs SOL/USDT:USDT --timerange=20260201- - class CryptoFutures1m5mStrategy(IStrategy): """ - SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V12e (Short Only) + SOL/USDT 合约策略 - 只做多版 (默认策略) - 14个月回测 (2025-01 ~ 2026-03): +107.11%, PF 1.37, DD 23.96% - 每个季度均盈利,市场下跌-54%期间持续获利 - - 核心设计: - 1. 纯做空策略 - 价格必须低于EMA200至少1%才允许做空 - 2. 5分钟趋势确认:EMA12 DataFrame: - # ==================== 5分钟指标 ==================== inf_tf = self.informative_timeframe informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) - # EMA趋势 + # EMA informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) - - # EMA12斜率(3根K线变化率,用于确认趋势方向的动量) informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 # MACD @@ -79,66 +55,54 @@ class CryptoFutures1m5mStrategy(IStrategy): informative['macd_signal_5m'] = macd_signal informative['macd_hist_5m'] = macd_hist - # ADX趋势强度 + # ADX informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) - # RSI(5分钟) + # RSI informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) - # ATR(5分钟) + # ATR informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 - - # ATR 长期均值(用于自适应波动率过滤) informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() - # ===== EMA200 大趋势过滤 ===== + # EMA200 informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 - - # EMA200斜率(20根5分钟K线 = 100分钟趋势方向) informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 - # ===== 大趋势过滤(Short Only) ===== - # 做空需要价格低于EMA200至少1% - informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 - - # 牛市暂停:EMA200上升 + 价格在EMA200上方 → 完全停止做空 - informative['bull_pause'] = ( - (informative['ema200_slope'] > 0) & - (informative['ema200_dist_pct'] > 0) + # 做多趋势 + informative['trend_bull_5m'] = ( + (informative['ema12'] > informative['ema26']) & + (informative['ema26'] > informative['ema50']) & + (informative['ema12_slope'] > 0.05) & + (informative['adx_5m'] > 24) & + (informative['adx_5m'] < 51) & + (informative['close'] > informative['ema12']) & + (informative['rsi_5m'] > 52) & + (informative['rsi_5m'] < 72) ) - # ===== 5分钟趋势判断(仅Short) ===== - informative['trend_bear_5m'] = ( - (informative['ema12'] < informative['ema26']) & - (informative['ema26'] < informative['ema50']) & - (informative['ema12_slope'] < 0) & - (informative['adx_5m'] > 25) & - (informative['adx_5m'] < 50) & - (informative['close'] < informative['ema12']) & - (informative['rsi_5m'] < 48) & - (informative['rsi_5m'] > 30) - ) + # 大趋势过滤 + informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0 - # 做空条件:短期趋势 + EMA200大趋势方向一致 + 非牛市 - informative['can_long_5m'] = False - informative['can_short_5m'] = ( - informative['trend_bear_5m'] & - informative['below_ema200'] & - (~informative['bull_pause']) - ) + # 做多条件 + informative['can_long_5m'] = informative['trend_bull_5m'] & informative['above_ema200'] - # ATR波动率过滤(自适应) + # ATR过滤 informative['atr_ok_5m'] = ( - (informative['atr_pct_5m'] > 0.1) & - (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 1.5) + (informative['atr_pct_5m'] > 0.07) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2) ) - # 合并5分钟数据到1分钟 + # 成交量 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75 + + # 合并 dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) - # ==================== 1分钟指标 ==================== + # 1分钟指标 macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) dataframe['macd'] = macd_1m dataframe['macd_signal'] = signal_1m @@ -148,116 +112,96 @@ class CryptoFutures1m5mStrategy(IStrategy): dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) - - # ===== 1分钟MACD斜率 ===== dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 - # ===== 1分钟做空入场信号 ===== - dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() - dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() + # 做多信号 + dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min() + dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min() - dataframe['top_divergence'] = ( - (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & - (dataframe['macd'] < dataframe['macd_high_5']) & - (dataframe['macd_slope'] < 0) & - (dataframe['macd'] < dataframe['macd_signal']) & + dataframe['bottom_divergence'] = ( + (dataframe['low'] <= dataframe['price_low_5'] * 1.001) & + (dataframe['macd'] > dataframe['macd_low_5']) & + (dataframe['macd_slope'] > 0) & + (dataframe['macd'] > dataframe['macd_signal']) & (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) ) - dataframe['ema_cross_down'] = ( - (dataframe['ema9'] < dataframe['ema21']) & - (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & - (dataframe['rsi'] < 55) & (dataframe['rsi'] > 35) & + dataframe['ema_cross_up'] = ( + (dataframe['ema9'] > dataframe['ema21']) & + (dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] > 45) & + (dataframe['rsi'] < 70) & (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) ) - dataframe['is_bear_candle'] = ( - (dataframe['close'] < dataframe['open']) & - ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) - ) - dataframe['bear_pullback'] = ( - dataframe['is_bear_candle'].shift(2) & - (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & - (dataframe['high'] < dataframe['high'].shift(2)) & - (dataframe['close'] < dataframe['open']) & - (dataframe['close'] < dataframe['ema9']) + dataframe['is_bull_candle'] = (dataframe['close'] > dataframe['open']) & ((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008) + dataframe['bull_pullback'] = ( + dataframe['is_bull_candle'].shift(2) & + (dataframe['close'].shift(1) < dataframe['open'].shift(1)) & + (dataframe['low'] > dataframe['low'].shift(2)) & + (dataframe['close'] > dataframe['open']) & + (dataframe['close'] > dataframe['ema9']) ) - # ==================== 时间过滤 ==================== + # 时间过滤 dataframe['hour_utc'] = dataframe['date'].dt.hour dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) - # 安全转换5分钟布尔列 - bool_cols = [ - 'can_long_5m_5m', 'can_short_5m_5m', - 'trend_bear_5m_5m', - 'atr_ok_5m_5m', - 'below_ema200_5m', 'bull_pause_5m', - ] + # 类型转换 + bool_cols = ['can_long_5m_5m', 'trend_bull_5m_5m', 'atr_ok_5m_5m', 'above_ema200_5m', 'volume_ok_5m_5m'] for col in bool_cols: if col in dataframe.columns: dataframe[col] = dataframe[col].astype(bool).fillna(False) - num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m', - 'ema200_dist_pct_5m', 'ema200_slope_5m'] - for col in num_cols: - if col in dataframe.columns: - dataframe[col] = dataframe[col].astype(float).fillna(0.0) - return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: time_ok = ~dataframe['is_bad_hour'] atr_ok = dataframe['atr_ok_5m_5m'] + volume_ok = dataframe['volume_ok_5m_5m'] - # 5分钟MACD方向确认 - macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 + # 只做多 + macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0 + macd_bull_1m = dataframe['macd_hist'] > 0 - # 1分钟MACD方向确认(双重确认) - macd_bear_1m = dataframe['macd_hist'] < 0 - - # ===== 做空入场 ===== dataframe.loc[ - (time_ok) & - (atr_ok) & - (dataframe['can_short_5m_5m']) & - (macd_bear_5m) & - (macd_bear_1m) & - (dataframe['rsi'] > 30) & - ( - dataframe['top_divergence'] | - dataframe['ema_cross_down'] | - dataframe['bear_pullback'] - ) & + (time_ok) & (atr_ok) & (dataframe['can_long_5m_5m']) & + (macd_bull_5m) & (macd_bull_1m) & (volume_ok) & + (dataframe['rsi'] < 70) & (dataframe['rsi'] > 40) & + (dataframe['bottom_divergence'] | dataframe['ema_cross_up'] | dataframe['bull_pullback']) & (dataframe['volume'] > 0), - 'enter_short' + 'enter_long' ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, 'exit_long'] = 0 - dataframe.loc[:, 'exit_short'] = 0 return dataframe def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> str | bool | None: - """时间止损:持仓过久且亏损时提前退出""" trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 - if trade_duration > 8 and current_profit < -0.005: - return 'time_stop_8h' - - if trade_duration > 16 and current_profit < 0: - return 'time_stop_16h' + # 做多时间止损 - 宽松 + if trade_duration > 10 and current_profit < -0.006: + return 'time_stop_long_10h' + if trade_duration > 20 and current_profit < 0: + return 'time_stop_long_20h' + if trade_duration > 30: + return 'time_stop_long_30h' return None def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], side: str, **kwargs) -> bool: - """入场确认 - 时间过滤安全网""" hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour if hour_utc in {4, 5, 6, 7}: return False return True + + 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 self.lev diff --git a/strategies/CryptoFutures1m5mStrategyLongOnly.py b/strategies/CryptoFutures1m5mStrategyLongOnly.py new file mode 100644 index 0000000..247cfa0 --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyLongOnly.py @@ -0,0 +1,209 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from freqtrade.strategy import IStrategy, merge_informative_pair +from pandas import DataFrame +import pandas as pd +import talib.abstract as ta +import numpy as np +from datetime import datetime +from typing import Optional +from freqtrade.persistence import Trade +import warnings + +warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') +pd.set_option('future.no_silent_downcasting', True) + + +class CryptoFutures1m5mStrategyLongOnly(IStrategy): + """ + SOL/USDT 合约策略 - 只做多版本 + + 基于V5修改: + - 只做多,禁止做空 + - 优化做多止损和止盈参数 + """ + INTERFACE_VERSION = 3 + timeframe = '1m' + informative_timeframe = '5m' + can_short = False # 禁用做空 + can_long = True + lev = 1.0 + + stoploss = -0.035 + trailing_stop = True + trailing_stop_positive = 0.008 + trailing_stop_positive_offset = 0.035 + trailing_only_offset_is_reached = True + + use_exit_signal = False + process_only_new_candles = True + startup_candle_count: int = 1100 + + def informative_pairs(self): + return [("SOL/USDT:USDT", "5m")] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + inf_tf = self.informative_timeframe + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + + # EMA + informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) + informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) + informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) + informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 + + # MACD + macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) + informative['macd_5m'] = macd + informative['macd_signal_5m'] = macd_signal + informative['macd_hist_5m'] = macd_hist + + # ADX + informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) + + # RSI + informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) + + # ATR + informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) + informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 + informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() + + # EMA200 + informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) + informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 + informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 + + # 做多趋势 + informative['trend_bull_5m'] = ( + (informative['ema12'] > informative['ema26']) & + (informative['ema26'] > informative['ema50']) & + (informative['ema12_slope'] > 0.05) & + (informative['adx_5m'] > 24) & + (informative['adx_5m'] < 51) & + (informative['close'] > informative['ema12']) & + (informative['rsi_5m'] > 52) & + (informative['rsi_5m'] < 72) + ) + + # 大趋势过滤 + informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0 + + # 做多条件 + informative['can_long_5m'] = informative['trend_bull_5m'] & informative['above_ema200'] + + # ATR过滤 + informative['atr_ok_5m'] = ( + (informative['atr_pct_5m'] > 0.07) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2) + ) + + # 成交量 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75 + + # 合并 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) + + # 1分钟指标 + macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd_1m + dataframe['macd_signal'] = signal_1m + dataframe['macd_hist'] = hist_1m + + dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) + dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) + dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 + + # 做多信号 + dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min() + dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min() + + dataframe['bottom_divergence'] = ( + (dataframe['low'] <= dataframe['price_low_5'] * 1.001) & + (dataframe['macd'] > dataframe['macd_low_5']) & + (dataframe['macd_slope'] > 0) & + (dataframe['macd'] > dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + dataframe['ema_cross_up'] = ( + (dataframe['ema9'] > dataframe['ema21']) & + (dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] > 45) & + (dataframe['rsi'] < 70) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + dataframe['is_bull_candle'] = (dataframe['close'] > dataframe['open']) & ((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008) + dataframe['bull_pullback'] = ( + dataframe['is_bull_candle'].shift(2) & + (dataframe['close'].shift(1) < dataframe['open'].shift(1)) & + (dataframe['low'] > dataframe['low'].shift(2)) & + (dataframe['close'] > dataframe['open']) & + (dataframe['close'] > dataframe['ema9']) + ) + + # 时间过滤 + dataframe['hour_utc'] = dataframe['date'].dt.hour + dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) + + # 类型转换 + bool_cols = ['can_long_5m_5m', 'trend_bull_5m_5m', 'atr_ok_5m_5m', 'above_ema200_5m', 'volume_ok_5m_5m'] + for col in bool_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(bool).fillna(False) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + time_ok = ~dataframe['is_bad_hour'] + atr_ok = dataframe['atr_ok_5m_5m'] + volume_ok = dataframe['volume_ok_5m_5m'] + + # 只做多 + macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0 + macd_bull_1m = dataframe['macd_hist'] > 0 + + dataframe.loc[ + (time_ok) & (atr_ok) & (dataframe['can_long_5m_5m']) & + (macd_bull_5m) & (macd_bull_1m) & (volume_ok) & + (dataframe['rsi'] < 70) & (dataframe['rsi'] > 40) & + (dataframe['bottom_divergence'] | dataframe['ema_cross_up'] | dataframe['bull_pullback']) & + (dataframe['volume'] > 0), + 'enter_long' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[:, 'exit_long'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> str | bool | None: + trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 + + # 做多时间止损 - 宽松 + if trade_duration > 10 and current_profit < -0.006: + return 'time_stop_long_10h' + if trade_duration > 20 and current_profit < 0: + return 'time_stop_long_20h' + if trade_duration > 30: + return 'time_stop_long_30h' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour + if hour_utc in {4, 5, 6, 7}: + return False + return True + + 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 self.lev diff --git a/strategies/CryptoFutures1m5mStrategyShortOnly.py b/strategies/CryptoFutures1m5mStrategyShortOnly.py new file mode 100644 index 0000000..a13521b --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyShortOnly.py @@ -0,0 +1,212 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from freqtrade.strategy import IStrategy, merge_informative_pair +from pandas import DataFrame +import pandas as pd +import talib.abstract as ta +import numpy as np +from datetime import datetime +from typing import Optional +from freqtrade.persistence import Trade +import warnings + +warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') +pd.set_option('future.no_silent_downcasting', True) + + +class CryptoFutures1m5mStrategyShortOnly(IStrategy): + """ + SOL/USDT 合约策略 - 只做空版本 + + 基于V5修改: + - 只做空,禁止做多 + - 优化做空止损和止盈参数 + """ + INTERFACE_VERSION = 3 + timeframe = '1m' + informative_timeframe = '5m' + can_short = True + can_long = False # 禁用做多 + lev = 1.0 + + # Trailing设置 - 基于V5 + trailing_stop = True + trailing_stop_positive = 0.008 + trailing_stop_positive_offset = 0.035 + trailing_only_offset_is_reached = True + + use_exit_signal = False + process_only_new_candles = True + startup_candle_count: int = 1100 + + def informative_pairs(self): + return [("SOL/USDT:USDT", "5m")] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + inf_tf = self.informative_timeframe + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + + # EMA + informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) + informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) + informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) + informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 + + # MACD + macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) + informative['macd_5m'] = macd + informative['macd_signal_5m'] = macd_signal + informative['macd_hist_5m'] = macd_hist + + # ADX + informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) + + # RSI + informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) + + # ATR + informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) + informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 + informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() + + # EMA200 + informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) + informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 + informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 + + # 做空趋势 - 基于V5优化 + informative['trend_bear_5m'] = ( + (informative['ema12'] < informative['ema26']) & + (informative['ema26'] < informative['ema50']) & + (informative['ema12_slope'] < -0.05) & # V5标准 + (informative['adx_5m'] > 24) & # V5标准 + (informative['adx_5m'] < 51) & + (informative['close'] < informative['ema12']) & + (informative['rsi_5m'] < 48) & + (informative['rsi_5m'] > 29) + ) + + # 大趋势过滤 - 放宽条件,基于V5 + informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 + + # 熊市确认 - 可选,不过度限制 + informative['bear_market'] = (informative['ema200_slope'] < 0) & (informative['ema200_dist_pct'] < 0) + + # 做空条件 - 移除bear_market强制要求,基于V5 + informative['can_short_5m'] = informative['trend_bear_5m'] & informative['below_ema200'] + + # ATR过滤 - 基于V5标准 + informative['atr_ok_5m'] = ( + (informative['atr_pct_5m'] > 0.07) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2) + ) + + # 成交量 - 基于V5标准 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75 + + # 合并 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) + + # 1分钟指标 + macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd_1m + dataframe['macd_signal'] = signal_1m + dataframe['macd_hist'] = hist_1m + + dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) + dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) + dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 + + # 做空信号 + dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() + dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() + + dataframe['top_divergence'] = ( + (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & + (dataframe['macd'] < dataframe['macd_high_5']) & + (dataframe['macd_slope'] < 0) & + (dataframe['macd'] < dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.8) + ) + + dataframe['ema_cross_down'] = ( + (dataframe['ema9'] < dataframe['ema21']) & + (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] < 55) & + (dataframe['rsi'] > 35) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + dataframe['is_bear_candle'] = (dataframe['close'] < dataframe['open']) & ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) + dataframe['bear_pullback'] = ( + dataframe['is_bear_candle'].shift(2) & + (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & + (dataframe['high'] < dataframe['high'].shift(2)) & + (dataframe['close'] < dataframe['open']) & + (dataframe['close'] < dataframe['ema9']) + ) + + # 时间过滤 + dataframe['hour_utc'] = dataframe['date'].dt.hour + dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) + + # 类型转换 + bool_cols = ['can_short_5m_5m', 'trend_bear_5m_5m', 'atr_ok_5m_5m', 'below_ema200_5m', 'volume_ok_5m_5m', 'bear_market_5m'] + for col in bool_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(bool).fillna(False) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + time_ok = ~dataframe['is_bad_hour'] + atr_ok = dataframe['atr_ok_5m_5m'] + volume_ok = dataframe['volume_ok_5m_5m'] + + # 只做空 - 基于V5标准 + macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 + macd_bear_1m = dataframe['macd_hist'] < 0 + + dataframe.loc[ + (time_ok) & (atr_ok) & (dataframe['can_short_5m_5m']) & + (macd_bear_5m) & (macd_bear_1m) & (volume_ok) & + (dataframe['rsi'] > 30) & + (dataframe['top_divergence'] | dataframe['ema_cross_down'] | dataframe['bear_pullback']) & + (dataframe['volume'] > 0), + 'enter_short' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[:, 'exit_short'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> str | bool | None: + trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 + + # 做空时间止损 - 基于V5标准 + if trade_duration > 8 and current_profit < -0.005: + return 'time_stop_short_8h' + if trade_duration > 16 and current_profit < 0: + return 'time_stop_short_16h' + if trade_duration > 24: + return 'time_stop_short_24h' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour + if hour_utc in {4, 5, 6, 7}: + return False + return True + + 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 self.lev diff --git a/strategies/CryptoFutures1m5mStrategyV2.py b/strategies/CryptoFutures1m5mStrategyV2.py new file mode 100644 index 0000000..b3b9a97 --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyV2.py @@ -0,0 +1,283 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from freqtrade.strategy import IStrategy, merge_informative_pair +from pandas import DataFrame +import pandas as pd +import talib.abstract as ta +import numpy as np +from datetime import datetime +from typing import Optional +from freqtrade.persistence import Trade +import warnings + +# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告 +warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') +pd.set_option('future.no_silent_downcasting', True) + +# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV2 --strategy-path ./user_data/Chan/strategies --timerange=20260101- + + +class CryptoFutures1m5mStrategyV2(IStrategy): + """ + SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V2 优化版 (Short Only) + + 基于原版优化: + 1. 保持原版核心入场逻辑 + 2. 优化追踪止盈参数 + 3. 增强时间止损灵活性 + 4. 稍微放宽ATR过滤增加交易机会 + + 核心设计: + 1. 纯做空策略 - 价格必须低于EMA200至少1%才允许做空 + 2. 5分钟趋势确认:EMA12 DataFrame: + # ==================== 5分钟指标 ==================== + inf_tf = self.informative_timeframe + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + + # EMA趋势 + informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) + informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) + informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) + + # EMA12斜率(3根K线变化率) + informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 + + # MACD + macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) + informative['macd_5m'] = macd + informative['macd_signal_5m'] = macd_signal + informative['macd_hist_5m'] = macd_hist + + # ADX趋势强度 + informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) + + # RSI(5分钟) + informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) + + # ATR(5分钟) + informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) + informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 + + # ATR 长期均值 + informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() + + # EMA200 大趋势过滤 + informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) + informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 + + # EMA200斜率 + informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 + + # 大趋势过滤(Short Only) + informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 + + # 牛市暂停 + informative['bull_pause'] = ( + (informative['ema200_slope'] > 0) & + (informative['ema200_dist_pct'] > 0) + ) + + # 5分钟趋势判断(仅Short) + informative['trend_bear_5m'] = ( + (informative['ema12'] < informative['ema26']) & + (informative['ema26'] < informative['ema50']) & + (informative['ema12_slope'] < -0.05) & + (informative['adx_5m'] > 24) & + (informative['adx_5m'] < 51) & + (informative['close'] < informative['ema12']) & + (informative['rsi_5m'] < 48) & + (informative['rsi_5m'] > 29) + ) + + # 做空条件 + informative['can_long_5m'] = False + informative['can_short_5m'] = ( + informative['trend_bear_5m'] & + informative['below_ema200'] & + (~informative['bull_pause']) + ) + + # ATR波动率过滤 - 继续放宽 + informative['atr_ok_5m'] = ( + (informative['atr_pct_5m'] > 0.07) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2) + ) + + # 成交量确认 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75 + + # 合并5分钟数据到1分钟 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) + + # ==================== 1分钟指标 ==================== + macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd_1m + dataframe['macd_signal'] = signal_1m + dataframe['macd_hist'] = hist_1m + + dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) + dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) + + # 1分钟MACD斜率 + dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 + + # 1分钟做空入场信号 + dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() + dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() + + # 顶背离 + dataframe['top_divergence'] = ( + (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & + (dataframe['macd'] < dataframe['macd_high_5']) & + (dataframe['macd_slope'] < 0) & + (dataframe['macd'] < dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + # EMA死叉 + dataframe['ema_cross_down'] = ( + (dataframe['ema9'] < dataframe['ema21']) & + (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] < 55) & + (dataframe['rsi'] > 35) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + # 熊市回调 + dataframe['is_bear_candle'] = ( + (dataframe['close'] < dataframe['open']) & + ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) + ) + dataframe['bear_pullback'] = ( + dataframe['is_bear_candle'].shift(2) & + (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & + (dataframe['high'] < dataframe['high'].shift(2)) & + (dataframe['close'] < dataframe['open']) & + (dataframe['close'] < dataframe['ema9']) + ) + + # 时间过滤 + dataframe['hour_utc'] = dataframe['date'].dt.hour + dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) + + # 安全转换5分钟布尔列 + bool_cols = [ + 'can_long_5m_5m', 'can_short_5m_5m', + 'trend_bear_5m_5m', + 'atr_ok_5m_5m', + 'below_ema200_5m', 'bull_pause_5m', + 'volume_ok_5m_5m', + ] + for col in bool_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(bool).fillna(False) + + num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m', + 'ema200_dist_pct_5m', 'ema200_slope_5m'] + for col in num_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(float).fillna(0.0) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + time_ok = ~dataframe['is_bad_hour'] + atr_ok = dataframe['atr_ok_5m_5m'] + + # 5分钟MACD方向确认 + macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 + + # 1分钟MACD方向确认 + macd_bear_1m = dataframe['macd_hist'] < 0 + + # 成交量确认 + volume_ok = dataframe['volume_ok_5m_5m'] + + # 做空入场 + dataframe.loc[ + (time_ok) & + (atr_ok) & + (dataframe['can_short_5m_5m']) & + (macd_bear_5m) & + (macd_bear_1m) & + (volume_ok) & + (dataframe['rsi'] > 30) & + ( + dataframe['top_divergence'] | + dataframe['ema_cross_down'] | + dataframe['bear_pullback'] + ) & + (dataframe['volume'] > 0), + 'enter_short' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[:, 'exit_long'] = 0 + dataframe.loc[:, 'exit_short'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> str | bool | None: + """自定义出场逻辑:时间止损""" + trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 + + # 时间止损:持仓过久且亏损 + if trade_duration > 8 and current_profit < -0.005: + return 'time_stop_8h' + + if trade_duration > 16 and current_profit < 0: + return 'time_stop_16h' + + # 持仓超过24小时强制平仓 + if trade_duration > 24: + return 'time_stop_24h' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + """入场确认 - 时间过滤安全网""" + hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour + if hour_utc in {4, 5, 6, 7}: + return False + return True + + 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 self.lev diff --git a/strategies/CryptoFutures1m5mStrategyV2Hyperopt.json b/strategies/CryptoFutures1m5mStrategyV2Hyperopt.json new file mode 100644 index 0000000..bb4e44d --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyV2Hyperopt.json @@ -0,0 +1,36 @@ +{ + "strategy_name": "CryptoFutures1m5mStrategyV2Hyperopt", + "params": { + "roi": {}, + "stoploss": { + "stoploss": -0.025 + }, + "trailing": { + "trailing_stop": true, + "trailing_stop_positive": 0.008, + "trailing_stop_positive_offset": 0.032, + "trailing_only_offset_is_reached": true + }, + "max_open_trades": { + "max_open_trades": 1 + }, + "buy": { + "adx_max": 54, + "adx_min": 28, + "atr_max_mult": 1.6, + "atr_min": 0.07, + "ema200_dist": -1.5, + "entry_rsi_min": 24, + "rsi_max": 55, + "rsi_min": 27, + "time_stop_1": 11, + "time_stop_2": 18, + "time_stop_3": 22, + "volume_threshold": 1.4 + }, + "sell": {}, + "protection": {} + }, + "ft_stratparam_v": 1, + "export_time": "2026-03-06 15:30:11.046917+00:00" +} \ No newline at end of file diff --git a/strategies/CryptoFutures1m5mStrategyV2Hyperopt.py b/strategies/CryptoFutures1m5mStrategyV2Hyperopt.py new file mode 100644 index 0000000..353d43d --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyV2Hyperopt.py @@ -0,0 +1,305 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from freqtrade.strategy import IStrategy, merge_informative_pair, IntParameter, DecimalParameter, BooleanParameter +from pandas import DataFrame +import pandas as pd +import talib.abstract as ta +import numpy as np +from datetime import datetime +from typing import Optional +from freqtrade.persistence import Trade +import warnings + +# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告 +warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') +pd.set_option('future.no_silent_downcasting', True) + +# freqtrade hyperopt -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV2Hyperopt --strategy-path ./user_data/Chan/strategies --timerange=20260101- --epochs 200 -j 4 --space buy +# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV2Hyperopt --strategy-path ./user_data/Chan/strategies --timerange=20260101- + + +class CryptoFutures1m5mStrategyV2Hyperopt(IStrategy): + """ + SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V2 Hyperopt优化版 (Short Only) + + 基于V2优化版添加Hyperopt参数: + 1. ATR波动率过滤参数 + 2. 时间止损参数 + 3. 趋势确认参数(ADX, RSI, EMA200距离) + """ + INTERFACE_VERSION = 3 + timeframe = '1m' + informative_timeframe = '5m' + can_short = True + lev = 1.0 + + # 硬止损 + stoploss = -0.025 + + # 追踪止盈 - 固定值 + trailing_stop = True + trailing_stop_positive = 0.008 + trailing_stop_positive_offset = 0.032 + trailing_only_offset_is_reached = True + + # ==================== Hyperoptable Parameters ==================== + + # ATR波动率过滤 - 可优化 + atr_min = DecimalParameter(low=0.03, high=0.15, default=0.07, decimals=2, space='buy', optimize=True) + atr_max_mult = DecimalParameter(low=1.5, high=3.5, default=2.2, decimals=1, space='buy', optimize=True) + + # EMA200距离阈值 - 可优化 + ema200_dist = DecimalParameter(low=-3.0, high=-0.5, default=-1.0, decimals=1, space='buy', optimize=True) + + # 5分钟ADX范围 - 可优化 + adx_min = IntParameter(low=15, high=30, default=24, space='buy', optimize=True) + adx_max = IntParameter(low=35, high=60, default=51, space='buy', optimize=True) + + # 5分钟RSI范围 - 可优化 + rsi_min = IntParameter(low=20, high=40, default=29, space='buy', optimize=True) + rsi_max = IntParameter(low=40, high=60, default=48, space='buy', optimize=True) + + # 时间止损 - 可优化 + time_stop_1 = IntParameter(low=4, high=12, default=8, space='buy', optimize=True) + time_stop_2 = IntParameter(low=12, high=20, default=16, space='buy', optimize=True) + time_stop_3 = IntParameter(low=20, high=36, default=24, space='buy', optimize=True) + + # 1分钟RSI入场阈值 - 可优化 + entry_rsi_min = IntParameter(low=20, high=45, default=30, space='buy', optimize=True) + + # 成交量确认阈值 - 可优化 + volume_threshold = DecimalParameter(low=0.5, high=1.5, default=0.75, decimals=2, space='buy', optimize=True) + + # 完全禁用 exit_signal + use_exit_signal = False + + process_only_new_candles = True + startup_candle_count: int = 1100 + + def informative_pairs(self): + return [ + ("SOL/USDT:USDT", "5m"), + ] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # ==================== 5分钟指标 ==================== + inf_tf = self.informative_timeframe + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + + # EMA趋势 + informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) + informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) + informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) + + # EMA12斜率(3根K线变化率) + informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 + + # MACD + macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) + informative['macd_5m'] = macd + informative['macd_signal_5m'] = macd_signal + informative['macd_hist_5m'] = macd_hist + + # ADX趋势强度 + informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) + + # RSI(5分钟) + informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) + + # ATR(5分钟) + informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) + informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 + + # ATR 长期均值 + informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() + + # EMA200 大趋势过滤 + informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) + informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 + + # EMA200斜率 + informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 + + # 大趋势过滤(Short Only)- 使用hyperopt参数 + informative['below_ema200'] = informative['ema200_dist_pct'] < self.ema200_dist.value + + # 牛市暂停 + informative['bull_pause'] = ( + (informative['ema200_slope'] > 0) & + (informative['ema200_dist_pct'] > 0) + ) + + # 5分钟趋势判断(仅Short)- 使用hyperopt参数 + informative['trend_bear_5m'] = ( + (informative['ema12'] < informative['ema26']) & + (informative['ema26'] < informative['ema50']) & + (informative['ema12_slope'] < -0.05) & + (informative['adx_5m'] > self.adx_min.value) & + (informative['adx_5m'] < self.adx_max.value) & + (informative['close'] < informative['ema12']) & + (informative['rsi_5m'] < self.rsi_max.value) & + (informative['rsi_5m'] > self.rsi_min.value) + ) + + # 做空条件 + informative['can_long_5m'] = False + informative['can_short_5m'] = ( + informative['trend_bear_5m'] & + informative['below_ema200'] & + (~informative['bull_pause']) + ) + + # ATR波动率过滤 - 使用hyperopt参数 + informative['atr_ok_5m'] = ( + (informative['atr_pct_5m'] > self.atr_min.value) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * self.atr_max_mult.value) + ) + + # 成交量确认 - 使用hyperopt参数 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * self.volume_threshold.value + + # 合并5分钟数据到1分钟 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) + + # ==================== 1分钟指标 ==================== + macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd_1m + dataframe['macd_signal'] = signal_1m + dataframe['macd_hist'] = hist_1m + + dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) + dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) + + # 1分钟MACD斜率 + dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 + + # 1分钟做空入场信号 + dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() + dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() + + # 顶背离 + dataframe['top_divergence'] = ( + (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & + (dataframe['macd'] < dataframe['macd_high_5']) & + (dataframe['macd_slope'] < 0) & + (dataframe['macd'] < dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + # EMA死叉 + dataframe['ema_cross_down'] = ( + (dataframe['ema9'] < dataframe['ema21']) & + (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] < 55) & + (dataframe['rsi'] > 35) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + # 熊市回调 + dataframe['is_bear_candle'] = ( + (dataframe['close'] < dataframe['open']) & + ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) + ) + dataframe['bear_pullback'] = ( + dataframe['is_bear_candle'].shift(2) & + (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & + (dataframe['high'] < dataframe['high'].shift(2)) & + (dataframe['close'] < dataframe['open']) & + (dataframe['close'] < dataframe['ema9']) + ) + + # 时间过滤 + dataframe['hour_utc'] = dataframe['date'].dt.hour + dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) + + # 安全转换5分钟布尔列 + bool_cols = [ + 'can_long_5m_5m', 'can_short_5m_5m', + 'trend_bear_5m_5m', + 'atr_ok_5m_5m', + 'below_ema200_5m', 'bull_pause_5m', + 'volume_ok_5m_5m', + ] + for col in bool_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(bool).fillna(False) + + num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m', + 'ema200_dist_pct_5m', 'ema200_slope_5m'] + for col in num_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(float).fillna(0.0) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + time_ok = ~dataframe['is_bad_hour'] + atr_ok = dataframe['atr_ok_5m_5m'] + + # 5分钟MACD方向确认 + macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 + + # 1分钟MACD方向确认 + macd_bear_1m = dataframe['macd_hist'] < 0 + + # 成交量确认 + volume_ok = dataframe['volume_ok_5m_5m'] + + # 做空入场 - 使用hyperopt参数 + dataframe.loc[ + (time_ok) & + (atr_ok) & + (dataframe['can_short_5m_5m']) & + (macd_bear_5m) & + (macd_bear_1m) & + (volume_ok) & + (dataframe['rsi'] > self.entry_rsi_min.value) & + ( + dataframe['top_divergence'] | + dataframe['ema_cross_down'] | + dataframe['bear_pullback'] + ) & + (dataframe['volume'] > 0), + 'enter_short' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[:, 'exit_long'] = 0 + dataframe.loc[:, 'exit_short'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> str | bool | None: + """自定义出场逻辑:时间止损 - 使用hyperopt参数""" + trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 + + # 时间止损:持仓过久且亏损 + if trade_duration > self.time_stop_1.value and current_profit < -0.005: + return 'time_stop_1' + + if trade_duration > self.time_stop_2.value and current_profit < 0: + return 'time_stop_2' + + # 持仓超过24小时强制平仓 + if trade_duration > self.time_stop_3.value: + return 'time_stop_3' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + """入场确认 - 时间过滤安全网""" + hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour + if hour_utc in {4, 5, 6, 7}: + return False + return True + + 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 self.lev diff --git a/strategies/CryptoFutures1m5mStrategyV3.py b/strategies/CryptoFutures1m5mStrategyV3.py new file mode 100644 index 0000000..894366b --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyV3.py @@ -0,0 +1,364 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from freqtrade.strategy import IStrategy, merge_informative_pair +from pandas import DataFrame +import pandas as pd +import talib.abstract as ta +import numpy as np +from datetime import datetime +from typing import Optional +from freqtrade.persistence import Trade +import warnings + +# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告 +warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') +pd.set_option('future.no_silent_downcasting', True) + +# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV3 --strategy-path ./user_data/Chan/strategies --timerange=20250101- + + +class CryptoFutures1m5mStrategyV3(IStrategy): + """ + SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V3 多空双开版 + + 基于V2优化: + 1. 多空双开 - 牛市做多,熊市做空 + 2. 做多:EMA多头排列 + ADX确认 + RSI超卖反弹 + 3. 做空:保持V2核心逻辑 + + 核心设计: + 1. 5分钟趋势确认: + - 做多:EMA12>EMA26>EMA50 + ADX>25 + RSI 52-70 + - 做空:EMA1225 + RSI 30-48 + 2. ATR自适应波动率过滤 + 3. 1分钟精确入场 + 4. trailing_stop_positive_offset = 0.035 + """ + INTERFACE_VERSION = 3 + timeframe = '1m' + informative_timeframe = '5m' + can_short = True + can_long = True + lev = 1.0 + + # 止损止盈 + stoploss = -0.028 # 2.8% 硬止损 + trailing_stop = True + trailing_stop_positive = 0.008 + trailing_stop_positive_offset = 0.035 + trailing_only_offset_is_reached = True + + # 完全禁用 exit_signal + use_exit_signal = False + + process_only_new_candles = True + startup_candle_count: int = 1100 + + def informative_pairs(self): + return [ + ("SOL/USDT:USDT", "5m"), + ] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # ==================== 5分钟指标 ==================== + inf_tf = self.informative_timeframe + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + + # EMA趋势 + informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) + informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) + informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) + + # EMA12斜率(3根K线变化率) + informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 + + # MACD + macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) + informative['macd_5m'] = macd + informative['macd_signal_5m'] = macd_signal + informative['macd_hist_5m'] = macd_hist + + # ADX趋势强度 + informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) + + # RSI(5分钟) + informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) + + # ATR(5分钟) + informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) + informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 + + # ATR 长期均值 + informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() + + # EMA200 大趋势过滤 + informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) + informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 + + # EMA200斜率 + informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 + + # ==================== 多空趋势判断 ==================== + + # 5分钟趋势判断 - 做多 (Bull) + informative['trend_bull_5m'] = ( + (informative['ema12'] > informative['ema26']) & + (informative['ema26'] > informative['ema50']) & + (informative['ema12_slope'] > 0.05) & + (informative['adx_5m'] > 24) & + (informative['adx_5m'] < 51) & + (informative['close'] > informative['ema12']) & + (informative['rsi_5m'] > 52) & + (informative['rsi_5m'] < 72) + ) + + # 5分钟趋势判断 - 做空 (Bear) - 保持V2逻辑 + informative['trend_bear_5m'] = ( + (informative['ema12'] < informative['ema26']) & + (informative['ema26'] < informative['ema50']) & + (informative['ema12_slope'] < -0.05) & + (informative['adx_5m'] > 24) & + (informative['adx_5m'] < 51) & + (informative['close'] < informative['ema12']) & + (informative['rsi_5m'] < 48) & + (informative['rsi_5m'] > 29) + ) + + # 大趋势过滤 + informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0 # 做多需要高于EMA200 + informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 # 做空需要低于EMA200 + + # 牛市环境 (仅做多) + informative['bull_market'] = ( + (informative['ema200_slope'] > 0) & + (informative['ema200_dist_pct'] > 0) + ) + + # 熊市环境 (仅做空) + informative['bear_market'] = ( + (informative['ema200_slope'] < 0) & + (informative['ema200_dist_pct'] < 0) + ) + + # 做多条件 + informative['can_long_5m'] = ( + informative['trend_bull_5m'] & + informative['above_ema200'] + ) + + # 做空条件 - 保持V2逻辑 + informative['can_short_5m'] = ( + informative['trend_bear_5m'] & + informative['below_ema200'] + ) + + # ATR波动率过滤 + informative['atr_ok_5m'] = ( + (informative['atr_pct_5m'] > 0.07) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2) + ) + + # 成交量确认 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75 + + # 合并5分钟数据到1分钟 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) + + # ==================== 1分钟指标 ==================== + macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd_1m + dataframe['macd_signal'] = signal_1m + dataframe['macd_hist'] = hist_1m + + dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) + dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) + + # 1分钟MACD斜率 + dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 + + # ==================== 做空信号 (保持V2) ==================== + + # 1分钟价格/MACD + dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() + dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() + + # 顶背离 (做空) + dataframe['top_divergence'] = ( + (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & + (dataframe['macd'] < dataframe['macd_high_5']) & + (dataframe['macd_slope'] < 0) & + (dataframe['macd'] < dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + # EMA死叉 (做空) + dataframe['ema_cross_down'] = ( + (dataframe['ema9'] < dataframe['ema21']) & + (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] < 55) & + (dataframe['rsi'] > 35) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + # 熊市回调 (做空) + dataframe['is_bear_candle'] = ( + (dataframe['close'] < dataframe['open']) & + ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) + ) + dataframe['bear_pullback'] = ( + dataframe['is_bear_candle'].shift(2) & + (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & + (dataframe['high'] < dataframe['high'].shift(2)) & + (dataframe['close'] < dataframe['open']) & + (dataframe['close'] < dataframe['ema9']) + ) + + # ==================== 做多信号 (新增) ==================== + + dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min() + dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min() + + # 底背离 (做多) + dataframe['bottom_divergence'] = ( + (dataframe['low'] <= dataframe['price_low_5'] * 1.001) & + (dataframe['macd'] > dataframe['macd_low_5']) & + (dataframe['macd_slope'] > 0) & + (dataframe['macd'] > dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + # EMA金叉 (做多) + dataframe['ema_cross_up'] = ( + (dataframe['ema9'] > dataframe['ema21']) & + (dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] > 45) & + (dataframe['rsi'] < 70) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + # 牛市回调 (做多) + dataframe['is_bull_candle'] = ( + (dataframe['close'] > dataframe['open']) & + ((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008) + ) + dataframe['bull_pullback'] = ( + dataframe['is_bull_candle'].shift(2) & + (dataframe['close'].shift(1) < dataframe['open'].shift(1)) & + (dataframe['low'] > dataframe['low'].shift(2)) & + (dataframe['close'] > dataframe['open']) & + (dataframe['close'] > dataframe['ema9']) + ) + + # 时间过滤 + dataframe['hour_utc'] = dataframe['date'].dt.hour + dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) + + # 安全转换5分钟布尔列 + bool_cols = [ + 'can_long_5m_5m', 'can_short_5m_5m', + 'trend_bull_5m_5m', 'trend_bear_5m_5m', + 'atr_ok_5m_5m', + 'above_ema200_5m', 'below_ema200_5m', + 'bull_market_5m', 'bear_market_5m', + 'volume_ok_5m_5m', + ] + for col in bool_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(bool).fillna(False) + + num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m', + 'ema200_dist_pct_5m', 'ema200_slope_5m'] + for col in num_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(float).fillna(0.0) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + time_ok = ~dataframe['is_bad_hour'] + atr_ok = dataframe['atr_ok_5m_5m'] + volume_ok = dataframe['volume_ok_5m_5m'] + + # ========== 做空入场 (保持V2逻辑) ========== + macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 + macd_bear_1m = dataframe['macd_hist'] < 0 + + dataframe.loc[ + (time_ok) & + (atr_ok) & + (dataframe['can_short_5m_5m']) & + (macd_bear_5m) & + (macd_bear_1m) & + (volume_ok) & + (dataframe['rsi'] > 30) & + ( + dataframe['top_divergence'] | + dataframe['ema_cross_down'] | + dataframe['bear_pullback'] + ) & + (dataframe['volume'] > 0), + 'enter_short' + ] = 1 + + # ========== 做多入场 (新增) ========== + macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0 + macd_bull_1m = dataframe['macd_hist'] > 0 + + dataframe.loc[ + (time_ok) & + (atr_ok) & + (dataframe['can_long_5m_5m']) & + (macd_bull_5m) & + (macd_bull_1m) & + (volume_ok) & + (dataframe['rsi'] < 70) & + (dataframe['rsi'] > 40) & + ( + dataframe['bottom_divergence'] | + dataframe['ema_cross_up'] | + dataframe['bull_pullback'] + ) & + (dataframe['volume'] > 0), + 'enter_long' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[:, 'exit_long'] = 0 + dataframe.loc[:, 'exit_short'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> str | bool | None: + """自定义出场逻辑:时间止损""" + trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 + + # 时间止损:持仓过久且亏损 + if trade_duration > 8 and current_profit < -0.005: + return 'time_stop_8h' + + if trade_duration > 16 and current_profit < 0: + return 'time_stop_16h' + + # 持仓超过24小时强制平仓 + if trade_duration > 24: + return 'time_stop_24h' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + """入场确认 - 时间过滤安全网""" + hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour + if hour_utc in {4, 5, 6, 7}: + return False + return True + + 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 self.lev diff --git a/strategies/CryptoFutures1m5mStrategyV4.py b/strategies/CryptoFutures1m5mStrategyV4.py new file mode 100644 index 0000000..1795260 --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyV4.py @@ -0,0 +1,404 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from freqtrade.strategy import IStrategy, merge_informative_pair, IntParameter, CategoricalParameter +from pandas import DataFrame +import pandas as pd +import talib.abstract as ta +import numpy as np +from datetime import datetime +from typing import Optional +from freqtrade.persistence import Trade +import warnings + +# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告 +warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') +pd.set_option('future.no_silent_downcasting', True) + +# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV4 --strategy-path ./user_data/Chan/strategies --timerange=20250101- + + +class CryptoFutures1m5mStrategyV4(IStrategy): + """ + SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V4 多空完全分离版 + + 基于V3优化: + 1. 多空参数完全分离 + 2. 分别优化做多做空的风险参数 + + 核心设计: + 1. 5分钟趋势确认 + 1分钟精确入场 + 2. ATR自适应波动率过滤 + 3. 多空trailing参数分离 + """ + INTERFACE_VERSION = 3 + timeframe = '1m' + informative_timeframe = '5m' + can_short = True + can_long = True + lev = 1.0 + + # ==================== 多空分离参数 ==================== + + # 做多止损 (更宽松,因为牛市回调幅度大) + stoploss_long = -0.035 + + # 做空止损 (相对紧凑,熊市反弹快) + stoploss_short = -0.025 + + # 统一下跌止损(取两者较宽松值) + stoploss = -0.035 + + # Trailing Stop - 做多 + trailing_stop_long = True + trailing_stop_positive_long = 0.006 + trailing_stop_positive_offset_long = 0.030 + + # Trailing Stop - 做空 + trailing_stop_short = True + trailing_stop_positive_short = 0.010 + trailing_stop_positive_offset_short = 0.038 + + # 统一设置 + trailing_stop = True + trailing_stop_positive = 0.008 + trailing_stop_positive_offset = 0.035 + trailing_only_offset_is_reached = True + + # 完全禁用 exit_signal + use_exit_signal = False + + process_only_new_candles = True + startup_candle_count: int = 1100 + + def informative_pairs(self): + return [ + ("SOL/USDT:USDT", "5m"), + ] + + def get_stoploss(self, side: str, trade: Optional[Trade] = None, current_rate: float = 0, + current_time: datetime = None, after_fill: bool = False, **kwargs) -> float: + """动态获取多空不同的止损""" + if side == "long": + return self.stoploss_long + elif side == "short": + return self.stoploss_short + return self.stoploss + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # ==================== 5分钟指标 ==================== + inf_tf = self.informative_timeframe + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + + # EMA趋势 + informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) + informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) + informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) + + # EMA12斜率(3根K线变化率) + informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 + + # MACD + macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) + informative['macd_5m'] = macd + informative['macd_signal_5m'] = macd_signal + informative['macd_hist_5m'] = macd_hist + + # ADX趋势强度 + informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) + + # RSI(5分钟) + informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) + + # ATR(5分钟) + informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) + informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 + + # ATR 长期均值 + informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() + + # ATR 短期均值 (用于做空过滤 - 更严格) + informative['atr_pct_ma_short_5m'] = informative['atr_pct_5m'].rolling(window=20).mean() + + # EMA200 大趋势过滤 + informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) + informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 + + # EMA200斜率 + informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 + + # ==================== 多空趋势判断 ==================== + + # 5分钟趋势判断 - 做多 (Bull) + informative['trend_bull_5m'] = ( + (informative['ema12'] > informative['ema26']) & + (informative['ema26'] > informative['ema50']) & + (informative['ema12_slope'] > 0.05) & + (informative['adx_5m'] > 22) & + (informative['adx_5m'] < 55) & + (informative['close'] > informative['ema12']) & + (informative['rsi_5m'] > 50) & + (informative['rsi_5m'] < 75) + ) + + # 5分钟趋势判断 - 做空 (Bear) + informative['trend_bear_5m'] = ( + (informative['ema12'] < informative['ema26']) & + (informative['ema26'] < informative['ema50']) & + (informative['ema12_slope'] < -0.05) & + (informative['adx_5m'] > 26) & + (informative['adx_5m'] < 50) & + (informative['close'] < informative['ema12']) & + (informative['rsi_5m'] < 50) & + (informative['rsi_5m'] > 28) + ) + + # 大趋势过滤 + informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0 + informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 + + # 牛市/熊市环境 + informative['bull_market'] = ( + (informative['ema200_slope'] > 0) & + (informative['ema200_dist_pct'] > 0) + ) + informative['bear_market'] = ( + (informative['ema200_slope'] < 0) & + (informative['ema200_dist_pct'] < 0) + ) + + # 做多条件 + informative['can_long_5m'] = ( + informative['trend_bull_5m'] & + informative['above_ema200'] + ) + + # 做空条件 + informative['can_short_5m'] = ( + informative['trend_bear_5m'] & + informative['below_ema200'] + ) + + # ==================== 多空分离的ATR过滤 ==================== + + # 做多ATR过滤 - 允许更大波动(牛市波动大) + informative['atr_ok_long_5m'] = ( + (informative['atr_pct_5m'] > 0.08) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.5) + ) + + # 做空ATR过滤 - 稍微严格(需要更明确的趋势) + informative['atr_ok_short_5m'] = ( + (informative['atr_pct_5m'] > 0.06) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_short_5m'] * 2.0) + ) + + # 成交量确认 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75 + + # 合并5分钟数据到1分钟 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) + + # ==================== 1分钟指标 ==================== + macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd_1m + dataframe['macd_signal'] = signal_1m + dataframe['macd_hist'] = hist_1m + + dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) + dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) + + # 1分钟MACD斜率 + dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 + + # ==================== 做空信号 ==================== + dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() + dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() + + # 顶背离 (做空) + dataframe['top_divergence'] = ( + (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & + (dataframe['macd'] < dataframe['macd_high_5']) & + (dataframe['macd_slope'] < 0) & + (dataframe['macd'] < dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + # EMA死叉 (做空) + dataframe['ema_cross_down'] = ( + (dataframe['ema9'] < dataframe['ema21']) & + (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] < 58) & + (dataframe['rsi'] > 35) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + # 熊市回调 (做空) + dataframe['is_bear_candle'] = ( + (dataframe['close'] < dataframe['open']) & + ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) + ) + dataframe['bear_pullback'] = ( + dataframe['is_bear_candle'].shift(2) & + (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & + (dataframe['high'] < dataframe['high'].shift(2)) & + (dataframe['close'] < dataframe['open']) & + (dataframe['close'] < dataframe['ema9']) + ) + + # ==================== 做多信号 ==================== + dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min() + dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min() + + # 底背离 (做多) + dataframe['bottom_divergence'] = ( + (dataframe['low'] <= dataframe['price_low_5'] * 1.001) & + (dataframe['macd'] > dataframe['macd_low_5']) & + (dataframe['macd_slope'] > 0) & + (dataframe['macd'] > dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + # EMA金叉 (做多) + dataframe['ema_cross_up'] = ( + (dataframe['ema9'] > dataframe['ema21']) & + (dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] > 42) & + (dataframe['rsi'] < 72) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + # 牛市回调 (做多) + dataframe['is_bull_candle'] = ( + (dataframe['close'] > dataframe['open']) & + ((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008) + ) + dataframe['bull_pullback'] = ( + dataframe['is_bull_candle'].shift(2) & + (dataframe['close'].shift(1) < dataframe['open'].shift(1)) & + (dataframe['low'] > dataframe['low'].shift(2)) & + (dataframe['close'] > dataframe['open']) & + (dataframe['close'] > dataframe['ema9']) + ) + + # ==================== 时间过滤 ==================== + dataframe['hour_utc'] = dataframe['date'].dt.hour + dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) + + # 安全转换5分钟布尔列 + bool_cols = [ + 'can_long_5m_5m', 'can_short_5m_5m', + 'trend_bull_5m_5m', 'trend_bear_5m_5m', + 'atr_ok_long_5m_5m', 'atr_ok_short_5m_5m', + 'above_ema200_5m', 'below_ema200_5m', + 'bull_market_5m', 'bear_market_5m', + 'volume_ok_5m_5m', + ] + for col in bool_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(bool).fillna(False) + + num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', + 'atr_pct_ma_5m_5m', 'atr_pct_ma_short_5m_5m', + 'ema200_dist_pct_5m', 'ema200_slope_5m'] + for col in num_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(float).fillna(0.0) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + time_ok = ~dataframe['is_bad_hour'] + volume_ok = dataframe['volume_ok_5m_5m'] + + # ========== 做空入场 ========== + atr_ok_short = dataframe['atr_ok_short_5m_5m'] + macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 + macd_bear_1m = dataframe['macd_hist'] < 0 + + dataframe.loc[ + (time_ok) & + (atr_ok_short) & + (dataframe['can_short_5m_5m']) & + (macd_bear_5m) & + (macd_bear_1m) & + (volume_ok) & + (dataframe['rsi'] > 32) & + ( + dataframe['top_divergence'] | + dataframe['ema_cross_down'] | + dataframe['bear_pullback'] + ) & + (dataframe['volume'] > 0), + 'enter_short' + ] = 1 + + # ========== 做多入场 ========== + atr_ok_long = dataframe['atr_ok_long_5m_5m'] + macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0 + macd_bull_1m = dataframe['macd_hist'] > 0 + + dataframe.loc[ + (time_ok) & + (atr_ok_long) & + (dataframe['can_long_5m_5m']) & + (macd_bull_5m) & + (macd_bull_1m) & + (volume_ok) & + (dataframe['rsi'] < 72) & + (dataframe['rsi'] > 38) & + ( + dataframe['bottom_divergence'] | + dataframe['ema_cross_up'] | + dataframe['bull_pullback'] + ) & + (dataframe['volume'] > 0), + 'enter_long' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[:, 'exit_long'] = 0 + dataframe.loc[:, 'exit_short'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> str | bool | None: + """自定义出场逻辑 - 多空不同的时间止损""" + trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 + + # ==================== 做空时间止损 (更激进) ==================== + if trade.trade_direction == 'short': + if trade_duration > 6 and current_profit < -0.004: + return 'time_stop_short_6h' + if trade_duration > 12 and current_profit < 0: + return 'time_stop_short_12h' + if trade_duration > 20: + return 'time_stop_short_20h' + + # ==================== 做多时间止损 (更宽松) ==================== + else: # long + if trade_duration > 10 and current_profit < -0.006: + return 'time_stop_long_10h' + if trade_duration > 20 and current_profit < 0: + return 'time_stop_long_20h' + if trade_duration > 30: + return 'time_stop_long_30h' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + """入场确认 - 时间过滤安全网""" + hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour + if hour_utc in {4, 5, 6, 7}: + return False + return True + + 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 self.lev diff --git a/strategies/CryptoFutures1m5mStrategyV5.py b/strategies/CryptoFutures1m5mStrategyV5.py new file mode 100644 index 0000000..97a3ba7 --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyV5.py @@ -0,0 +1,307 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from freqtrade.strategy import IStrategy, merge_informative_pair +from pandas import DataFrame +import pandas as pd +import talib.abstract as ta +import numpy as np +from datetime import datetime +from typing import Optional +from freqtrade.persistence import Trade +import warnings + +# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告 +warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') +pd.set_option('future.no_silent_downcasting', True) + +# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV5 --strategy-path ./user_data/Chan/strategies --timerange=20250101- + + +class CryptoFutures1m5mStrategyV5(IStrategy): + """ + SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V5 多空分离版 + + 基于V3优化: + 1. 多空止损完全分离 + 2. 保持V3的入场逻辑不变 + + 多空参数分离: + - 做多止损: -3.5% (更宽松) + - 做空止损: -2.5% (更紧凑) + - 做多时间止损更宽松 + - 做空时间止损更激进 + """ + INTERFACE_VERSION = 3 + timeframe = '1m' + informative_timeframe = '5m' + can_short = True + can_long = True + lev = 1.0 + + # 统一止损(兜底) + stoploss = -0.035 + + # Trailing设置 + trailing_stop = True + trailing_stop_positive = 0.008 + trailing_stop_positive_offset = 0.035 + trailing_only_offset_is_reached = True + + use_exit_signal = False + + process_only_new_candles = True + startup_candle_count: int = 1100 + + def informative_pairs(self): + return [ + ("SOL/USDT:USDT", "5m"), + ] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # ==================== 5分钟指标 ==================== + inf_tf = self.informative_timeframe + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + + # EMA趋势 + informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) + informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) + informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) + + # EMA12斜率 + informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 + + # MACD + macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) + informative['macd_5m'] = macd + informative['macd_signal_5m'] = macd_signal + informative['macd_hist_5m'] = macd_hist + + # ADX + informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) + + # RSI + informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) + + # ATR + informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) + informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 + informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() + + # EMA200 + informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) + informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 + informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 + + # ==================== 多空趋势 ==================== + + # 做多趋势 + informative['trend_bull_5m'] = ( + (informative['ema12'] > informative['ema26']) & + (informative['ema26'] > informative['ema50']) & + (informative['ema12_slope'] > 0.05) & + (informative['adx_5m'] > 24) & + (informative['adx_5m'] < 51) & + (informative['close'] > informative['ema12']) & + (informative['rsi_5m'] > 52) & + (informative['rsi_5m'] < 72) + ) + + # 做空趋势 + informative['trend_bear_5m'] = ( + (informative['ema12'] < informative['ema26']) & + (informative['ema26'] < informative['ema50']) & + (informative['ema12_slope'] < -0.05) & + (informative['adx_5m'] > 24) & + (informative['adx_5m'] < 51) & + (informative['close'] < informative['ema12']) & + (informative['rsi_5m'] < 48) & + (informative['rsi_5m'] > 29) + ) + + # 大趋势过滤 + informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0 + informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 + + # 牛熊市 + informative['bull_market'] = (informative['ema200_slope'] > 0) & (informative['ema200_dist_pct'] > 0) + informative['bear_market'] = (informative['ema200_slope'] < 0) & (informative['ema200_dist_pct'] < 0) + + # 做多/做空条件 + informative['can_long_5m'] = informative['trend_bull_5m'] & informative['above_ema200'] + informative['can_short_5m'] = informative['trend_bear_5m'] & informative['below_ema200'] + + # ATR过滤 (保持V3) + informative['atr_ok_5m'] = ( + (informative['atr_pct_5m'] > 0.07) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2) + ) + + # 成交量 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75 + + # 合并 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) + + # ==================== 1分钟指标 ==================== + macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd_1m + dataframe['macd_signal'] = signal_1m + dataframe['macd_hist'] = hist_1m + + dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) + dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) + dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 + + # ==================== 做空信号 ==================== + dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() + dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() + + dataframe['top_divergence'] = ( + (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & + (dataframe['macd'] < dataframe['macd_high_5']) & + (dataframe['macd_slope'] < 0) & + (dataframe['macd'] < dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + dataframe['ema_cross_down'] = ( + (dataframe['ema9'] < dataframe['ema21']) & + (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] < 55) & + (dataframe['rsi'] > 35) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + dataframe['is_bear_candle'] = (dataframe['close'] < dataframe['open']) & ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) + dataframe['bear_pullback'] = ( + dataframe['is_bear_candle'].shift(2) & + (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & + (dataframe['high'] < dataframe['high'].shift(2)) & + (dataframe['close'] < dataframe['open']) & + (dataframe['close'] < dataframe['ema9']) + ) + + # ==================== 做多信号 ==================== + dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min() + dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min() + + dataframe['bottom_divergence'] = ( + (dataframe['low'] <= dataframe['price_low_5'] * 1.001) & + (dataframe['macd'] > dataframe['macd_low_5']) & + (dataframe['macd_slope'] > 0) & + (dataframe['macd'] > dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + dataframe['ema_cross_up'] = ( + (dataframe['ema9'] > dataframe['ema21']) & + (dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] > 45) & + (dataframe['rsi'] < 70) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + dataframe['is_bull_candle'] = (dataframe['close'] > dataframe['open']) & ((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008) + dataframe['bull_pullback'] = ( + dataframe['is_bull_candle'].shift(2) & + (dataframe['close'].shift(1) < dataframe['open'].shift(1)) & + (dataframe['low'] > dataframe['low'].shift(2)) & + (dataframe['close'] > dataframe['open']) & + (dataframe['close'] > dataframe['ema9']) + ) + + # 时间过滤 + dataframe['hour_utc'] = dataframe['date'].dt.hour + dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) + + # 类型转换 + bool_cols = ['can_long_5m_5m', 'can_short_5m_5m', 'trend_bull_5m_5m', 'trend_bear_5m_5m', + 'atr_ok_5m_5m', 'above_ema200_5m', 'below_ema200_5m', 'bull_market_5m', 'bear_market_5m', 'volume_ok_5m_5m'] + for col in bool_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(bool).fillna(False) + + num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m', 'ema200_dist_pct_5m', 'ema200_slope_5m'] + for col in num_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(float).fillna(0.0) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + time_ok = ~dataframe['is_bad_hour'] + atr_ok = dataframe['atr_ok_5m_5m'] + volume_ok = dataframe['volume_ok_5m_5m'] + + # 做空入场 (完全保持V3) + macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 + macd_bear_1m = dataframe['macd_hist'] < 0 + + dataframe.loc[ + (time_ok) & (atr_ok) & (dataframe['can_short_5m_5m']) & + (macd_bear_5m) & (macd_bear_1m) & (volume_ok) & + (dataframe['rsi'] > 30) & + (dataframe['top_divergence'] | dataframe['ema_cross_down'] | dataframe['bear_pullback']) & + (dataframe['volume'] > 0), + 'enter_short' + ] = 1 + + # 做多入场 (完全保持V3) + macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0 + macd_bull_1m = dataframe['macd_hist'] > 0 + + dataframe.loc[ + (time_ok) & (atr_ok) & (dataframe['can_long_5m_5m']) & + (macd_bull_5m) & (macd_bull_1m) & (volume_ok) & + (dataframe['rsi'] < 70) & (dataframe['rsi'] > 40) & + (dataframe['bottom_divergence'] | dataframe['ema_cross_up'] | dataframe['bull_pullback']) & + (dataframe['volume'] > 0), + 'enter_long' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[:, 'exit_long'] = 0 + dataframe.loc[:, 'exit_short'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> str | bool | None: + """多空分离的时间止损""" + trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 + + # 做空时间止损 - 更激进 + if trade.trade_direction == 'short': + if trade_duration > 8 and current_profit < -0.005: + return 'time_stop_short_8h' + if trade_duration > 16 and current_profit < 0: + return 'time_stop_short_16h' + if trade_duration > 24: + return 'time_stop_short_24h' + + # 做多时间止损 - 更宽松 + else: + if trade_duration > 10 and current_profit < -0.006: + return 'time_stop_long_10h' + if trade_duration > 20 and current_profit < 0: + return 'time_stop_long_20h' + if trade_duration > 30: + return 'time_stop_long_30h' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour + if hour_utc in {4, 5, 6, 7}: + return False + return True + + 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 self.lev diff --git a/strategies/CryptoFutures1m5mStrategyV6.py b/strategies/CryptoFutures1m5mStrategyV6.py new file mode 100644 index 0000000..5cda8ff --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategyV6.py @@ -0,0 +1,304 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from freqtrade.strategy import IStrategy, merge_informative_pair +from pandas import DataFrame +import pandas as pd +import talib.abstract as ta +import numpy as np +from datetime import datetime +from typing import Optional +from freqtrade.persistence import Trade +import warnings + +warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') +pd.set_option('future.no_silent_downcasting', True) + +# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV6 --strategy-path ./user_data/Chan/strategies --timerange=20250101- + + +class CryptoFutures1m5mStrategyV6(IStrategy): + """ + SOL/USDT 合约策略 - V6 强化做空版 + + 基于V5优化: + 1. 做空条件更严格 - 需要更强的趋势确认 + 2. 做空ATR过滤更严格 - 避免震荡市 + 3. 做空入场增加"超跌反弹"信号 + + 核心改动: + - Short: 只做"主跌浪",不抄反弹 + - Long: 保持原有逻辑 + """ + INTERFACE_VERSION = 3 + timeframe = '1m' + informative_timeframe = '5m' + can_short = True + can_long = True + lev = 1.0 + + stoploss = -0.030 + trailing_stop = True + trailing_stop_positive = 0.008 + trailing_stop_positive_offset = 0.035 + trailing_only_offset_is_reached = True + + use_exit_signal = False + process_only_new_candles = True + startup_candle_count: int = 1100 + + def informative_pairs(self): + return [("SOL/USDT:USDT", "5m")] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + inf_tf = self.informative_timeframe + informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) + + # EMA + informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) + informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) + informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) + informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 + + # MACD + macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) + informative['macd_5m'] = macd + informative['macd_signal_5m'] = macd_signal + informative['macd_hist_5m'] = macd_hist + + # ADX + informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) + + # RSI + informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) + + # ATR + informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) + informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 + informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() + + # EMA200 + informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) + informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 + informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 + + # ==================== 趋势判断 - 做空更严格 ==================== + + # 做多趋势 - 保持不变 + informative['trend_bull_5m'] = ( + (informative['ema12'] > informative['ema26']) & + (informative['ema26'] > informative['ema50']) & + (informative['ema12_slope'] > 0.05) & + (informative['adx_5m'] > 24) & + (informative['adx_5m'] < 51) & + (informative['close'] > informative['ema12']) & + (informative['rsi_5m'] > 52) & + (informative['rsi_5m'] < 72) + ) + + # 做空趋势 - 更严格!需要更强的ADX + informative['trend_bear_5m'] = ( + (informative['ema12'] < informative['ema26']) & + (informative['ema26'] < informative['ema50']) & + (informative['ema12_slope'] < -0.08) & # 更陡的斜率 + (informative['adx_5m'] > 28) & # 更强的趋势确认 + (informative['adx_5m'] < 50) & + (informative['close'] < informative['ema12']) & + (informative['rsi_5m'] < 45) & # 更低RSI + (informative['rsi_5m'] > 25) + ) + + # 大趋势过滤 + informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0 + informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 + + # 牛熊市 + informative['bull_market'] = (informative['ema200_slope'] > 0) & (informative['ema200_dist_pct'] > 0) + informative['bear_market'] = (informative['ema200_slope'] < 0) & (informative['ema200_dist_pct'] < 0) + + # 做空条件 - 必须确认在熊市 + informative['can_long_5m'] = informative['trend_bull_5m'] & informative['above_ema200'] + informative['can_short_5m'] = ( + informative['trend_bear_5m'] & + informative['below_ema200'] & + informative['bear_market'] # 必须确认熊市 + ) + + # ==================== ATR过滤 - 做空更严格 ==================== + + # 做多ATR - 保持宽松 + informative['atr_ok_5m'] = ( + (informative['atr_pct_5m'] > 0.07) & + (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2) + ) + + # 成交量 + informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20) + informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75 + + # 合并 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) + + # ==================== 1分钟指标 ==================== + macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd_1m + dataframe['macd_signal'] = signal_1m + dataframe['macd_hist'] = hist_1m + + dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) + dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) + dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) + dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) + dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 + + # ==================== 做空信号 ==================== + dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() + dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() + + # 顶背离 - 强化版 + dataframe['top_divergence'] = ( + (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & + (dataframe['macd'] < dataframe['macd_high_5']) & + (dataframe['macd_slope'] < 0) & + (dataframe['macd'] < dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.8) # 更强成交量确认 + ) + + # EMA死叉 + dataframe['ema_cross_down'] = ( + (dataframe['ema9'] < dataframe['ema21']) & + (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] < 55) & + (dataframe['rsi'] > 35) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + # 熊市回调 + dataframe['is_bear_candle'] = (dataframe['close'] < dataframe['open']) & ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) + dataframe['bear_pullback'] = ( + dataframe['is_bear_candle'].shift(2) & + (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & + (dataframe['high'] < dataframe['high'].shift(2)) & + (dataframe['close'] < dataframe['open']) & + (dataframe['close'] < dataframe['ema9']) + ) + + # ==================== 做多信号 ==================== + dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min() + dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min() + + dataframe['bottom_divergence'] = ( + (dataframe['low'] <= dataframe['price_low_5'] * 1.001) & + (dataframe['macd'] > dataframe['macd_low_5']) & + (dataframe['macd_slope'] > 0) & + (dataframe['macd'] > dataframe['macd_signal']) & + (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) + ) + + dataframe['ema_cross_up'] = ( + (dataframe['ema9'] > dataframe['ema21']) & + (dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) & + (dataframe['rsi'] > 45) & + (dataframe['rsi'] < 70) & + (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) + ) + + dataframe['is_bull_candle'] = (dataframe['close'] > dataframe['open']) & ((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008) + dataframe['bull_pullback'] = ( + dataframe['is_bull_candle'].shift(2) & + (dataframe['close'].shift(1) < dataframe['open'].shift(1)) & + (dataframe['low'] > dataframe['low'].shift(2)) & + (dataframe['close'] > dataframe['open']) & + (dataframe['close'] > dataframe['ema9']) + ) + + # 时间过滤 + dataframe['hour_utc'] = dataframe['date'].dt.hour + dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) + + # 类型转换 + bool_cols = ['can_long_5m_5m', 'can_short_5m_5m', 'trend_bull_5m_5m', 'trend_bear_5m_5m', + 'atr_ok_5m_5m', 'above_ema200_5m', 'below_ema200_5m', 'bull_market_5m', 'bear_market_5m', 'volume_ok_5m_5m'] + for col in bool_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(bool).fillna(False) + + num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m', 'ema200_dist_pct_5m', 'ema200_slope_5m'] + for col in num_cols: + if col in dataframe.columns: + dataframe[col] = dataframe[col].astype(float).fillna(0.0) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + time_ok = ~dataframe['is_bad_hour'] + atr_ok = dataframe['atr_ok_5m_5m'] + volume_ok = dataframe['volume_ok_5m_5m'] + + # 做空入场 - 更严格的熊市条件 + macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 + macd_bear_1m = dataframe['macd_hist'] < 0 + + dataframe.loc[ + (time_ok) & (atr_ok) & (dataframe['can_short_5m_5m']) & + (macd_bear_5m) & (macd_bear_1m) & (volume_ok) & + (dataframe['rsi'] > 28) & # 更低RSI + (dataframe['top_divergence'] | dataframe['ema_cross_down'] | dataframe['bear_pullback']) & + (dataframe['volume'] > 0), + 'enter_short' + ] = 1 + + # 做多入场 + macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0 + macd_bull_1m = dataframe['macd_hist'] > 0 + + dataframe.loc[ + (time_ok) & (atr_ok) & (dataframe['can_long_5m_5m']) & + (macd_bull_5m) & (macd_bull_1m) & (volume_ok) & + (dataframe['rsi'] < 70) & (dataframe['rsi'] > 40) & + (dataframe['bottom_divergence'] | dataframe['ema_cross_up'] | dataframe['bull_pullback']) & + (dataframe['volume'] > 0), + 'enter_long' + ] = 1 + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[:, 'exit_long'] = 0 + dataframe.loc[:, 'exit_short'] = 0 + return dataframe + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> str | bool | None: + trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 + + # 做空 - 更激进的时间止损 + if trade.trade_direction == 'short': + if trade_duration > 6 and current_profit < -0.004: + return 'time_stop_short_6h' + if trade_duration > 12 and current_profit < 0: + return 'time_stop_short_12h' + if trade_duration > 20: + return 'time_stop_short_20h' + + # 做多 - 保持宽松 + else: + if trade_duration > 10 and current_profit < -0.006: + return 'time_stop_long_10h' + if trade_duration > 20 and current_profit < 0: + return 'time_stop_long_20h' + if trade_duration > 30: + return 'time_stop_long_30h' + + return None + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, entry_tag: Optional[str], + side: str, **kwargs) -> bool: + hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour + if hour_utc in {4, 5, 6, 7}: + return False + return True + + 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 self.lev diff --git a/strategies/PanZhengBeiChiStrategy.py b/strategies/PanZhengBeiChiStrategy.py new file mode 100644 index 0000000..30321fb --- /dev/null +++ b/strategies/PanZhengBeiChiStrategy.py @@ -0,0 +1,444 @@ +""" +盘整背驰策略 (PanZhengBeiChi Strategy) + +基于缠论的盘整背驰进行交易: +- 盘整背驰:同级别走势中,Ai与Ai+2比较力度减弱 +- 顶背驰(卖点):价格创新高或接近,但MACD力度明显减弱 +- 底背驰(买点):价格创新低或接近,但MACD力度明显减弱 + +使用命令: + freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json \ + --strategy PanZhengBeiChiStrategy --strategy-path ./user_data/Chan/strategies \ + --timerange=20250301- +""" + +import logging +from datetime import datetime +from typing import Optional +import numpy as np +import pandas as pd +import talib.abstract as ta +from pandas import DataFrame +from technical.util import resample_to_interval, resampled_merge +from freqtrade.strategy import IStrategy + +logger = logging.getLogger(__name__) + + +class PanZhengBeiChiStrategy(IStrategy): + """ + 盘整背驰策略 + + 核心逻辑: + 1. 在5分钟级别识别同级别走势段(Ai) + 2. 比较Ai与Ai+2的MACD力度,判断盘整背驰 + 3. 盘整顶背驰(i+2为偶数)-> 卖出 + 4. 盘整底背驰(i+2为奇数)-> 买入 + """ + INTERFACE_VERSION: int = 3 + + # === 基础配置 === + timeframe = '1m' + informative_timeframe = '5m' + can_short = True + can_long = True + + startup_candle_count: int = 2000 # 需要足够的数据来识别走势段 + + # === 止损止盈配置 === + stoploss = -0.02 # 2% 硬止损 + use_custom_stoploss = False + + # Trailing stop + trailing_stop = True + trailing_stop_positive = 0.008 # 回撤 0.8% 触发退出 + trailing_stop_positive_offset = 0.015 # 盈利 1.5% 后才开始追踪 + trailing_only_offset_is_reached = True + + # ROI - 调整止盈策略 + minimal_roi = { + "0": 0.015, # 1.5% 立即止盈(更保守) + "30": 0.01, # 30分钟后 1% + "120": 0.008, # 2小时后 0.8% + } + + order_types = { + "entry": "market", + "exit": "market", + "stoploss": "market", + "stoploss_on_exchange": False, + } + + # === 盘整背驰参数 === + same_level_timeframe = 5 # 5分钟级别 + pivot_window = 4 # 转折点确认窗口(增大减少噪音) + min_segment_length = 5 # 最小段长度(K线数)(增大减少假信号) + + # 背驰判断参数(更严格) + beichi_price_threshold = 1.10 # 价格涨幅/跌幅阈值(允许10%范围内,更严格) + beichi_macd_threshold = 0.75 # MACD力度阈值(低于75%即背驰,更严格) + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """计算指标并识别盘整背驰""" + ticker = self.get_ticker_indicator() + + # Resample 到 5m 进行同级别分解 + dataframe_5m = resample_to_interval(dataframe, ticker * self.same_level_timeframe) + + # 在 5m 上计算指标 + dataframe_5m = self.add_indicators_5m(dataframe_5m) + + # 识别盘整背驰 + dataframe_5m = self.identify_panzheng_beichi(dataframe_5m) + + # 合并回 1m dataframe + dataframe = resampled_merge(dataframe, dataframe_5m) + + # 在 1m 上也计算基础指标 + dataframe = self.add_indicators_1m(dataframe) + + return dataframe + + def add_indicators_5m(self, dataframe: DataFrame) -> DataFrame: + """在5m级别计算指标""" + # MACD 用于识别背驰 + macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # EMA 用于识别趋势 + dataframe['ema12'] = ta.EMA(dataframe, timeperiod=12) + dataframe['ema26'] = ta.EMA(dataframe, timeperiod=26) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200) + + # EMA趋势方向 + dataframe['ema_trend_up'] = (dataframe['ema12'] > dataframe['ema26']) & (dataframe['ema26'] > dataframe['ema50']) + dataframe['ema_trend_dn'] = (dataframe['ema12'] < dataframe['ema26']) & (dataframe['ema26'] < dataframe['ema50']) + + # 价格与EMA200关系 + dataframe['price_above_ema200'] = dataframe['close'] > dataframe['ema200'] + dataframe['price_below_ema200'] = dataframe['close'] < dataframe['ema200'] + + # 趋势强度 + dataframe['ema12_slope'] = dataframe['ema12'].diff(5) / dataframe['ema12'].shift(5) + dataframe['ema26_slope'] = dataframe['ema26'].diff(5) / dataframe['ema26'].shift(5) + + # 强趋势判断 + dataframe['strong_uptrend'] = ( + (dataframe['ema12_slope'] > 0) & + (dataframe['ema26_slope'] > 0) & + (dataframe['price_above_ema200']) + ) + dataframe['strong_downtrend'] = ( + (dataframe['ema12_slope'] < 0) & + (dataframe['ema26_slope'] < 0) & + (dataframe['price_below_ema200']) + ) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + + # ATR 用于波动率过滤 + dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) + dataframe['atr_mean'] = dataframe['atr'].rolling(window=20).mean() + dataframe['volatility_ok'] = dataframe['atr'] > dataframe['atr_mean'] * 0.8 + + return dataframe + + def add_indicators_1m(self, dataframe: DataFrame) -> DataFrame: + """在1m级别计算基础指标""" + dataframe['rsi_1m'] = ta.RSI(dataframe, timeperiod=14) + dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean() + + # MACD 用于1m级别确认 + macd_1m = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9) + dataframe['macd_1m'] = macd_1m['macd'] + dataframe['macdsignal_1m'] = macd_1m['macdsignal'] + dataframe['macdhist_1m'] = macd_1m['macdhist'] + + # MACD交叉 + dataframe['macd_cross_up'] = ( + (dataframe['macd_1m'] > dataframe['macdsignal_1m']) & + (dataframe['macd_1m'].shift(1) <= dataframe['macdsignal_1m'].shift(1)) + ) + dataframe['macd_cross_dn'] = ( + (dataframe['macd_1m'] < dataframe['macdsignal_1m']) & + (dataframe['macd_1m'].shift(1) >= dataframe['macdsignal_1m'].shift(1)) + ) + + return dataframe + + def identify_panzheng_beichi(self, dataframe: DataFrame) -> DataFrame: + """ + 识别盘整背驰 + + 核心逻辑: + 1. 识别局部转折点(高低点) + 2. 构建同级别走势段(Ai) + 3. 比较Ai与Ai+2的MACD力度 + 4. 判断盘整背驰:价格涨幅相近但MACD力度减弱 + """ + df = dataframe.copy() + window = self.pivot_window + lookback = window + 1 + + # 初始化列 + df['ai_index'] = -1 + df['ai_type'] = 0 # 1: 上涨, -1: 下跌 + df['ai_high'] = np.nan + df['ai_low'] = np.nan + df['ai_macd_max'] = np.nan + df['ai_macd_min'] = np.nan + df['beichi_long'] = False # 盘整底背驰(买入信号) + df['beichi_short'] = False # 盘整顶背驰(卖出信号) + + # 识别局部高点 + df['temp_high'] = df['high'].shift(window) + df['is_pivot_high'] = ( + (df['temp_high'] == df['temp_high'].rolling(window=lookback).max()) & + (df['temp_high'].notna()) + ) + + # 识别局部低点 + df['temp_low'] = df['low'].shift(window) + df['is_pivot_low'] = ( + (df['temp_low'] == df['temp_low'].rolling(window=lookback).min()) & + (df['temp_low'].notna()) + ) + + # 逐行处理,识别走势段和背驰 + ai_list = [] + current_ai_start = None + current_ai_type = None + last_pivot_idx = None + + for i in range(window, len(df)): + # 检查新的转折点 + is_new_pivot = False + pivot_type = None + + if df.iloc[i]['is_pivot_high']: + is_new_pivot = True + pivot_type = 'high' + elif df.iloc[i]['is_pivot_low']: + is_new_pivot = True + pivot_type = 'low' + + if is_new_pivot and last_pivot_idx is not None: + # 完成一个走势段 + if current_ai_start is not None: + seg_df = df.iloc[current_ai_start:last_pivot_idx] + if len(seg_df) >= self.min_segment_length: + high_val = seg_df['high'].max() + low_val = seg_df['low'].min() + macd_max = seg_df['macd'].max() + macd_min = seg_df['macd'].min() + + # 判断走势类型 + if current_ai_type is None: + if high_val > df.iloc[current_ai_start]['close']: + current_ai_type = 1 + else: + current_ai_type = -1 + + ai_info = { + 'start': current_ai_start, + 'end': last_pivot_idx, + 'type': current_ai_type, + 'high': high_val, + 'low': low_val, + 'macd_max': macd_max, + 'macd_min': macd_min, + } + ai_list.append(ai_info) + + # 标记该段 + df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_index')] = len(ai_list) - 1 + df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_type')] = current_ai_type + df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_high')] = high_val + df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_low')] = low_val + df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_macd_max')] = macd_max + df.iloc[current_ai_start:last_pivot_idx, df.columns.get_loc('ai_macd_min')] = macd_min + + # 判断背驰(Ai与Ai+2比较) + if len(ai_list) >= 3: + ai = ai_list[-3] # Ai + ai_plus_2 = ai_list[-1] # Ai+2 + + if ai['type'] == ai_plus_2['type']: + # 上涨段:比较向上力度 + if ai['type'] == 1: + price_chg = (ai_plus_2['high'] - ai_plus_2['low']) / ai_plus_2['low'] if ai_plus_2['low'] > 0 else 0 + price_chg_prev = (ai['high'] - ai['low']) / ai['low'] if ai['low'] > 0 else 0 + macd_chg = ai_plus_2['macd_max'] + macd_chg_prev = ai['macd_max'] + + # 顶背驰:价格涨幅相近但MACD力度减弱 + if price_chg <= price_chg_prev * self.beichi_price_threshold and \ + macd_chg < macd_chg_prev * self.beichi_macd_threshold: + idx = len(ai_list) - 1 # i+2的索引 + if idx % 2 == 0: # 偶数 -> 卖出 + df.iloc[last_pivot_idx, df.columns.get_loc('beichi_short')] = True + else: # 奇数 -> 买入 + df.iloc[last_pivot_idx, df.columns.get_loc('beichi_long')] = True + + # 下跌段:比较向下力度 + else: + price_chg = abs((ai_plus_2['high'] - ai_plus_2['low']) / ai_plus_2['low']) if ai_plus_2['low'] > 0 else 0 + price_chg_prev = abs((ai['high'] - ai['low']) / ai['low']) if ai['low'] > 0 else 0 + macd_chg = abs(ai_plus_2['macd_min']) + macd_chg_prev = abs(ai['macd_min']) + + # 底背驰:价格跌幅相近但MACD力度减弱 + if price_chg <= price_chg_prev * self.beichi_price_threshold and \ + macd_chg < macd_chg_prev * self.beichi_macd_threshold: + idx = len(ai_list) - 1 + if idx % 2 == 0: # 偶数 -> 卖出 + df.iloc[last_pivot_idx, df.columns.get_loc('beichi_short')] = True + else: # 奇数 -> 买入 + df.iloc[last_pivot_idx, df.columns.get_loc('beichi_long')] = True + + # 更新当前段信息 + if pivot_type == 'high': + current_ai_type = -1 # 高点后向下 + else: + current_ai_type = 1 # 低点后向上 + current_ai_start = last_pivot_idx + + if is_new_pivot: + last_pivot_idx = i + + return df + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + ticker = self.get_ticker_indicator() + resample_col = f"resample_{ticker * self.same_level_timeframe}_" + + # 5m 级别指标列名 + beichi_long_col = f"{resample_col}beichi_long" + beichi_short_col = f"{resample_col}beichi_short" + ai_type_col = f"{resample_col}ai_type" + rsi_5m_col = f"{resample_col}rsi" + ema_trend_up_col = f"{resample_col}ema_trend_up" + ema_trend_dn_col = f"{resample_col}ema_trend_dn" + volatility_ok_col = f"{resample_col}volatility_ok" + strong_uptrend_col = f"{resample_col}strong_uptrend" + strong_downtrend_col = f"{resample_col}strong_downtrend" + price_above_ema200_col = f"{resample_col}price_above_ema200" + price_below_ema200_col = f"{resample_col}price_below_ema200" + + # === 做多入场 === + # 条件:盘整底背驰 + 强上升趋势确认 + dataframe.loc[ + ( + # 核心信号:盘整底背驰 + (dataframe[beichi_long_col] == True) & + + # 强上升趋势确认(更严格) + (dataframe[strong_uptrend_col] == True) & + + # RSI 确认(更严格:只在大趋势中操作) + (dataframe[rsi_5m_col] > 40) & + (dataframe[rsi_5m_col] < 60) & + + # 1m 指标确认 + (dataframe['macd_1m'] > dataframe['macdsignal_1m']) & + + # 成交量确认 + (dataframe['volume'] > dataframe['volume_mean'] * 1.5) + ), + ['enter_long', 'enter_tag'] + ] = (1, "pzbc_long") + + # === 做空入场 === + # 条件:盘整顶背驰 + 强下降趋势确认(更严格) + dataframe.loc[ + ( + # 核心信号:盘整顶背驰 + (dataframe[beichi_short_col] == True) & + + # 强下降趋势确认 + (dataframe[strong_downtrend_col] == True) & + + # RSI 确认 + (dataframe[rsi_5m_col] > 40) & + (dataframe[rsi_5m_col] < 60) & + + # 1m 指标确认 + (dataframe['macd_1m'] < dataframe['macdsignal_1m']) & + + # 成交量确认 + (dataframe['volume'] > dataframe['volume_mean'] * 1.5) + ), + ['enter_short', 'enter_tag'] + ] = (1, "pzbc_short") + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + 出场逻辑 + + 多头出场: + 1. 出现盘整顶背驰 + 2. 趋势转弱 + + 空头出场: + 1. 出现盘整底背驰 + 2. 趋势转弱 + """ + ticker = self.get_ticker_indicator() + resample_col = f"resample_{ticker * self.same_level_timeframe}_" + + beichi_long_col = f"{resample_col}beichi_long" + beichi_short_col = f"{resample_col}beichi_short" + ai_type_col = f"{resample_col}ai_type" + rsi_5m_col = f"{resample_col}rsi" + ema_trend_dn_col = f"{resample_col}ema_trend_dn" + strong_downtrend_col = f"{resample_col}strong_downtrend" + strong_uptrend_col = f"{resample_col}strong_uptrend" + + # === 多头出场 === + dataframe.loc[ + ( + # 出现盘整顶背驰 -> 退出多头 + (dataframe[beichi_short_col] == True) | + + # 趋势转弱 + ( + (dataframe[ai_type_col] == -1) & + (dataframe[rsi_5m_col] > 55) + ) | + + # 强下跌趋势 + (dataframe[strong_downtrend_col] == True) + ), + ['exit_long', 'exit_tag'] + ] = (1, "pzbc_exit_long") + + # === 空头出场 === + dataframe.loc[ + ( + # 出现盘整底背驰 -> 退出空头 + (dataframe[beichi_long_col] == True) | + + # 趋势转弱 + ( + (dataframe[ai_type_col] == 1) & + (dataframe[rsi_5m_col] < 45) + ) | + + # 强上涨趋势 + (dataframe[strong_uptrend_col] == True) + ), + ['exit_short', 'exit_tag'] + ] = (1, "pzbc_exit_short") + + return dataframe + + def get_ticker_indicator(self) -> int: + """获取 timeframe 的分钟数""" + return int(self.timeframe[:-1]) + diff --git a/strategies/PureRandomRuleStrategy.py b/strategies/PureRandomRuleStrategy.py new file mode 100644 index 0000000..b16baf2 --- /dev/null +++ b/strategies/PureRandomRuleStrategy.py @@ -0,0 +1,204 @@ +""" +纯随机规则策略 (PureRandomRuleStrategy) + +完全不看 K 线、不看指标、不看量价、不看趋势、不看形态的纯规则交易系统。 + +规则: +- 固定时间周期开仓(例如:每 4 小时一单) +- 方向随机多空,不做任何行情判断 +- 每次只开1个方向,不对冲 +- 固定止盈:2% +- 固定止损:1% +- 到价立即平仓,不移动、不修改 +- 单笔仓位:总资金的 5% +- 单笔最大风险:总资金的 0.05% +- 连续止损 3 次,当天停止交易 +- 总持仓不超过 20% + +使用命令: + freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy PureRandomRuleStrategy --strategy-path ./user_data/Chan/strategies --timerange=20260101- + +实盘命令: + freqtrade trade -c ./user_data/Chan/config/Chan.json \ + --strategy PureRandomRuleStrategy --strategy-path ./user_data/Chan/strategies +""" + +import logging +from datetime import datetime +from typing import Optional +import random +import pandas as pd +import talib.abstract as ta +from pandas import DataFrame +from freqtrade.strategy import IStrategy + +logger = logging.getLogger(__name__) + + +class PureRandomRuleStrategy(IStrategy): + """ + 纯随机规则策略 + + 核心特点: + 1. 不看任何行情数据 + 2. 固定时间开仓(可配置间隔) + 3. 随机选择多空方向 + 4. 固定止盈止损 + 5. 风险控制(连续止损、持仓限制) + """ + INTERFACE_VERSION: int = 3 + + # === 基础配置 === + timeframe = '1m' # 主时间框架 + informative_timeframe = '1h' # 1小时作为参考(需要数据支持) + can_short = True + can_long = True + + startup_candle_count = 200 # 需要更多数据计算 EMA + + # === 交易时间间隔配置 === + trade_interval_hours = 4 + + # === 止盈止损配置 === + take_profit_pct = 0.024 + stop_loss_pct = 0.01 + + # === 仓位配置 === + entry_percent = 0.05 + max_position_pct = 0.20 + + # === 风险控制 === + max_consecutive_losses = 3 + + # === 订单类型 === + order_types = { + "entry": "market", + "exit": "market", + "stoploss": "market", + "stoploss_on_exchange": False, + } + + # === 最小 ROI === + minimal_roi = { + "0": take_profit_pct, + } + + # === 止损 === + stoploss = -stop_loss_pct + + # === 追踪止损 === + trailing_stop = False + + # === 策略状态 === + _last_entry_time: Optional[datetime] = None + _consecutive_losses: int = 0 + _last_loss_date: Optional[datetime] = None + _today_loss_count: int = 0 + + def __init__(self, config: dict) -> None: + super().__init__(config) + random.seed() + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + 计算 1h EMA26 和波动幅度 + 使用 resampled_merge 合并 1h 数据 + """ + from technical.util import resample_to_interval, resampled_merge + + # 重采样到 1h (1m * 60 = 60) + dataframe_1h = resample_to_interval(dataframe, 60) + + # 计算 1h EMA26 + dataframe_1h['ema26'] = ta.EMA(dataframe_1h, timeperiod=26) + + # 计算 1h 波动幅度: (high - low) / open * 100% + dataframe_1h['volatility'] = (dataframe_1h['high'] - dataframe_1h['low']) / dataframe_1h['open'] + + # 合并到主 dataframe + # 列名格式: resample_60_ema26, resample_60_volatility + dataframe = resampled_merge(dataframe, dataframe_1h) + + return dataframe + + def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + 入场逻辑:固定时间 + 1h EMA26方向过滤 + 波动过滤 + 时间过滤 + + 规则: + 1. 1h EMA26 上方 -> 只做多 + 2. 1h EMA26 下方 -> 只做空 + 3. 固定时间间隔开仓(4小时) + 4. 1h 波动幅度 > 0.5% 且 < 5% + 5. UTC 8:00-20:00 + """ + dataframe['enter_long'] = 0 + dataframe['enter_short'] = 0 + dataframe['enter_tag'] = '' + + last_entry_idx = None + + # 1h EMA26 列名 + ema26_col = 'resample_60_ema26' + # 1h 波动幅度列名 + volatility_col = 'resample_60_volatility' + + # 波动幅度阈值 + min_volatility = 0.005 # 0.5% + max_volatility = 0.05 # 5% + + for i in range(len(dataframe)): + current_time = dataframe['date'].iloc[i] + current_price = dataframe['close'].iloc[i] + + # 使用 resample 后的 EMA26 列 + ema26_1h = dataframe[ema26_col].iloc[i] + # 波动幅度 + volatility = dataframe[volatility_col].iloc[i] + + # 跳过没有 EMA 数据的情况 + if pd.isna(ema26_1h): + continue + + # 检查时间间隔(4小时) + can_entry = True + if last_entry_idx is not None: + hours_since_last = (current_time - dataframe['date'].iloc[last_entry_idx]).total_seconds() / 3600 + if hours_since_last < self.trade_interval_hours: + can_entry = False + + # 检查当天连续止损 + if self._today_loss_count >= self.max_consecutive_losses: + can_entry = False + + # 检查波动幅度(>0.5% 且 <5%) + if not pd.isna(volatility): + if volatility < min_volatility or volatility > max_volatility: + can_entry = False + else: + can_entry = False + + # 检查时间过滤(UTC 8:00-20:00) + utc_hour = current_time.hour + #if utc_hour < 8 or utc_hour >= 20: + #can_entry = False + + if can_entry: + # 判断方向:价格 > 1h EMA26 做多,价格 < 1h EMA26 做空 + if current_price > ema26_1h: + dataframe.loc[dataframe.index[i], 'enter_long'] = 1 + dataframe.loc[dataframe.index[i], 'enter_tag'] = 'long_above_ema' + elif current_price < ema26_1h: + dataframe.loc[dataframe.index[i], 'enter_short'] = 1 + dataframe.loc[dataframe.index[i], 'enter_tag'] = 'short_below_ema' + + if dataframe.loc[dataframe.index[i], 'enter_long'] == 1 or dataframe.loc[dataframe.index[i], 'enter_short'] == 1: + last_entry_idx = i + self._last_entry_time = current_time + + return dataframe + + def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['exit_long'] = 0 + dataframe['exit_short'] = 0 + return dataframe diff --git a/web/app.py b/web/app.py index 40896f2..a509f00 100644 --- a/web/app.py +++ b/web/app.py @@ -438,6 +438,7 @@ def add_indicators(df): df['ema10'] = (ta.EMA(df, timeperiod=10)).fillna(0) 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) # 常用SMA 24/52 try: df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0) diff --git a/web/templates/index.html b/web/templates/index.html index 20d9ec0..671b25f 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -5477,7 +5477,7 @@ if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 } - displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("01", ""); + displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "").replace("01", ""); // 添加标记配置 const markerConfig = { time: timestamp,