Merge pull request #12 from jackyu66git/dev

Dev
This commit is contained in:
jackyu66git
2026-03-11 11:38:12 +08:00
committed by GitHub
20 changed files with 3432 additions and 324 deletions
Vendored
BIN
View File
Binary file not shown.
+3
View File
@@ -46,6 +46,7 @@ class ChanKLC():
self.continue_div = False self.continue_div = False
self.separate_div = False self.separate_div = False
self.ema24 = klu.ema24 self.ema24 = klu.ema24
self.ema26 = klu.ema26
self.ema52 = klu.ema52 self.ema52 = klu.ema52
self.ema104 = klu.ema104 self.ema104 = klu.ema104
self.ema156 = klu.ema156 self.ema156 = klu.ema156
@@ -387,6 +388,7 @@ class ChanKLC():
self.rsi += self.klu_list[index].rsi self.rsi += self.klu_list[index].rsi
self.volume_ratio += self.klu_list[index].volume_ratio self.volume_ratio += self.klu_list[index].volume_ratio
self.macdhist += self.klu_list[index].macdhist self.macdhist += self.klu_list[index].macdhist
self.ema26 += self.klu_list[index].ema26
self.ema24 += self.klu_list[index].ema24 self.ema24 += self.klu_list[index].ema24
self.ema52 += self.klu_list[index].ema52 self.ema52 += self.klu_list[index].ema52
self.ema104 += self.klu_list[index].ema104 self.ema104 += self.klu_list[index].ema104
@@ -404,6 +406,7 @@ class ChanKLC():
self.volume_ratio = self.volume_ratio / n self.volume_ratio = self.volume_ratio / n
self.volume = self.volume / n self.volume = self.volume / n
self.macdhist = self.macdhist / n self.macdhist = self.macdhist / n
self.ema26 = self.ema26 / n
self.ema24 = self.ema24 / n self.ema24 = self.ema24 / n
self.ema52 = self.ema52 / n self.ema52 = self.ema52 / n
self.ema104 = self.ema104 / n self.ema104 = self.ema104 / n
+10 -8
View File
@@ -46,6 +46,7 @@ class ChanKLU:
self.near0_return = 0 self.near0_return = 0
self.ema52 = 0 self.ema52 = 0
self.ema24 = 0 self.ema24 = 0
self.ema26 = 0
self.ema104 = 0 self.ema104 = 0
self.ema156 = 0 self.ema156 = 0
self.ema208 = 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.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.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.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.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.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 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: elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52:
self.near0_return = 0 self.near0_return = 0
if self.close > self.ema52 and self.open < self.ema52: if self.close > self.ema52 and self.open < self.ema52:
if self.pre.near0_return == 0: self.near0_return = 7
self.near0_return = 7
elif self.pre.near0_return == 8:
self.pre.near0_return = 0
elif self.close < self.ema52 and self.open > self.ema52: elif self.close < self.ema52 and self.open > self.ema52:
if self.pre.near0_return == 0: self.near0_return = 8
self.near0_return = 8 if self.pre.near0_return == 7:
elif self.pre.near0_return == 7: if self.low > self.ema52 and self.close > self.open:
self.pre.near0_return = 0 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 穿越零轴判定 # CROSS0 仅以 Signal 穿越零轴判定
if self.pre.signal >= 0 and self.signal < 0: if self.pre.signal >= 0 and self.signal < 0:
self.macd_state = Chan_MACD_STATE.CROSS0_DOWN self.macd_state = Chan_MACD_STATE.CROSS0_DOWN
+19
View File
@@ -70,3 +70,22 @@ class ChanZS():
self.gg = gg self.gg = gg
def set_dd(self, dd): def set_dd(self, dd):
self.dd = dd 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 # 由外部设置
+242 -175
View File
@@ -6,7 +6,7 @@ from ChanKLC import ChanKLC
from ChanBI import ChanBI from ChanBI import ChanBI
from ChanSBI import ChanSBI from ChanSBI import ChanSBI
from ChanSEG import ChanSEG from ChanSEG import ChanSEG
from ChanZS import ChanZS from ChanZS import ChanZS, ChanZS_Big
from ChanBSP import ChanBSP from ChanBSP import ChanBSP
import talib.abstract as ta import talib.abstract as ta
import pandas as pd import pandas as pd
@@ -46,6 +46,7 @@ class TF_DF():
self.bi_list = self.cal_bi_list(self.klc_list) self.bi_list = self.cal_bi_list(self.klc_list)
self.seg_list = self.get_seg_list(self.bi_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.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.chanmacd = ChanMACD(self.klu_list)
self.klu_list = self.chanmacd.cal_macd_state() self.klu_list = self.chanmacd.cal_macd_state()
@@ -571,7 +572,27 @@ class TF_DF():
last_klu = None last_klu = None
macd = ChanMACD(klu_list) macd = ChanMACD(klu_list)
klu_list = macd.cal_macd_state() 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: 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: if len(klc_list) > 0:
last_klc = klc_list[-1] last_klc = klc_list[-1]
if klu.exception: if klu.exception:
@@ -609,6 +630,7 @@ class TF_DF():
klc_list.append(klc) klc_list.append(klc)
last_klu = klu last_klu = klu
klc_list = self.cal_trend(klc_list) klc_list = self.cal_trend(klc_list)
#print(ema52_up_list, ema52_down_list)
return klc_list return klc_list
def get_seg_list(self, bi_list): def get_seg_list(self, bi_list):
@@ -965,7 +987,7 @@ class TF_DF():
if last_top.high > klc.high: if last_top.high > klc.high:
bi_list[-1].add_klc(klc) bi_list[-1].add_klc(klc)
klc.set_bi(bi_list[-1]) 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") #print(klc.end_time, klc.fx, "二类卖点Sell 1")
else: else:
# A new top found # A new top found
@@ -979,7 +1001,7 @@ class TF_DF():
klc.set_bi(bi_list[-1]) klc.set_bi(bi_list[-1])
# 不满足结合律的分型 # 不满足结合律的分型
else: 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_bottom.index + bi_klc_min > klc.index:
if last_top.high > klc.high: if last_top.high > klc.high:
#print(klc.start_time, klc.fx, "二类卖点Sell 1") #print(klc.start_time, klc.fx, "二类卖点Sell 1")
@@ -1082,7 +1104,7 @@ class TF_DF():
if last_bottom.low < klc.low: if last_bottom.low < klc.low:
bi_list[-1].add_klc(klc) bi_list[-1].add_klc(klc)
klc.set_bi(bi_list[-1]) 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(last_bottom.start_time, last_bottom.end_time, "--------------------------------1")
#print(klc.end_time, klc.fx, "二类买点Buy 1") #print(klc.end_time, klc.fx, "二类买点Buy 1")
else: else:
@@ -1096,7 +1118,7 @@ class TF_DF():
klc.set_bi(bi_list[-1]) klc.set_bi(bi_list[-1])
# 不满足结合律的分型 # 不满足结合律的分型
else: 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_top.index + bi_klc_min > klc.index:
if last_bottom.low < klc.low: if last_bottom.low < klc.low:
#print(klc.end_time, klc.fx, "中枢买点Buy 1") #print(klc.end_time, klc.fx, "中枢买点Buy 1")
@@ -1849,180 +1871,225 @@ class TF_DF():
def calculate_zs(self, bi_list, seg_list): def calculate_zs(self, bi_list, seg_list):
return self.get_zs_list(bi_list, seg_list) return self.get_zs_list(bi_list, seg_list)
def get_zs_list(self, bi_list, seg_list): def get_zs_list(self, bi_list, seg_list):
"""
根据缠论线段中枢定义计算中枢
从第4根线段开始(索引3),每3根线段为一组检查
后一个中枢比前一个高 -> 上涨中枢,以下跌开始、以下跌结束
后一个中枢比前一个低 -> 下跌中枢,以上涨开始、以上涨结束
中枢可以扩展到5根、7根...
"""
zs_list = [] zs_list = []
bsp_list = [] if len(seg_list) < 3:
if len(seg_list) > 3: return zs_list
last_zs = None
first_bi_out = None last_zs = None
in_again = False
bi_out_count = 0 # 从第4根线段开始(索引3),每3根为一组
zs_count = 0 start_idx = 3
for seg in seg_list:
# No zs or Last ZS is completed while start_idx < len(seg_list):
if len(zs_list) == 0 or (last_zs and last_zs.is_sure): # 取连续3个线段
# Has three completed segments if start_idx + 2 >= len(seg_list):
if seg.next and seg.next.next: break
if seg.next.next.is_sure:
zg = min(seg.high, seg.next.high, seg.next.next.high) seg1 = seg_list[start_idx]
zd = max(seg.low, seg.next.low, seg.next.next.low) seg2 = seg_list[start_idx + 1]
gg = max(seg.high, seg.next.high, seg.next.next.high) seg3 = seg_list[start_idx + 2]
dd = min(seg.low, seg.next.low, seg.next.next.low)
ddir = None # 三个线段都必须是已确认的
if last_zs: if not (seg1.is_sure and seg2.is_sure and seg3.is_sure):
if zg < last_zs.zd: start_idx += 1
ddir = Chan_ZS_DIR.DOWN continue
else:
if zd > last_zs.zg: # 计算这3个线段的中枢区间
ddir = Chan_ZS_DIR.UP zg = min(seg1.high, seg2.high, seg3.high)
else: zd = max(seg1.low, seg2.low, seg3.low)
ddir = None
else: if zg <= zd:
if seg.dir == Chan_SEG_DIR.UP: start_idx += 1
ddir = Chan_ZS_DIR.DOWN continue
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: if last_zs is None:
# New ZS # 第一个中枢
zs = ChanZS(seg, len(zs_list), ddir) if seg1.dir == Chan_SEG_DIR.DOWN:
zs.set_zg(zg) # 下跌开始 -> 上涨中枢
zs.set_zd(zd) zs_dir = Chan_ZS_DIR.DOWN
zs.set_gg(gg) # 验证模式:下跌+上涨+下跌
zs.set_dd(dd) valid = (seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN)
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
else: else:
# Last ZS is not completed # 上涨开始 -> 下跌中枢
if last_zs and not last_zs.is_sure: zs_dir = Chan_ZS_DIR.UP
if first_bi_out: # 验证模式:上涨+下跌+上涨
# SEG is not in ZS valid = (seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP)
if seg.is_sure: else:
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) if zg > last_zs.zg:
bi_out_count = 0 # 上涨中枢:以下跌开始、以下跌结束
#print(seg.start_bi.start_klc.start_time) zs_dir = Chan_ZS_DIR.DOWN
first_bi_out = None valid = (seg1.dir == Chan_SEG_DIR.DOWN and seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN)
# Last ZS is completed and look for new ZS else:
if seg.next and seg.next.next: # 下跌中枢:以上涨开始、以上涨结束
if seg.next.next.is_sure: zs_dir = Chan_ZS_DIR.UP
zg = min(seg.high, seg.next.high, seg.next.next.high) valid = (seg1.dir == Chan_SEG_DIR.UP and seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP)
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) if not valid:
ddir = None start_idx += 1
if last_zs: continue
if zg < last_zs.zd:
ddir = Chan_ZS_DIR.DOWN # 检查是否与前一个中枢重叠
else: if last_zs:
if zd > last_zs.zg: # 判断是否有重叠
ddir = Chan_ZS_DIR.UP overlap = (zg >= last_zs.zd and zd <= last_zs.zg)
else:
ddir = None if overlap:
else: # 有重叠,扩展中枢到5根、7根...(缠论:合并为同一中枢)
if seg.dir == Chan_SEG_DIR.UP: # 本组先纳入当前 3 根,再向后逐根尝试;遇到与 [zd,zg] 不重叠(离开中枢)则停止扩展
ddir = Chan_ZS_DIR.DOWN added_segs = [seg_list[start_idx], seg_list[start_idx + 1], seg_list[start_idx + 2]]
else: cur_idx = start_idx + 3
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): while cur_idx < len(seg_list):
ddir = None next_seg = seg_list[cur_idx]
if ddir and zg > zd: if not next_seg.is_sure:
# New ZS break
zs = ChanZS(seg, len(zs_list), ddir)
zs.set_zg(zg) # 扩展条件:新线段与中枢区间 [zd, zg] 有重叠即并入;不重叠则停止,离开中枢的线段不包含
zs.set_zd(zd) # 用起止笔的极值算线段区间,避免 seg.high/seg.low 在个别线段上未同步导致的误判
zs.set_gg(gg) seg_high = max(next_seg.start_bi.high, next_seg.end_bi.high) if next_seg.end_bi else next_seg.start_bi.high
zs.set_dd(dd) seg_low = min(next_seg.start_bi.low, next_seg.end_bi.low) if next_seg.end_bi else next_seg.start_bi.low
last_zs.set_next(zs) overlap_with_zs = (seg_high >= last_zs.zd and seg_low <= last_zs.zg)
zs.set_pre(last_zs) if not overlap_with_zs:
zs_list.append(zs) break
if last_zs and last_zs.dir == zs.dir: added_segs.append(next_seg)
zs_count += 1 cur_idx += 1
else:
zs_count = 1 # 扩展中枢 = 原中枢线段 + 本组并入的线段(缠论合并)
last_zs = zs segs_for_zs = list(last_zs.seg_list) + list(added_segs)
# Last SEG is in ZS
else: # 中枢开始与结束线段方向一致:上涨中枢结束于 DOWN,下跌中枢结束于 UP
# SEG is inside ZS required_end_seg_dir = Chan_SEG_DIR.DOWN if last_zs.dir == Chan_ZS_DIR.DOWN else Chan_SEG_DIR.UP
if seg.end_bi: while len(segs_for_zs) >= 3 and segs_for_zs[-1].dir != required_end_seg_dir:
for index in range(seg.start_bi.index, seg.end_bi.index+1): segs_for_zs.pop()
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): # 扩展时只更新 gg、dd 和 seg_list;zg、zd 由前 3 根线段确定,不随扩展改变
in_again = True seg_highs = [s.high for s in segs_for_zs]
last_zs.set_bi_out(None, None) seg_lows = [s.low for s in segs_for_zs]
last_zs.set_last_bi_in(None) last_zs.set_gg(max(seg_highs))
last_zs.set_end_seg(None) last_zs.set_dd(min(seg_lows))
first_bi_out = None last_zs.seg_list = segs_for_zs
#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_seg = segs_for_zs[-1]
last_zs.set_last_bi_in(bi_list[index - 1]) if last_seg.end_bi:
#last_zs.set_end_seg(seg.next.next) last_zs.set_end_klc(last_seg.end_bi.end_klc, last_seg.sure_time, 0, last_seg)
bi_out_count += 1 last_zs.set_end_seg(last_seg)
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): # 跳过本组已扫描的线段(从 start_idx 到 cur_idx-1),下一组从 cur_idx 起可能再形成新中枢
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) start_idx = cur_idx
bsp_list.append(bsp) continue
#print("First bi out 3", first_bi_out.start_klc.start_time) else:
in_again = False # 没有重叠,创建新中枢
"""" # 先确认前一个中枢 - 使用前一个中枢本身的最后一个线段
if first_bi_out: if last_zs.seg_list and len(last_zs.seg_list) > 0:
if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP: prev_zs_last_seg = last_zs.seg_list[-1]
#print(bi.start_klc.start_time, bi.high, seg.high) if prev_zs_last_seg.end_bi:
if bi.high == seg.high: last_zs.set_end_klc(prev_zs_last_seg.end_bi.end_klc, prev_zs_last_seg.sure_time, 0, prev_zs_last_seg)
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) last_zs.set_end_seg(prev_zs_last_seg)
bsp_list.append(bsp) last_zs.is_sure = True
else:
if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN: # 创建新中枢
if bi.low == seg.low: gg = max(seg1.high, seg2.high, seg3.high)
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) dd = min(seg1.low, seg2.low, seg3.low)
bsp_list.append(bsp)
""" zs = ChanZS(seg1, len(zs_list), zs_dir)
else: zs.set_zg(zg)
# SEG in ZS and not out and find first bi out zs.set_zd(zd)
if seg.end_bi: zs.set_gg(gg)
for index in range(seg.start_bi.index, seg.end_bi.index+1): zs.set_dd(dd)
bi = bi_list[index] zs.set_end_klc(seg3.end_bi.end_klc, seg3.sure_time, 0, seg3)
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): zs.set_end_seg(seg3)
in_again = True zs.is_sure = False
last_zs.set_bi_out(None, None) zs.seg_list = [seg1, seg2, seg3]
last_zs.set_last_bi_in(None)
last_zs.set_end_seg(None) if last_zs:
first_bi_out = None last_zs.set_next(zs)
#print("Bi in again 4", bi.start_klc.start_time) zs.set_pre(last_zs)
if in_again and (bi.low > last_zs.zg or bi.high < last_zs.zd):
last_zs.set_bi_out(bi, seg) zs_list.append(zs)
last_zs.set_last_bi_in(bi_list[index - 1]) last_zs = zs
#last_zs.set_end_seg(seg.next.next)
bi_out_count += 1 # 移动到下一组
first_bi_out = bi start_idx += 3
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) if last_zs and not last_zs.is_sure:
#print("First bi out 4", first_bi_out.start_klc.start_time) # 获取中枢最后一个线段的索引
in_again = False if last_zs.seg_list and len(last_zs.seg_list) > 0:
if first_bi_out: last_seg_of_zs = last_zs.seg_list[-1]
if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP: # 找到这个线段在seg_list中的索引
#print(bi.start_klc.start_time, bi.high, seg.high) last_seg_idx = -1
if bi.high == seg.high: for i, seg in enumerate(seg_list):
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) if seg == last_seg_of_zs:
bsp_list.append(bsp) last_seg_idx = i
else: break
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) has_leave = False
bsp_list.append(bsp) if last_seg_idx >= 0 and last_seg_idx + 1 < len(seg_list):
#self.print_zs(zs_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 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): def get_klu_list(self, dataframe):
klu_list = self.get_kl_data(dataframe) klu_list = self.get_kl_data(dataframe)
#klu_list = self.cal_klu_pattern(klu_list) #klu_list = self.cal_klu_pattern(klu_list)
+1 -1
View File
@@ -42,7 +42,7 @@
"ccxt_config": {}, "ccxt_config": {},
"ccxt_async_config": {}, "ccxt_async_config": {},
"pair_whitelist": [ "pair_whitelist": [
"SOL/USDT:USDT" "BTC/USDT:USDT"
], ],
"pair_blacklist": [ "pair_blacklist": [
"BNB/.*" "BNB/.*"
+82 -138
View File
@@ -9,68 +9,44 @@ from typing import Optional
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
import warnings import warnings
# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告
# 这个警告来自 freqtrade 库的 strategy_helper.py
warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*') warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*')
# 或者启用未来行为(推荐)
pd.set_option('future.no_silent_downcasting', True) 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): 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% 基于V5修改禁用做空只做多
每个季度均盈利市场下跌-54%期间持续获利
核心设计
1. 纯做空策略 - 价格必须低于EMA200至少1%才允许做空
2. 5分钟趋势确认EMA12<EMA26<EMA50 + ADX 25-50 + RSI 30-48
3. ATR自适应波动率过滤ATR < 长期均值 * 1.5避免极端波动
4. 1分钟精确入场顶背离 / EMA死叉 / 熊市回调
5. 双重MACD确认5分钟+1分钟MACD柱状图均为负
6. trailing_stop_positive_offset = 0.030
7. 时间止损持仓过久且亏损时提前退出
""" """
INTERFACE_VERSION = 3 INTERFACE_VERSION = 3
timeframe = '1m' timeframe = '1m'
informative_timeframe = '5m' informative_timeframe = '5m'
can_short = True can_short = False # 禁用做空
can_long = True
lev = 1.0
# 止损止盈 stoploss = -0.035
stoploss = -0.025 # 2.5% 硬止损
trailing_stop = True trailing_stop = True
trailing_stop_positive = 0.008 trailing_stop_positive = 0.008
trailing_stop_positive_offset = 0.030 trailing_stop_positive_offset = 0.035
trailing_only_offset_is_reached = True trailing_only_offset_is_reached = True
# 不使用custom_stoploss(会干扰trailing_stop
use_custom_stoploss = False
# 完全禁用 exit_signal
use_exit_signal = False use_exit_signal = False
process_only_new_candles = True process_only_new_candles = True
startup_candle_count: int = 1100 startup_candle_count: int = 1100
def informative_pairs(self): def informative_pairs(self):
return [ return [("SOL/USDT:USDT", "5m")]
("SOL/USDT:USDT", "5m"),
]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# ==================== 5分钟指标 ====================
inf_tf = self.informative_timeframe inf_tf = self.informative_timeframe
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf)
# EMA趋势 # EMA
informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) informative['ema12'] = ta.EMA(informative['close'], timeperiod=12)
informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) informative['ema26'] = ta.EMA(informative['close'], timeperiod=26)
informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) 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 informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100
# MACD # MACD
@@ -79,66 +55,54 @@ class CryptoFutures1m5mStrategy(IStrategy):
informative['macd_signal_5m'] = macd_signal informative['macd_signal_5m'] = macd_signal
informative['macd_hist_5m'] = macd_hist informative['macd_hist_5m'] = macd_hist
# ADX趋势强度 # ADX
informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14)
# RSI5分钟) # RSI
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
# ATR5分钟) # ATR
informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) 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_5m'] = informative['atr_5m'] / informative['close'] * 100
# ATR 长期均值(用于自适应波动率过滤)
informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() 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'] = ta.EMA(informative['close'], timeperiod=200)
informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 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 informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100
# ===== 大趋势过滤(Short Only ===== # 做多趋势
# 做空需要价格低于EMA200至少1% informative['trend_bull_5m'] = (
informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 (informative['ema12'] > informative['ema26']) &
(informative['ema26'] > informative['ema50']) &
# 牛市暂停:EMA200上升 + 价格在EMA200上方 → 完全停止做空 (informative['ema12_slope'] > 0.05) &
informative['bull_pause'] = ( (informative['adx_5m'] > 24) &
(informative['ema200_slope'] > 0) & (informative['adx_5m'] < 51) &
(informative['ema200_dist_pct'] > 0) (informative['close'] > informative['ema12']) &
(informative['rsi_5m'] > 52) &
(informative['rsi_5m'] < 72)
) )
# ===== 5分钟趋势判断(仅Short ===== # 大趋势过滤
informative['trend_bear_5m'] = ( informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0
(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)
)
# 做条件:短期趋势 + EMA200大趋势方向一致 + 非牛市 # 做条件
informative['can_long_5m'] = False informative['can_long_5m'] = informative['trend_bull_5m'] & informative['above_ema200']
informative['can_short_5m'] = (
informative['trend_bear_5m'] &
informative['below_ema200'] &
(~informative['bull_pause'])
)
# ATR波动率过滤(自适应) # ATR过滤
informative['atr_ok_5m'] = ( informative['atr_ok_5m'] = (
(informative['atr_pct_5m'] > 0.1) & (informative['atr_pct_5m'] > 0.07) &
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 1.5) (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) 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) macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd_1m dataframe['macd'] = macd_1m
dataframe['macd_signal'] = signal_1m dataframe['macd_signal'] = signal_1m
@@ -148,116 +112,96 @@ class CryptoFutures1m5mStrategy(IStrategy):
dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21)
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20)
# ===== 1分钟MACD斜率 =====
dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3
# ===== 1分钟做空入场信号 ===== # 做多信号
dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min()
dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min()
dataframe['top_divergence'] = ( dataframe['bottom_divergence'] = (
(dataframe['high'] >= dataframe['price_high_5'] * 0.999) & (dataframe['low'] <= dataframe['price_low_5'] * 1.001) &
(dataframe['macd'] < dataframe['macd_high_5']) & (dataframe['macd'] > dataframe['macd_low_5']) &
(dataframe['macd_slope'] < 0) & (dataframe['macd_slope'] > 0) &
(dataframe['macd'] < dataframe['macd_signal']) & (dataframe['macd'] > dataframe['macd_signal']) &
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6) (dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
) )
dataframe['ema_cross_down'] = ( dataframe['ema_cross_up'] = (
(dataframe['ema9'] < dataframe['ema21']) & (dataframe['ema9'] > dataframe['ema21']) &
(dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & (dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) &
(dataframe['rsi'] < 55) & (dataframe['rsi'] > 35) & (dataframe['rsi'] > 45) &
(dataframe['rsi'] < 70) &
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0) (dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
) )
dataframe['is_bear_candle'] = ( dataframe['is_bull_candle'] = (dataframe['close'] > dataframe['open']) & ((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008)
(dataframe['close'] < dataframe['open']) & dataframe['bull_pullback'] = (
((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) dataframe['is_bull_candle'].shift(2) &
) (dataframe['close'].shift(1) < dataframe['open'].shift(1)) &
dataframe['bear_pullback'] = ( (dataframe['low'] > dataframe['low'].shift(2)) &
dataframe['is_bear_candle'].shift(2) & (dataframe['close'] > dataframe['open']) &
(dataframe['close'].shift(1) > dataframe['open'].shift(1)) & (dataframe['close'] > dataframe['ema9'])
(dataframe['high'] < dataframe['high'].shift(2)) &
(dataframe['close'] < dataframe['open']) &
(dataframe['close'] < dataframe['ema9'])
) )
# ==================== 时间过滤 ==================== # 时间过滤
dataframe['hour_utc'] = dataframe['date'].dt.hour dataframe['hour_utc'] = dataframe['date'].dt.hour
dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7])
# 安全转换5分钟布尔列 # 类型转换
bool_cols = [ bool_cols = ['can_long_5m_5m', 'trend_bull_5m_5m', 'atr_ok_5m_5m', 'above_ema200_5m', 'volume_ok_5m_5m']
'can_long_5m_5m', 'can_short_5m_5m',
'trend_bear_5m_5m',
'atr_ok_5m_5m',
'below_ema200_5m', 'bull_pause_5m',
]
for col in bool_cols: for col in bool_cols:
if col in dataframe.columns: if col in dataframe.columns:
dataframe[col] = dataframe[col].astype(bool).fillna(False) 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 return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
time_ok = ~dataframe['is_bad_hour'] time_ok = ~dataframe['is_bad_hour']
atr_ok = dataframe['atr_ok_5m_5m'] 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[ dataframe.loc[
(time_ok) & (time_ok) & (atr_ok) & (dataframe['can_long_5m_5m']) &
(atr_ok) & (macd_bull_5m) & (macd_bull_1m) & (volume_ok) &
(dataframe['can_short_5m_5m']) & (dataframe['rsi'] < 70) & (dataframe['rsi'] > 40) &
(macd_bear_5m) & (dataframe['bottom_divergence'] | dataframe['ema_cross_up'] | dataframe['bull_pullback']) &
(macd_bear_1m) &
(dataframe['rsi'] > 30) &
(
dataframe['top_divergence'] |
dataframe['ema_cross_down'] |
dataframe['bear_pullback']
) &
(dataframe['volume'] > 0), (dataframe['volume'] > 0),
'enter_short' 'enter_long'
] = 1 ] = 1
return dataframe return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[:, 'exit_long'] = 0 dataframe.loc[:, 'exit_long'] = 0
dataframe.loc[:, 'exit_short'] = 0
return dataframe return dataframe
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> str | bool | None: current_rate: float, current_profit: float, **kwargs) -> str | bool | None:
"""时间止损:持仓过久且亏损时提前退出"""
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 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 > 10 and current_profit < -0.006:
return 'time_stop_long_10h'
if trade_duration > 16 and current_profit < 0: if trade_duration > 20 and current_profit < 0:
return 'time_stop_16h' return 'time_stop_long_20h'
if trade_duration > 30:
return 'time_stop_long_30h'
return None return None
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, 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], time_in_force: str, current_time: datetime, entry_tag: Optional[str],
side: str, **kwargs) -> bool: side: str, **kwargs) -> bool:
"""入场确认 - 时间过滤安全网"""
hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour
if hour_utc in {4, 5, 6, 7}: if hour_utc in {4, 5, 6, 7}:
return False return False
return True 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
@@ -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
@@ -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
+283
View File
@@ -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<EMA26<EMA50 + ADX 25-50 + RSI 30-48
3. ATR自适应波动率过滤
4. 1分钟精确入场顶背离 / EMA死叉 / 熊市回调
5. 双重MACD确认5分钟+1分钟MACD柱状图均为负
6. trailing_stop_positive_offset = 0.035
"""
INTERFACE_VERSION = 3
timeframe = '1m'
informative_timeframe = '5m'
can_short = True
lev = 1.0
# 止损止盈 - 优化版
stoploss = -0.026 # 2.6% 硬止损
trailing_stop = True
trailing_stop_positive = 0.008
trailing_stop_positive_offset = 0.032
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)
# RSI5分钟)
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
# ATR5分钟)
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
@@ -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"
}
@@ -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)
# RSI5分钟)
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
# ATR5分钟)
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
+364
View File
@@ -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
- 做空EMA12<EMA26<EMA50 + ADX>25 + 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)
# RSI5分钟)
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
# ATR5分钟)
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
+404
View File
@@ -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)
# RSI5分钟)
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
# ATR5分钟)
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
+307
View File
@@ -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
+304
View File
@@ -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
+444
View File
@@ -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])
+204
View File
@@ -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
+1
View File
@@ -438,6 +438,7 @@ def add_indicators(df):
df['ema10'] = (ta.EMA(df, timeperiod=10)).fillna(0) df['ema10'] = (ta.EMA(df, timeperiod=10)).fillna(0)
df['ema24'] = (ta.EMA(df, timeperiod=24)).fillna(0) df['ema24'] = (ta.EMA(df, timeperiod=24)).fillna(0)
df['ema52'] = (ta.EMA(df, timeperiod=52)).fillna(0) df['ema52'] = (ta.EMA(df, timeperiod=52)).fillna(0)
df['ema26'] = (ta.EMA(df, timeperiod=26)).fillna(0)
# 常用SMA 24/52 # 常用SMA 24/52
try: try:
df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0) df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0)
+1 -1
View File
@@ -5477,7 +5477,7 @@
if (fx.fx_strength < 1.0) { // 降低阈值让更多分型显示 if (fx.fx_strength < 1.0) { // 降低阈值让更多分型显示
displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 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 = { const markerConfig = {
time: timestamp, time: timestamp,