@@ -46,6 +46,7 @@ class ChanKLC():
|
||||
self.continue_div = False
|
||||
self.separate_div = False
|
||||
self.ema24 = klu.ema24
|
||||
self.ema26 = klu.ema26
|
||||
self.ema52 = klu.ema52
|
||||
self.ema104 = klu.ema104
|
||||
self.ema156 = klu.ema156
|
||||
@@ -387,6 +388,7 @@ class ChanKLC():
|
||||
self.rsi += self.klu_list[index].rsi
|
||||
self.volume_ratio += self.klu_list[index].volume_ratio
|
||||
self.macdhist += self.klu_list[index].macdhist
|
||||
self.ema26 += self.klu_list[index].ema26
|
||||
self.ema24 += self.klu_list[index].ema24
|
||||
self.ema52 += self.klu_list[index].ema52
|
||||
self.ema104 += self.klu_list[index].ema104
|
||||
@@ -404,6 +406,7 @@ class ChanKLC():
|
||||
self.volume_ratio = self.volume_ratio / n
|
||||
self.volume = self.volume / n
|
||||
self.macdhist = self.macdhist / n
|
||||
self.ema26 = self.ema26 / n
|
||||
self.ema24 = self.ema24 / n
|
||||
self.ema52 = self.ema52 / n
|
||||
self.ema104 = self.ema104 / n
|
||||
|
||||
+8
-6
@@ -46,6 +46,7 @@ class ChanKLU:
|
||||
self.near0_return = 0
|
||||
self.ema52 = 0
|
||||
self.ema24 = 0
|
||||
self.ema26 = 0
|
||||
self.ema104 = 0
|
||||
self.ema156 = 0
|
||||
self.ema208 = 0
|
||||
@@ -175,6 +176,7 @@ class ChanKLU:
|
||||
self.macd = float(item['macd']) if 'macd' in item and item['macd'] else 0
|
||||
self.signal = float(item['macdsignal']) if 'macdsignal' in item and item['macdsignal'] else 0
|
||||
self.macdhist = float(item['macdhist']) if 'macdhist' in item and item['macdhist'] else 0
|
||||
self.ema26 = float(item['ema26']) if 'ema26' in item and item['ema26'] else 0
|
||||
self.ema52 = float(item['ema52']) if 'ema52' in item and item['ema52'] else 0
|
||||
self.ema24 = float(item['ema24']) if 'ema24' in item and item['ema24'] else 0
|
||||
self.ema104 = float(item['ema104']) if 'ema104' in item and item['ema104'] else 0
|
||||
@@ -230,15 +232,15 @@ class ChanKLU:
|
||||
elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52:
|
||||
self.near0_return = 0
|
||||
if self.close > self.ema52 and self.open < self.ema52:
|
||||
if self.pre.near0_return == 0:
|
||||
self.near0_return = 7
|
||||
elif self.pre.near0_return == 8:
|
||||
self.pre.near0_return = 0
|
||||
elif self.close < self.ema52 and self.open > self.ema52:
|
||||
if self.pre.near0_return == 0:
|
||||
self.near0_return = 8
|
||||
elif self.pre.near0_return == 7:
|
||||
self.pre.near0_return = 0
|
||||
if self.pre.near0_return == 7:
|
||||
if self.low > self.ema52 and self.close > self.open:
|
||||
self.near0_return = 9
|
||||
if self.pre.near0_return == 8:
|
||||
if self.high < self.ema52 and self.close < self.open:
|
||||
self.near0_return = 10
|
||||
# CROSS0 仅以 Signal 穿越零轴判定
|
||||
if self.pre.signal >= 0 and self.signal < 0:
|
||||
self.macd_state = Chan_MACD_STATE.CROSS0_DOWN
|
||||
|
||||
@@ -70,3 +70,22 @@ class ChanZS():
|
||||
self.gg = gg
|
||||
def set_dd(self, 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 # 由外部设置
|
||||
@@ -6,7 +6,7 @@ from ChanKLC import ChanKLC
|
||||
from ChanBI import ChanBI
|
||||
from ChanSBI import ChanSBI
|
||||
from ChanSEG import ChanSEG
|
||||
from ChanZS import ChanZS
|
||||
from ChanZS import ChanZS, ChanZS_Big
|
||||
from ChanBSP import ChanBSP
|
||||
import talib.abstract as ta
|
||||
import pandas as pd
|
||||
@@ -46,6 +46,7 @@ class TF_DF():
|
||||
self.bi_list = self.cal_bi_list(self.klc_list)
|
||||
self.seg_list = self.get_seg_list(self.bi_list)
|
||||
self.zs_list = self.get_zs_list(self.bi_list, self.seg_list)
|
||||
self.big_zs_list = self.get_big_zs_list(self.zs_list)
|
||||
self.chanmacd = ChanMACD(self.klu_list)
|
||||
self.klu_list = self.chanmacd.cal_macd_state()
|
||||
|
||||
@@ -571,7 +572,27 @@ class TF_DF():
|
||||
last_klu = None
|
||||
macd = ChanMACD(klu_list)
|
||||
klu_list = macd.cal_macd_state()
|
||||
ema_up_list = []
|
||||
ema_down_list = []
|
||||
ema_up_count = 0
|
||||
ema_down_count = 0
|
||||
last_klu = None
|
||||
for klu in klu_list:
|
||||
ema = klu.ema52
|
||||
last_ema = last_klu.ema52 if last_klu else 0
|
||||
if klu.close >= ema:
|
||||
ema_up_count += 1
|
||||
elif klu.close < ema:
|
||||
ema_down_count += 1
|
||||
if last_klu and last_klu.close >= last_ema and klu.close < ema:
|
||||
ema_up_list.append(ema_up_count)
|
||||
#print(last_klu.time, ema_up_count, "UP END")
|
||||
ema_up_count = 0
|
||||
elif last_klu and last_klu.close < last_ema and klu.close >= ema:
|
||||
ema_down_list.append(ema_down_count)
|
||||
#print(last_klu.time, ema_down_count, "DOWN END")
|
||||
ema_down_count = 0
|
||||
last_klu = klu
|
||||
if len(klc_list) > 0:
|
||||
last_klc = klc_list[-1]
|
||||
if klu.exception:
|
||||
@@ -609,6 +630,7 @@ class TF_DF():
|
||||
klc_list.append(klc)
|
||||
last_klu = klu
|
||||
klc_list = self.cal_trend(klc_list)
|
||||
#print(ema52_up_list, ema52_down_list)
|
||||
return klc_list
|
||||
|
||||
def get_seg_list(self, bi_list):
|
||||
@@ -965,7 +987,7 @@ class TF_DF():
|
||||
if last_top.high > klc.high:
|
||||
bi_list[-1].add_klc(klc)
|
||||
klc.set_bi(bi_list[-1])
|
||||
klc.set_klc_fx_type(Chan_KLC_FX.TOP3)
|
||||
#klc.set_klc_fx_type(Chan_KLC_FX.TOP3)
|
||||
#print(klc.end_time, klc.fx, "二类卖点Sell 1")
|
||||
else:
|
||||
# A new top found
|
||||
@@ -979,7 +1001,7 @@ class TF_DF():
|
||||
klc.set_bi(bi_list[-1])
|
||||
# 不满足结合律的分型
|
||||
else:
|
||||
#klc.set_klc_fx_type(Chan_KLC_FX.TOP0)
|
||||
klc.set_klc_fx_type(Chan_KLC_FX.TOP0)
|
||||
if last_bottom.index + bi_klc_min > klc.index:
|
||||
if last_top.high > klc.high:
|
||||
#print(klc.start_time, klc.fx, "二类卖点Sell 1")
|
||||
@@ -1082,7 +1104,7 @@ class TF_DF():
|
||||
if last_bottom.low < klc.low:
|
||||
bi_list[-1].add_klc(klc)
|
||||
klc.set_bi(bi_list[-1])
|
||||
klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3)
|
||||
#klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3)
|
||||
#print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1")
|
||||
#print(klc.end_time, klc.fx, "二类买点Buy 1")
|
||||
else:
|
||||
@@ -1096,7 +1118,7 @@ class TF_DF():
|
||||
klc.set_bi(bi_list[-1])
|
||||
# 不满足结合律的分型
|
||||
else:
|
||||
#klc.set_klc_fx_type(Chan_KLC_FX.TOP0)
|
||||
klc.set_klc_fx_type(Chan_KLC_FX.TOP0)
|
||||
if last_top.index + bi_klc_min > klc.index:
|
||||
if last_bottom.low < klc.low:
|
||||
#print(klc.end_time, klc.fx, "中枢买点Buy 1")
|
||||
@@ -1849,180 +1871,225 @@ class TF_DF():
|
||||
def calculate_zs(self, bi_list, seg_list):
|
||||
return self.get_zs_list(bi_list, seg_list)
|
||||
def get_zs_list(self, bi_list, seg_list):
|
||||
zs_list = []
|
||||
bsp_list = []
|
||||
if len(seg_list) > 3:
|
||||
last_zs = None
|
||||
first_bi_out = None
|
||||
in_again = False
|
||||
bi_out_count = 0
|
||||
zs_count = 0
|
||||
for seg in seg_list:
|
||||
# No zs or Last ZS is completed
|
||||
if len(zs_list) == 0 or (last_zs and last_zs.is_sure):
|
||||
# Has three completed segments
|
||||
if seg.next and seg.next.next:
|
||||
if seg.next.next.is_sure:
|
||||
zg = min(seg.high, seg.next.high, seg.next.next.high)
|
||||
zd = max(seg.low, seg.next.low, seg.next.next.low)
|
||||
gg = max(seg.high, seg.next.high, seg.next.next.high)
|
||||
dd = min(seg.low, seg.next.low, seg.next.next.low)
|
||||
ddir = None
|
||||
if last_zs:
|
||||
if zg < last_zs.zd:
|
||||
ddir = Chan_ZS_DIR.DOWN
|
||||
else:
|
||||
if zd > last_zs.zg:
|
||||
ddir = Chan_ZS_DIR.UP
|
||||
else:
|
||||
ddir = None
|
||||
else:
|
||||
if seg.dir == Chan_SEG_DIR.UP:
|
||||
ddir = Chan_ZS_DIR.DOWN
|
||||
else:
|
||||
ddir = Chan_ZS_DIR.UP
|
||||
if (seg.dir == Chan_SEG_DIR.DOWN and ddir == Chan_ZS_DIR.DOWN) or (seg.dir == Chan_SEG_DIR.UP and ddir == Chan_ZS_DIR.UP):
|
||||
ddir = None
|
||||
if ddir and zg > zd:
|
||||
# New ZS
|
||||
zs = ChanZS(seg, len(zs_list), ddir)
|
||||
zs.set_zg(zg)
|
||||
zs.set_zd(zd)
|
||||
zs.set_gg(gg)
|
||||
zs.set_dd(dd)
|
||||
if last_zs:
|
||||
last_zs.set_next(zs)
|
||||
zs.set_pre(last_zs)
|
||||
zs_list.append(zs)
|
||||
if last_zs and last_zs.dir == zs.dir:
|
||||
zs_count += 1
|
||||
else:
|
||||
zs_count = 1
|
||||
last_zs = zs
|
||||
# Last ZS is not completed
|
||||
else:
|
||||
# Last ZS is not completed
|
||||
if last_zs and not last_zs.is_sure:
|
||||
if first_bi_out:
|
||||
# SEG is not in ZS
|
||||
if seg.is_sure:
|
||||
if ((seg.low > last_zs.zg and seg.high > last_zs.zg) or (seg.high < last_zs.zd and seg.low < last_zs.zd)):
|
||||
last_zs.set_end_klc(seg.pre.end_bi.end_klc, seg.sure_time, bi_out_count, seg.pre)
|
||||
bi_out_count = 0
|
||||
#print(seg.start_bi.start_klc.start_time)
|
||||
first_bi_out = None
|
||||
# Last ZS is completed and look for new ZS
|
||||
if seg.next and seg.next.next:
|
||||
if seg.next.next.is_sure:
|
||||
zg = min(seg.high, seg.next.high, seg.next.next.high)
|
||||
zd = max(seg.low, seg.next.low, seg.next.next.low)
|
||||
gg = max(seg.high, seg.next.high, seg.next.next.high)
|
||||
dd = min(seg.low, seg.next.low, seg.next.next.low)
|
||||
ddir = None
|
||||
if last_zs:
|
||||
if zg < last_zs.zd:
|
||||
ddir = Chan_ZS_DIR.DOWN
|
||||
else:
|
||||
if zd > last_zs.zg:
|
||||
ddir = Chan_ZS_DIR.UP
|
||||
else:
|
||||
ddir = None
|
||||
else:
|
||||
if seg.dir == Chan_SEG_DIR.UP:
|
||||
ddir = Chan_ZS_DIR.DOWN
|
||||
else:
|
||||
ddir = Chan_ZS_DIR.UP
|
||||
if (seg.dir == Chan_SEG_DIR.DOWN and ddir == Chan_ZS_DIR.DOWN) or (seg.dir == Chan_SEG_DIR.UP and ddir == Chan_ZS_DIR.UP):
|
||||
ddir = None
|
||||
if ddir and zg > zd:
|
||||
# New ZS
|
||||
zs = ChanZS(seg, len(zs_list), ddir)
|
||||
zs.set_zg(zg)
|
||||
zs.set_zd(zd)
|
||||
zs.set_gg(gg)
|
||||
zs.set_dd(dd)
|
||||
last_zs.set_next(zs)
|
||||
zs.set_pre(last_zs)
|
||||
zs_list.append(zs)
|
||||
if last_zs and last_zs.dir == zs.dir:
|
||||
zs_count += 1
|
||||
else:
|
||||
zs_count = 1
|
||||
last_zs = zs
|
||||
# Last SEG is in ZS
|
||||
else:
|
||||
# SEG is inside ZS
|
||||
if seg.end_bi:
|
||||
for index in range(seg.start_bi.index, seg.end_bi.index+1):
|
||||
bi = bi_list[index]
|
||||
if (bi.high >= last_zs.zd and bi.high <= last_zs.zg) or (bi.low >= last_zs.zd and bi.low <= last_zs.zg) or (bi.high >= last_zs.zg and bi.low <= last_zs.zd):
|
||||
in_again = True
|
||||
last_zs.set_bi_out(None, None)
|
||||
last_zs.set_last_bi_in(None)
|
||||
last_zs.set_end_seg(None)
|
||||
first_bi_out = None
|
||||
#print("Bi in again 3", bi.start_klc.start_time)
|
||||
if in_again and (bi.low > last_zs.zg or bi.high < last_zs.zd):
|
||||
last_zs.set_bi_out(bi, seg)
|
||||
last_zs.set_last_bi_in(bi_list[index - 1])
|
||||
#last_zs.set_end_seg(seg.next.next)
|
||||
bi_out_count += 1
|
||||
first_bi_out = bi
|
||||
if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP):
|
||||
bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg)
|
||||
bsp_list.append(bsp)
|
||||
#print("First bi out 3", first_bi_out.start_klc.start_time)
|
||||
in_again = False
|
||||
""""
|
||||
if first_bi_out:
|
||||
if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP:
|
||||
#print(bi.start_klc.start_time, bi.high, seg.high)
|
||||
if bi.high == seg.high:
|
||||
bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg)
|
||||
bsp_list.append(bsp)
|
||||
else:
|
||||
if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN:
|
||||
if bi.low == seg.low:
|
||||
bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.T3E, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg)
|
||||
bsp_list.append(bsp)
|
||||
"""
|
||||
else:
|
||||
# SEG in ZS and not out and find first bi out
|
||||
if seg.end_bi:
|
||||
for index in range(seg.start_bi.index, seg.end_bi.index+1):
|
||||
bi = bi_list[index]
|
||||
if (bi.high >= last_zs.zd and bi.high <= last_zs.zg) or (bi.low >= last_zs.zd and bi.low <= last_zs.zg) or (bi.high >= last_zs.zg and bi.low <= last_zs.zd):
|
||||
in_again = True
|
||||
last_zs.set_bi_out(None, None)
|
||||
last_zs.set_last_bi_in(None)
|
||||
last_zs.set_end_seg(None)
|
||||
first_bi_out = None
|
||||
#print("Bi in again 4", bi.start_klc.start_time)
|
||||
if in_again and (bi.low > last_zs.zg or bi.high < last_zs.zd):
|
||||
last_zs.set_bi_out(bi, seg)
|
||||
last_zs.set_last_bi_in(bi_list[index - 1])
|
||||
#last_zs.set_end_seg(seg.next.next)
|
||||
bi_out_count += 1
|
||||
first_bi_out = bi
|
||||
if (bi.dir == Chan_BI_DIR.UP and seg.dir == Chan_SEG_DIR.DOWN) or (bi.dir == Chan_BI_DIR.DOWN and seg.dir == Chan_SEG_DIR.UP):
|
||||
bsp = ChanBSP(first_bi_out, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if first_bi_out.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, first_bi_out.sure_time, zs_count, zs, seg)
|
||||
bsp_list.append(bsp)
|
||||
#print("First bi out 4", first_bi_out.start_klc.start_time)
|
||||
in_again = False
|
||||
if first_bi_out:
|
||||
if seg.dir == Chan_SEG_DIR.UP and bi.dir == Chan_BI_DIR.UP:
|
||||
#print(bi.start_klc.start_time, bi.high, seg.high)
|
||||
if bi.high == seg.high:
|
||||
bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.S3, Chan_BSP_DIR.SELL if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.BUY, bi.sure_time, zs_count, zs, seg)
|
||||
bsp_list.append(bsp)
|
||||
else:
|
||||
if seg.dir == Chan_SEG_DIR.DOWN and bi.dir == Chan_BI_DIR.DOWN:
|
||||
if bi.low == seg.low:
|
||||
bsp = ChanBSP(bi, len(bsp_list), Chan_BSP_TYPE.B3, Chan_BSP_DIR.BUY if bi.dir == Chan_BI_DIR.DOWN else Chan_BSP_DIR.SELL, bi.sure_time, zs_count, zs, seg)
|
||||
bsp_list.append(bsp)
|
||||
#self.print_zs(zs_list)
|
||||
根据缠论线段中枢定义计算中枢
|
||||
从第4根线段开始(索引3),每3根线段为一组检查
|
||||
后一个中枢比前一个高 -> 上涨中枢,以下跌开始、以下跌结束
|
||||
后一个中枢比前一个低 -> 下跌中枢,以上涨开始、以上涨结束
|
||||
中枢可以扩展到5根、7根...
|
||||
"""
|
||||
zs_list = []
|
||||
if len(seg_list) < 3:
|
||||
return zs_list
|
||||
|
||||
last_zs = None
|
||||
|
||||
# 从第4根线段开始(索引3),每3根为一组
|
||||
start_idx = 3
|
||||
|
||||
while start_idx < len(seg_list):
|
||||
# 取连续3个线段
|
||||
if start_idx + 2 >= len(seg_list):
|
||||
break
|
||||
|
||||
seg1 = seg_list[start_idx]
|
||||
seg2 = seg_list[start_idx + 1]
|
||||
seg3 = seg_list[start_idx + 2]
|
||||
|
||||
# 三个线段都必须是已确认的
|
||||
if not (seg1.is_sure and seg2.is_sure and seg3.is_sure):
|
||||
start_idx += 1
|
||||
continue
|
||||
|
||||
# 计算这3个线段的中枢区间
|
||||
zg = min(seg1.high, seg2.high, seg3.high)
|
||||
zd = max(seg1.low, seg2.low, seg3.low)
|
||||
|
||||
if zg <= zd:
|
||||
start_idx += 1
|
||||
continue
|
||||
|
||||
# 判断中枢类型
|
||||
# 上涨中枢:以下跌开始、以下跌结束(下跌+上涨+下跌)
|
||||
# 下跌中枢:以上涨开始、以上涨结束(上涨+下跌+上涨)
|
||||
if last_zs is None:
|
||||
# 第一个中枢
|
||||
if seg1.dir == Chan_SEG_DIR.DOWN:
|
||||
# 下跌开始 -> 上涨中枢
|
||||
zs_dir = Chan_ZS_DIR.DOWN
|
||||
# 验证模式:下跌+上涨+下跌
|
||||
valid = (seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN)
|
||||
else:
|
||||
# 上涨开始 -> 下跌中枢
|
||||
zs_dir = Chan_ZS_DIR.UP
|
||||
# 验证模式:上涨+下跌+上涨
|
||||
valid = (seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP)
|
||||
else:
|
||||
# 根据与前一个中枢的高低比较判断
|
||||
if zg > last_zs.zg:
|
||||
# 上涨中枢:以下跌开始、以下跌结束
|
||||
zs_dir = Chan_ZS_DIR.DOWN
|
||||
valid = (seg1.dir == Chan_SEG_DIR.DOWN and seg2.dir == Chan_SEG_DIR.UP and seg3.dir == Chan_SEG_DIR.DOWN)
|
||||
else:
|
||||
# 下跌中枢:以上涨开始、以上涨结束
|
||||
zs_dir = Chan_ZS_DIR.UP
|
||||
valid = (seg1.dir == Chan_SEG_DIR.UP and seg2.dir == Chan_SEG_DIR.DOWN and seg3.dir == Chan_SEG_DIR.UP)
|
||||
|
||||
# 验证是否有效
|
||||
if not valid:
|
||||
start_idx += 1
|
||||
continue
|
||||
|
||||
# 检查是否与前一个中枢重叠
|
||||
if last_zs:
|
||||
# 判断是否有重叠
|
||||
overlap = (zg >= last_zs.zd and zd <= last_zs.zg)
|
||||
|
||||
if overlap:
|
||||
# 有重叠,扩展中枢到5根、7根...(缠论:合并为同一中枢)
|
||||
# 本组先纳入当前 3 根,再向后逐根尝试;遇到与 [zd,zg] 不重叠(离开中枢)则停止扩展
|
||||
added_segs = [seg_list[start_idx], seg_list[start_idx + 1], seg_list[start_idx + 2]]
|
||||
cur_idx = start_idx + 3
|
||||
|
||||
while cur_idx < len(seg_list):
|
||||
next_seg = seg_list[cur_idx]
|
||||
if not next_seg.is_sure:
|
||||
break
|
||||
|
||||
# 扩展条件:新线段与中枢区间 [zd, zg] 有重叠即并入;不重叠则停止,离开中枢的线段不包含
|
||||
# 用起止笔的极值算线段区间,避免 seg.high/seg.low 在个别线段上未同步导致的误判
|
||||
seg_high = max(next_seg.start_bi.high, next_seg.end_bi.high) if next_seg.end_bi else next_seg.start_bi.high
|
||||
seg_low = min(next_seg.start_bi.low, next_seg.end_bi.low) if next_seg.end_bi else next_seg.start_bi.low
|
||||
overlap_with_zs = (seg_high >= last_zs.zd and seg_low <= last_zs.zg)
|
||||
if not overlap_with_zs:
|
||||
break
|
||||
added_segs.append(next_seg)
|
||||
cur_idx += 1
|
||||
|
||||
# 扩展中枢 = 原中枢线段 + 本组并入的线段(缠论合并)
|
||||
segs_for_zs = list(last_zs.seg_list) + list(added_segs)
|
||||
|
||||
# 中枢开始与结束线段方向一致:上涨中枢结束于 DOWN,下跌中枢结束于 UP
|
||||
required_end_seg_dir = Chan_SEG_DIR.DOWN if last_zs.dir == Chan_ZS_DIR.DOWN else Chan_SEG_DIR.UP
|
||||
while len(segs_for_zs) >= 3 and segs_for_zs[-1].dir != required_end_seg_dir:
|
||||
segs_for_zs.pop()
|
||||
|
||||
# 扩展时只更新 gg、dd 和 seg_list;zg、zd 由前 3 根线段确定,不随扩展改变
|
||||
seg_highs = [s.high for s in segs_for_zs]
|
||||
seg_lows = [s.low for s in segs_for_zs]
|
||||
last_zs.set_gg(max(seg_highs))
|
||||
last_zs.set_dd(min(seg_lows))
|
||||
last_zs.seg_list = segs_for_zs
|
||||
|
||||
# 更新结束时间(以裁剪后的最后一段为准)
|
||||
last_seg = segs_for_zs[-1]
|
||||
if last_seg.end_bi:
|
||||
last_zs.set_end_klc(last_seg.end_bi.end_klc, last_seg.sure_time, 0, last_seg)
|
||||
last_zs.set_end_seg(last_seg)
|
||||
|
||||
# 跳过本组已扫描的线段(从 start_idx 到 cur_idx-1),下一组从 cur_idx 起可能再形成新中枢
|
||||
start_idx = cur_idx
|
||||
continue
|
||||
else:
|
||||
# 没有重叠,创建新中枢
|
||||
# 先确认前一个中枢 - 使用前一个中枢本身的最后一个线段
|
||||
if last_zs.seg_list and len(last_zs.seg_list) > 0:
|
||||
prev_zs_last_seg = last_zs.seg_list[-1]
|
||||
if prev_zs_last_seg.end_bi:
|
||||
last_zs.set_end_klc(prev_zs_last_seg.end_bi.end_klc, prev_zs_last_seg.sure_time, 0, prev_zs_last_seg)
|
||||
last_zs.set_end_seg(prev_zs_last_seg)
|
||||
last_zs.is_sure = True
|
||||
|
||||
# 创建新中枢
|
||||
gg = max(seg1.high, seg2.high, seg3.high)
|
||||
dd = min(seg1.low, seg2.low, seg3.low)
|
||||
|
||||
zs = ChanZS(seg1, len(zs_list), zs_dir)
|
||||
zs.set_zg(zg)
|
||||
zs.set_zd(zd)
|
||||
zs.set_gg(gg)
|
||||
zs.set_dd(dd)
|
||||
zs.set_end_klc(seg3.end_bi.end_klc, seg3.sure_time, 0, seg3)
|
||||
zs.set_end_seg(seg3)
|
||||
zs.is_sure = False
|
||||
zs.seg_list = [seg1, seg2, seg3]
|
||||
|
||||
if last_zs:
|
||||
last_zs.set_next(zs)
|
||||
zs.set_pre(last_zs)
|
||||
|
||||
zs_list.append(zs)
|
||||
last_zs = zs
|
||||
|
||||
# 移动到下一组
|
||||
start_idx += 3
|
||||
|
||||
# 处理最后一个未确认的中枢 - 不自动扩展,保持未完成状态
|
||||
if last_zs and not last_zs.is_sure:
|
||||
# 获取中枢最后一个线段的索引
|
||||
if last_zs.seg_list and len(last_zs.seg_list) > 0:
|
||||
last_seg_of_zs = last_zs.seg_list[-1]
|
||||
# 找到这个线段在seg_list中的索引
|
||||
last_seg_idx = -1
|
||||
for i, seg in enumerate(seg_list):
|
||||
if seg == last_seg_of_zs:
|
||||
last_seg_idx = i
|
||||
break
|
||||
|
||||
# 从中枢最后一个线段之后检查是否有离开
|
||||
has_leave = False
|
||||
if last_seg_idx >= 0 and last_seg_idx + 1 < len(seg_list):
|
||||
for i in range(last_seg_idx + 1, len(seg_list)):
|
||||
seg = seg_list[i]
|
||||
if seg.is_sure:
|
||||
# 检查是否离开中枢
|
||||
leave = (seg.low > last_zs.zg and seg.high > last_zs.zg) or \
|
||||
(seg.high < last_zs.zd and seg.low < last_zs.zd)
|
||||
if leave:
|
||||
has_leave = True
|
||||
break
|
||||
|
||||
if not has_leave:
|
||||
# 没有离开,保持未完成状态
|
||||
pass
|
||||
else:
|
||||
# 有离开,确认中枢
|
||||
if last_seg_of_zs.end_bi:
|
||||
last_zs.set_end_klc(last_seg_of_zs.end_bi.end_klc, last_seg_of_zs.sure_time, 0, last_seg_of_zs)
|
||||
last_zs.set_end_seg(last_seg_of_zs)
|
||||
last_zs.is_sure = True
|
||||
|
||||
return zs_list
|
||||
|
||||
def get_big_zs_list(self, zs_list):
|
||||
"""
|
||||
中枢扩张:将区间重叠的连续中枢合并为大级别中枢,便于显示更大级别的震荡区间。
|
||||
重叠定义:两中枢 [zd,zg] 有交集,即 (zs_i.zg >= zs_j.zd and zs_i.zd <= zs_j.zg)。
|
||||
"""
|
||||
big_list = []
|
||||
if len(zs_list) < 2:
|
||||
return big_list
|
||||
i = 0
|
||||
while i < len(zs_list):
|
||||
group = [zs_list[i]]
|
||||
j = i + 1
|
||||
while j < len(zs_list):
|
||||
cur = zs_list[j]
|
||||
# 与当前组内任一中枢有重叠即算扩张(通常只需与组内最后一个比)
|
||||
last_in_group = group[-1]
|
||||
overlap = (last_in_group.zg >= cur.zd and last_in_group.zd <= cur.zg)
|
||||
if overlap:
|
||||
group.append(cur)
|
||||
j += 1
|
||||
else:
|
||||
break
|
||||
if len(group) >= 2:
|
||||
big = ChanZS_Big(group)
|
||||
big.index = len(big_list)
|
||||
big_list.append(big)
|
||||
i = j if len(group) >= 2 else i + 1
|
||||
return big_list
|
||||
|
||||
def get_klu_list(self, dataframe):
|
||||
klu_list = self.get_kl_data(dataframe)
|
||||
#klu_list = self.cal_klu_pattern(klu_list)
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
"SOL/USDT:USDT"
|
||||
"BTC/USDT:USDT"
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"BNB/.*"
|
||||
|
||||
@@ -9,68 +9,44 @@ from typing import Optional
|
||||
from freqtrade.persistence import Trade
|
||||
import warnings
|
||||
|
||||
# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告
|
||||
# 这个警告来自 freqtrade 库的 strategy_helper.py
|
||||
warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*')
|
||||
# 或者启用未来行为(推荐)
|
||||
pd.set_option('future.no_silent_downcasting', True)
|
||||
|
||||
# freqtrade trade -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategy --strategy-path ./user_data/Chan/strategies
|
||||
# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategy --strategy-path ./user_data/Chan/strategies --timerange=20260304-
|
||||
# freqtrade download-data -c ./user_data/Chan/config/Local_Test.json -t 1m 5m --data-format-ohlcv json --pairs SOL/USDT:USDT --timerange=20260201-
|
||||
|
||||
|
||||
class CryptoFutures1m5mStrategy(IStrategy):
|
||||
"""
|
||||
SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V12e (Short Only)
|
||||
SOL/USDT 合约策略 - 只做多版 (默认策略)
|
||||
|
||||
14个月回测 (2025-01 ~ 2026-03): +107.11%, PF 1.37, DD 23.96%
|
||||
每个季度均盈利,市场下跌-54%期间持续获利
|
||||
|
||||
核心设计:
|
||||
1. 纯做空策略 - 价格必须低于EMA200至少1%才允许做空
|
||||
2. 5分钟趋势确认:EMA12<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. 时间止损:持仓过久且亏损时提前退出
|
||||
基于V5修改:禁用做空,只做多
|
||||
"""
|
||||
INTERFACE_VERSION = 3
|
||||
timeframe = '1m'
|
||||
informative_timeframe = '5m'
|
||||
can_short = True
|
||||
can_short = False # 禁用做空
|
||||
can_long = True
|
||||
lev = 1.0
|
||||
|
||||
# 止损止盈
|
||||
stoploss = -0.025 # 2.5% 硬止损
|
||||
stoploss = -0.035
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.008
|
||||
trailing_stop_positive_offset = 0.030
|
||||
trailing_stop_positive_offset = 0.035
|
||||
trailing_only_offset_is_reached = True
|
||||
|
||||
# 不使用custom_stoploss(会干扰trailing_stop)
|
||||
use_custom_stoploss = False
|
||||
|
||||
# 完全禁用 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"),
|
||||
]
|
||||
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趋势
|
||||
# EMA
|
||||
informative['ema12'] = ta.EMA(informative['close'], timeperiod=12)
|
||||
informative['ema26'] = ta.EMA(informative['close'], timeperiod=26)
|
||||
informative['ema50'] = ta.EMA(informative['close'], timeperiod=50)
|
||||
|
||||
# EMA12斜率(3根K线变化率,用于确认趋势方向的动量)
|
||||
informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100
|
||||
|
||||
# MACD
|
||||
@@ -79,66 +55,54 @@ class CryptoFutures1m5mStrategy(IStrategy):
|
||||
informative['macd_signal_5m'] = macd_signal
|
||||
informative['macd_hist_5m'] = macd_hist
|
||||
|
||||
# ADX趋势强度
|
||||
# ADX
|
||||
informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14)
|
||||
|
||||
# RSI(5分钟)
|
||||
# RSI
|
||||
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
|
||||
|
||||
# ATR(5分钟)
|
||||
# ATR
|
||||
informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)
|
||||
informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100
|
||||
|
||||
# ATR 长期均值(用于自适应波动率过滤)
|
||||
informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean()
|
||||
|
||||
# ===== EMA200 大趋势过滤 =====
|
||||
# EMA200
|
||||
informative['ema200'] = ta.EMA(informative['close'], timeperiod=200)
|
||||
informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100
|
||||
|
||||
# EMA200斜率(20根5分钟K线 = 100分钟趋势方向)
|
||||
informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100
|
||||
|
||||
# ===== 大趋势过滤(Short Only) =====
|
||||
# 做空需要价格低于EMA200至少1%
|
||||
informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0
|
||||
|
||||
# 牛市暂停:EMA200上升 + 价格在EMA200上方 → 完全停止做空
|
||||
informative['bull_pause'] = (
|
||||
(informative['ema200_slope'] > 0) &
|
||||
(informative['ema200_dist_pct'] > 0)
|
||||
# 做多趋势
|
||||
informative['trend_bull_5m'] = (
|
||||
(informative['ema12'] > informative['ema26']) &
|
||||
(informative['ema26'] > informative['ema50']) &
|
||||
(informative['ema12_slope'] > 0.05) &
|
||||
(informative['adx_5m'] > 24) &
|
||||
(informative['adx_5m'] < 51) &
|
||||
(informative['close'] > informative['ema12']) &
|
||||
(informative['rsi_5m'] > 52) &
|
||||
(informative['rsi_5m'] < 72)
|
||||
)
|
||||
|
||||
# ===== 5分钟趋势判断(仅Short) =====
|
||||
informative['trend_bear_5m'] = (
|
||||
(informative['ema12'] < informative['ema26']) &
|
||||
(informative['ema26'] < informative['ema50']) &
|
||||
(informative['ema12_slope'] < 0) &
|
||||
(informative['adx_5m'] > 25) &
|
||||
(informative['adx_5m'] < 50) &
|
||||
(informative['close'] < informative['ema12']) &
|
||||
(informative['rsi_5m'] < 48) &
|
||||
(informative['rsi_5m'] > 30)
|
||||
)
|
||||
# 大趋势过滤
|
||||
informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0
|
||||
|
||||
# 做空条件:短期趋势 + EMA200大趋势方向一致 + 非牛市
|
||||
informative['can_long_5m'] = False
|
||||
informative['can_short_5m'] = (
|
||||
informative['trend_bear_5m'] &
|
||||
informative['below_ema200'] &
|
||||
(~informative['bull_pause'])
|
||||
)
|
||||
# 做多条件
|
||||
informative['can_long_5m'] = informative['trend_bull_5m'] & informative['above_ema200']
|
||||
|
||||
# ATR波动率过滤(自适应)
|
||||
# ATR过滤
|
||||
informative['atr_ok_5m'] = (
|
||||
(informative['atr_pct_5m'] > 0.1) &
|
||||
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 1.5)
|
||||
(informative['atr_pct_5m'] > 0.07) &
|
||||
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2)
|
||||
)
|
||||
|
||||
# 合并5分钟数据到1分钟
|
||||
# 成交量
|
||||
informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20)
|
||||
informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75
|
||||
|
||||
# 合并
|
||||
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True)
|
||||
|
||||
# ==================== 1分钟指标 ====================
|
||||
# 1分钟指标
|
||||
macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe['macd'] = macd_1m
|
||||
dataframe['macd_signal'] = signal_1m
|
||||
@@ -148,116 +112,96 @@ class CryptoFutures1m5mStrategy(IStrategy):
|
||||
dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21)
|
||||
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
|
||||
dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20)
|
||||
|
||||
# ===== 1分钟MACD斜率 =====
|
||||
dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3
|
||||
|
||||
# ===== 1分钟做空入场信号 =====
|
||||
dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max()
|
||||
dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max()
|
||||
# 做多信号
|
||||
dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min()
|
||||
dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min()
|
||||
|
||||
dataframe['top_divergence'] = (
|
||||
(dataframe['high'] >= dataframe['price_high_5'] * 0.999) &
|
||||
(dataframe['macd'] < dataframe['macd_high_5']) &
|
||||
(dataframe['macd_slope'] < 0) &
|
||||
(dataframe['macd'] < dataframe['macd_signal']) &
|
||||
dataframe['bottom_divergence'] = (
|
||||
(dataframe['low'] <= dataframe['price_low_5'] * 1.001) &
|
||||
(dataframe['macd'] > dataframe['macd_low_5']) &
|
||||
(dataframe['macd_slope'] > 0) &
|
||||
(dataframe['macd'] > dataframe['macd_signal']) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
|
||||
)
|
||||
|
||||
dataframe['ema_cross_down'] = (
|
||||
(dataframe['ema9'] < dataframe['ema21']) &
|
||||
(dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) &
|
||||
(dataframe['rsi'] < 55) & (dataframe['rsi'] > 35) &
|
||||
dataframe['ema_cross_up'] = (
|
||||
(dataframe['ema9'] > dataframe['ema21']) &
|
||||
(dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) &
|
||||
(dataframe['rsi'] > 45) &
|
||||
(dataframe['rsi'] < 70) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
|
||||
)
|
||||
|
||||
dataframe['is_bear_candle'] = (
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008)
|
||||
)
|
||||
dataframe['bear_pullback'] = (
|
||||
dataframe['is_bear_candle'].shift(2) &
|
||||
(dataframe['close'].shift(1) > dataframe['open'].shift(1)) &
|
||||
(dataframe['high'] < dataframe['high'].shift(2)) &
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
(dataframe['close'] < dataframe['ema9'])
|
||||
dataframe['is_bull_candle'] = (dataframe['close'] > dataframe['open']) & ((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008)
|
||||
dataframe['bull_pullback'] = (
|
||||
dataframe['is_bull_candle'].shift(2) &
|
||||
(dataframe['close'].shift(1) < dataframe['open'].shift(1)) &
|
||||
(dataframe['low'] > dataframe['low'].shift(2)) &
|
||||
(dataframe['close'] > dataframe['open']) &
|
||||
(dataframe['close'] > dataframe['ema9'])
|
||||
)
|
||||
|
||||
# ==================== 时间过滤 ====================
|
||||
# 时间过滤
|
||||
dataframe['hour_utc'] = dataframe['date'].dt.hour
|
||||
dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7])
|
||||
|
||||
# 安全转换5分钟布尔列
|
||||
bool_cols = [
|
||||
'can_long_5m_5m', 'can_short_5m_5m',
|
||||
'trend_bear_5m_5m',
|
||||
'atr_ok_5m_5m',
|
||||
'below_ema200_5m', 'bull_pause_5m',
|
||||
]
|
||||
# 类型转换
|
||||
bool_cols = ['can_long_5m_5m', 'trend_bull_5m_5m', 'atr_ok_5m_5m', 'above_ema200_5m', 'volume_ok_5m_5m']
|
||||
for col in bool_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(bool).fillna(False)
|
||||
|
||||
num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m',
|
||||
'ema200_dist_pct_5m', 'ema200_slope_5m']
|
||||
for col in num_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(float).fillna(0.0)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
time_ok = ~dataframe['is_bad_hour']
|
||||
atr_ok = dataframe['atr_ok_5m_5m']
|
||||
volume_ok = dataframe['volume_ok_5m_5m']
|
||||
|
||||
# 5分钟MACD方向确认
|
||||
macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0
|
||||
# 只做多
|
||||
macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0
|
||||
macd_bull_1m = dataframe['macd_hist'] > 0
|
||||
|
||||
# 1分钟MACD方向确认(双重确认)
|
||||
macd_bear_1m = dataframe['macd_hist'] < 0
|
||||
|
||||
# ===== 做空入场 =====
|
||||
dataframe.loc[
|
||||
(time_ok) &
|
||||
(atr_ok) &
|
||||
(dataframe['can_short_5m_5m']) &
|
||||
(macd_bear_5m) &
|
||||
(macd_bear_1m) &
|
||||
(dataframe['rsi'] > 30) &
|
||||
(
|
||||
dataframe['top_divergence'] |
|
||||
dataframe['ema_cross_down'] |
|
||||
dataframe['bear_pullback']
|
||||
) &
|
||||
(time_ok) & (atr_ok) & (dataframe['can_long_5m_5m']) &
|
||||
(macd_bull_5m) & (macd_bull_1m) & (volume_ok) &
|
||||
(dataframe['rsi'] < 70) & (dataframe['rsi'] > 40) &
|
||||
(dataframe['bottom_divergence'] | dataframe['ema_cross_up'] | dataframe['bull_pullback']) &
|
||||
(dataframe['volume'] > 0),
|
||||
'enter_short'
|
||||
'enter_long'
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[:, 'exit_long'] = 0
|
||||
dataframe.loc[:, 'exit_short'] = 0
|
||||
return dataframe
|
||||
|
||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> str | bool | None:
|
||||
"""时间止损:持仓过久且亏损时提前退出"""
|
||||
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600
|
||||
|
||||
if trade_duration > 8 and current_profit < -0.005:
|
||||
return 'time_stop_8h'
|
||||
|
||||
if trade_duration > 16 and current_profit < 0:
|
||||
return 'time_stop_16h'
|
||||
# 做多时间止损 - 宽松
|
||||
if trade_duration > 10 and current_profit < -0.006:
|
||||
return 'time_stop_long_10h'
|
||||
if trade_duration > 20 and current_profit < 0:
|
||||
return 'time_stop_long_20h'
|
||||
if trade_duration > 30:
|
||||
return 'time_stop_long_30h'
|
||||
|
||||
return None
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> bool:
|
||||
"""入场确认 - 时间过滤安全网"""
|
||||
hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour
|
||||
if hour_utc in {4, 5, 6, 7}:
|
||||
return False
|
||||
return True
|
||||
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
return self.lev
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
# RSI(5分钟)
|
||||
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
|
||||
|
||||
# ATR(5分钟)
|
||||
informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)
|
||||
informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100
|
||||
|
||||
# ATR 长期均值
|
||||
informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean()
|
||||
|
||||
# EMA200 大趋势过滤
|
||||
informative['ema200'] = ta.EMA(informative['close'], timeperiod=200)
|
||||
informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100
|
||||
|
||||
# EMA200斜率
|
||||
informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100
|
||||
|
||||
# 大趋势过滤(Short Only)
|
||||
informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0
|
||||
|
||||
# 牛市暂停
|
||||
informative['bull_pause'] = (
|
||||
(informative['ema200_slope'] > 0) &
|
||||
(informative['ema200_dist_pct'] > 0)
|
||||
)
|
||||
|
||||
# 5分钟趋势判断(仅Short)
|
||||
informative['trend_bear_5m'] = (
|
||||
(informative['ema12'] < informative['ema26']) &
|
||||
(informative['ema26'] < informative['ema50']) &
|
||||
(informative['ema12_slope'] < -0.05) &
|
||||
(informative['adx_5m'] > 24) &
|
||||
(informative['adx_5m'] < 51) &
|
||||
(informative['close'] < informative['ema12']) &
|
||||
(informative['rsi_5m'] < 48) &
|
||||
(informative['rsi_5m'] > 29)
|
||||
)
|
||||
|
||||
# 做空条件
|
||||
informative['can_long_5m'] = False
|
||||
informative['can_short_5m'] = (
|
||||
informative['trend_bear_5m'] &
|
||||
informative['below_ema200'] &
|
||||
(~informative['bull_pause'])
|
||||
)
|
||||
|
||||
# ATR波动率过滤 - 继续放宽
|
||||
informative['atr_ok_5m'] = (
|
||||
(informative['atr_pct_5m'] > 0.07) &
|
||||
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2)
|
||||
)
|
||||
|
||||
# 成交量确认
|
||||
informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20)
|
||||
informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75
|
||||
|
||||
# 合并5分钟数据到1分钟
|
||||
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True)
|
||||
|
||||
# ==================== 1分钟指标 ====================
|
||||
macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe['macd'] = macd_1m
|
||||
dataframe['macd_signal'] = signal_1m
|
||||
dataframe['macd_hist'] = hist_1m
|
||||
|
||||
dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9)
|
||||
dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21)
|
||||
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
|
||||
dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20)
|
||||
|
||||
# 1分钟MACD斜率
|
||||
dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3
|
||||
|
||||
# 1分钟做空入场信号
|
||||
dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max()
|
||||
dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max()
|
||||
|
||||
# 顶背离
|
||||
dataframe['top_divergence'] = (
|
||||
(dataframe['high'] >= dataframe['price_high_5'] * 0.999) &
|
||||
(dataframe['macd'] < dataframe['macd_high_5']) &
|
||||
(dataframe['macd_slope'] < 0) &
|
||||
(dataframe['macd'] < dataframe['macd_signal']) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
|
||||
)
|
||||
|
||||
# EMA死叉
|
||||
dataframe['ema_cross_down'] = (
|
||||
(dataframe['ema9'] < dataframe['ema21']) &
|
||||
(dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) &
|
||||
(dataframe['rsi'] < 55) &
|
||||
(dataframe['rsi'] > 35) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
|
||||
)
|
||||
|
||||
# 熊市回调
|
||||
dataframe['is_bear_candle'] = (
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008)
|
||||
)
|
||||
dataframe['bear_pullback'] = (
|
||||
dataframe['is_bear_candle'].shift(2) &
|
||||
(dataframe['close'].shift(1) > dataframe['open'].shift(1)) &
|
||||
(dataframe['high'] < dataframe['high'].shift(2)) &
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
(dataframe['close'] < dataframe['ema9'])
|
||||
)
|
||||
|
||||
# 时间过滤
|
||||
dataframe['hour_utc'] = dataframe['date'].dt.hour
|
||||
dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7])
|
||||
|
||||
# 安全转换5分钟布尔列
|
||||
bool_cols = [
|
||||
'can_long_5m_5m', 'can_short_5m_5m',
|
||||
'trend_bear_5m_5m',
|
||||
'atr_ok_5m_5m',
|
||||
'below_ema200_5m', 'bull_pause_5m',
|
||||
'volume_ok_5m_5m',
|
||||
]
|
||||
for col in bool_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(bool).fillna(False)
|
||||
|
||||
num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m',
|
||||
'ema200_dist_pct_5m', 'ema200_slope_5m']
|
||||
for col in num_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(float).fillna(0.0)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
time_ok = ~dataframe['is_bad_hour']
|
||||
atr_ok = dataframe['atr_ok_5m_5m']
|
||||
|
||||
# 5分钟MACD方向确认
|
||||
macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0
|
||||
|
||||
# 1分钟MACD方向确认
|
||||
macd_bear_1m = dataframe['macd_hist'] < 0
|
||||
|
||||
# 成交量确认
|
||||
volume_ok = dataframe['volume_ok_5m_5m']
|
||||
|
||||
# 做空入场
|
||||
dataframe.loc[
|
||||
(time_ok) &
|
||||
(atr_ok) &
|
||||
(dataframe['can_short_5m_5m']) &
|
||||
(macd_bear_5m) &
|
||||
(macd_bear_1m) &
|
||||
(volume_ok) &
|
||||
(dataframe['rsi'] > 30) &
|
||||
(
|
||||
dataframe['top_divergence'] |
|
||||
dataframe['ema_cross_down'] |
|
||||
dataframe['bear_pullback']
|
||||
) &
|
||||
(dataframe['volume'] > 0),
|
||||
'enter_short'
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[:, 'exit_long'] = 0
|
||||
dataframe.loc[:, 'exit_short'] = 0
|
||||
return dataframe
|
||||
|
||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> str | bool | None:
|
||||
"""自定义出场逻辑:时间止损"""
|
||||
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600
|
||||
|
||||
# 时间止损:持仓过久且亏损
|
||||
if trade_duration > 8 and current_profit < -0.005:
|
||||
return 'time_stop_8h'
|
||||
|
||||
if trade_duration > 16 and current_profit < 0:
|
||||
return 'time_stop_16h'
|
||||
|
||||
# 持仓超过24小时强制平仓
|
||||
if trade_duration > 24:
|
||||
return 'time_stop_24h'
|
||||
|
||||
return None
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> bool:
|
||||
"""入场确认 - 时间过滤安全网"""
|
||||
hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour
|
||||
if hour_utc in {4, 5, 6, 7}:
|
||||
return False
|
||||
return True
|
||||
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
return self.lev
|
||||
@@ -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)
|
||||
|
||||
# RSI(5分钟)
|
||||
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
|
||||
|
||||
# ATR(5分钟)
|
||||
informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)
|
||||
informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100
|
||||
|
||||
# ATR 长期均值
|
||||
informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean()
|
||||
|
||||
# EMA200 大趋势过滤
|
||||
informative['ema200'] = ta.EMA(informative['close'], timeperiod=200)
|
||||
informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100
|
||||
|
||||
# EMA200斜率
|
||||
informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100
|
||||
|
||||
# 大趋势过滤(Short Only)- 使用hyperopt参数
|
||||
informative['below_ema200'] = informative['ema200_dist_pct'] < self.ema200_dist.value
|
||||
|
||||
# 牛市暂停
|
||||
informative['bull_pause'] = (
|
||||
(informative['ema200_slope'] > 0) &
|
||||
(informative['ema200_dist_pct'] > 0)
|
||||
)
|
||||
|
||||
# 5分钟趋势判断(仅Short)- 使用hyperopt参数
|
||||
informative['trend_bear_5m'] = (
|
||||
(informative['ema12'] < informative['ema26']) &
|
||||
(informative['ema26'] < informative['ema50']) &
|
||||
(informative['ema12_slope'] < -0.05) &
|
||||
(informative['adx_5m'] > self.adx_min.value) &
|
||||
(informative['adx_5m'] < self.adx_max.value) &
|
||||
(informative['close'] < informative['ema12']) &
|
||||
(informative['rsi_5m'] < self.rsi_max.value) &
|
||||
(informative['rsi_5m'] > self.rsi_min.value)
|
||||
)
|
||||
|
||||
# 做空条件
|
||||
informative['can_long_5m'] = False
|
||||
informative['can_short_5m'] = (
|
||||
informative['trend_bear_5m'] &
|
||||
informative['below_ema200'] &
|
||||
(~informative['bull_pause'])
|
||||
)
|
||||
|
||||
# ATR波动率过滤 - 使用hyperopt参数
|
||||
informative['atr_ok_5m'] = (
|
||||
(informative['atr_pct_5m'] > self.atr_min.value) &
|
||||
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * self.atr_max_mult.value)
|
||||
)
|
||||
|
||||
# 成交量确认 - 使用hyperopt参数
|
||||
informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20)
|
||||
informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * self.volume_threshold.value
|
||||
|
||||
# 合并5分钟数据到1分钟
|
||||
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True)
|
||||
|
||||
# ==================== 1分钟指标 ====================
|
||||
macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe['macd'] = macd_1m
|
||||
dataframe['macd_signal'] = signal_1m
|
||||
dataframe['macd_hist'] = hist_1m
|
||||
|
||||
dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9)
|
||||
dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21)
|
||||
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
|
||||
dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20)
|
||||
|
||||
# 1分钟MACD斜率
|
||||
dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3
|
||||
|
||||
# 1分钟做空入场信号
|
||||
dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max()
|
||||
dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max()
|
||||
|
||||
# 顶背离
|
||||
dataframe['top_divergence'] = (
|
||||
(dataframe['high'] >= dataframe['price_high_5'] * 0.999) &
|
||||
(dataframe['macd'] < dataframe['macd_high_5']) &
|
||||
(dataframe['macd_slope'] < 0) &
|
||||
(dataframe['macd'] < dataframe['macd_signal']) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
|
||||
)
|
||||
|
||||
# EMA死叉
|
||||
dataframe['ema_cross_down'] = (
|
||||
(dataframe['ema9'] < dataframe['ema21']) &
|
||||
(dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) &
|
||||
(dataframe['rsi'] < 55) &
|
||||
(dataframe['rsi'] > 35) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
|
||||
)
|
||||
|
||||
# 熊市回调
|
||||
dataframe['is_bear_candle'] = (
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008)
|
||||
)
|
||||
dataframe['bear_pullback'] = (
|
||||
dataframe['is_bear_candle'].shift(2) &
|
||||
(dataframe['close'].shift(1) > dataframe['open'].shift(1)) &
|
||||
(dataframe['high'] < dataframe['high'].shift(2)) &
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
(dataframe['close'] < dataframe['ema9'])
|
||||
)
|
||||
|
||||
# 时间过滤
|
||||
dataframe['hour_utc'] = dataframe['date'].dt.hour
|
||||
dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7])
|
||||
|
||||
# 安全转换5分钟布尔列
|
||||
bool_cols = [
|
||||
'can_long_5m_5m', 'can_short_5m_5m',
|
||||
'trend_bear_5m_5m',
|
||||
'atr_ok_5m_5m',
|
||||
'below_ema200_5m', 'bull_pause_5m',
|
||||
'volume_ok_5m_5m',
|
||||
]
|
||||
for col in bool_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(bool).fillna(False)
|
||||
|
||||
num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m',
|
||||
'ema200_dist_pct_5m', 'ema200_slope_5m']
|
||||
for col in num_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(float).fillna(0.0)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
time_ok = ~dataframe['is_bad_hour']
|
||||
atr_ok = dataframe['atr_ok_5m_5m']
|
||||
|
||||
# 5分钟MACD方向确认
|
||||
macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0
|
||||
|
||||
# 1分钟MACD方向确认
|
||||
macd_bear_1m = dataframe['macd_hist'] < 0
|
||||
|
||||
# 成交量确认
|
||||
volume_ok = dataframe['volume_ok_5m_5m']
|
||||
|
||||
# 做空入场 - 使用hyperopt参数
|
||||
dataframe.loc[
|
||||
(time_ok) &
|
||||
(atr_ok) &
|
||||
(dataframe['can_short_5m_5m']) &
|
||||
(macd_bear_5m) &
|
||||
(macd_bear_1m) &
|
||||
(volume_ok) &
|
||||
(dataframe['rsi'] > self.entry_rsi_min.value) &
|
||||
(
|
||||
dataframe['top_divergence'] |
|
||||
dataframe['ema_cross_down'] |
|
||||
dataframe['bear_pullback']
|
||||
) &
|
||||
(dataframe['volume'] > 0),
|
||||
'enter_short'
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[:, 'exit_long'] = 0
|
||||
dataframe.loc[:, 'exit_short'] = 0
|
||||
return dataframe
|
||||
|
||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> str | bool | None:
|
||||
"""自定义出场逻辑:时间止损 - 使用hyperopt参数"""
|
||||
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600
|
||||
|
||||
# 时间止损:持仓过久且亏损
|
||||
if trade_duration > self.time_stop_1.value and current_profit < -0.005:
|
||||
return 'time_stop_1'
|
||||
|
||||
if trade_duration > self.time_stop_2.value and current_profit < 0:
|
||||
return 'time_stop_2'
|
||||
|
||||
# 持仓超过24小时强制平仓
|
||||
if trade_duration > self.time_stop_3.value:
|
||||
return 'time_stop_3'
|
||||
|
||||
return None
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> bool:
|
||||
"""入场确认 - 时间过滤安全网"""
|
||||
hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour
|
||||
if hour_utc in {4, 5, 6, 7}:
|
||||
return False
|
||||
return True
|
||||
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
return self.lev
|
||||
@@ -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)
|
||||
|
||||
# RSI(5分钟)
|
||||
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
|
||||
|
||||
# ATR(5分钟)
|
||||
informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)
|
||||
informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100
|
||||
|
||||
# ATR 长期均值
|
||||
informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean()
|
||||
|
||||
# EMA200 大趋势过滤
|
||||
informative['ema200'] = ta.EMA(informative['close'], timeperiod=200)
|
||||
informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100
|
||||
|
||||
# EMA200斜率
|
||||
informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100
|
||||
|
||||
# ==================== 多空趋势判断 ====================
|
||||
|
||||
# 5分钟趋势判断 - 做多 (Bull)
|
||||
informative['trend_bull_5m'] = (
|
||||
(informative['ema12'] > informative['ema26']) &
|
||||
(informative['ema26'] > informative['ema50']) &
|
||||
(informative['ema12_slope'] > 0.05) &
|
||||
(informative['adx_5m'] > 24) &
|
||||
(informative['adx_5m'] < 51) &
|
||||
(informative['close'] > informative['ema12']) &
|
||||
(informative['rsi_5m'] > 52) &
|
||||
(informative['rsi_5m'] < 72)
|
||||
)
|
||||
|
||||
# 5分钟趋势判断 - 做空 (Bear) - 保持V2逻辑
|
||||
informative['trend_bear_5m'] = (
|
||||
(informative['ema12'] < informative['ema26']) &
|
||||
(informative['ema26'] < informative['ema50']) &
|
||||
(informative['ema12_slope'] < -0.05) &
|
||||
(informative['adx_5m'] > 24) &
|
||||
(informative['adx_5m'] < 51) &
|
||||
(informative['close'] < informative['ema12']) &
|
||||
(informative['rsi_5m'] < 48) &
|
||||
(informative['rsi_5m'] > 29)
|
||||
)
|
||||
|
||||
# 大趋势过滤
|
||||
informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0 # 做多需要高于EMA200
|
||||
informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 # 做空需要低于EMA200
|
||||
|
||||
# 牛市环境 (仅做多)
|
||||
informative['bull_market'] = (
|
||||
(informative['ema200_slope'] > 0) &
|
||||
(informative['ema200_dist_pct'] > 0)
|
||||
)
|
||||
|
||||
# 熊市环境 (仅做空)
|
||||
informative['bear_market'] = (
|
||||
(informative['ema200_slope'] < 0) &
|
||||
(informative['ema200_dist_pct'] < 0)
|
||||
)
|
||||
|
||||
# 做多条件
|
||||
informative['can_long_5m'] = (
|
||||
informative['trend_bull_5m'] &
|
||||
informative['above_ema200']
|
||||
)
|
||||
|
||||
# 做空条件 - 保持V2逻辑
|
||||
informative['can_short_5m'] = (
|
||||
informative['trend_bear_5m'] &
|
||||
informative['below_ema200']
|
||||
)
|
||||
|
||||
# ATR波动率过滤
|
||||
informative['atr_ok_5m'] = (
|
||||
(informative['atr_pct_5m'] > 0.07) &
|
||||
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.2)
|
||||
)
|
||||
|
||||
# 成交量确认
|
||||
informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20)
|
||||
informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75
|
||||
|
||||
# 合并5分钟数据到1分钟
|
||||
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True)
|
||||
|
||||
# ==================== 1分钟指标 ====================
|
||||
macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe['macd'] = macd_1m
|
||||
dataframe['macd_signal'] = signal_1m
|
||||
dataframe['macd_hist'] = hist_1m
|
||||
|
||||
dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9)
|
||||
dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21)
|
||||
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
|
||||
dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20)
|
||||
|
||||
# 1分钟MACD斜率
|
||||
dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3
|
||||
|
||||
# ==================== 做空信号 (保持V2) ====================
|
||||
|
||||
# 1分钟价格/MACD
|
||||
dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max()
|
||||
dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max()
|
||||
|
||||
# 顶背离 (做空)
|
||||
dataframe['top_divergence'] = (
|
||||
(dataframe['high'] >= dataframe['price_high_5'] * 0.999) &
|
||||
(dataframe['macd'] < dataframe['macd_high_5']) &
|
||||
(dataframe['macd_slope'] < 0) &
|
||||
(dataframe['macd'] < dataframe['macd_signal']) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
|
||||
)
|
||||
|
||||
# EMA死叉 (做空)
|
||||
dataframe['ema_cross_down'] = (
|
||||
(dataframe['ema9'] < dataframe['ema21']) &
|
||||
(dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) &
|
||||
(dataframe['rsi'] < 55) &
|
||||
(dataframe['rsi'] > 35) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
|
||||
)
|
||||
|
||||
# 熊市回调 (做空)
|
||||
dataframe['is_bear_candle'] = (
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008)
|
||||
)
|
||||
dataframe['bear_pullback'] = (
|
||||
dataframe['is_bear_candle'].shift(2) &
|
||||
(dataframe['close'].shift(1) > dataframe['open'].shift(1)) &
|
||||
(dataframe['high'] < dataframe['high'].shift(2)) &
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
(dataframe['close'] < dataframe['ema9'])
|
||||
)
|
||||
|
||||
# ==================== 做多信号 (新增) ====================
|
||||
|
||||
dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min()
|
||||
dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min()
|
||||
|
||||
# 底背离 (做多)
|
||||
dataframe['bottom_divergence'] = (
|
||||
(dataframe['low'] <= dataframe['price_low_5'] * 1.001) &
|
||||
(dataframe['macd'] > dataframe['macd_low_5']) &
|
||||
(dataframe['macd_slope'] > 0) &
|
||||
(dataframe['macd'] > dataframe['macd_signal']) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
|
||||
)
|
||||
|
||||
# EMA金叉 (做多)
|
||||
dataframe['ema_cross_up'] = (
|
||||
(dataframe['ema9'] > dataframe['ema21']) &
|
||||
(dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) &
|
||||
(dataframe['rsi'] > 45) &
|
||||
(dataframe['rsi'] < 70) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
|
||||
)
|
||||
|
||||
# 牛市回调 (做多)
|
||||
dataframe['is_bull_candle'] = (
|
||||
(dataframe['close'] > dataframe['open']) &
|
||||
((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008)
|
||||
)
|
||||
dataframe['bull_pullback'] = (
|
||||
dataframe['is_bull_candle'].shift(2) &
|
||||
(dataframe['close'].shift(1) < dataframe['open'].shift(1)) &
|
||||
(dataframe['low'] > dataframe['low'].shift(2)) &
|
||||
(dataframe['close'] > dataframe['open']) &
|
||||
(dataframe['close'] > dataframe['ema9'])
|
||||
)
|
||||
|
||||
# 时间过滤
|
||||
dataframe['hour_utc'] = dataframe['date'].dt.hour
|
||||
dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7])
|
||||
|
||||
# 安全转换5分钟布尔列
|
||||
bool_cols = [
|
||||
'can_long_5m_5m', 'can_short_5m_5m',
|
||||
'trend_bull_5m_5m', 'trend_bear_5m_5m',
|
||||
'atr_ok_5m_5m',
|
||||
'above_ema200_5m', 'below_ema200_5m',
|
||||
'bull_market_5m', 'bear_market_5m',
|
||||
'volume_ok_5m_5m',
|
||||
]
|
||||
for col in bool_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(bool).fillna(False)
|
||||
|
||||
num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m',
|
||||
'ema200_dist_pct_5m', 'ema200_slope_5m']
|
||||
for col in num_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(float).fillna(0.0)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
time_ok = ~dataframe['is_bad_hour']
|
||||
atr_ok = dataframe['atr_ok_5m_5m']
|
||||
volume_ok = dataframe['volume_ok_5m_5m']
|
||||
|
||||
# ========== 做空入场 (保持V2逻辑) ==========
|
||||
macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0
|
||||
macd_bear_1m = dataframe['macd_hist'] < 0
|
||||
|
||||
dataframe.loc[
|
||||
(time_ok) &
|
||||
(atr_ok) &
|
||||
(dataframe['can_short_5m_5m']) &
|
||||
(macd_bear_5m) &
|
||||
(macd_bear_1m) &
|
||||
(volume_ok) &
|
||||
(dataframe['rsi'] > 30) &
|
||||
(
|
||||
dataframe['top_divergence'] |
|
||||
dataframe['ema_cross_down'] |
|
||||
dataframe['bear_pullback']
|
||||
) &
|
||||
(dataframe['volume'] > 0),
|
||||
'enter_short'
|
||||
] = 1
|
||||
|
||||
# ========== 做多入场 (新增) ==========
|
||||
macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0
|
||||
macd_bull_1m = dataframe['macd_hist'] > 0
|
||||
|
||||
dataframe.loc[
|
||||
(time_ok) &
|
||||
(atr_ok) &
|
||||
(dataframe['can_long_5m_5m']) &
|
||||
(macd_bull_5m) &
|
||||
(macd_bull_1m) &
|
||||
(volume_ok) &
|
||||
(dataframe['rsi'] < 70) &
|
||||
(dataframe['rsi'] > 40) &
|
||||
(
|
||||
dataframe['bottom_divergence'] |
|
||||
dataframe['ema_cross_up'] |
|
||||
dataframe['bull_pullback']
|
||||
) &
|
||||
(dataframe['volume'] > 0),
|
||||
'enter_long'
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[:, 'exit_long'] = 0
|
||||
dataframe.loc[:, 'exit_short'] = 0
|
||||
return dataframe
|
||||
|
||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> str | bool | None:
|
||||
"""自定义出场逻辑:时间止损"""
|
||||
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600
|
||||
|
||||
# 时间止损:持仓过久且亏损
|
||||
if trade_duration > 8 and current_profit < -0.005:
|
||||
return 'time_stop_8h'
|
||||
|
||||
if trade_duration > 16 and current_profit < 0:
|
||||
return 'time_stop_16h'
|
||||
|
||||
# 持仓超过24小时强制平仓
|
||||
if trade_duration > 24:
|
||||
return 'time_stop_24h'
|
||||
|
||||
return None
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> bool:
|
||||
"""入场确认 - 时间过滤安全网"""
|
||||
hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour
|
||||
if hour_utc in {4, 5, 6, 7}:
|
||||
return False
|
||||
return True
|
||||
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
return self.lev
|
||||
@@ -0,0 +1,404 @@
|
||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||
from freqtrade.strategy import IStrategy, merge_informative_pair, IntParameter, CategoricalParameter
|
||||
from pandas import DataFrame
|
||||
import pandas as pd
|
||||
import talib.abstract as ta
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from freqtrade.persistence import Trade
|
||||
import warnings
|
||||
|
||||
# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告
|
||||
warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*')
|
||||
pd.set_option('future.no_silent_downcasting', True)
|
||||
|
||||
# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV4 --strategy-path ./user_data/Chan/strategies --timerange=20250101-
|
||||
|
||||
|
||||
class CryptoFutures1m5mStrategyV4(IStrategy):
|
||||
"""
|
||||
SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V4 多空完全分离版
|
||||
|
||||
基于V3优化:
|
||||
1. 多空参数完全分离
|
||||
2. 分别优化做多做空的风险参数
|
||||
|
||||
核心设计:
|
||||
1. 5分钟趋势确认 + 1分钟精确入场
|
||||
2. ATR自适应波动率过滤
|
||||
3. 多空trailing参数分离
|
||||
"""
|
||||
INTERFACE_VERSION = 3
|
||||
timeframe = '1m'
|
||||
informative_timeframe = '5m'
|
||||
can_short = True
|
||||
can_long = True
|
||||
lev = 1.0
|
||||
|
||||
# ==================== 多空分离参数 ====================
|
||||
|
||||
# 做多止损 (更宽松,因为牛市回调幅度大)
|
||||
stoploss_long = -0.035
|
||||
|
||||
# 做空止损 (相对紧凑,熊市反弹快)
|
||||
stoploss_short = -0.025
|
||||
|
||||
# 统一下跌止损(取两者较宽松值)
|
||||
stoploss = -0.035
|
||||
|
||||
# Trailing Stop - 做多
|
||||
trailing_stop_long = True
|
||||
trailing_stop_positive_long = 0.006
|
||||
trailing_stop_positive_offset_long = 0.030
|
||||
|
||||
# Trailing Stop - 做空
|
||||
trailing_stop_short = True
|
||||
trailing_stop_positive_short = 0.010
|
||||
trailing_stop_positive_offset_short = 0.038
|
||||
|
||||
# 统一设置
|
||||
trailing_stop = True
|
||||
trailing_stop_positive = 0.008
|
||||
trailing_stop_positive_offset = 0.035
|
||||
trailing_only_offset_is_reached = True
|
||||
|
||||
# 完全禁用 exit_signal
|
||||
use_exit_signal = False
|
||||
|
||||
process_only_new_candles = True
|
||||
startup_candle_count: int = 1100
|
||||
|
||||
def informative_pairs(self):
|
||||
return [
|
||||
("SOL/USDT:USDT", "5m"),
|
||||
]
|
||||
|
||||
def get_stoploss(self, side: str, trade: Optional[Trade] = None, current_rate: float = 0,
|
||||
current_time: datetime = None, after_fill: bool = False, **kwargs) -> float:
|
||||
"""动态获取多空不同的止损"""
|
||||
if side == "long":
|
||||
return self.stoploss_long
|
||||
elif side == "short":
|
||||
return self.stoploss_short
|
||||
return self.stoploss
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
# ==================== 5分钟指标 ====================
|
||||
inf_tf = self.informative_timeframe
|
||||
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf)
|
||||
|
||||
# EMA趋势
|
||||
informative['ema12'] = ta.EMA(informative['close'], timeperiod=12)
|
||||
informative['ema26'] = ta.EMA(informative['close'], timeperiod=26)
|
||||
informative['ema50'] = ta.EMA(informative['close'], timeperiod=50)
|
||||
|
||||
# EMA12斜率(3根K线变化率)
|
||||
informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100
|
||||
|
||||
# MACD
|
||||
macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
informative['macd_5m'] = macd
|
||||
informative['macd_signal_5m'] = macd_signal
|
||||
informative['macd_hist_5m'] = macd_hist
|
||||
|
||||
# ADX趋势强度
|
||||
informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14)
|
||||
|
||||
# RSI(5分钟)
|
||||
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
|
||||
|
||||
# ATR(5分钟)
|
||||
informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)
|
||||
informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100
|
||||
|
||||
# ATR 长期均值
|
||||
informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean()
|
||||
|
||||
# ATR 短期均值 (用于做空过滤 - 更严格)
|
||||
informative['atr_pct_ma_short_5m'] = informative['atr_pct_5m'].rolling(window=20).mean()
|
||||
|
||||
# EMA200 大趋势过滤
|
||||
informative['ema200'] = ta.EMA(informative['close'], timeperiod=200)
|
||||
informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100
|
||||
|
||||
# EMA200斜率
|
||||
informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100
|
||||
|
||||
# ==================== 多空趋势判断 ====================
|
||||
|
||||
# 5分钟趋势判断 - 做多 (Bull)
|
||||
informative['trend_bull_5m'] = (
|
||||
(informative['ema12'] > informative['ema26']) &
|
||||
(informative['ema26'] > informative['ema50']) &
|
||||
(informative['ema12_slope'] > 0.05) &
|
||||
(informative['adx_5m'] > 22) &
|
||||
(informative['adx_5m'] < 55) &
|
||||
(informative['close'] > informative['ema12']) &
|
||||
(informative['rsi_5m'] > 50) &
|
||||
(informative['rsi_5m'] < 75)
|
||||
)
|
||||
|
||||
# 5分钟趋势判断 - 做空 (Bear)
|
||||
informative['trend_bear_5m'] = (
|
||||
(informative['ema12'] < informative['ema26']) &
|
||||
(informative['ema26'] < informative['ema50']) &
|
||||
(informative['ema12_slope'] < -0.05) &
|
||||
(informative['adx_5m'] > 26) &
|
||||
(informative['adx_5m'] < 50) &
|
||||
(informative['close'] < informative['ema12']) &
|
||||
(informative['rsi_5m'] < 50) &
|
||||
(informative['rsi_5m'] > 28)
|
||||
)
|
||||
|
||||
# 大趋势过滤
|
||||
informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0
|
||||
informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0
|
||||
|
||||
# 牛市/熊市环境
|
||||
informative['bull_market'] = (
|
||||
(informative['ema200_slope'] > 0) &
|
||||
(informative['ema200_dist_pct'] > 0)
|
||||
)
|
||||
informative['bear_market'] = (
|
||||
(informative['ema200_slope'] < 0) &
|
||||
(informative['ema200_dist_pct'] < 0)
|
||||
)
|
||||
|
||||
# 做多条件
|
||||
informative['can_long_5m'] = (
|
||||
informative['trend_bull_5m'] &
|
||||
informative['above_ema200']
|
||||
)
|
||||
|
||||
# 做空条件
|
||||
informative['can_short_5m'] = (
|
||||
informative['trend_bear_5m'] &
|
||||
informative['below_ema200']
|
||||
)
|
||||
|
||||
# ==================== 多空分离的ATR过滤 ====================
|
||||
|
||||
# 做多ATR过滤 - 允许更大波动(牛市波动大)
|
||||
informative['atr_ok_long_5m'] = (
|
||||
(informative['atr_pct_5m'] > 0.08) &
|
||||
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.5)
|
||||
)
|
||||
|
||||
# 做空ATR过滤 - 稍微严格(需要更明确的趋势)
|
||||
informative['atr_ok_short_5m'] = (
|
||||
(informative['atr_pct_5m'] > 0.06) &
|
||||
(informative['atr_pct_5m'] < informative['atr_pct_ma_short_5m'] * 2.0)
|
||||
)
|
||||
|
||||
# 成交量确认
|
||||
informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20)
|
||||
informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75
|
||||
|
||||
# 合并5分钟数据到1分钟
|
||||
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True)
|
||||
|
||||
# ==================== 1分钟指标 ====================
|
||||
macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
dataframe['macd'] = macd_1m
|
||||
dataframe['macd_signal'] = signal_1m
|
||||
dataframe['macd_hist'] = hist_1m
|
||||
|
||||
dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9)
|
||||
dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21)
|
||||
dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
|
||||
dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20)
|
||||
|
||||
# 1分钟MACD斜率
|
||||
dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3
|
||||
|
||||
# ==================== 做空信号 ====================
|
||||
dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max()
|
||||
dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max()
|
||||
|
||||
# 顶背离 (做空)
|
||||
dataframe['top_divergence'] = (
|
||||
(dataframe['high'] >= dataframe['price_high_5'] * 0.999) &
|
||||
(dataframe['macd'] < dataframe['macd_high_5']) &
|
||||
(dataframe['macd_slope'] < 0) &
|
||||
(dataframe['macd'] < dataframe['macd_signal']) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
|
||||
)
|
||||
|
||||
# EMA死叉 (做空)
|
||||
dataframe['ema_cross_down'] = (
|
||||
(dataframe['ema9'] < dataframe['ema21']) &
|
||||
(dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) &
|
||||
(dataframe['rsi'] < 58) &
|
||||
(dataframe['rsi'] > 35) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
|
||||
)
|
||||
|
||||
# 熊市回调 (做空)
|
||||
dataframe['is_bear_candle'] = (
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008)
|
||||
)
|
||||
dataframe['bear_pullback'] = (
|
||||
dataframe['is_bear_candle'].shift(2) &
|
||||
(dataframe['close'].shift(1) > dataframe['open'].shift(1)) &
|
||||
(dataframe['high'] < dataframe['high'].shift(2)) &
|
||||
(dataframe['close'] < dataframe['open']) &
|
||||
(dataframe['close'] < dataframe['ema9'])
|
||||
)
|
||||
|
||||
# ==================== 做多信号 ====================
|
||||
dataframe['price_low_5'] = dataframe['low'].rolling(window=5).min()
|
||||
dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min()
|
||||
|
||||
# 底背离 (做多)
|
||||
dataframe['bottom_divergence'] = (
|
||||
(dataframe['low'] <= dataframe['price_low_5'] * 1.001) &
|
||||
(dataframe['macd'] > dataframe['macd_low_5']) &
|
||||
(dataframe['macd_slope'] > 0) &
|
||||
(dataframe['macd'] > dataframe['macd_signal']) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
|
||||
)
|
||||
|
||||
# EMA金叉 (做多)
|
||||
dataframe['ema_cross_up'] = (
|
||||
(dataframe['ema9'] > dataframe['ema21']) &
|
||||
(dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) &
|
||||
(dataframe['rsi'] > 42) &
|
||||
(dataframe['rsi'] < 72) &
|
||||
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
|
||||
)
|
||||
|
||||
# 牛市回调 (做多)
|
||||
dataframe['is_bull_candle'] = (
|
||||
(dataframe['close'] > dataframe['open']) &
|
||||
((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008)
|
||||
)
|
||||
dataframe['bull_pullback'] = (
|
||||
dataframe['is_bull_candle'].shift(2) &
|
||||
(dataframe['close'].shift(1) < dataframe['open'].shift(1)) &
|
||||
(dataframe['low'] > dataframe['low'].shift(2)) &
|
||||
(dataframe['close'] > dataframe['open']) &
|
||||
(dataframe['close'] > dataframe['ema9'])
|
||||
)
|
||||
|
||||
# ==================== 时间过滤 ====================
|
||||
dataframe['hour_utc'] = dataframe['date'].dt.hour
|
||||
dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7])
|
||||
|
||||
# 安全转换5分钟布尔列
|
||||
bool_cols = [
|
||||
'can_long_5m_5m', 'can_short_5m_5m',
|
||||
'trend_bull_5m_5m', 'trend_bear_5m_5m',
|
||||
'atr_ok_long_5m_5m', 'atr_ok_short_5m_5m',
|
||||
'above_ema200_5m', 'below_ema200_5m',
|
||||
'bull_market_5m', 'bear_market_5m',
|
||||
'volume_ok_5m_5m',
|
||||
]
|
||||
for col in bool_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(bool).fillna(False)
|
||||
|
||||
num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m',
|
||||
'atr_pct_ma_5m_5m', 'atr_pct_ma_short_5m_5m',
|
||||
'ema200_dist_pct_5m', 'ema200_slope_5m']
|
||||
for col in num_cols:
|
||||
if col in dataframe.columns:
|
||||
dataframe[col] = dataframe[col].astype(float).fillna(0.0)
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
time_ok = ~dataframe['is_bad_hour']
|
||||
volume_ok = dataframe['volume_ok_5m_5m']
|
||||
|
||||
# ========== 做空入场 ==========
|
||||
atr_ok_short = dataframe['atr_ok_short_5m_5m']
|
||||
macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0
|
||||
macd_bear_1m = dataframe['macd_hist'] < 0
|
||||
|
||||
dataframe.loc[
|
||||
(time_ok) &
|
||||
(atr_ok_short) &
|
||||
(dataframe['can_short_5m_5m']) &
|
||||
(macd_bear_5m) &
|
||||
(macd_bear_1m) &
|
||||
(volume_ok) &
|
||||
(dataframe['rsi'] > 32) &
|
||||
(
|
||||
dataframe['top_divergence'] |
|
||||
dataframe['ema_cross_down'] |
|
||||
dataframe['bear_pullback']
|
||||
) &
|
||||
(dataframe['volume'] > 0),
|
||||
'enter_short'
|
||||
] = 1
|
||||
|
||||
# ========== 做多入场 ==========
|
||||
atr_ok_long = dataframe['atr_ok_long_5m_5m']
|
||||
macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0
|
||||
macd_bull_1m = dataframe['macd_hist'] > 0
|
||||
|
||||
dataframe.loc[
|
||||
(time_ok) &
|
||||
(atr_ok_long) &
|
||||
(dataframe['can_long_5m_5m']) &
|
||||
(macd_bull_5m) &
|
||||
(macd_bull_1m) &
|
||||
(volume_ok) &
|
||||
(dataframe['rsi'] < 72) &
|
||||
(dataframe['rsi'] > 38) &
|
||||
(
|
||||
dataframe['bottom_divergence'] |
|
||||
dataframe['ema_cross_up'] |
|
||||
dataframe['bull_pullback']
|
||||
) &
|
||||
(dataframe['volume'] > 0),
|
||||
'enter_long'
|
||||
] = 1
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
dataframe.loc[:, 'exit_long'] = 0
|
||||
dataframe.loc[:, 'exit_short'] = 0
|
||||
return dataframe
|
||||
|
||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, **kwargs) -> str | bool | None:
|
||||
"""自定义出场逻辑 - 多空不同的时间止损"""
|
||||
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600
|
||||
|
||||
# ==================== 做空时间止损 (更激进) ====================
|
||||
if trade.trade_direction == 'short':
|
||||
if trade_duration > 6 and current_profit < -0.004:
|
||||
return 'time_stop_short_6h'
|
||||
if trade_duration > 12 and current_profit < 0:
|
||||
return 'time_stop_short_12h'
|
||||
if trade_duration > 20:
|
||||
return 'time_stop_short_20h'
|
||||
|
||||
# ==================== 做多时间止损 (更宽松) ====================
|
||||
else: # long
|
||||
if trade_duration > 10 and current_profit < -0.006:
|
||||
return 'time_stop_long_10h'
|
||||
if trade_duration > 20 and current_profit < 0:
|
||||
return 'time_stop_long_20h'
|
||||
if trade_duration > 30:
|
||||
return 'time_stop_long_30h'
|
||||
|
||||
return None
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
||||
side: str, **kwargs) -> bool:
|
||||
"""入场确认 - 时间过滤安全网"""
|
||||
hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour
|
||||
if hour_utc in {4, 5, 6, 7}:
|
||||
return False
|
||||
return True
|
||||
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
return self.lev
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
@@ -438,6 +438,7 @@ def add_indicators(df):
|
||||
df['ema10'] = (ta.EMA(df, timeperiod=10)).fillna(0)
|
||||
df['ema24'] = (ta.EMA(df, timeperiod=24)).fillna(0)
|
||||
df['ema52'] = (ta.EMA(df, timeperiod=52)).fillna(0)
|
||||
df['ema26'] = (ta.EMA(df, timeperiod=26)).fillna(0)
|
||||
# 常用SMA 24/52
|
||||
try:
|
||||
df['sma24'] = (ta.SMA(df, timeperiod=24)).fillna(0)
|
||||
|
||||
@@ -5477,7 +5477,7 @@
|
||||
if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示
|
||||
displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
|
||||
}
|
||||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("01", "");
|
||||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "").replace("01", "");
|
||||
// 添加标记配置
|
||||
const markerConfig = {
|
||||
time: timestamp,
|
||||
|
||||
Reference in New Issue
Block a user