添加新的策略
This commit is contained in:
@@ -173,6 +173,7 @@ class Chan_PRICE_TREND(Enum):
|
|||||||
FLAT = auto()
|
FLAT = auto()
|
||||||
UNKNOWN = auto()
|
UNKNOWN = auto()
|
||||||
class Chan_KLC_FX(Enum):
|
class Chan_KLC_FX(Enum):
|
||||||
|
TOP0 = auto()
|
||||||
TOP1 = auto()
|
TOP1 = auto()
|
||||||
TOP2 = auto()
|
TOP2 = auto()
|
||||||
TOP3 = auto()
|
TOP3 = auto()
|
||||||
@@ -181,6 +182,7 @@ class Chan_KLC_FX(Enum):
|
|||||||
TOP6 = auto()
|
TOP6 = auto()
|
||||||
TOP7 = auto()
|
TOP7 = auto()
|
||||||
TOP8 = auto()
|
TOP8 = auto()
|
||||||
|
BOTTOM0 = auto()
|
||||||
BOTTOM1 = auto()
|
BOTTOM1 = auto()
|
||||||
BOTTOM2 = auto()
|
BOTTOM2 = auto()
|
||||||
BOTTOM3 = auto()
|
BOTTOM3 = auto()
|
||||||
|
|||||||
+14
@@ -64,6 +64,8 @@ class ChanKLC():
|
|||||||
self.bb2633upper = klu.bb2633upper
|
self.bb2633upper = klu.bb2633upper
|
||||||
self.bb2633lower = klu.bb2633lower
|
self.bb2633lower = klu.bb2633lower
|
||||||
self.bb2633middle = klu.bb2633middle
|
self.bb2633middle = klu.bb2633middle
|
||||||
|
self.ema5 = klu.ema5
|
||||||
|
self.ma5 = klu.ma5
|
||||||
# ==================== EMA 通用计算方法 ====================
|
# ==================== EMA 通用计算方法 ====================
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -337,6 +339,14 @@ class ChanKLC():
|
|||||||
self.klc_dir = Chan_KLINE_DIR.UP if self.close > self.open else Chan_KLINE_DIR.DOWN
|
self.klc_dir = Chan_KLINE_DIR.UP if self.close > self.open else Chan_KLINE_DIR.DOWN
|
||||||
self.cal_indicators()
|
self.cal_indicators()
|
||||||
self.cal_all_ema_status()
|
self.cal_all_ema_status()
|
||||||
|
if self.open > self.high:
|
||||||
|
self.open = self.high
|
||||||
|
if self.close > self.high:
|
||||||
|
self.close = self.high
|
||||||
|
if self.close < self.low:
|
||||||
|
self.close = self.low
|
||||||
|
if self.close > self.high:
|
||||||
|
self.close = self.high
|
||||||
def cal_fx(self):
|
def cal_fx(self):
|
||||||
if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2:
|
if self.klc_fx_type == Chan_KLC_FX.TOP1 or self.klc_fx_type == Chan_KLC_FX.TOP2:
|
||||||
#print(self.end_time, self.fx, self.macd, self.macdhist, len(self.klu_list))
|
#print(self.end_time, self.fx, self.macd, self.macdhist, len(self.klu_list))
|
||||||
@@ -385,6 +395,8 @@ class ChanKLC():
|
|||||||
self.bb2633upper += self.klu_list[index].bb2633upper
|
self.bb2633upper += self.klu_list[index].bb2633upper
|
||||||
self.bb2633lower += self.klu_list[index].bb2633lower
|
self.bb2633lower += self.klu_list[index].bb2633lower
|
||||||
self.bb2633middle += self.klu_list[index].bb2633middle
|
self.bb2633middle += self.klu_list[index].bb2633middle
|
||||||
|
self.ma5 += self.klu_list[index].ma5
|
||||||
|
self.ema5 += self.klu_list[index].ema5
|
||||||
if self.ema_dir != self.klu_list[index].ema_dir:
|
if self.ema_dir != self.klu_list[index].ema_dir:
|
||||||
self.ema_dir = 0
|
self.ema_dir = 0
|
||||||
n = len(self.klu_list)
|
n = len(self.klu_list)
|
||||||
@@ -397,6 +409,8 @@ class ChanKLC():
|
|||||||
self.ema104 = self.ema104 / n
|
self.ema104 = self.ema104 / n
|
||||||
self.ema156 = self.ema156 / n
|
self.ema156 = self.ema156 / n
|
||||||
self.ema208 = self.ema208 / n
|
self.ema208 = self.ema208 / n
|
||||||
|
self.ma5 = self.ma5 / n
|
||||||
|
self.ema5 = self.ema5 / n
|
||||||
self.bb2633upper = self.bb2633upper / n
|
self.bb2633upper = self.bb2633upper / n
|
||||||
self.bb2633lower = self.bb2633lower / n
|
self.bb2633lower = self.bb2633lower / n
|
||||||
self.bb2633middle = self.bb2633middle / n
|
self.bb2633middle = self.bb2633middle / n
|
||||||
|
|||||||
+14
@@ -74,6 +74,8 @@ class ChanKLU:
|
|||||||
self.bb2633upper = 0
|
self.bb2633upper = 0
|
||||||
self.bb2633lower = 0
|
self.bb2633lower = 0
|
||||||
self.bb2633middle = 0
|
self.bb2633middle = 0
|
||||||
|
self.ma5 = 0
|
||||||
|
self.ema5 = 0
|
||||||
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
|
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
|
||||||
def set_macd_state(self, state):
|
def set_macd_state(self, state):
|
||||||
self.macd_state = state
|
self.macd_state = state
|
||||||
@@ -185,6 +187,8 @@ class ChanKLU:
|
|||||||
self.bb2633upper = float(item['bb2633upper']) if 'bb2633upper' in item and item['bb2633upper'] else 0
|
self.bb2633upper = float(item['bb2633upper']) if 'bb2633upper' in item and item['bb2633upper'] else 0
|
||||||
self.bb2633lower = float(item['bb2633lower']) if 'bb2633lower' in item and item['bb2633lower'] else 0
|
self.bb2633lower = float(item['bb2633lower']) if 'bb2633lower' in item and item['bb2633lower'] else 0
|
||||||
self.bb2633middle = float(item['bb2633middle']) if 'bb2633middle' in item and item['bb2633middle'] else 0
|
self.bb2633middle = float(item['bb2633middle']) if 'bb2633middle' in item and item['bb2633middle'] else 0
|
||||||
|
self.ma5 = float(item['ma5']) if 'ma5' in item and item['ma5'] else 0
|
||||||
|
self.ema5 = float(item['ema5']) if 'ema5' in item and item['ema5'] else 0
|
||||||
def cal_macd_state(self):
|
def cal_macd_state(self):
|
||||||
# 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN
|
# 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN
|
||||||
# 首条或缺前一根
|
# 首条或缺前一根
|
||||||
@@ -225,6 +229,16 @@ class ChanKLU:
|
|||||||
self.near0_return = 0
|
self.near0_return = 0
|
||||||
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.pre.near0_return == 0:
|
||||||
|
self.near0_return = 7
|
||||||
|
elif self.pre.near0_return == 8:
|
||||||
|
self.pre.near0_return = 0
|
||||||
|
elif self.close < self.ema52 and self.open > self.ema52:
|
||||||
|
if self.pre.near0_return == 0:
|
||||||
|
self.near0_return = 8
|
||||||
|
elif self.pre.near0_return == 7:
|
||||||
|
self.pre.near0_return = 0
|
||||||
# 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
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ class TF_DF():
|
|||||||
else:
|
else:
|
||||||
klu_state_list.append("00")
|
klu_state_list.append("00")
|
||||||
return klu_state_list
|
return klu_state_list
|
||||||
|
|
||||||
def get_ema_state(self, dataframe):
|
def get_ema_state(self, dataframe):
|
||||||
klu_list = self.get_klu_list(dataframe)
|
klu_list = self.get_klu_list(dataframe)
|
||||||
klc_list = self.get_klc_list(klu_list)
|
klc_list = self.get_klc_list(klu_list)
|
||||||
@@ -918,6 +919,7 @@ class TF_DF():
|
|||||||
if last_bottom and klc.high > last_bi.high:
|
if last_bottom and klc.high > last_bi.high:
|
||||||
#print(klc.end_time, "Top 7, 1", last_bi.start_time, klc.high, last_bi.high)
|
#print(klc.end_time, "Top 7, 1", last_bi.start_time, klc.high, last_bi.high)
|
||||||
#klc.klc_fx_type = Chan_KLC_FX.TOP7
|
#klc.klc_fx_type = Chan_KLC_FX.TOP7
|
||||||
|
#klc.fx = Chan_FX_TYPE.TOP
|
||||||
"""
|
"""
|
||||||
last_bi.set_end_klc(last_bottom, klc)
|
last_bi.set_end_klc(last_bottom, klc)
|
||||||
bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP)
|
bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP)
|
||||||
@@ -934,10 +936,10 @@ class TF_DF():
|
|||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
if last_bottom and last_bi.dir == Chan_BI_DIR.UP:
|
if last_bottom and last_bi.dir == Chan_BI_DIR.UP:
|
||||||
|
|
||||||
if last_top and klc.low < last_bi.low:
|
if last_top and klc.low < last_bi.low:
|
||||||
#print(klc.end_time, "Bottom 8, 2", last_bi.start_time)
|
#print(klc.end_time, "Bottom 8, 2", last_bi.start_time)
|
||||||
#klc.klc_fx_type = Chan_KLC_FX.BOTTOM8
|
#klc.klc_fx_type = Chan_KLC_FX.BOTTOM8
|
||||||
|
#klc.fx = Chan_FX_TYPE.BOTTOM
|
||||||
"""
|
"""
|
||||||
last_bi.set_end_klc(last_top, klc)
|
last_bi.set_end_klc(last_top, klc)
|
||||||
bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN)
|
bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN)
|
||||||
@@ -954,6 +956,7 @@ class TF_DF():
|
|||||||
"""
|
"""
|
||||||
else:
|
else:
|
||||||
if fx == Chan_FX_TYPE.TOP:
|
if fx == Chan_FX_TYPE.TOP:
|
||||||
|
#print(klc.end_time, fx, klc.pre.high, klc.high, klc.pre.start_time, klc.pre.end_time)
|
||||||
if last_top:
|
if last_top:
|
||||||
if last_bottom:
|
if last_bottom:
|
||||||
#print(klc.start_time, last_bottom.start_time, last_top.start_time)
|
#print(klc.start_time, last_bottom.start_time, last_top.start_time)
|
||||||
@@ -962,6 +965,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)
|
||||||
#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
|
||||||
@@ -973,16 +977,18 @@ class TF_DF():
|
|||||||
#print(klc.end_time, klc.fx, "一类卖点Sell 1")
|
#print(klc.end_time, klc.fx, "一类卖点Sell 1")
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
|
# 不满足结合律的分型
|
||||||
else:
|
else:
|
||||||
# 不满足结合律的分型
|
#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")
|
||||||
#klc.set_fx(Chan_FX_TYPE.PTOP)
|
#klc.set_fx(Chan_FX_TYPE.PTOP)
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
# New TOP Found没有意义,前面的UKNOWN已经出现TOP7
|
# New TOP Found前面的UKNOWN可能出现TOP7,但是这里的也可能出现TOP8分型
|
||||||
else:
|
else:
|
||||||
|
# 顶分型在出现2之前超过前一个笔的顶 TOP8
|
||||||
if last_top.index + bi_klc_min < klc.index and len(bi_list) > 1:
|
if last_top.index + bi_klc_min < klc.index and len(bi_list) > 1:
|
||||||
pre_last_bi = bi_list[-2]
|
pre_last_bi = bi_list[-2]
|
||||||
last_bi = bi_list[-1]
|
last_bi = bi_list[-1]
|
||||||
@@ -1001,11 +1007,13 @@ class TF_DF():
|
|||||||
###klc.set_klc_fx_type(Chan_KLC_FX.TOP2) # when bi is down but the fx is top
|
###klc.set_klc_fx_type(Chan_KLC_FX.TOP2) # when bi is down but the fx is top
|
||||||
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.TOP8)
|
||||||
|
#print(klc.start_time, last_bi.start_klc.start_time, "New TOP Found reset last bi")
|
||||||
else:
|
else:
|
||||||
klc.set_fx(Chan_FX_TYPE.PTOP)
|
#klc.set_fx(Chan_FX_TYPE.PTOP)
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
#print(klc.start_time, klc.fx, "无效分型")
|
print(klc.end_time, klc.fx, "无效顶分型")
|
||||||
# 满足结合律
|
# 满足结合律
|
||||||
else:
|
else:
|
||||||
# New Temp TOP and last bottom confirmed ***** confirm last down bi(last bottom and last top)
|
# New Temp TOP and last bottom confirmed ***** confirm last down bi(last bottom and last top)
|
||||||
@@ -1024,8 +1032,9 @@ class TF_DF():
|
|||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
#print(klc.start_time, last_bottom.start_time, "Normal TOP Found, Confirm down bi 4")
|
#print(klc.start_time, last_bottom.start_time, "Normal TOP Found, Confirm down bi 4")
|
||||||
# last bottom = None
|
# last bottom = None 初始化的时候用,其他时间不用
|
||||||
else:
|
else:
|
||||||
|
# 初始化的时候用,其他时间不用
|
||||||
if last_top.high < klc.high:
|
if last_top.high < klc.high:
|
||||||
last_bi = bi_list[-1]
|
last_bi = bi_list[-1]
|
||||||
last_bi.set_start_klc(klc, Chan_BI_DIR.DOWN)
|
last_bi.set_start_klc(klc, Chan_BI_DIR.DOWN)
|
||||||
@@ -1033,11 +1042,13 @@ class TF_DF():
|
|||||||
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 3")
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 3")
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
|
# 初始化的时候用,其他时间不用
|
||||||
else:
|
else:
|
||||||
klc.set_fx(Chan_FX_TYPE.TT)
|
klc.set_fx(Chan_FX_TYPE.TT)
|
||||||
#print(klc.start_time, klc.fx, "二类卖点Sell 2")
|
#print(klc.start_time, klc.fx, "二类卖点Sell 2")
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
|
# last_top == None 初始化的时候用,其他时间不用
|
||||||
else:
|
else:
|
||||||
if last_bottom:
|
if last_bottom:
|
||||||
# 不满足结合律的分型
|
# 不满足结合律的分型
|
||||||
@@ -1052,7 +1063,7 @@ class TF_DF():
|
|||||||
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 4")
|
#print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 4")
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
# Last top = None, last bottom = None, create first down bi
|
# Last top = None, last bottom = None, create first down bi 初始化的时候用,其他时间不用
|
||||||
else:
|
else:
|
||||||
# First temp top
|
# First temp top
|
||||||
last_top = klc
|
last_top = klc
|
||||||
@@ -1071,6 +1082,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)
|
||||||
#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:
|
||||||
@@ -1082,8 +1094,9 @@ class TF_DF():
|
|||||||
#print(klc.end_time, klc.fx, "一类买点Buy 1")
|
#print(klc.end_time, klc.fx, "一类买点Buy 1")
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
|
# 不满足结合律的分型
|
||||||
else:
|
else:
|
||||||
# 不满足结合律的分型
|
#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")
|
||||||
@@ -1108,11 +1121,12 @@ class TF_DF():
|
|||||||
###klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) # when bi is up but the fx is bottom
|
###klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM2) # when bi is up but the fx is bottom
|
||||||
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.BOTTOM8)
|
||||||
else:
|
else:
|
||||||
#klc.set_fx(Chan_FX_TYPE.UNKNOWN)
|
#klc.set_fx(Chan_FX_TYPE.UNKNOWN)
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
#print(klc.start_time, klc.fx, "无效分型")
|
print(klc.end_time, klc.fx, "无效底分型")
|
||||||
# 满足结合律的分型
|
# 满足结合律的分型
|
||||||
else:
|
else:
|
||||||
# New Temp Bottom and last top confirmed ***** confirm last up bi(last bottom and last top)
|
# New Temp Bottom and last top confirmed ***** confirm last up bi(last bottom and last top)
|
||||||
@@ -1132,7 +1146,7 @@ class TF_DF():
|
|||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
#print(klc.start_time, last_top.start_time, "Normal Bottom Found, Confirm up bi 6")
|
#print(klc.start_time, last_top.start_time, "Normal Bottom Found, Confirm up bi 6")
|
||||||
# last_top = None
|
# last_top = None 初始化的时候用,其他时间不用
|
||||||
else:
|
else:
|
||||||
if last_bottom.low > klc.low:
|
if last_bottom.low > klc.low:
|
||||||
last_bi = bi_list[-1]
|
last_bi = bi_list[-1]
|
||||||
@@ -1149,7 +1163,7 @@ class TF_DF():
|
|||||||
#print(klc.start_time, klc.fx, "二类买点Buy 2")
|
#print(klc.start_time, klc.fx, "二类买点Buy 2")
|
||||||
bi_list[-1].add_klc(klc)
|
bi_list[-1].add_klc(klc)
|
||||||
klc.set_bi(bi_list[-1])
|
klc.set_bi(bi_list[-1])
|
||||||
# last_bottom = None
|
# last_bottom = None 初始化的时候用,其他时间不用
|
||||||
else:
|
else:
|
||||||
if last_top:
|
if last_top:
|
||||||
# 不满足结合律的分型
|
# 不满足结合律的分型
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||||
|
"max_open_trades": 1,
|
||||||
|
"stake_currency": "USDT",
|
||||||
|
"stake_amount": "unlimited",
|
||||||
|
"tradable_balance_ratio": 0.99,
|
||||||
|
"fiat_display_currency": "USD",
|
||||||
|
"dry_run": true,
|
||||||
|
"db_url": "sqlite:///tradesv3.freqai_sol.sqlite",
|
||||||
|
"dry_run_wallet": 1000,
|
||||||
|
"cancel_open_orders_on_exit": true,
|
||||||
|
"trading_mode": "futures",
|
||||||
|
"margin_mode": "isolated",
|
||||||
|
"can_short": true,
|
||||||
|
"timeframe": "5m",
|
||||||
|
"process_only_new_candles": true,
|
||||||
|
"unfilledtimeout": {
|
||||||
|
"entry": 1,
|
||||||
|
"exit": 1,
|
||||||
|
"exit_timeout_count": 5,
|
||||||
|
"unit": "minutes"
|
||||||
|
},
|
||||||
|
"entry_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing": {
|
||||||
|
"price_side": "same",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"name": "binance",
|
||||||
|
"key": "",
|
||||||
|
"secret": "",
|
||||||
|
"ccxt_config": {},
|
||||||
|
"ccxt_async_config": {},
|
||||||
|
"pair_whitelist": [
|
||||||
|
"SOL/USDT:USDT"
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
"BNB/.*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pairlists": [
|
||||||
|
{
|
||||||
|
"method": "StaticPairList"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"freqai": {
|
||||||
|
"enabled": true,
|
||||||
|
"purge_old_models": 2,
|
||||||
|
"train_period_days": 10,
|
||||||
|
"backtest_period_days": 7,
|
||||||
|
"live_retrain_hours": 1,
|
||||||
|
"identifier": "sol_futures_lgbm_v1",
|
||||||
|
"feature_parameters": {
|
||||||
|
"include_timeframes": [
|
||||||
|
"5m",
|
||||||
|
"15m"
|
||||||
|
],
|
||||||
|
"include_corr_pairlist": [
|
||||||
|
"BTC/USDT:USDT"
|
||||||
|
],
|
||||||
|
"label_period_candles": 12,
|
||||||
|
"include_shifted_candles": 1,
|
||||||
|
"DI_threshold": 0.9,
|
||||||
|
"weight_factor": 0.9,
|
||||||
|
"principal_component_analysis": false,
|
||||||
|
"use_SVM_to_remove_outliers": true,
|
||||||
|
"indicator_periods_candles": [
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"plot_feature_importances": 0
|
||||||
|
},
|
||||||
|
"data_split_parameters": {
|
||||||
|
"test_size": 0.15,
|
||||||
|
"random_state": 42
|
||||||
|
},
|
||||||
|
"model_training_parameters": {
|
||||||
|
"n_estimators": 300,
|
||||||
|
"learning_rate": 0.05,
|
||||||
|
"max_depth": 5,
|
||||||
|
"num_leaves": 31,
|
||||||
|
"min_child_samples": 20,
|
||||||
|
"subsample": 0.8,
|
||||||
|
"colsample_bytree": 0.8,
|
||||||
|
"reg_alpha": 0.1,
|
||||||
|
"reg_lambda": 0.1,
|
||||||
|
"n_jobs": 1,
|
||||||
|
"verbosity": -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
"enabled": false,
|
||||||
|
"token": "",
|
||||||
|
"chat_id": ""
|
||||||
|
},
|
||||||
|
"api_server": {
|
||||||
|
"enabled": true,
|
||||||
|
"listen_ip_address": "0.0.0.0",
|
||||||
|
"listen_port": 8822,
|
||||||
|
"verbosity": "error",
|
||||||
|
"enable_openapi": false,
|
||||||
|
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||||
|
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||||
|
"CORS_origins": [],
|
||||||
|
"username": "freqtrader",
|
||||||
|
"password": "FreqTrade007"
|
||||||
|
},
|
||||||
|
"bot_name": "freqai_sol",
|
||||||
|
"initial_state": "running",
|
||||||
|
"force_entry_enable": false,
|
||||||
|
"internals": {
|
||||||
|
"process_throttle_secs": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,9 @@
|
|||||||
"cancel_open_orders_on_exit": true,
|
"cancel_open_orders_on_exit": true,
|
||||||
"trading_mode": "futures",
|
"trading_mode": "futures",
|
||||||
"margin_mode": "isolated",
|
"margin_mode": "isolated",
|
||||||
|
"timeframe": "1m",
|
||||||
"can_short" : true,
|
"can_short" : true,
|
||||||
"timeframe" : "1m",
|
"process_only_new_candles" : true,
|
||||||
"process_only_new_candles" : false,
|
|
||||||
"unfilledtimeout": {
|
"unfilledtimeout": {
|
||||||
"entry": 1,
|
"entry": 1,
|
||||||
"exit": 1,
|
"exit": 1,
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"unit": "minutes"
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"entry_pricing": {
|
"entry_pricing": {
|
||||||
"price_side": "same",
|
"price_side": "other",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1,
|
"order_book_top": 1,
|
||||||
"price_last_balance": 0.0,
|
"price_last_balance": 0.0,
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exit_pricing":{
|
"exit_pricing":{
|
||||||
"price_side": "same",
|
"price_side": "other",
|
||||||
"use_order_book": true,
|
"use_order_book": true,
|
||||||
"order_book_top": 1
|
"order_book_top": 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||||
|
"max_open_trades": 1,
|
||||||
|
"stake_currency": "USDT",
|
||||||
|
"stake_amount": "unlimited",
|
||||||
|
"tradable_balance_ratio": 0.99,
|
||||||
|
"fiat_display_currency": "USD",
|
||||||
|
"dry_run": true,
|
||||||
|
"db_url": "sqlite:///tradesv3.sol5m.sqlite",
|
||||||
|
"dry_run_wallet": 1000,
|
||||||
|
"cancel_open_orders_on_exit": true,
|
||||||
|
"trading_mode": "futures",
|
||||||
|
"margin_mode": "isolated",
|
||||||
|
"can_short": true,
|
||||||
|
"unfilledtimeout": {
|
||||||
|
"entry": 1,
|
||||||
|
"exit": 1,
|
||||||
|
"exit_timeout_count": 5,
|
||||||
|
"unit": "minutes"
|
||||||
|
},
|
||||||
|
"entry_pricing": {
|
||||||
|
"price_side": "other",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"price_last_balance": 0.0,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exit_pricing": {
|
||||||
|
"price_side": "other",
|
||||||
|
"use_order_book": true,
|
||||||
|
"order_book_top": 1
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"name": "binance",
|
||||||
|
"key": "hvoXanRExQvcN4tyGFvEnsSF4gqxXp6ZJnBu5lnhvlVuHaDbj2PhLBQGCLkkyeI8",
|
||||||
|
"secret": "3UKA2oyDj7OoXrausmnaLwLlNfXmlNf2imBdmQqqKHArcJfk6X9xjaUF19wzu82l",
|
||||||
|
"ccxt_config": {},
|
||||||
|
"ccxt_async_config": {},
|
||||||
|
"pair_whitelist": [
|
||||||
|
"SOL/USDT:USDT"
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
"BNB/.*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pairlists": [
|
||||||
|
{
|
||||||
|
"method": "StaticPairList",
|
||||||
|
"number_assets": 1,
|
||||||
|
"sort_key": "quoteVolume",
|
||||||
|
"min_value": 0,
|
||||||
|
"refresh_period": 1800
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"telegram": {
|
||||||
|
"enabled": false,
|
||||||
|
"token": "7677670958:AAFL_jgZvNUTPR3R3vWieREX_tDVi9w2C1Y",
|
||||||
|
"chat_id": "580807463"
|
||||||
|
},
|
||||||
|
"api_server": {
|
||||||
|
"enabled": true,
|
||||||
|
"listen_ip_address": "0.0.0.0",
|
||||||
|
"listen_port": 8822,
|
||||||
|
"verbosity": "error",
|
||||||
|
"enable_openapi": false,
|
||||||
|
"jwt_secret_key": "14d3510740e2c39a973a8895f1aa2704d98d08b86170260085709fa5ea48251d",
|
||||||
|
"ws_token": "dtKKDnafBrX4icq_ZCw7acJTahTK4h_yvg",
|
||||||
|
"CORS_origins": [],
|
||||||
|
"username": "freqtrader",
|
||||||
|
"password": "FreqTrade007"
|
||||||
|
},
|
||||||
|
"bot_name": "SOL5m",
|
||||||
|
"initial_state": "running",
|
||||||
|
"force_entry_enable": false,
|
||||||
|
"internals": {
|
||||||
|
"process_throttle_secs": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,664 @@
|
|||||||
|
"""
|
||||||
|
缠论同级别分解策略 (Chan Same-Level Decomposition Strategy)
|
||||||
|
|
||||||
|
核心思想:按同级别分解操作,实现a+A结构的机械化操作
|
||||||
|
|
||||||
|
以5分钟级别为例:
|
||||||
|
1. a+A结构:a是5分钟走势类型(定义为A0),A分解为m段5分钟走势类型:A=A1+A2+...+Am
|
||||||
|
2. 如果a+A向上,则Ai当i为奇数时向下,i为偶数时向上
|
||||||
|
3. 中枢形成:
|
||||||
|
- A1不能跌破a的低点
|
||||||
|
- 如果A2升破a的高点而A3不跌回a的高点,可以把a+A1+A2+A3当成一个新的a'(还是5分钟级别)
|
||||||
|
- 如果A3跌破a的高点,则A1、A2、A3必然构成30分钟中枢
|
||||||
|
|
||||||
|
操作程式(机械化操作):
|
||||||
|
1. 盘整背驰情况:
|
||||||
|
- Ai与Ai+2之间比较力度(盘整背驰)
|
||||||
|
- i+2为偶数时卖出
|
||||||
|
- i+2为奇数时买入
|
||||||
|
|
||||||
|
2. 非背驰情况:
|
||||||
|
- 当i为偶数,若Ai+3不跌破Ai高点,则继续持有到Ai+k+3跌破Ai+k高点后在不创新高或盘整顶背驰的Ai+k+4卖出,其中k为偶数
|
||||||
|
- 当i为奇数,若Ai+3不升破Ai低点,则继续保持不回补直到Ai+k+3升破Ai+k低点后在不创新低或盘整底背驰的Ai+k+4回补
|
||||||
|
|
||||||
|
使用命令:
|
||||||
|
freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json \
|
||||||
|
--strategy ChanSameLevelStrategy --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 ChanSameLevelStrategy(IStrategy):
|
||||||
|
INTERFACE_VERSION: int = 3
|
||||||
|
|
||||||
|
# === 基础配置 ===
|
||||||
|
# 底层使用 1m K线,resample 到 30m 进行同级别分解
|
||||||
|
can_short = True
|
||||||
|
startup_candle_count: int = 2000 # 需要足够的数据来识别走势段
|
||||||
|
|
||||||
|
# 止损和止盈(优化:改善风险回报比)
|
||||||
|
stoploss = -0.015 # 1.5% 硬止损(更紧,减少单笔亏损)
|
||||||
|
use_custom_stoploss = False
|
||||||
|
|
||||||
|
# Trailing stop(优化:更激进的保护利润)
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.006 # 回撤 0.6% 触发退出(更紧)
|
||||||
|
trailing_stop_positive_offset = 0.012 # 盈利 1.2% 后才开始追踪(降低门槛)
|
||||||
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
|
# ROI(优化:更合理的止盈目标,改善风险回报比)
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.03, # 3% 立即止盈(降低目标,提高胜率)
|
||||||
|
"60": 0.02, # 60分钟后 2%
|
||||||
|
"120": 0.015, # 120分钟后 1.5%
|
||||||
|
"240": 0.01, # 240分钟后 1%
|
||||||
|
"480": 0.005, # 480分钟后 0.5%
|
||||||
|
"720": 0, # 720分钟后不设止盈
|
||||||
|
}
|
||||||
|
|
||||||
|
order_types = {
|
||||||
|
"entry": "market",
|
||||||
|
"exit": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同级别分解的级别(5分钟)
|
||||||
|
same_level_timeframe = 5
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""计算指标并识别同级别走势段"""
|
||||||
|
ticker = self.get_ticker_indicator()
|
||||||
|
|
||||||
|
# Resample 到 30m 进行同级别分解
|
||||||
|
dataframe_30m = resample_to_interval(dataframe, ticker * self.same_level_timeframe)
|
||||||
|
|
||||||
|
# 在 30m 上计算指标
|
||||||
|
dataframe_30m = self.add_indicators_30m(dataframe_30m)
|
||||||
|
|
||||||
|
# 识别同级别走势段和背驰
|
||||||
|
dataframe_30m = self.identify_same_level_segments(dataframe_30m)
|
||||||
|
|
||||||
|
# 合并回 1m dataframe
|
||||||
|
dataframe = resampled_merge(dataframe, dataframe_30m)
|
||||||
|
|
||||||
|
# 在 1m 上也计算基础指标
|
||||||
|
dataframe = self.add_indicators_1m(dataframe)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def add_indicators_30m(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""在30m级别计算指标"""
|
||||||
|
# MACD 用于识别背驰
|
||||||
|
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
|
|
||||||
|
# EMA 用于识别趋势(增加更多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']
|
||||||
|
|
||||||
|
# 趋势强度(EMA斜率)
|
||||||
|
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_1m'] = (
|
||||||
|
(dataframe['macd_1m'] > dataframe['macdsignal_1m']) &
|
||||||
|
(dataframe['macd_1m'].shift(1) <= dataframe['macdsignal_1m'].shift(1))
|
||||||
|
)
|
||||||
|
dataframe['macd_cross_dn_1m'] = (
|
||||||
|
(dataframe['macd_1m'] < dataframe['macdsignal_1m']) &
|
||||||
|
(dataframe['macd_1m'].shift(1) >= dataframe['macdsignal_1m'].shift(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def identify_same_level_segments(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
识别同级别走势段(a+A结构)
|
||||||
|
|
||||||
|
实现5分钟级别的同级别分解:
|
||||||
|
1. 识别A0(即a)、A1、A2、A3等走势段
|
||||||
|
2. 判断每个段的类型(上涨/下跌):如果a+A向上,则Ai当i为奇数时向下,i为偶数时向上
|
||||||
|
3. 识别中枢形成条件
|
||||||
|
4. 计算盘整背驰(Ai与Ai+2比较力度)
|
||||||
|
5. 标记买卖点
|
||||||
|
"""
|
||||||
|
df = dataframe.copy()
|
||||||
|
|
||||||
|
# 初始化列
|
||||||
|
df['ai_index'] = -1 # Ai的索引(A0, A1, A2, ...)
|
||||||
|
df['ai_type'] = 0 # 1: 上涨, -1: 下跌
|
||||||
|
df['ai_high'] = np.nan # Ai的高点
|
||||||
|
df['ai_low'] = np.nan # Ai的低点
|
||||||
|
df['ai_macd_max'] = np.nan # Ai的MACD最大值
|
||||||
|
df['ai_macd_min'] = np.nan # Ai的MACD最小值
|
||||||
|
df['zs_formed'] = False # 是否形成中枢
|
||||||
|
df['panzheng_beichi'] = False # 盘整背驰信号
|
||||||
|
df['buy_signal'] = False # 买入信号
|
||||||
|
df['sell_signal'] = False # 卖出信号
|
||||||
|
|
||||||
|
# 识别关键转折点(局部高点和低点)
|
||||||
|
window = 3 # 确认窗口
|
||||||
|
lookback = window + 1
|
||||||
|
|
||||||
|
# 高点识别(延迟确认)
|
||||||
|
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 = [] # 存储Ai段的信息:[(start_idx, end_idx, type, high, low, macd_max, macd_min), ...]
|
||||||
|
current_ai_start = None
|
||||||
|
current_ai_type = None # 1: 上涨, -1: 下跌
|
||||||
|
last_pivot_idx = None
|
||||||
|
last_pivot_type = None # 'high' or 'low'
|
||||||
|
|
||||||
|
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:i]
|
||||||
|
if len(seg_df) >= 3: # 至少3根K线
|
||||||
|
# 使用已确认的数据计算(不包括当前转折点)
|
||||||
|
# 为了安全,只使用到 last_pivot_idx 之前的数据
|
||||||
|
confirmed_seg_df = df.iloc[current_ai_start:last_pivot_idx] if last_pivot_idx > current_ai_start else seg_df
|
||||||
|
if len(confirmed_seg_df) > 0:
|
||||||
|
high_val = confirmed_seg_df['high'].max()
|
||||||
|
low_val = confirmed_seg_df['low'].min()
|
||||||
|
macd_max = confirmed_seg_df['macd'].max()
|
||||||
|
macd_min = confirmed_seg_df['macd'].min()
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
# 第一个段(A0),根据价格变化判断
|
||||||
|
if high_val > df.iloc[current_ai_start]['close']:
|
||||||
|
current_ai_type = 1 # 上涨
|
||||||
|
else:
|
||||||
|
current_ai_type = -1 # 下跌
|
||||||
|
else:
|
||||||
|
# 后续段:如果a+A向上,则Ai当i为奇数时向下,i为偶数时向上
|
||||||
|
# 简化处理:根据转折点类型判断
|
||||||
|
if pivot_type == 'high' and last_pivot_type == 'low':
|
||||||
|
current_ai_type = 1 # 上涨段
|
||||||
|
elif pivot_type == 'low' and last_pivot_type == 'high':
|
||||||
|
current_ai_type = -1 # 下跌段
|
||||||
|
|
||||||
|
ai_list.append({
|
||||||
|
'start': current_ai_start,
|
||||||
|
'end': i,
|
||||||
|
'type': current_ai_type,
|
||||||
|
'high': high_val,
|
||||||
|
'low': low_val,
|
||||||
|
'macd_max': macd_max,
|
||||||
|
'macd_min': macd_min
|
||||||
|
})
|
||||||
|
|
||||||
|
# 标记到dataframe(只在段结束时标记,避免未来数据)
|
||||||
|
# 使用滚动窗口:只在确认转折点后才标记前一段的信息
|
||||||
|
# 为了安全,只在段的最后几根K线标记(确认段已结束)
|
||||||
|
confirm_window = min(3, i - current_ai_start) # 确认窗口,最多3根K线
|
||||||
|
mark_start = max(current_ai_start, i - confirm_window)
|
||||||
|
df.iloc[mark_start:i, df.columns.get_loc('ai_index')] = len(ai_list) - 1
|
||||||
|
df.iloc[mark_start:i, df.columns.get_loc('ai_type')] = current_ai_type
|
||||||
|
# 高点和低点使用已确认的数据
|
||||||
|
df.iloc[mark_start:i, df.columns.get_loc('ai_high')] = high_val
|
||||||
|
df.iloc[mark_start:i, df.columns.get_loc('ai_low')] = low_val
|
||||||
|
df.iloc[mark_start:i, df.columns.get_loc('ai_macd_max')] = macd_max
|
||||||
|
df.iloc[mark_start:i, df.columns.get_loc('ai_macd_min')] = macd_min
|
||||||
|
|
||||||
|
# 开始新的走势段
|
||||||
|
current_ai_start = last_pivot_idx
|
||||||
|
last_pivot_idx = i
|
||||||
|
last_pivot_type = pivot_type
|
||||||
|
elif is_new_pivot:
|
||||||
|
# 第一个转折点
|
||||||
|
last_pivot_idx = i
|
||||||
|
last_pivot_type = pivot_type
|
||||||
|
if current_ai_start is None:
|
||||||
|
current_ai_start = 0
|
||||||
|
|
||||||
|
# 处理最后一个段
|
||||||
|
if current_ai_start is not None:
|
||||||
|
# 标记当前未完成的段
|
||||||
|
if len(df) - current_ai_start >= 3:
|
||||||
|
seg_df = df.iloc[current_ai_start:]
|
||||||
|
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 len(ai_list) > 0:
|
||||||
|
last_type = ai_list[-1]['type']
|
||||||
|
# 如果上一个段是上涨,当前应该是下跌(或相反)
|
||||||
|
current_ai_type = -last_type
|
||||||
|
else:
|
||||||
|
current_ai_type = 1 if high_val > df.iloc[current_ai_start]['close'] else -1
|
||||||
|
|
||||||
|
ai_list.append({
|
||||||
|
'start': current_ai_start,
|
||||||
|
'end': len(df),
|
||||||
|
'type': current_ai_type,
|
||||||
|
'high': high_val,
|
||||||
|
'low': low_val,
|
||||||
|
'macd_max': macd_max,
|
||||||
|
'macd_min': macd_min
|
||||||
|
})
|
||||||
|
|
||||||
|
df.iloc[current_ai_start:, df.columns.get_loc('ai_index')] = len(ai_list) - 1
|
||||||
|
df.iloc[current_ai_start:, df.columns.get_loc('ai_type')] = current_ai_type
|
||||||
|
df.iloc[current_ai_start:, df.columns.get_loc('ai_high')] = high_val
|
||||||
|
df.iloc[current_ai_start:, df.columns.get_loc('ai_low')] = low_val
|
||||||
|
df.iloc[current_ai_start:, df.columns.get_loc('ai_macd_max')] = macd_max
|
||||||
|
df.iloc[current_ai_start:, df.columns.get_loc('ai_macd_min')] = macd_min
|
||||||
|
|
||||||
|
# 识别中枢和盘整背驰
|
||||||
|
df = self.identify_zs_and_beichi(df, ai_list)
|
||||||
|
|
||||||
|
# 清理临时列
|
||||||
|
df = df.drop(columns=['temp_high', 'temp_low', 'is_pivot_high', 'is_pivot_low'])
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def identify_zs_and_beichi(self, dataframe: DataFrame, ai_list: list) -> DataFrame:
|
||||||
|
"""
|
||||||
|
识别中枢和盘整背驰
|
||||||
|
|
||||||
|
1. 中枢形成:如果A3跌破a的高点,则A1、A2、A3必然构成30分钟中枢
|
||||||
|
2. 盘整背驰:Ai与Ai+2之间比较力度(MACD面积或幅度)
|
||||||
|
3. 标记买卖点:
|
||||||
|
- 盘整背驰:i+2为偶数时卖出,i+2为奇数时买入
|
||||||
|
- 非背驰情况:根据Ai+3是否跌破/升破Ai的高低点决定
|
||||||
|
|
||||||
|
注意:为了避免未来数据,只在段确认结束后才标记信号
|
||||||
|
"""
|
||||||
|
df = dataframe.copy()
|
||||||
|
|
||||||
|
if len(ai_list) < 3:
|
||||||
|
return df
|
||||||
|
|
||||||
|
# 逐行处理,只在当前行可以确认历史段的信息时才标记
|
||||||
|
# 这样可以避免使用未来数据
|
||||||
|
for row_idx in range(len(df)):
|
||||||
|
# 找到当前行属于哪个段
|
||||||
|
current_ai_idx = -1
|
||||||
|
for ai_idx, ai in enumerate(ai_list):
|
||||||
|
if ai['start'] <= row_idx < ai['end']:
|
||||||
|
current_ai_idx = ai_idx
|
||||||
|
break
|
||||||
|
|
||||||
|
if current_ai_idx < 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 只在段的最后几根K线才处理,确保段已确认结束
|
||||||
|
current_ai = ai_list[current_ai_idx]
|
||||||
|
if row_idx < current_ai['end'] - 3: # 只在段的最后3根K线处理
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 识别中枢(A1、A2、A3构成中枢)
|
||||||
|
# 只在A3段结束时才标记中枢,避免使用未来数据
|
||||||
|
if current_ai_idx >= 2: # 至少需要A0, A1, A2
|
||||||
|
a0 = ai_list[0]
|
||||||
|
a1 = ai_list[current_ai_idx - 2] if current_ai_idx >= 2 else None
|
||||||
|
a2 = ai_list[current_ai_idx - 1] if current_ai_idx >= 1 else None
|
||||||
|
a3 = ai_list[current_ai_idx]
|
||||||
|
|
||||||
|
if a1 and a2 and a3:
|
||||||
|
# 如果A3跌破a(A0)的高点,则A1、A2、A3构成中枢
|
||||||
|
if a3['low'] < a0['high']:
|
||||||
|
# 只在A3段的最后几根K线标记中枢
|
||||||
|
df.iloc[row_idx, df.columns.get_loc('zs_formed')] = True
|
||||||
|
|
||||||
|
# 盘整背驰判断:Ai与Ai+2比较力度
|
||||||
|
# 只在ai_plus_2段结束时才判断,避免使用未来数据
|
||||||
|
if current_ai_idx >= 2:
|
||||||
|
ai = ai_list[current_ai_idx - 2]
|
||||||
|
ai_plus_2 = ai_list[current_ai_idx]
|
||||||
|
|
||||||
|
# 计算力度(使用MACD面积或价格幅度)
|
||||||
|
if ai['type'] == ai_plus_2['type']: # 同方向才能比较
|
||||||
|
# 上涨段:比较MACD最大值和价格涨幅
|
||||||
|
if ai['type'] == 1: # 上涨
|
||||||
|
price_strength_ai = (ai['high'] - ai['low']) / ai['low'] if ai['low'] > 0 else 0
|
||||||
|
price_strength_ai2 = (ai_plus_2['high'] - ai_plus_2['low']) / ai_plus_2['low'] if ai_plus_2['low'] > 0 else 0
|
||||||
|
macd_strength_ai = ai['macd_max']
|
||||||
|
macd_strength_ai2 = ai_plus_2['macd_max']
|
||||||
|
|
||||||
|
# 盘整顶背驰:价格创新高或接近,但MACD力度减弱
|
||||||
|
beichi = (
|
||||||
|
(price_strength_ai2 <= price_strength_ai * 1.1) & # 价格涨幅相近或更小
|
||||||
|
(macd_strength_ai2 < macd_strength_ai * 0.9) # MACD力度明显减弱
|
||||||
|
)
|
||||||
|
else: # 下跌
|
||||||
|
price_strength_ai = (ai['high'] - ai['low']) / ai['low'] if ai['low'] > 0 else 0
|
||||||
|
price_strength_ai2 = (ai_plus_2['high'] - ai_plus_2['low']) / ai_plus_2['low'] if ai_plus_2['low'] > 0 else 0
|
||||||
|
macd_strength_ai = abs(ai['macd_min'])
|
||||||
|
macd_strength_ai2 = abs(ai_plus_2['macd_min'])
|
||||||
|
|
||||||
|
# 盘整底背驰:价格创新低或接近,但MACD力度减弱
|
||||||
|
beichi = (
|
||||||
|
(abs(price_strength_ai2) <= abs(price_strength_ai) * 1.1) & # 价格跌幅相近或更小
|
||||||
|
(macd_strength_ai2 < macd_strength_ai * 0.9) # MACD力度明显减弱
|
||||||
|
)
|
||||||
|
|
||||||
|
if beichi:
|
||||||
|
# 只在ai_plus_2段的最后几根K线标记信号
|
||||||
|
# i+2为偶数时卖出,i+2为奇数时买入
|
||||||
|
if (current_ai_idx) % 2 == 0: # 偶数,卖出
|
||||||
|
df.iloc[row_idx, df.columns.get_loc('sell_signal')] = True
|
||||||
|
df.iloc[row_idx, df.columns.get_loc('panzheng_beichi')] = True
|
||||||
|
else: # 奇数,买入
|
||||||
|
df.iloc[row_idx, df.columns.get_loc('buy_signal')] = True
|
||||||
|
df.iloc[row_idx, df.columns.get_loc('panzheng_beichi')] = True
|
||||||
|
|
||||||
|
# 非背驰情况的处理(简化版)
|
||||||
|
# 只在Ai+4段结束时才标记,避免使用未来数据
|
||||||
|
if current_ai_idx >= 4:
|
||||||
|
ai = ai_list[current_ai_idx - 4]
|
||||||
|
ai_plus_3 = ai_list[current_ai_idx - 1]
|
||||||
|
ai_plus_4 = ai_list[current_ai_idx]
|
||||||
|
|
||||||
|
if (current_ai_idx - 4) % 2 == 0: # i为偶数
|
||||||
|
# 若Ai+3不跌破Ai高点,继续持有(不标记卖出)
|
||||||
|
if ai_plus_3['low'] < ai['high']:
|
||||||
|
# Ai+3跌破Ai高点,在不创新高或盘整顶背驰的Ai+k+4卖出
|
||||||
|
if ai_plus_4['high'] <= ai_plus_3['high']: # 不创新高
|
||||||
|
df.iloc[row_idx, df.columns.get_loc('sell_signal')] = True
|
||||||
|
else: # i为奇数
|
||||||
|
# 若Ai+3不升破Ai低点,继续保持不回补
|
||||||
|
if ai_plus_3['high'] > ai['low']:
|
||||||
|
# Ai+3升破Ai低点,在不创新低或盘整底背驰的Ai+k+4回补
|
||||||
|
if ai_plus_4['low'] >= ai_plus_3['low']: # 不创新低
|
||||||
|
df.iloc[row_idx, df.columns.get_loc('buy_signal')] = True
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def detect_divergence(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
检测背驰(使用滚动窗口,避免未来函数)
|
||||||
|
|
||||||
|
顶背驰:价格创新高,但MACD不创新高
|
||||||
|
底背驰:价格创新低,但MACD不创新低
|
||||||
|
"""
|
||||||
|
df = dataframe.copy()
|
||||||
|
|
||||||
|
# 使用滚动窗口检测背驰(只使用历史数据)
|
||||||
|
lookback = 20 # 向前看20根K线
|
||||||
|
|
||||||
|
# 顶背驰检测:当前价格是近期最高,但MACD不是近期最高
|
||||||
|
df['recent_high'] = df['high'].rolling(window=lookback).max()
|
||||||
|
df['recent_macd_max'] = df['macd'].rolling(window=lookback).max()
|
||||||
|
df['prev_recent_high'] = df['high'].rolling(window=lookback).max().shift(1)
|
||||||
|
df['prev_recent_macd_max'] = df['macd'].rolling(window=lookback).max().shift(1)
|
||||||
|
|
||||||
|
# 当前价格创新高,但MACD没有创新高(或降低)
|
||||||
|
df['divergence_top'] = (
|
||||||
|
(df['high'] >= df['recent_high']) & # 当前是近期最高
|
||||||
|
(df['high'] > df['prev_recent_high']) & # 比之前的最高更高
|
||||||
|
(df['macd'] < df['prev_recent_macd_max']) & # MACD没有创新高
|
||||||
|
(df['macd'] < 0) # MACD在零轴下方(下跌趋势中的顶背驰)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 底背驰检测:当前价格是近期最低,但MACD不是近期最低
|
||||||
|
df['recent_low'] = df['low'].rolling(window=lookback).min()
|
||||||
|
df['recent_macd_min'] = df['macd'].rolling(window=lookback).min()
|
||||||
|
df['prev_recent_low'] = df['low'].rolling(window=lookback).min().shift(1)
|
||||||
|
df['prev_recent_macd_min'] = df['macd'].rolling(window=lookback).min().shift(1)
|
||||||
|
|
||||||
|
# 当前价格创新低,但MACD没有创新低(或升高)
|
||||||
|
df['divergence_bottom'] = (
|
||||||
|
(df['low'] <= df['recent_low']) & # 当前是近期最低
|
||||||
|
(df['low'] < df['prev_recent_low']) & # 比之前的最低更低
|
||||||
|
(df['macd'] > df['prev_recent_macd_min']) & # MACD没有创新低
|
||||||
|
(df['macd'] > 0) # MACD在零轴上方(上涨趋势中的底背驰)
|
||||||
|
)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
入场逻辑:基于同级别分解的a+A结构
|
||||||
|
|
||||||
|
1. 盘整背驰买入:i+2为奇数时的盘整背驰信号
|
||||||
|
2. 非背驰情况的买入:Ai+3升破Ai低点后的回补信号
|
||||||
|
"""
|
||||||
|
ticker = self.get_ticker_indicator()
|
||||||
|
resample_col = f"resample_{ticker * self.same_level_timeframe}_"
|
||||||
|
|
||||||
|
# 获取5m级别的指标
|
||||||
|
buy_signal_col = f"{resample_col}buy_signal"
|
||||||
|
panzheng_beichi_col = f"{resample_col}panzheng_beichi"
|
||||||
|
ai_type_col = f"{resample_col}ai_type"
|
||||||
|
rsi_5m_col = f"{resample_col}rsi"
|
||||||
|
|
||||||
|
# 获取30m级别的趋势指标
|
||||||
|
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"
|
||||||
|
|
||||||
|
# 做多条件(激进优化:在下跌趋势中禁止做多,只在强上涨趋势中做多)
|
||||||
|
# 1. 盘整背驰买入信号(i+2为奇数)
|
||||||
|
# 2. 非背驰情况的回补信号
|
||||||
|
# 3. 确认是上涨段或即将上涨
|
||||||
|
# 4. 强上涨趋势确认(必须价格在EMA200上方且EMA斜率向上)
|
||||||
|
# 5. 波动率确认
|
||||||
|
# 6. MACD确认
|
||||||
|
# 7. 禁止在下跌趋势中做多
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe[buy_signal_col] == True) & # 买入信号
|
||||||
|
(
|
||||||
|
(dataframe[panzheng_beichi_col] == True) | # 盘整背驰
|
||||||
|
(dataframe[ai_type_col] == 1) # 或当前是上涨段
|
||||||
|
) &
|
||||||
|
(dataframe[strong_uptrend_col] == True) & # 强上涨趋势(新增:必须强趋势)
|
||||||
|
(dataframe[price_above_ema200_col] == True) & # 价格在EMA200上方(新增)
|
||||||
|
(dataframe[volatility_ok_col] == True) & # 波动率足够
|
||||||
|
(dataframe[rsi_5m_col] < 60) & # RSI不过度超买(收紧)
|
||||||
|
(dataframe[rsi_5m_col] > 40) & # RSI在合理区间(收紧)
|
||||||
|
(dataframe['rsi_1m'] > 40) & # 1m RSI确认(收紧)
|
||||||
|
(dataframe['rsi_1m'] < 65) & # 1m RSI不过度超买(收紧)
|
||||||
|
(dataframe['macd_1m'] > dataframe['macdsignal_1m']) & # MACD向上
|
||||||
|
(dataframe['macd_1m'] > 0) & # MACD在零轴上方(新增)
|
||||||
|
(dataframe['volume'] > dataframe['volume_mean'] * 1.5) & # 成交量确认(提高阈值)
|
||||||
|
~(dataframe[strong_downtrend_col] == True) # 禁止在强下跌趋势中做多(新增)
|
||||||
|
),
|
||||||
|
["enter_long", "enter_tag"],
|
||||||
|
] = (1, "same_level_long")
|
||||||
|
|
||||||
|
# 做空条件(优化:收紧条件,提高质量)
|
||||||
|
# 1. 盘整背驰卖出信号(i+2为偶数,但这里作为做空入场)
|
||||||
|
# 2. 非背驰情况的卖出信号
|
||||||
|
# 3. 确认是下跌段或即将下跌
|
||||||
|
# 4. 强下跌趋势确认(必须价格在EMA200下方且EMA斜率向下)
|
||||||
|
# 5. 波动率确认
|
||||||
|
# 6. MACD确认
|
||||||
|
sell_signal_col = f"{resample_col}sell_signal"
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe[sell_signal_col] == True) & # 卖出信号
|
||||||
|
(
|
||||||
|
(dataframe[panzheng_beichi_col] == True) | # 盘整背驰(但i+2为偶数)
|
||||||
|
(dataframe[ai_type_col] == -1) # 或当前是下跌段
|
||||||
|
) &
|
||||||
|
(
|
||||||
|
(dataframe[strong_downtrend_col] == True) | # 强下跌趋势(优先)
|
||||||
|
(
|
||||||
|
(dataframe[ema_trend_dn_col] == True) & # 30m趋势向下
|
||||||
|
(dataframe[price_below_ema200_col] == True) # 且价格在EMA200下方
|
||||||
|
)
|
||||||
|
) &
|
||||||
|
(dataframe[volatility_ok_col] == True) & # 波动率足够
|
||||||
|
(dataframe[rsi_5m_col] > 40) & # RSI不过度超卖(收紧)
|
||||||
|
(dataframe[rsi_5m_col] < 65) & # RSI不过度超买(收紧)
|
||||||
|
(dataframe['rsi_1m'] < 65) & # 1m RSI确认(收紧)
|
||||||
|
(dataframe['rsi_1m'] > 35) & # 1m RSI不过度超卖(收紧)
|
||||||
|
(dataframe['macd_1m'] < dataframe['macdsignal_1m']) & # MACD向下
|
||||||
|
(dataframe['macd_1m'] < 0) & # MACD在零轴下方(新增)
|
||||||
|
(dataframe['volume'] > dataframe['volume_mean'] * 1.3) # 成交量确认(提高阈值)
|
||||||
|
),
|
||||||
|
["enter_short", "enter_tag"],
|
||||||
|
] = (1, "same_level_short")
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
出场逻辑:基于同级别分解的a+A结构
|
||||||
|
|
||||||
|
1. 盘整背驰卖出:i+2为偶数时的盘整背驰信号
|
||||||
|
2. 非背驰情况的卖出:Ai+3跌破Ai高点后的卖出信号
|
||||||
|
"""
|
||||||
|
ticker = self.get_ticker_indicator()
|
||||||
|
resample_col = f"resample_{ticker * self.same_level_timeframe}_"
|
||||||
|
|
||||||
|
# 获取5m级别的指标
|
||||||
|
sell_signal_col = f"{resample_col}sell_signal"
|
||||||
|
buy_signal_col = f"{resample_col}buy_signal"
|
||||||
|
panzheng_beichi_col = f"{resample_col}panzheng_beichi"
|
||||||
|
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"
|
||||||
|
strong_uptrend_col = f"{resample_col}strong_uptrend"
|
||||||
|
strong_downtrend_col = f"{resample_col}strong_downtrend"
|
||||||
|
|
||||||
|
# 做多出场(优化:更早退出,保护利润)
|
||||||
|
# 在趋势转弱或明确反转时退出
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(dataframe[sell_signal_col] == True) & # 明确的卖出信号
|
||||||
|
(dataframe[panzheng_beichi_col] == True) # 且是背驰信号
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe[ai_type_col] == -1) & # 转为下跌段
|
||||||
|
(dataframe[rsi_5m_col] > 55) & # RSI确认(降低阈值,更早退出)
|
||||||
|
(dataframe[ema_trend_dn_col] == True) # 且趋势确实向下
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe[strong_downtrend_col] == True) & # 强下跌趋势(新增)
|
||||||
|
(dataframe[rsi_5m_col] > 50) # RSI确认
|
||||||
|
)
|
||||||
|
),
|
||||||
|
["exit_long", "exit_tag"],
|
||||||
|
] = (1, "same_level_exit_long")
|
||||||
|
|
||||||
|
# 做空出场(优化:更早退出,保护利润)
|
||||||
|
# 在趋势转弱或明确反转时退出
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(dataframe[buy_signal_col] == True) & # 明确的买入信号
|
||||||
|
(dataframe[panzheng_beichi_col] == True) # 且是背驰信号
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe[ai_type_col] == 1) & # 转为上涨段
|
||||||
|
(dataframe[rsi_5m_col] < 45) & # RSI确认(提高阈值,更早退出)
|
||||||
|
(dataframe[ema_trend_up_col] == True) # 且趋势确实向上
|
||||||
|
) |
|
||||||
|
(
|
||||||
|
(dataframe[strong_uptrend_col] == True) & # 强上涨趋势(新增)
|
||||||
|
(dataframe[rsi_5m_col] < 50) # RSI确认
|
||||||
|
)
|
||||||
|
),
|
||||||
|
["exit_short", "exit_tag"],
|
||||||
|
] = (1, "same_level_exit_short")
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def leverage(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
current_time: datetime,
|
||||||
|
current_rate: float,
|
||||||
|
proposed_leverage: float,
|
||||||
|
max_leverage: float,
|
||||||
|
entry_tag: Optional[str],
|
||||||
|
side: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> float:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
def get_ticker_indicator(self) -> int:
|
||||||
|
"""获取 timeframe 的分钟数"""
|
||||||
|
return int(self.timeframe[:-1])
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
from freqtrade.strategy import IStrategy, merge_informative_pair
|
||||||
|
from pandas import DataFrame
|
||||||
|
import talib.abstract as ta
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
# freqtrade trade -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategy --strategy-path ./user_data/Chan/strategies
|
||||||
|
# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategy --strategy-path ./user_data/Chan/strategies --timerange=20260304-
|
||||||
|
# freqtrade download-data -c ./user_data/Chan/config/Local_Test.json -t 1m 5m --data-format-ohlcv json --pairs SOL/USDT:USDT --timerange=20260201-
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoFutures1m5mStrategy(IStrategy):
|
||||||
|
"""
|
||||||
|
SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V12e (Short Only)
|
||||||
|
|
||||||
|
14个月回测 (2025-01 ~ 2026-03): +107.11%, PF 1.37, DD 23.96%
|
||||||
|
每个季度均盈利,市场下跌-54%期间持续获利
|
||||||
|
|
||||||
|
核心设计:
|
||||||
|
1. 纯做空策略 - 价格必须低于EMA200至少1%才允许做空
|
||||||
|
2. 5分钟趋势确认:EMA12<EMA26<EMA50 + ADX 25-50 + RSI 30-48
|
||||||
|
3. ATR自适应波动率过滤:ATR < 长期均值 * 1.5(避免极端波动)
|
||||||
|
4. 1分钟精确入场:顶背离 / EMA死叉 / 熊市回调
|
||||||
|
5. 双重MACD确认(5分钟+1分钟MACD柱状图均为负)
|
||||||
|
6. trailing_stop_positive_offset = 0.030
|
||||||
|
7. 时间止损:持仓过久且亏损时提前退出
|
||||||
|
"""
|
||||||
|
INTERFACE_VERSION = 3
|
||||||
|
timeframe = '1m'
|
||||||
|
informative_timeframe = '5m'
|
||||||
|
can_short = True
|
||||||
|
|
||||||
|
# 止损止盈
|
||||||
|
stoploss = -0.025 # 2.5% 硬止损
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.008
|
||||||
|
trailing_stop_positive_offset = 0.030
|
||||||
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
|
# 不使用custom_stoploss(会干扰trailing_stop)
|
||||||
|
use_custom_stoploss = False
|
||||||
|
|
||||||
|
# 完全禁用 exit_signal
|
||||||
|
use_exit_signal = False
|
||||||
|
|
||||||
|
process_only_new_candles = True
|
||||||
|
startup_candle_count: int = 1100
|
||||||
|
def informative_pairs(self):
|
||||||
|
return [
|
||||||
|
("SOL/USDT:USDT", "5m"),
|
||||||
|
]
|
||||||
|
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斜率(20根5分钟K线 = 100分钟趋势方向)
|
||||||
|
informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100
|
||||||
|
|
||||||
|
# ===== 大趋势过滤(Short Only) =====
|
||||||
|
# 做空需要价格低于EMA200至少1%
|
||||||
|
informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0
|
||||||
|
|
||||||
|
# 牛市暂停:EMA200上升 + 价格在EMA200上方 → 完全停止做空
|
||||||
|
informative['bull_pause'] = (
|
||||||
|
(informative['ema200_slope'] > 0) &
|
||||||
|
(informative['ema200_dist_pct'] > 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== 5分钟趋势判断(仅Short) =====
|
||||||
|
informative['trend_bear_5m'] = (
|
||||||
|
(informative['ema12'] < informative['ema26']) &
|
||||||
|
(informative['ema26'] < informative['ema50']) &
|
||||||
|
(informative['ema12_slope'] < 0) &
|
||||||
|
(informative['adx_5m'] > 25) &
|
||||||
|
(informative['adx_5m'] < 50) &
|
||||||
|
(informative['close'] < informative['ema12']) &
|
||||||
|
(informative['rsi_5m'] < 48) &
|
||||||
|
(informative['rsi_5m'] > 30)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 做空条件:短期趋势 + EMA200大趋势方向一致 + 非牛市
|
||||||
|
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.1) &
|
||||||
|
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 1.5)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 合并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)
|
||||||
|
)
|
||||||
|
|
||||||
|
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',
|
||||||
|
]
|
||||||
|
for col in bool_cols:
|
||||||
|
if col in dataframe.columns:
|
||||||
|
dataframe[col] = dataframe[col].fillna(False).astype(bool)
|
||||||
|
|
||||||
|
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].fillna(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
|
||||||
|
|
||||||
|
# ===== 做空入场 =====
|
||||||
|
dataframe.loc[
|
||||||
|
(time_ok) &
|
||||||
|
(atr_ok) &
|
||||||
|
(dataframe['can_short_5m_5m']) &
|
||||||
|
(macd_bear_5m) &
|
||||||
|
(macd_bear_1m) &
|
||||||
|
(dataframe['rsi'] > 30) &
|
||||||
|
(
|
||||||
|
dataframe['top_divergence'] |
|
||||||
|
dataframe['ema_cross_down'] |
|
||||||
|
dataframe['bear_pullback']
|
||||||
|
) &
|
||||||
|
(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'
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
import logging
|
||||||
|
from functools import reduce
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.strategy import IStrategy, merge_informative_pair
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
# freqtrade backtesting -c ./user_data/Chan/config/FreqAI_Test.json --strategy CryptoFuturesAIStrategy --strategy-path ./user_data/Chan/strategies --freqaimodel LightGBMRegressor --timerange=20260201-
|
||||||
|
# freqtrade trade -c ./user_data/Chan/config/FreqAI_Test.json --strategy CryptoFuturesAIStrategy --strategy-path ./user_data/Chan/strategies --freqaimodel LightGBMRegressor
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoFuturesAIStrategy(IStrategy):
|
||||||
|
"""
|
||||||
|
SOL/USDT 合约 AI 策略 - FreqAI + LightGBM
|
||||||
|
|
||||||
|
核心思路:
|
||||||
|
1. 用 FreqAI 的 LightGBM 回归模型预测未来价格变化方向和幅度
|
||||||
|
2. 模型自动在滚动窗口上重新训练,适应市场变化
|
||||||
|
3. 结合传统技术指标作为特征输入,让 AI 学习最优组合
|
||||||
|
4. 用 z-score 动态阈值代替固定参数,自适应不同市场环境
|
||||||
|
5. 保留 trailing_stop 作为风控(这是原策略的盈利核心)
|
||||||
|
|
||||||
|
相比固定参数策略的优势:
|
||||||
|
- 参数自适应:模型每隔一段时间重新训练,适应市场状态变化
|
||||||
|
- 特征自动选择:LightGBM 自动学习哪些指标在当前市场最有用
|
||||||
|
- 动态阈值:用预测值的统计分布来决定入场,而非固定数值
|
||||||
|
- 多维度输入:同时考虑价格、成交量、波动率、时间等多维信息
|
||||||
|
"""
|
||||||
|
INTERFACE_VERSION = 3
|
||||||
|
timeframe = '5m' # FreqAI 用5分钟作为基础时间框架,更稳定
|
||||||
|
can_short = True
|
||||||
|
|
||||||
|
# === 风控参数(保留原策略的盈利核心) ===
|
||||||
|
stoploss = -0.025
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.008
|
||||||
|
trailing_stop_positive_offset = 0.015
|
||||||
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
|
use_custom_stoploss = False
|
||||||
|
use_exit_signal = False # 禁用exit_signal,让trailing_stop管理退出
|
||||||
|
|
||||||
|
process_only_new_candles = True
|
||||||
|
startup_candle_count: int = 100 # 需要足够的历史数据计算指标
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# FreqAI 特征工程函数
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
def feature_engineering_expand_all(
|
||||||
|
self, dataframe: DataFrame, period: int, metadata: dict, **kwargs
|
||||||
|
) -> DataFrame:
|
||||||
|
"""
|
||||||
|
自动扩展特征 - 精简版,减少特征数量防止内存溢出
|
||||||
|
"""
|
||||||
|
# 核心动量指标
|
||||||
|
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||||
|
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||||
|
|
||||||
|
# 相对成交量
|
||||||
|
dataframe["%-relative_volume-period"] = (
|
||||||
|
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def feature_engineering_expand_basic(
|
||||||
|
self, dataframe: DataFrame, metadata: dict, **kwargs
|
||||||
|
) -> DataFrame:
|
||||||
|
"""
|
||||||
|
基础特征 - 在所有时间框架上展开,但不按周期展开
|
||||||
|
"""
|
||||||
|
# 价格变化率
|
||||||
|
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||||
|
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||||
|
dataframe["%-raw_price"] = dataframe["close"]
|
||||||
|
|
||||||
|
# K线形态特征
|
||||||
|
dataframe["%-candle_body"] = (
|
||||||
|
(dataframe["close"] - dataframe["open"]) / dataframe["open"]
|
||||||
|
)
|
||||||
|
dataframe["%-upper_shadow"] = (
|
||||||
|
(dataframe["high"] - dataframe[["open", "close"]].max(axis=1))
|
||||||
|
/ dataframe["close"]
|
||||||
|
)
|
||||||
|
dataframe["%-lower_shadow"] = (
|
||||||
|
(dataframe[["open", "close"]].min(axis=1) - dataframe["low"])
|
||||||
|
/ dataframe["close"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 价格与高低点的关系
|
||||||
|
dataframe["%-high_low_range"] = (
|
||||||
|
(dataframe["high"] - dataframe["low"]) / dataframe["close"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def feature_engineering_standard(
|
||||||
|
self, dataframe: DataFrame, metadata: dict, **kwargs
|
||||||
|
) -> DataFrame:
|
||||||
|
"""
|
||||||
|
标准特征 - 不自动展开,只在基础时间框架上计算一次
|
||||||
|
适合放时间特征等不需要跨时间框架的特征
|
||||||
|
"""
|
||||||
|
# 时间特征(让模型学习时间规律)
|
||||||
|
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||||||
|
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||||||
|
dataframe["%-minute_of_hour"] = dataframe["date"].dt.minute
|
||||||
|
|
||||||
|
# 是否是高波动时段(美国开市等)
|
||||||
|
hour = dataframe["date"].dt.hour
|
||||||
|
dataframe["%-is_us_session"] = (
|
||||||
|
((hour >= 13) & (hour <= 21)) # UTC 13-21 = 美东 8am-4pm
|
||||||
|
).astype(int)
|
||||||
|
dataframe["%-is_asia_session"] = (
|
||||||
|
((hour >= 0) & (hour <= 8)) # UTC 0-8 = 亚洲时段
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# 连续涨跌统计
|
||||||
|
pct = dataframe["close"].pct_change()
|
||||||
|
dataframe["%-consec_up"] = (pct > 0).astype(int)
|
||||||
|
dataframe["%-consec_up"] = dataframe["%-consec_up"].groupby(
|
||||||
|
(dataframe["%-consec_up"] != dataframe["%-consec_up"].shift()).cumsum()
|
||||||
|
).cumcount() + 1
|
||||||
|
dataframe["%-consec_up"] = dataframe["%-consec_up"] * (pct > 0).astype(int)
|
||||||
|
|
||||||
|
dataframe["%-consec_down"] = (pct < 0).astype(int)
|
||||||
|
dataframe["%-consec_down"] = dataframe["%-consec_down"].groupby(
|
||||||
|
(dataframe["%-consec_down"] != dataframe["%-consec_down"].shift()).cumsum()
|
||||||
|
).cumcount() + 1
|
||||||
|
dataframe["%-consec_down"] = dataframe["%-consec_down"] * (pct < 0).astype(int)
|
||||||
|
|
||||||
|
# 近期波动率变化
|
||||||
|
dataframe["%-vol_change_5"] = (
|
||||||
|
dataframe["volume"].rolling(5).mean()
|
||||||
|
/ dataframe["volume"].rolling(20).mean()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 价格距离近期高低点
|
||||||
|
dataframe["%-dist_high_20"] = (
|
||||||
|
dataframe["close"] / dataframe["high"].rolling(20).max() - 1
|
||||||
|
)
|
||||||
|
dataframe["%-dist_low_20"] = (
|
||||||
|
dataframe["close"] / dataframe["low"].rolling(20).min() - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||||
|
"""
|
||||||
|
设置 AI 模型的预测目标
|
||||||
|
|
||||||
|
目标:预测未来 N 根K线的平均价格变化率
|
||||||
|
模型会学习:当前市场状态 → 未来价格走向
|
||||||
|
"""
|
||||||
|
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||||
|
|
||||||
|
# 回归目标:未来 N 根K线的平均收盘价相对当前的变化率
|
||||||
|
dataframe["&-s_close"] = (
|
||||||
|
dataframe["close"]
|
||||||
|
.shift(-label_period)
|
||||||
|
.rolling(label_period)
|
||||||
|
.mean()
|
||||||
|
/ dataframe["close"]
|
||||||
|
- 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# =====================================================
|
||||||
|
# 策略核心逻辑
|
||||||
|
# =====================================================
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
populate_indicators 中调用 FreqAI
|
||||||
|
所有指标由 feature_engineering_*() 函数定义
|
||||||
|
"""
|
||||||
|
# FreqAI 会自动调用所有 feature_engineering_*() 函数
|
||||||
|
# 然后训练模型并返回预测结果
|
||||||
|
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||||
|
|
||||||
|
# 计算动态阈值(z-score 方式)
|
||||||
|
# &-s_close 是模型预测的未来价格变化
|
||||||
|
# &-s_close_mean 和 &-s_close_std 是训练期间的统计值
|
||||||
|
# 当预测值超过 mean + factor * std 时,说明模型认为有较强的方向性
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
入场信号 - 基于 AI 预测
|
||||||
|
|
||||||
|
核心逻辑:
|
||||||
|
1. do_predict == 1:模型认为当前数据在训练分布内(可信)
|
||||||
|
2. &-s_close > threshold:预测未来上涨幅度超过阈值
|
||||||
|
3. 动态阈值 = mean + 1.0 * std(约 84% 置信度)
|
||||||
|
"""
|
||||||
|
# 动态阈值:使用训练期间的统计值
|
||||||
|
# 当 &-s_close_mean 和 &-s_close_std 可用时,用 z-score
|
||||||
|
# 否则用固定阈值
|
||||||
|
if "&-s_close_mean" in df.columns and "&-s_close_std" in df.columns:
|
||||||
|
long_threshold = df["&-s_close_mean"] + df["&-s_close_std"] * 1.0
|
||||||
|
short_threshold = df["&-s_close_mean"] - df["&-s_close_std"] * 1.0
|
||||||
|
else:
|
||||||
|
long_threshold = 0.005
|
||||||
|
short_threshold = -0.005
|
||||||
|
|
||||||
|
# 做多条件
|
||||||
|
enter_long_conditions = [
|
||||||
|
df["do_predict"] == 1, # 模型预测可信
|
||||||
|
df["&-s_close"] > long_threshold, # 预测超过动态阈值
|
||||||
|
]
|
||||||
|
|
||||||
|
if enter_long_conditions:
|
||||||
|
df.loc[
|
||||||
|
reduce(lambda x, y: x & y, enter_long_conditions),
|
||||||
|
["enter_long", "enter_tag"]
|
||||||
|
] = (1, "ai_long")
|
||||||
|
|
||||||
|
# 做空条件
|
||||||
|
enter_short_conditions = [
|
||||||
|
df["do_predict"] == 1, # 模型预测可信
|
||||||
|
df["&-s_close"] < short_threshold, # 预测低于动态阈值
|
||||||
|
]
|
||||||
|
|
||||||
|
if enter_short_conditions:
|
||||||
|
df.loc[
|
||||||
|
reduce(lambda x, y: x & y, enter_short_conditions),
|
||||||
|
["enter_short", "enter_tag"]
|
||||||
|
] = (1, "ai_short")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
出场信号 - AI 预测方向反转时退出
|
||||||
|
"""
|
||||||
|
# 多头退出:预测转为下跌
|
||||||
|
exit_long_conditions = [
|
||||||
|
df["do_predict"] == 1,
|
||||||
|
df["&-s_close"] < 0, # 预测未来下跌
|
||||||
|
]
|
||||||
|
if exit_long_conditions:
|
||||||
|
df.loc[reduce(lambda x, y: x & y, exit_long_conditions), "exit_long"] = 1
|
||||||
|
|
||||||
|
# 空头退出:预测转为上涨
|
||||||
|
exit_short_conditions = [
|
||||||
|
df["do_predict"] == 1,
|
||||||
|
df["&-s_close"] > 0, # 预测未来上涨
|
||||||
|
]
|
||||||
|
if exit_short_conditions:
|
||||||
|
df.loc[reduce(lambda x, y: x & y, exit_short_conditions), "exit_short"] = 1
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
实盘入场确认 - 防止滑点过大
|
||||||
|
"""
|
||||||
|
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
last_candle = df.iloc[-1].squeeze()
|
||||||
|
|
||||||
|
if side == "long":
|
||||||
|
if rate > (last_candle["close"] * (1 + 0.0025)):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if rate < (last_candle["close"] * (1 - 0.0025)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# --- Do not remove these libs ---
|
||||||
|
from statistics import median
|
||||||
|
from freqtrade.strategy import IStrategy, stoploss_from_absolute
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
# 添加父目录到系统路径
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from ChanLun import ChanLun
|
||||||
|
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||||
|
# --------------------------------
|
||||||
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from freqtrade.persistence import Trade, Order
|
||||||
|
from typing import Optional
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
### Now you can use logger.info('asfd') to log
|
||||||
|
# freqtrade plot-dataframe --strategy ChanLun_BTC --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309-
|
||||||
|
|
||||||
|
# freqtrade trade -c ./user_data/Chan/config/Local_Test.json --strategy EMA26_EMA52_Cross --strategy-path ./user_data/Chan/strategies
|
||||||
|
# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy EMA26_EMA52_Cross --strategy-path ./user_data/Chan/strategies --timerange=20260304-
|
||||||
|
# freqtrade download-data -c ./user_data/Chan/config/Local_Test.json -t 1m 5m 15m 1h 1d 1w 1M --pairs SOL/USDT:USDT --timerange=20240101-
|
||||||
|
# freqtrade download-data -c ./user_data/Chan/config/Local_Test.json -t 1m 1h 1d 1M --pairs SOL/USDT:USDT --timerange=20170101-
|
||||||
|
# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy EMA26_EMA52_Cross --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/Local_Test.json -e 200 --timerange=20250201-20250901
|
||||||
|
# freqtrade edge -c ./user_data/Chan/config/Local_Test.json --strategy EMA26_EMA52_Cross --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901
|
||||||
|
# freqtrade plot-dataframe -c ./user_data/Chan/config/Local_Test.json --strategy EMA26_EMA52_Cross --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901
|
||||||
|
|
||||||
|
# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/EMA26_EMA52_Cross.json --strategy EMA26_EMA52_Cross --strategy-path ./user_data/Chan/strategies --timerange=20250721-
|
||||||
|
# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/EMA26_EMA52_Cross.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101-
|
||||||
|
# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/EMA26_EMA52_Cross.json --strategy EMA26_EMA52_Cross --strategy-path ./user_data/Chan/strategies
|
||||||
|
|
||||||
|
class EMA_Cross(IStrategy):
|
||||||
|
INTERFACE_VERSION: int = 3
|
||||||
|
# Minimal ROI designed for the strategy.
|
||||||
|
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||||
|
# 30m and 1h
|
||||||
|
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.05,
|
||||||
|
"50": 0.025,
|
||||||
|
"120": 0.015,
|
||||||
|
"180": 0.01,
|
||||||
|
"240": 0
|
||||||
|
}
|
||||||
|
# 5m and 15m
|
||||||
|
minimal_roi_1 = {
|
||||||
|
"0": 0.1,
|
||||||
|
"60": 0.05,
|
||||||
|
"120": 0.02,
|
||||||
|
"240": 0
|
||||||
|
}
|
||||||
|
# 15m and 30m
|
||||||
|
minimal_roi_1 = {
|
||||||
|
"0": 0.1,
|
||||||
|
"240": 0.05,
|
||||||
|
"480": 0.03,
|
||||||
|
"600": 0
|
||||||
|
}
|
||||||
|
minimal_roi_1 = {
|
||||||
|
"0": 1.50,
|
||||||
|
"120": 0.05,
|
||||||
|
"240": 0.025,
|
||||||
|
"360": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
can_short = True
|
||||||
|
lev = 1.0
|
||||||
|
stoploss = -0.1 # 设置为很大的负值,让custom_stoploss来控制
|
||||||
|
use_custom_stoploss = False # 启用自定义止损
|
||||||
|
startup_candle_count = 1600
|
||||||
|
trailing_stop = False
|
||||||
|
trailing_stop_positive = 0.03
|
||||||
|
trailing_stop_positive_offset = 0.06
|
||||||
|
trailing_only_offset_is_reached = False
|
||||||
|
price_offset = 0.01
|
||||||
|
df_dict = {}
|
||||||
|
tf_list = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 30]
|
||||||
|
time = 30
|
||||||
|
startup_candle_count: int = 1100
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe = self.merge_df_dict(dataframe)
|
||||||
|
return dataframe
|
||||||
|
def merge_df_dict(self, dataframe):
|
||||||
|
df_dict = self.init_df_dict(dataframe)
|
||||||
|
for tf in df_dict.keys():
|
||||||
|
dataframe = resampled_merge(dataframe, df_dict[tf])
|
||||||
|
return dataframe
|
||||||
|
def init_df_dict(self, dataframe):
|
||||||
|
df_dict = {}
|
||||||
|
if len(dataframe) > 1000:
|
||||||
|
for tf in self.tf_list:
|
||||||
|
df_dict[tf] = resample_to_interval(dataframe, self.get_ticker_indicator() * tf)
|
||||||
|
df_dict[tf] = self.add_indicators(df_dict[tf])
|
||||||
|
return df_dict
|
||||||
|
def add_indicators(self, dataframe):
|
||||||
|
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||||
|
dataframe['ema13'] = ta.EMA(dataframe, timeperiod=26)
|
||||||
|
dataframe['ema26'] = ta.EMA(dataframe, timeperiod=26)
|
||||||
|
dataframe['ema52'] = ta.EMA(dataframe, timeperiod=52)
|
||||||
|
# 上穿:本根 26 > 52,上一根 26 ≤ 52
|
||||||
|
dataframe['ema26_cross_up_52'] = (
|
||||||
|
(dataframe['ema26'] > dataframe['ema52']) &
|
||||||
|
(dataframe['ema26'].shift(1) <= dataframe['ema52'].shift(1))
|
||||||
|
)
|
||||||
|
# 下穿:本根 26 < 52,上一根 26 ≥ 52
|
||||||
|
dataframe['ema26_cross_down_52'] = (
|
||||||
|
(dataframe['ema26'] < dataframe['ema52']) &
|
||||||
|
(dataframe['ema26'].shift(1) >= dataframe['ema52'].shift(1))
|
||||||
|
)
|
||||||
|
# 上穿:本根 2 > 13,上一根 2 ≤ 13
|
||||||
|
dataframe['ema5_cross_up_13'] = (
|
||||||
|
(dataframe['ema5'] > dataframe['ema13']) &
|
||||||
|
(dataframe['ema5'].shift(1) <= dataframe['ema13'].shift(1))
|
||||||
|
)
|
||||||
|
# 下穿:本根 2 < 13,上一根 2 ≥ 13
|
||||||
|
dataframe['ema5_cross_down_13'] = (
|
||||||
|
(dataframe['ema5'] < dataframe['ema13']) &
|
||||||
|
(dataframe['ema5'].shift(1) >= dataframe['ema13'].shift(1))
|
||||||
|
)
|
||||||
|
dataframe_macd = ta.MACD(dataframe, fast=12, slow=26, signal=9)
|
||||||
|
dataframe['macdsignal'] = dataframe_macd['macdsignal']
|
||||||
|
dataframe['macd'] = dataframe_macd['macd']
|
||||||
|
dataframe['macdhist'] = dataframe_macd['macdhist']
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
|
||||||
|
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||||
|
new_entryprice = proposed_rate
|
||||||
|
if trade:
|
||||||
|
if trade.is_short:
|
||||||
|
new_entryprice = proposed_rate - self.price_offset
|
||||||
|
else:
|
||||||
|
new_entryprice = proposed_rate + self.price_offset
|
||||||
|
return new_entryprice
|
||||||
|
|
||||||
|
def custom_exit_price(self, pair: str, trade: Trade,
|
||||||
|
current_time: datetime, proposed_rate: float,
|
||||||
|
current_profit: float, exit_tag: str | None, **kwargs) -> float:
|
||||||
|
new_exitprice = proposed_rate
|
||||||
|
if trade:
|
||||||
|
if trade.is_short:
|
||||||
|
new_exitprice = proposed_rate + self.price_offset
|
||||||
|
else:
|
||||||
|
new_exitprice = proposed_rate - self.price_offset
|
||||||
|
return new_exitprice
|
||||||
|
|
||||||
|
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float,
|
||||||
|
min_stake: Optional[float], max_stake: float,
|
||||||
|
current_entry_rate: float, current_exit_rate: float,
|
||||||
|
current_entry_profit: float, current_exit_profit: float,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
# 关闭分批止盈,始终不调整仓位
|
||||||
|
return None
|
||||||
|
|
||||||
|
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||||
|
current_profit: float, **kwargs):
|
||||||
|
# 不做分批止盈/最终止盈处理,退出由策略信号/ROI/止损决定
|
||||||
|
return None
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
cross_up = 'resample_{}_ema26_cross_up_52'.format(self.get_ticker_indicator() * self.time)
|
||||||
|
cross_down = 'resample_{}_ema26_cross_down_52'.format(self.get_ticker_indicator() * self.time)
|
||||||
|
#time = 1
|
||||||
|
#cross_up = 'ema26_cross_up_52'
|
||||||
|
#cross_down = 'ema26_cross_down_52'
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe[cross_up].shift(self.time) == True),
|
||||||
|
['enter_long', 'enter_tag']] = (1, 'long_signal')
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe[cross_down].shift(self.time) == True),
|
||||||
|
['enter_short', 'enter_tag']] = (1, 'short_signal')
|
||||||
|
return dataframe
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
cross_up = 'resample_{}_ema26_cross_up_52'.format(self.get_ticker_indicator() * self.time)
|
||||||
|
cross_down = 'resample_{}_ema26_cross_down_52'.format(self.get_ticker_indicator() * self.time)
|
||||||
|
#time = 1
|
||||||
|
#cross_up = 'ema26_cross_up_52'
|
||||||
|
#cross_down = 'ema26_cross_down_52'
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe[cross_down].shift(self.time) == True),
|
||||||
|
['exit_long', 'exit_tag']] = (1, 'long_signal')
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe[cross_up].shift(self.time) == True),
|
||||||
|
['exit_short', 'exit_tag']] = (1, 'short_signal')
|
||||||
|
return dataframe
|
||||||
|
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
|
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||||
|
**kwargs) -> float:
|
||||||
|
return self.lev
|
||||||
|
def get_ticker_indicator(self):
|
||||||
|
return int(self.timeframe[:-1])
|
||||||
@@ -0,0 +1,395 @@
|
|||||||
|
"""
|
||||||
|
PriceActionStrategy - 纯价格行为策略
|
||||||
|
|
||||||
|
核心原则:
|
||||||
|
零指标。不用 EMA、RSI、MACD、ATR 或任何计算指标。
|
||||||
|
只看 K线本身(Open/High/Low/Close/Volume)。
|
||||||
|
|
||||||
|
价格行为判断方法:
|
||||||
|
1. 市场结构(趋势):用 Swing High / Swing Low 判断
|
||||||
|
- 上升趋势 = Higher High + Higher Low
|
||||||
|
- 下降趋势 = Lower High + Lower Low
|
||||||
|
2. 入场信号:纯K线形态
|
||||||
|
- Pin Bar(锤子线/射击之星)
|
||||||
|
- 吞没形态(Engulfing)
|
||||||
|
- Inside Bar 突破
|
||||||
|
3. 出场:用前一个 Swing High/Low 作为止盈目标
|
||||||
|
4. 止损:放在信号K线的另一端
|
||||||
|
|
||||||
|
使用时间框架:
|
||||||
|
- 5m(主时间框架):入场/出场 + 结构判断
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class PriceActionStrategy(IStrategy):
|
||||||
|
"""
|
||||||
|
纯价格行为策略 - 零指标
|
||||||
|
"""
|
||||||
|
|
||||||
|
INTERFACE_VERSION = 3
|
||||||
|
|
||||||
|
# === 基础配置 ===
|
||||||
|
timeframe = "5m"
|
||||||
|
can_short = True
|
||||||
|
stoploss = -0.03 # 3% 硬止损安全网
|
||||||
|
trailing_stop = False
|
||||||
|
use_custom_stoploss = False
|
||||||
|
|
||||||
|
startup_candle_count: int = 100
|
||||||
|
|
||||||
|
# 不用 ROI 自动止盈,让价格行为决定出场
|
||||||
|
minimal_roi = {}
|
||||||
|
|
||||||
|
order_types = {
|
||||||
|
"entry": "market",
|
||||||
|
"exit": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
use_exit_signal = True
|
||||||
|
exit_profit_only = False
|
||||||
|
ignore_roi_if_entry_signal = False
|
||||||
|
|
||||||
|
# === 参数 ===
|
||||||
|
swing_lookback = 10 # Swing High/Low 回看K线数
|
||||||
|
min_body_ratio = 0.55 # 最小实体占比(实体/全幅)
|
||||||
|
pin_shadow_ratio = 2.5 # Pin Bar 影线至少是实体的 N 倍
|
||||||
|
engulf_body_ratio = 1.2 # 吞没K线实体至少是前一根的 N 倍
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
纯价格行为 — 只从 OHLCV 提取结构信息,不计算任何技术指标。
|
||||||
|
"""
|
||||||
|
df = dataframe
|
||||||
|
|
||||||
|
# ========== K线基础属性 ==========
|
||||||
|
df["body"] = abs(df["close"] - df["open"])
|
||||||
|
df["candle_range"] = df["high"] - df["low"]
|
||||||
|
df["body_ratio"] = df["body"] / (df["candle_range"] + 1e-10)
|
||||||
|
df["upper_shadow"] = df["high"] - df[["close", "open"]].max(axis=1)
|
||||||
|
df["lower_shadow"] = df[["close", "open"]].min(axis=1) - df["low"]
|
||||||
|
df["is_bull"] = (df["close"] > df["open"]).astype(int)
|
||||||
|
df["is_bear"] = (df["close"] < df["open"]).astype(int)
|
||||||
|
|
||||||
|
# ========== Swing High / Swing Low ==========
|
||||||
|
# Swing High: 当前 high 是前后 N 根K线中最高的
|
||||||
|
# Swing Low: 当前 low 是前后 N 根K线中最低的
|
||||||
|
n = self.swing_lookback
|
||||||
|
df["swing_high"] = df["high"].rolling(window=2 * n + 1, center=True).apply(
|
||||||
|
lambda x: 1 if x.iloc[n] == x.max() else 0, raw=False
|
||||||
|
)
|
||||||
|
df["swing_low"] = df["low"].rolling(window=2 * n + 1, center=True).apply(
|
||||||
|
lambda x: 1 if x.iloc[n] == x.min() else 0, raw=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 记录最近的 Swing High/Low 价格
|
||||||
|
df["last_swing_high"] = np.nan
|
||||||
|
df["last_swing_low"] = np.nan
|
||||||
|
df["prev_swing_high"] = np.nan
|
||||||
|
df["prev_swing_low"] = np.nan
|
||||||
|
|
||||||
|
df.loc[df["swing_high"] == 1, "last_swing_high"] = df["high"]
|
||||||
|
df["last_swing_high"] = df["last_swing_high"].ffill()
|
||||||
|
|
||||||
|
df.loc[df["swing_low"] == 1, "last_swing_low"] = df["low"]
|
||||||
|
df["last_swing_low"] = df["last_swing_low"].ffill()
|
||||||
|
|
||||||
|
# 前一个 Swing High/Low(用于判断 HH/HL/LH/LL)
|
||||||
|
swing_high_prices = df.loc[df["swing_high"] == 1, "high"]
|
||||||
|
swing_low_prices = df.loc[df["swing_low"] == 1, "low"]
|
||||||
|
|
||||||
|
# 构建 prev_swing_high: 每个 swing high 点对应的上一个 swing high
|
||||||
|
sh_idx = swing_high_prices.index.tolist()
|
||||||
|
for i in range(1, len(sh_idx)):
|
||||||
|
df.loc[sh_idx[i], "prev_swing_high"] = swing_high_prices.loc[sh_idx[i - 1]]
|
||||||
|
df["prev_swing_high"] = df["prev_swing_high"].ffill()
|
||||||
|
|
||||||
|
sl_idx = swing_low_prices.index.tolist()
|
||||||
|
for i in range(1, len(sl_idx)):
|
||||||
|
df.loc[sl_idx[i], "prev_swing_low"] = swing_low_prices.loc[sl_idx[i - 1]]
|
||||||
|
df["prev_swing_low"] = df["prev_swing_low"].ffill()
|
||||||
|
|
||||||
|
# ========== 市场结构(趋势)==========
|
||||||
|
# Higher High + Higher Low = 上升趋势
|
||||||
|
# Lower High + Lower Low = 下降趋势
|
||||||
|
df["higher_high"] = (df["last_swing_high"] > df["prev_swing_high"]).astype(int)
|
||||||
|
df["higher_low"] = (df["last_swing_low"] > df["prev_swing_low"]).astype(int)
|
||||||
|
df["lower_high"] = (df["last_swing_high"] < df["prev_swing_high"]).astype(int)
|
||||||
|
df["lower_low"] = (df["last_swing_low"] < df["prev_swing_low"]).astype(int)
|
||||||
|
|
||||||
|
df["uptrend"] = ((df["higher_high"] == 1) & (df["higher_low"] == 1)).astype(int)
|
||||||
|
df["downtrend"] = ((df["lower_high"] == 1) & (df["lower_low"] == 1)).astype(int)
|
||||||
|
|
||||||
|
# ========== 价格行为形态 ==========
|
||||||
|
|
||||||
|
# --- Pin Bar(锤子线 / 射击之星)---
|
||||||
|
# 看涨 Pin Bar: 长下影线,短上影线,实体在上半部分
|
||||||
|
df["bullish_pin"] = (
|
||||||
|
(df["lower_shadow"] > df["body"] * self.pin_shadow_ratio)
|
||||||
|
& (df["lower_shadow"] > df["upper_shadow"] * 2)
|
||||||
|
& (df["body_ratio"] > 0.15) # 不是十字星
|
||||||
|
& (df["is_bull"] == 1)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# 看跌 Pin Bar: 长上影线,短下影线,实体在下半部分
|
||||||
|
df["bearish_pin"] = (
|
||||||
|
(df["upper_shadow"] > df["body"] * self.pin_shadow_ratio)
|
||||||
|
& (df["upper_shadow"] > df["lower_shadow"] * 2)
|
||||||
|
& (df["body_ratio"] > 0.15)
|
||||||
|
& (df["is_bear"] == 1)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# --- 吞没形态(Engulfing)---
|
||||||
|
prev_body = df["body"].shift(1)
|
||||||
|
prev_open = df["open"].shift(1)
|
||||||
|
prev_close = df["close"].shift(1)
|
||||||
|
|
||||||
|
# 看涨吞没: 前一根阴线,当前阳线完全包住前一根
|
||||||
|
df["bullish_engulf"] = (
|
||||||
|
(df["is_bull"] == 1)
|
||||||
|
& (prev_close < prev_open) # 前一根是阴线
|
||||||
|
& (df["open"] <= prev_close) # 开盘 <= 前收盘(低开或平开)
|
||||||
|
& (df["close"] >= prev_open) # 收盘 >= 前开盘(完全吞没)
|
||||||
|
& (df["body"] > prev_body * self.engulf_body_ratio) # 实体更大
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# 看跌吞没
|
||||||
|
df["bearish_engulf"] = (
|
||||||
|
(df["is_bear"] == 1)
|
||||||
|
& (prev_close > prev_open) # 前一根是阳线
|
||||||
|
& (df["open"] >= prev_close) # 开盘 >= 前收盘
|
||||||
|
& (df["close"] <= prev_open) # 收盘 <= 前开盘
|
||||||
|
& (df["body"] > prev_body * self.engulf_body_ratio)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# --- Inside Bar 突破 ---
|
||||||
|
# Inside Bar: 当前K线的 high/low 完全在前一根范围内
|
||||||
|
prev_high = df["high"].shift(1)
|
||||||
|
prev_low = df["low"].shift(1)
|
||||||
|
df["inside_bar"] = (
|
||||||
|
(df["high"] <= prev_high)
|
||||||
|
& (df["low"] >= prev_low)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# Inside Bar 之后的突破
|
||||||
|
# 向上突破: 前一根是 inside bar,当前收盘 > 母线(前两根)的 high
|
||||||
|
mother_high = df["high"].shift(2)
|
||||||
|
mother_low = df["low"].shift(2)
|
||||||
|
df["inside_break_up"] = (
|
||||||
|
(df["inside_bar"].shift(1) == 1)
|
||||||
|
& (df["close"] > mother_high)
|
||||||
|
& (df["is_bull"] == 1)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
df["inside_break_down"] = (
|
||||||
|
(df["inside_bar"].shift(1) == 1)
|
||||||
|
& (df["close"] < mother_low)
|
||||||
|
& (df["is_bear"] == 1)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# --- 支撑/阻力突破 ---
|
||||||
|
# 突破前一个 Swing High(做多)
|
||||||
|
df["break_swing_high"] = (
|
||||||
|
(df["close"] > df["last_swing_high"].shift(1))
|
||||||
|
& (df["close"].shift(1) <= df["last_swing_high"].shift(1))
|
||||||
|
& (df["is_bull"] == 1)
|
||||||
|
& (df["body_ratio"] > self.min_body_ratio) # 实体饱满(有力度)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# 跌破前一个 Swing Low(做空)
|
||||||
|
df["break_swing_low"] = (
|
||||||
|
(df["close"] < df["last_swing_low"].shift(1))
|
||||||
|
& (df["close"].shift(1) >= df["last_swing_low"].shift(1))
|
||||||
|
& (df["is_bear"] == 1)
|
||||||
|
& (df["body_ratio"] > self.min_body_ratio)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
# --- 成交量确认(只用原始 volume 对比,不算均线)---
|
||||||
|
# 当前成交量 > 前3根的最大成交量 = 放量
|
||||||
|
df["vol_expand"] = (
|
||||||
|
df["volume"] > df["volume"].rolling(3).max().shift(1)
|
||||||
|
).astype(int)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
入场条件 — 纯价格行为
|
||||||
|
|
||||||
|
做多条件(满足任一组):
|
||||||
|
A) 上升趋势 + 看涨 Pin Bar(回调到支撑后反弹信号)
|
||||||
|
B) 上升趋势 + 看涨吞没(回调后强势反转)
|
||||||
|
C) 上升趋势 + Inside Bar 向上突破(蓄力后爆发)
|
||||||
|
D) 突破 Swing High + 放量(结构性突破)
|
||||||
|
|
||||||
|
做空条件(镜像)
|
||||||
|
"""
|
||||||
|
df = dataframe
|
||||||
|
|
||||||
|
# ===== 做多 =====
|
||||||
|
conditions_long = []
|
||||||
|
|
||||||
|
# A) 上升趋势 + 看涨 Pin Bar
|
||||||
|
conditions_long.append(
|
||||||
|
(df["uptrend"] == 1)
|
||||||
|
& (df["bullish_pin"] == 1)
|
||||||
|
& (df["vol_expand"] == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# B) 上升趋势 + 看涨吞没
|
||||||
|
conditions_long.append(
|
||||||
|
(df["uptrend"] == 1)
|
||||||
|
& (df["bullish_engulf"] == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# C) 上升趋势 + Inside Bar 向上突破
|
||||||
|
conditions_long.append(
|
||||||
|
(df["uptrend"] == 1)
|
||||||
|
& (df["inside_break_up"] == 1)
|
||||||
|
& (df["vol_expand"] == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# D) 突破 Swing High + 放量(不需要已确认趋势,突破本身建立趋势)
|
||||||
|
conditions_long.append(
|
||||||
|
(df["break_swing_high"] == 1)
|
||||||
|
& (df["vol_expand"] == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if conditions_long:
|
||||||
|
import pandas as pd
|
||||||
|
combined = pd.concat(conditions_long, axis=1).any(axis=1)
|
||||||
|
dataframe.loc[combined, "enter_long"] = 1
|
||||||
|
|
||||||
|
# ===== 做空 =====
|
||||||
|
conditions_short = []
|
||||||
|
|
||||||
|
# A) 下降趋势 + 看跌 Pin Bar
|
||||||
|
conditions_short.append(
|
||||||
|
(df["downtrend"] == 1)
|
||||||
|
& (df["bearish_pin"] == 1)
|
||||||
|
& (df["vol_expand"] == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# B) 下降趋势 + 看跌吞没
|
||||||
|
conditions_short.append(
|
||||||
|
(df["downtrend"] == 1)
|
||||||
|
& (df["bearish_engulf"] == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# C) 下降趋势 + Inside Bar 向下突破
|
||||||
|
conditions_short.append(
|
||||||
|
(df["downtrend"] == 1)
|
||||||
|
& (df["inside_break_down"] == 1)
|
||||||
|
& (df["vol_expand"] == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# D) 跌破 Swing Low + 放量
|
||||||
|
conditions_short.append(
|
||||||
|
(df["break_swing_low"] == 1)
|
||||||
|
& (df["vol_expand"] == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if conditions_short:
|
||||||
|
import pandas as pd
|
||||||
|
combined = pd.concat(conditions_short, axis=1).any(axis=1)
|
||||||
|
dataframe.loc[combined, "enter_short"] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
出场条件 — 纯价格行为
|
||||||
|
|
||||||
|
做多出场:
|
||||||
|
- 出现看跌吞没
|
||||||
|
- 出现看跌 Pin Bar
|
||||||
|
- 市场结构转为下降趋势
|
||||||
|
- 跌破前一个 Swing Low
|
||||||
|
|
||||||
|
做空出场(镜像)
|
||||||
|
"""
|
||||||
|
df = dataframe
|
||||||
|
|
||||||
|
# 做多出场
|
||||||
|
dataframe.loc[
|
||||||
|
(df["bearish_engulf"] == 1)
|
||||||
|
| (df["bearish_pin"] == 1)
|
||||||
|
| (df["downtrend"] == 1)
|
||||||
|
| (df["break_swing_low"] == 1),
|
||||||
|
"exit_long",
|
||||||
|
] = 1
|
||||||
|
|
||||||
|
# 做空出场
|
||||||
|
dataframe.loc[
|
||||||
|
(df["bullish_engulf"] == 1)
|
||||||
|
| (df["bullish_pin"] == 1)
|
||||||
|
| (df["uptrend"] == 1)
|
||||||
|
| (df["break_swing_high"] == 1),
|
||||||
|
"exit_short",
|
||||||
|
] = 1
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def custom_exit(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
trade,
|
||||||
|
current_time,
|
||||||
|
current_rate: float,
|
||||||
|
current_profit: float,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
自定义出场 — 基于价格行为的动态止盈
|
||||||
|
|
||||||
|
1. 利润 > 2% 且出现反转K线 → 锁利
|
||||||
|
2. 持仓超过 2 小时且利润 < 0.3% → 超时退出(行情没走出来)
|
||||||
|
"""
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
if dataframe.empty or len(dataframe) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
last = dataframe.iloc[-1]
|
||||||
|
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 60
|
||||||
|
|
||||||
|
# 1. 有利润 + 反转K线 → 锁利
|
||||||
|
if not trade.is_short:
|
||||||
|
if current_profit > 0.02:
|
||||||
|
if last.get("bearish_pin", 0) == 1 or last.get("bearish_engulf", 0) == 1:
|
||||||
|
return "reversal_signal_tp"
|
||||||
|
if current_profit > 0.035:
|
||||||
|
# 大利润时,任何阴线都考虑锁利
|
||||||
|
if last.get("is_bear", 0) == 1 and last.get("body_ratio", 0) > 0.6:
|
||||||
|
return "strong_bear_candle_tp"
|
||||||
|
else:
|
||||||
|
if current_profit > 0.02:
|
||||||
|
if last.get("bullish_pin", 0) == 1 or last.get("bullish_engulf", 0) == 1:
|
||||||
|
return "reversal_signal_tp"
|
||||||
|
if current_profit > 0.035:
|
||||||
|
if last.get("is_bull", 0) == 1 and last.get("body_ratio", 0) > 0.6:
|
||||||
|
return "strong_bull_candle_tp"
|
||||||
|
|
||||||
|
# 2. 超时退出 — 行情没走出来
|
||||||
|
if trade_duration > 120 and current_profit < 0.003:
|
||||||
|
return "timeout_exit"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def leverage(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
current_time,
|
||||||
|
current_rate: float,
|
||||||
|
proposed_leverage: float,
|
||||||
|
max_leverage: float,
|
||||||
|
entry_tag: str | None,
|
||||||
|
side: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> float:
|
||||||
|
return 3.0
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""
|
||||||
|
SOL15mStrategy - 基于15m时间框架的SOL/USDT期货策略
|
||||||
|
|
||||||
|
核心逻辑:
|
||||||
|
- 15m EMA26/EMA52 交叉做多做空
|
||||||
|
- RSI过滤:做多要求RSI<65,做空要求RSI>35(避免超买超卖区入场)
|
||||||
|
- 使用 trailing_stop 让利润奔跑
|
||||||
|
- 宽止损(2%),给交易足够呼吸空间
|
||||||
|
|
||||||
|
使用命令:
|
||||||
|
freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy SOL15mStrategy --strategy-path ./user_data/Chan/strategies --timerange=20250301-
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class SOL15mStrategy(IStrategy):
|
||||||
|
INTERFACE_VERSION: int = 3
|
||||||
|
|
||||||
|
# === 基础配置 ===
|
||||||
|
timeframe = "15m"
|
||||||
|
can_short = True
|
||||||
|
startup_candle_count: int = 200
|
||||||
|
|
||||||
|
# 止损 2%
|
||||||
|
stoploss = -0.02
|
||||||
|
use_custom_stoploss = False
|
||||||
|
|
||||||
|
# trailing stop: 利润达到1.5%后开始追踪,回撤0.5%止盈
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.005
|
||||||
|
trailing_stop_positive_offset = 0.015
|
||||||
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
|
# ROI: 阶梯式止盈
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.04, # 4%直接止盈
|
||||||
|
"60": 0.025, # 60分钟后 2.5%
|
||||||
|
"180": 0.015, # 3小时后 1.5%
|
||||||
|
"480": 0.005, # 8小时后 0.5%
|
||||||
|
"720": 0, # 12小时后保本退出
|
||||||
|
}
|
||||||
|
|
||||||
|
order_types = {
|
||||||
|
"entry": "market",
|
||||||
|
"exit": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# EMA
|
||||||
|
dataframe["ema26"] = ta.EMA(dataframe, timeperiod=26)
|
||||||
|
dataframe["ema52"] = ta.EMA(dataframe, timeperiod=52)
|
||||||
|
dataframe["ema100"] = ta.EMA(dataframe, timeperiod=100)
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||||
|
dataframe["macd"] = macd["macd"]
|
||||||
|
dataframe["macdsignal"] = macd["macdsignal"]
|
||||||
|
|
||||||
|
# EMA26上穿EMA52
|
||||||
|
dataframe["ema26_cross_up_52"] = (
|
||||||
|
(dataframe["ema26"] > dataframe["ema52"])
|
||||||
|
& (dataframe["ema26"].shift(1) <= dataframe["ema52"].shift(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
# EMA26下穿EMA52
|
||||||
|
dataframe["ema26_cross_dn_52"] = (
|
||||||
|
(dataframe["ema26"] < dataframe["ema52"])
|
||||||
|
& (dataframe["ema26"].shift(1) >= dataframe["ema52"].shift(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
# MACD死叉
|
||||||
|
dataframe["macd_cross_dn"] = (
|
||||||
|
(dataframe["macd"] < dataframe["macdsignal"])
|
||||||
|
& (dataframe["macd"].shift(1) >= dataframe["macdsignal"].shift(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# 做多: EMA26上穿EMA52 + RSI < 65 (不在超买区)
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe["ema26_cross_up_52"])
|
||||||
|
& (dataframe["rsi"] < 65),
|
||||||
|
["enter_long", "enter_tag"],
|
||||||
|
] = (1, "ema26x52_long")
|
||||||
|
|
||||||
|
# 做空: EMA26下穿EMA52 + RSI > 35 (不在超卖区)
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe["ema26_cross_dn_52"])
|
||||||
|
& (dataframe["rsi"] > 35),
|
||||||
|
["enter_short", "enter_tag"],
|
||||||
|
] = (1, "ema26x52_short")
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# 不使用信号退出,完全由 trailing_stop + ROI + stoploss 控制
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def leverage(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
current_time: datetime,
|
||||||
|
current_rate: float,
|
||||||
|
proposed_leverage: float,
|
||||||
|
max_leverage: float,
|
||||||
|
entry_tag: Optional[str],
|
||||||
|
side: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> float:
|
||||||
|
return 1.0
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
SOL5mStrategy - 基于 EMA26_EMA52_Cross 的改进版
|
||||||
|
|
||||||
|
核心改进(相比原版):
|
||||||
|
★ 去掉了反向交叉退出信号(原版中这是最大亏损来源,206笔亏-3021 USDT)
|
||||||
|
★ 加入 trailing stop 保护利润
|
||||||
|
★ 只靠 ROI + trailing stop + 硬止损 管理退出
|
||||||
|
|
||||||
|
逻辑:
|
||||||
|
- 底层使用 1m K线(由 config 中 timeframe: "1m" 控制)
|
||||||
|
- resample 到 30m 计算 EMA26/EMA52 交叉
|
||||||
|
- 交叉后延迟 30 根 1m K线入场(等待确认)
|
||||||
|
- ROI 从 15% 逐步递减
|
||||||
|
- Trailing stop:盈利 6% 后激活,回撤 3% 退出
|
||||||
|
- 硬止损 -15%(安全网)
|
||||||
|
|
||||||
|
使用命令:
|
||||||
|
freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json \
|
||||||
|
--strategy SOL5mStrategy --strategy-path ./user_data/Chan/strategies \
|
||||||
|
--timerange=20250301-
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
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 SOL5mStrategy(IStrategy):
|
||||||
|
INTERFACE_VERSION: int = 3
|
||||||
|
|
||||||
|
# === 基础配置 ===
|
||||||
|
# 注意:实际 timeframe 由 config 文件中的 "timeframe": "1m" 控制
|
||||||
|
# 这里不设置 timeframe,让 config 覆盖
|
||||||
|
can_short = True
|
||||||
|
startup_candle_count: int = 1600
|
||||||
|
|
||||||
|
# 硬止损 -3%(超短线合理止损,配合更严格的入场过滤)
|
||||||
|
stoploss = -0.03
|
||||||
|
use_custom_stoploss = False
|
||||||
|
|
||||||
|
# Trailing stop:盈利 3% 后激活,回撤 1.5% 退出
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.015 # 回撤 1.5% 触发退出
|
||||||
|
trailing_stop_positive_offset = 0.03 # 盈利 3% 后才开始追踪
|
||||||
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
|
# ROI:从 6% 逐步递减(给盈利交易更多空间)
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.06, # 6% 立即止盈
|
||||||
|
"60": 0.04, # 60分钟后 4%
|
||||||
|
"120": 0.03, # 120分钟后 3%
|
||||||
|
"240": 0.02, # 240分钟后 2%
|
||||||
|
"480": 0.01, # 480分钟后 1%
|
||||||
|
"720": 0, # 720分钟后不设止盈
|
||||||
|
}
|
||||||
|
|
||||||
|
order_types = {
|
||||||
|
"entry": "market",
|
||||||
|
"exit": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# resample 时间倍数
|
||||||
|
time15 = 15
|
||||||
|
time30 = 30
|
||||||
|
time60 = 60
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""在 15m / 30m / 60m 级别计算 EMA26/52 交叉信号"""
|
||||||
|
ticker = self.get_ticker_indicator()
|
||||||
|
|
||||||
|
# resample 到更大时间框架
|
||||||
|
dataframe_15m = resample_to_interval(dataframe, ticker * self.time15)
|
||||||
|
dataframe_30m = resample_to_interval(dataframe, ticker * self.time30)
|
||||||
|
dataframe_60m = resample_to_interval(dataframe, ticker * self.time60)
|
||||||
|
|
||||||
|
# 在每个时间框架上计算指标
|
||||||
|
dataframe_15m = self.add_indicators(dataframe_15m)
|
||||||
|
dataframe_30m = self.add_indicators(dataframe_30m)
|
||||||
|
dataframe_60m = self.add_indicators(dataframe_60m)
|
||||||
|
dataframe = self.add_indicators(dataframe)
|
||||||
|
|
||||||
|
# 合并回 1m dataframe
|
||||||
|
dataframe = resampled_merge(dataframe, dataframe_15m)
|
||||||
|
dataframe = resampled_merge(dataframe, dataframe_30m)
|
||||||
|
dataframe = resampled_merge(dataframe, dataframe_60m)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def add_indicators(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""计算 EMA26/52 及其交叉信号,以及RSI和成交量过滤"""
|
||||||
|
dataframe["ema26"] = ta.EMA(dataframe, timeperiod=26)
|
||||||
|
dataframe["ema52"] = ta.EMA(dataframe, timeperiod=52)
|
||||||
|
|
||||||
|
# RSI用于确认趋势强度
|
||||||
|
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
|
||||||
|
# 成交量均线用于确认成交量
|
||||||
|
dataframe["volume_mean"] = dataframe["volume"].rolling(window=20).mean()
|
||||||
|
|
||||||
|
# 上穿:本根 EMA26 > EMA52,上一根 EMA26 ≤ EMA52
|
||||||
|
dataframe["ema26_cross_up_52"] = (
|
||||||
|
(dataframe["ema26"] > dataframe["ema52"])
|
||||||
|
& (dataframe["ema26"].shift(1) <= dataframe["ema52"].shift(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
# 下穿:本根 EMA26 < EMA52,上一根 EMA26 ≥ EMA52
|
||||||
|
dataframe["ema26_cross_down_52"] = (
|
||||||
|
(dataframe["ema26"] < dataframe["ema52"])
|
||||||
|
& (dataframe["ema26"].shift(1) >= dataframe["ema52"].shift(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""30m EMA26/52 交叉入场,延迟 15 根 1m K线,加入RSI和成交量确认"""
|
||||||
|
ticker = self.get_ticker_indicator()
|
||||||
|
time = self.time30
|
||||||
|
delay = 15 # 减少延迟从30到15分钟
|
||||||
|
cross_up = f"resample_{ticker * time}_ema26_cross_up_52"
|
||||||
|
cross_down = f"resample_{ticker * time}_ema26_cross_down_52"
|
||||||
|
|
||||||
|
# 获取当前时间框架的RSI和成交量
|
||||||
|
rsi_col = "rsi"
|
||||||
|
volume_col = "volume"
|
||||||
|
volume_mean_col = "volume_mean"
|
||||||
|
|
||||||
|
# 做多:30m EMA26 上穿 EMA52 + RSI > 50(确认上涨趋势)+ 成交量确认
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe[cross_up].shift(delay) == True) &
|
||||||
|
(dataframe[rsi_col] > 50) & # RSI确认上涨趋势
|
||||||
|
(dataframe[volume_col] > dataframe[volume_mean_col]), # 成交量确认
|
||||||
|
["enter_long", "enter_tag"],
|
||||||
|
] = (1, "ema26x52_long")
|
||||||
|
|
||||||
|
# 做空:30m EMA26 下穿 EMA52 + RSI < 50(确认下跌趋势)+ 成交量确认
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe[cross_down].shift(delay) == True) &
|
||||||
|
(dataframe[rsi_col] < 50) & # RSI确认下跌趋势
|
||||||
|
(dataframe[volume_col] > dataframe[volume_mean_col]), # 成交量确认
|
||||||
|
["enter_short", "enter_tag"],
|
||||||
|
] = (1, "ema26x52_short")
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""不使用信号退出,完全依赖 ROI / trailing stop / 硬止损"""
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def leverage(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
current_time: datetime,
|
||||||
|
current_rate: float,
|
||||||
|
proposed_leverage: float,
|
||||||
|
max_leverage: float,
|
||||||
|
entry_tag: Optional[str],
|
||||||
|
side: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> float:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
def get_ticker_indicator(self) -> int:
|
||||||
|
"""获取 timeframe 的分钟数"""
|
||||||
|
return int(self.timeframe[:-1])
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""
|
||||||
|
SOL5mStrategyV6 - 趋势跟随策略 V6(基于V5改进)
|
||||||
|
|
||||||
|
核心改进:
|
||||||
|
- 去掉trend_reversal退出(V5中54笔全亏 -611 USDT)
|
||||||
|
- 完全依赖 ROI + trailing_stop + stoploss 管理退出
|
||||||
|
- 更激进的trailing:1.5%盈利后激活,0.6%回撤
|
||||||
|
- 保持V5的入场逻辑(EMA排列 + 回调入场 + RSI + MACD + ADX)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class SOL5mStrategyV6(IStrategy):
|
||||||
|
INTERFACE_VERSION: int = 3
|
||||||
|
|
||||||
|
timeframe = "15m"
|
||||||
|
can_short = True
|
||||||
|
startup_candle_count: int = 200
|
||||||
|
|
||||||
|
# 止损
|
||||||
|
stoploss = -0.025
|
||||||
|
use_custom_stoploss = False
|
||||||
|
|
||||||
|
# 更激进的trailing stop
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.006 # 0.6% 回撤止盈
|
||||||
|
trailing_stop_positive_offset = 0.015 # 1.5% 盈利后激活
|
||||||
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
|
# ROI
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.04, # 4%直接止盈
|
||||||
|
"60": 0.03, # 1小时后 3%
|
||||||
|
"180": 0.02, # 3小时后 2%
|
||||||
|
"360": 0.01, # 6小时后 1%
|
||||||
|
"720": 0.005, # 12小时后 0.5%
|
||||||
|
"1440": 0, # 24小时后保本
|
||||||
|
}
|
||||||
|
|
||||||
|
order_types = {
|
||||||
|
"entry": "market",
|
||||||
|
"exit": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# EMA趋势
|
||||||
|
dataframe["ema20"] = ta.EMA(dataframe, timeperiod=20)
|
||||||
|
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
|
||||||
|
dataframe["ema100"] = ta.EMA(dataframe, timeperiod=100)
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||||
|
dataframe["macd"] = macd["macd"]
|
||||||
|
dataframe["macdsignal"] = macd["macdsignal"]
|
||||||
|
dataframe["macdhist"] = macd["macdhist"]
|
||||||
|
|
||||||
|
# ADX (趋势强度)
|
||||||
|
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# 做多条件(与V5相同)
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe["ema20"] > dataframe["ema50"])
|
||||||
|
& (dataframe["ema50"] > dataframe["ema100"])
|
||||||
|
& (dataframe["close"] <= dataframe["ema20"] * 1.005)
|
||||||
|
& (dataframe["close"] >= dataframe["ema50"])
|
||||||
|
& (dataframe["rsi"] > 40)
|
||||||
|
& (dataframe["rsi"] < 65)
|
||||||
|
& (dataframe["macdhist"] > 0)
|
||||||
|
& (dataframe["adx"] > 20),
|
||||||
|
["enter_long", "enter_tag"],
|
||||||
|
] = (1, "trend_pullback_long")
|
||||||
|
|
||||||
|
# 做空条件(与V5相同)
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe["ema20"] < dataframe["ema50"])
|
||||||
|
& (dataframe["ema50"] < dataframe["ema100"])
|
||||||
|
& (dataframe["close"] >= dataframe["ema20"] * 0.995)
|
||||||
|
& (dataframe["close"] <= dataframe["ema50"])
|
||||||
|
& (dataframe["rsi"] < 60)
|
||||||
|
& (dataframe["rsi"] > 35)
|
||||||
|
& (dataframe["macdhist"] < 0)
|
||||||
|
& (dataframe["adx"] > 20),
|
||||||
|
["enter_short", "enter_tag"],
|
||||||
|
] = (1, "trend_pullback_short")
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# 不使用信号退出,完全依赖 ROI + trailing + stoploss
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def leverage(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
current_time: datetime,
|
||||||
|
current_rate: float,
|
||||||
|
proposed_leverage: float,
|
||||||
|
max_leverage: float,
|
||||||
|
entry_tag: Optional[str],
|
||||||
|
side: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> float:
|
||||||
|
return 1.0
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"""
|
||||||
|
SOL5mStrategyV7 - 趋势跟随仅做空策略
|
||||||
|
|
||||||
|
基于V5分析:
|
||||||
|
- 做空 +7.42%(盈利)
|
||||||
|
- 做多 -13.63%(亏损)
|
||||||
|
- 市场整体下跌 -33.15%,做空顺势
|
||||||
|
|
||||||
|
改进:
|
||||||
|
- 只做空,不做多
|
||||||
|
- 去掉trend_reversal退出
|
||||||
|
- 更宽松的做空入场条件(ADX > 15,降低门槛)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class SOL5mStrategyV7(IStrategy):
|
||||||
|
INTERFACE_VERSION: int = 3
|
||||||
|
|
||||||
|
timeframe = "15m"
|
||||||
|
can_short = True
|
||||||
|
startup_candle_count: int = 200
|
||||||
|
|
||||||
|
stoploss = -0.025
|
||||||
|
use_custom_stoploss = False
|
||||||
|
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.006
|
||||||
|
trailing_stop_positive_offset = 0.015
|
||||||
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.05,
|
||||||
|
"60": 0.035,
|
||||||
|
"180": 0.02,
|
||||||
|
"360": 0.01,
|
||||||
|
"720": 0.005,
|
||||||
|
"1440": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
order_types = {
|
||||||
|
"entry": "market",
|
||||||
|
"exit": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe["ema20"] = ta.EMA(dataframe, timeperiod=20)
|
||||||
|
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
|
||||||
|
dataframe["ema100"] = ta.EMA(dataframe, timeperiod=100)
|
||||||
|
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
|
||||||
|
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||||
|
dataframe["macd"] = macd["macd"]
|
||||||
|
dataframe["macdsignal"] = macd["macdsignal"]
|
||||||
|
dataframe["macdhist"] = macd["macdhist"]
|
||||||
|
|
||||||
|
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# 只做空 - 下降趋势回调入场
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe["ema20"] < dataframe["ema50"])
|
||||||
|
& (dataframe["ema50"] < dataframe["ema100"])
|
||||||
|
& (dataframe["close"] >= dataframe["ema20"] * 0.995)
|
||||||
|
& (dataframe["close"] <= dataframe["ema50"])
|
||||||
|
& (dataframe["rsi"] < 60)
|
||||||
|
& (dataframe["rsi"] > 35)
|
||||||
|
& (dataframe["macdhist"] < 0)
|
||||||
|
& (dataframe["adx"] > 15), # 更宽松的ADX门槛
|
||||||
|
["enter_short", "enter_tag"],
|
||||||
|
] = (1, "trend_pullback_short")
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# 不使用信号退出
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def leverage(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
current_time: datetime,
|
||||||
|
current_rate: float,
|
||||||
|
proposed_leverage: float,
|
||||||
|
max_leverage: float,
|
||||||
|
entry_tag: Optional[str],
|
||||||
|
side: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> float:
|
||||||
|
return 1.0
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""
|
||||||
|
SOL5mStrategy_ShortTerm - 真正的短线交易策略
|
||||||
|
|
||||||
|
核心特征:
|
||||||
|
★ 使用5分钟快速EMA交叉(EMA9/EMA21)作为信号源
|
||||||
|
★ 无延迟入场,信号出现立即入场
|
||||||
|
★ 小止损(-1%),快速止盈(+0.8%)
|
||||||
|
★ 平均持仓时间:15-60分钟
|
||||||
|
★ 交易频率:每天5-20笔
|
||||||
|
|
||||||
|
逻辑:
|
||||||
|
- 5分钟K线,EMA9/EMA21交叉入场
|
||||||
|
- RSI过滤(避免极端超买超卖)
|
||||||
|
- 成交量确认
|
||||||
|
- 快速止盈止损,不持仓过夜
|
||||||
|
|
||||||
|
使用命令:
|
||||||
|
freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json \
|
||||||
|
--strategy SOL5mStrategy_ShortTerm --strategy-path ./user_data/Chan/strategies \
|
||||||
|
--timerange=20250301-
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SOL5mStrategy_ShortTerm(IStrategy):
|
||||||
|
INTERFACE_VERSION: int = 3
|
||||||
|
|
||||||
|
# === 基础配置 ===
|
||||||
|
timeframe = "5m" # 使用5分钟K线
|
||||||
|
can_short = True
|
||||||
|
startup_candle_count: int = 100
|
||||||
|
|
||||||
|
# 小止损(-1%),适合短线
|
||||||
|
stoploss = -0.01
|
||||||
|
use_custom_stoploss = False
|
||||||
|
|
||||||
|
# Trailing stop:盈利0.5%后激活,回撤0.3%退出
|
||||||
|
trailing_stop = True
|
||||||
|
trailing_stop_positive = 0.003 # 回撤0.3%触发退出
|
||||||
|
trailing_stop_positive_offset = 0.005 # 盈利0.5%后才开始追踪
|
||||||
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
|
# ROI:快速止盈,从0.8%逐步递减
|
||||||
|
minimal_roi = {
|
||||||
|
"0": 0.008, # 0.8% 立即止盈
|
||||||
|
"15": 0.005, # 15分钟后 0.5%
|
||||||
|
"30": 0.003, # 30分钟后 0.3%
|
||||||
|
"60": 0.001, # 60分钟后 0.1%
|
||||||
|
"120": 0, # 120分钟后不设止盈(但trailing会保护)
|
||||||
|
}
|
||||||
|
|
||||||
|
order_types = {
|
||||||
|
"entry": "market",
|
||||||
|
"exit": "market",
|
||||||
|
"stoploss": "market",
|
||||||
|
"stoploss_on_exchange": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""计算快速EMA交叉信号"""
|
||||||
|
# 快速EMA(9)和慢速EMA(21)
|
||||||
|
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=9)
|
||||||
|
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=21)
|
||||||
|
|
||||||
|
# RSI用于过滤极端情况
|
||||||
|
dataframe["rsi"] = ta.EMA(dataframe, timeperiod=14)
|
||||||
|
|
||||||
|
# 成交量均线用于确认
|
||||||
|
dataframe["volume_mean"] = dataframe["volume"].rolling(window=20).mean()
|
||||||
|
|
||||||
|
# 上穿:本根 EMA9 > EMA21,上一根 EMA9 ≤ EMA21
|
||||||
|
dataframe["ema_cross_up"] = (
|
||||||
|
(dataframe["ema_fast"] > dataframe["ema_slow"])
|
||||||
|
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
# 下穿:本根 EMA9 < EMA21,上一根 EMA9 ≥ EMA21
|
||||||
|
dataframe["ema_cross_down"] = (
|
||||||
|
(dataframe["ema_fast"] < dataframe["ema_slow"])
|
||||||
|
& (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
|
||||||
|
)
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""5分钟EMA交叉立即入场,无延迟"""
|
||||||
|
# 做多:EMA9上穿EMA21 + RSI > 45(避免极端超卖)+ 成交量确认
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe["ema_cross_up"] == True) &
|
||||||
|
(dataframe["rsi"] > 45) & # RSI过滤,避免极端超卖
|
||||||
|
(dataframe["volume"] > dataframe["volume_mean"] * 0.8), # 成交量确认(稍微宽松)
|
||||||
|
["enter_long", "enter_tag"],
|
||||||
|
] = (1, "ema9x21_long")
|
||||||
|
|
||||||
|
# 做空:EMA9下穿EMA21 + RSI < 55(避免极端超买)+ 成交量确认
|
||||||
|
dataframe.loc[
|
||||||
|
(dataframe["ema_cross_down"] == True) &
|
||||||
|
(dataframe["rsi"] < 55) & # RSI过滤,避免极端超买
|
||||||
|
(dataframe["volume"] > dataframe["volume_mean"] * 0.8), # 成交量确认
|
||||||
|
["enter_short", "enter_tag"],
|
||||||
|
] = (1, "ema9x21_short")
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""不使用信号退出,完全依赖 ROI / trailing stop / 硬止损"""
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def leverage(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
current_time: datetime,
|
||||||
|
current_rate: float,
|
||||||
|
proposed_leverage: float,
|
||||||
|
max_leverage: float,
|
||||||
|
entry_tag: Optional[str],
|
||||||
|
side: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> float:
|
||||||
|
return 1.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("31", "").replace("41", "").replace("51", "");
|
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("01", "");
|
||||||
// 添加标记配置
|
// 添加标记配置
|
||||||
const markerConfig = {
|
const markerConfig = {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
@@ -5541,7 +5541,7 @@
|
|||||||
if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示
|
if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示
|
||||||
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
|
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
|
||||||
}
|
}
|
||||||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "");
|
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "");
|
||||||
// 添加标记配置
|
// 添加标记配置
|
||||||
const markerConfig = {
|
const markerConfig = {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
@@ -5625,7 +5625,7 @@
|
|||||||
if (fx.fx_strength < 1.0){ // 调整小周期阈值
|
if (fx.fx_strength < 1.0){ // 调整小周期阈值
|
||||||
displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点
|
displayText = fx.fx_strength >= 0.6 ? '' : '' // 0.6以上显示点
|
||||||
}
|
}
|
||||||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "");
|
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", "").replace("41", "").replace("51", "").replace("0", "");
|
||||||
// 小周期分型标记配置
|
// 小周期分型标记配置
|
||||||
const markerConfig = {
|
const markerConfig = {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
@@ -5681,7 +5681,7 @@
|
|||||||
if (fx.fx_strength < 2.0){ // 调整小周期阈值
|
if (fx.fx_strength < 2.0){ // 调整小周期阈值
|
||||||
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.6以上显示点
|
displayText = fx.fx_strength >= 1.5 ? '' : '' // 0.6以上显示点
|
||||||
}
|
}
|
||||||
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("31", "").replace("41", "").replace("51", "");
|
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "");
|
||||||
// 小周期KLU分型标记配置
|
// 小周期KLU分型标记配置
|
||||||
const markerConfig = {
|
const markerConfig = {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
|
|||||||
Reference in New Issue
Block a user