添加新的策略

This commit is contained in:
jackyu66git
2026-03-06 22:08:24 +08:00
parent d8f2269a49
commit d2107db6c0
19 changed files with 2712 additions and 19 deletions
Vendored
BIN
View File
Binary file not shown.
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+25 -11
View File
@@ -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:
# 不满足结合律的分型 # 不满足结合律的分型
+123
View File
@@ -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
}
}
+4 -4
View File
@@ -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
}, },
+81
View File
@@ -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
}
}
+664
View File
@@ -0,0 +1,664 @@
"""
缠论同级别分解策略 (Chan Same-Level Decomposition Strategy)
核心思想按同级别分解操作实现a+A结构的机械化操作
以5分钟级别为例
1. a+A结构a是5分钟走势类型定义为A0A分解为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的高点则A1A2A3必然构成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即aA1A2A3等走势段
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的高点则A1A2A3必然构成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])
+255
View File
@@ -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)
# RSI5分钟)
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
# ATR5分钟)
informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14)
informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100
# ATR 长期均值(用于自适应波动率过滤)
informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean()
# ===== EMA200 大趋势过滤 =====
informative['ema200'] = ta.EMA(informative['close'], timeperiod=200)
informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100
# EMA200斜率(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
+285
View File
@@ -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
+197
View File
@@ -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])
+395
View File
@@ -0,0 +1,395 @@
"""
PriceActionStrategy - 纯价格行为策略
核心原则
零指标不用 EMARSIMACDATR 或任何计算指标
只看 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
+124
View File
@@ -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
+174
View File
@@ -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])
+119
View File
@@ -0,0 +1,119 @@
"""
SOL5mStrategyV6 - 趋势跟随策略 V6基于V5改进
核心改进
- 去掉trend_reversal退出V5中54笔全亏 -611 USDT
- 完全依赖 ROI + trailing_stop + stoploss 管理退出
- 更激进的trailing1.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
+102
View File
@@ -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
+130
View File
@@ -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交叉信号"""
# 快速EMA9)和慢速EMA21
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
+4 -4
View File
@@ -5477,7 +5477,7 @@
if (fx.fx_strength < 1.0) { // 降低阈值让更多分型显示 if (fx.fx_strength < 1.0) { // 降低阈值让更多分型显示
displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本
} }
displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("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,