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