From d2107db6c02921b8ccbdec2d0f32d3584b24212a Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Fri, 6 Mar 2026 22:08:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=B0=E7=9A=84=E7=AD=96?= =?UTF-8?q?=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 10244 -> 14340 bytes ChanEnum.py | 2 + ChanKLC.py | 14 + ChanKLU.py | 14 + TF_DF.py | 36 +- config/FreqAI_Test.json | 123 +++++ config/Local_Test.json | 8 +- config/SOL5m.json | 81 +++ strategies/ChanSameLevelStrategy.py | 664 ++++++++++++++++++++++++ strategies/CryptoFutures1m5mStrategy.py | 255 +++++++++ strategies/CryptoFuturesAIStrategy.py | 285 ++++++++++ strategies/EMA_Cross.py | 197 +++++++ strategies/PriceActionStrategy.py | 395 ++++++++++++++ strategies/SOL15mStrategy.py | 124 +++++ strategies/SOL5mStrategy.py | 174 +++++++ strategies/SOL5mStrategyV6.py | 119 +++++ strategies/SOL5mStrategyV7.py | 102 ++++ strategies/SOL5mStrategy_ShortTerm.py | 130 +++++ web/templates/index.html | 8 +- 19 files changed, 2712 insertions(+), 19 deletions(-) create mode 100644 config/FreqAI_Test.json create mode 100644 config/SOL5m.json create mode 100644 strategies/ChanSameLevelStrategy.py create mode 100644 strategies/CryptoFutures1m5mStrategy.py create mode 100644 strategies/CryptoFuturesAIStrategy.py create mode 100644 strategies/EMA_Cross.py create mode 100644 strategies/PriceActionStrategy.py create mode 100644 strategies/SOL15mStrategy.py create mode 100644 strategies/SOL5mStrategy.py create mode 100644 strategies/SOL5mStrategyV6.py create mode 100644 strategies/SOL5mStrategyV7.py create mode 100644 strategies/SOL5mStrategy_ShortTerm.py diff --git a/.DS_Store b/.DS_Store index 90f040dd410d45c5a300ce99e16c5047f441248b..b539a4d8178eed9910acba05f71302c266c37129 100644 GIT binary patch delta 1859 zcmdUwTWl0n7{|~5v`c4TWDn57w(O(}y98*K(k<97H|>QgAW)#xZVOU&cSbufomqBv zTcFl8T4SPuY4!o*ohOY%5-f<(2Tepo9~B=k#s|E_TY?Y1s66=1&K41kK6{cg=X~G! z&iT&e_s>l1n8<_}W9Z0hF~*vxD^Yb@q0r}X#zM+uXsKF32B@}ErM_UyVg^eyn`N0b znG6@3B9%Tjpt9Hz=q&b#)hG3;sVb>bLZCPi_zLe7hiVTsEF2qaZfS`}L$Q`M$Kyz2ER@7cInbJ+6$zG!P8b$3J@t#23Q+95s zR@6k=9v-GcI%QvbhpO&#^KMN#x$cZ^(Vi}qlix*S0|jR&nbo^TEj?RyZy|T-S;_Ct z>e7syT^gkO*iPr@ch_szw9+Z+0=0g%N9~owClbAqVT~9$dL()pYVwX0o_?BeTZ7jP zzjuU?r&7A=f*fvhCE^x|sjQi8WLw#OlEN4}#-3rP*je@gyU0FcU$XDnHTElj8IUm( zvrvsXL=eU8Xhj@r(1C93#4hw;FNR>?02~~~QHhm54-A z5kAjcsw|gCBn2_Uv!lUAiG(S5$y8#&RWhkhRCs1I*eXi`p0QS0FOv>MrDv*2n=A`f zGJ>7KE{U`xr2k~xL%dMJ;olg)!mhIK*w5@bF+Lmf5WrG25!rWO6SfoEF4?`O%EH6;qkI6Xb5>wLUMp sX%@9zH~;v~B(GHz{gD`0alNvzxIa?mi@UMKOk}dC*3?f0YJTE>0J`+vI{*Lx delta 233 zcmZoEXbF&DU|?W$DortDU{C-uIe-{M3-C-V6q~3gIoZI3MHI+qum@sBpg2QLx?yl~ zer~~LLB?-vlNA^@Zf56T;b64i%%iZKg^_Xd5tTaTi4&YB^9np-+%@@wK%u5ob+xIP znT~?7fnlwVLbaihiG_}Wp@D@_Z7nBbj5e($^0gUtU$*xG)#6hQR379DFJc~ OCTEyT+{~!T%nSevGBmgV diff --git a/ChanEnum.py b/ChanEnum.py index 9ee4f06..e1c5cb0 100644 --- a/ChanEnum.py +++ b/ChanEnum.py @@ -173,6 +173,7 @@ class Chan_PRICE_TREND(Enum): FLAT = auto() UNKNOWN = auto() class Chan_KLC_FX(Enum): + TOP0 = auto() TOP1 = auto() TOP2 = auto() TOP3 = auto() @@ -181,6 +182,7 @@ class Chan_KLC_FX(Enum): TOP6 = auto() TOP7 = auto() TOP8 = auto() + BOTTOM0 = auto() BOTTOM1 = auto() BOTTOM2 = auto() BOTTOM3 = auto() diff --git a/ChanKLC.py b/ChanKLC.py index 4439f00..edb6fd8 100644 --- a/ChanKLC.py +++ b/ChanKLC.py @@ -64,6 +64,8 @@ class ChanKLC(): self.bb2633upper = klu.bb2633upper self.bb2633lower = klu.bb2633lower self.bb2633middle = klu.bb2633middle + self.ema5 = klu.ema5 + self.ma5 = klu.ma5 # ==================== EMA 通用计算方法 ==================== @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.cal_indicators() 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): 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)) @@ -385,6 +395,8 @@ class ChanKLC(): self.bb2633upper += self.klu_list[index].bb2633upper self.bb2633lower += self.klu_list[index].bb2633lower 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: self.ema_dir = 0 n = len(self.klu_list) @@ -397,6 +409,8 @@ class ChanKLC(): self.ema104 = self.ema104 / n self.ema156 = self.ema156 / n self.ema208 = self.ema208 / n + self.ma5 = self.ma5 / n + self.ema5 = self.ema5 / n self.bb2633upper = self.bb2633upper / n self.bb2633lower = self.bb2633lower / n self.bb2633middle = self.bb2633middle / n diff --git a/ChanKLU.py b/ChanKLU.py index 9ebbe0f..7c869c0 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -74,6 +74,8 @@ class ChanKLU: self.bb2633upper = 0 self.bb2633lower = 0 self.bb2633middle = 0 + self.ma5 = 0 + self.ema5 = 0 #print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength) def set_macd_state(self, 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.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.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): # 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN # 首条或缺前一根 @@ -225,6 +229,16 @@ class ChanKLU: self.near0_return = 0 elif self.close > self.ema52 and self.high > self.ema52 and self.low < self.ema52: self.near0_return = 0 + if self.close > self.ema52 and self.open < self.ema52: + if self.pre.near0_return == 0: + self.near0_return = 7 + elif self.pre.near0_return == 8: + self.pre.near0_return = 0 + elif self.close < self.ema52 and self.open > self.ema52: + if self.pre.near0_return == 0: + self.near0_return = 8 + elif self.pre.near0_return == 7: + self.pre.near0_return = 0 # CROSS0 仅以 Signal 穿越零轴判定 if self.pre.signal >= 0 and self.signal < 0: self.macd_state = Chan_MACD_STATE.CROSS0_DOWN diff --git a/TF_DF.py b/TF_DF.py index 64c0ad8..1ba436f 100644 --- a/TF_DF.py +++ b/TF_DF.py @@ -161,6 +161,7 @@ class TF_DF(): else: klu_state_list.append("00") return klu_state_list + def get_ema_state(self, dataframe): klu_list = self.get_klu_list(dataframe) klc_list = self.get_klc_list(klu_list) @@ -918,6 +919,7 @@ class TF_DF(): 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) #klc.klc_fx_type = Chan_KLC_FX.TOP7 + #klc.fx = Chan_FX_TYPE.TOP """ last_bi.set_end_klc(last_bottom, klc) bi = ChanBI(last_bottom, len(bi_list), Chan_BI_DIR.UP) @@ -934,10 +936,10 @@ class TF_DF(): """ else: if last_bottom and last_bi.dir == Chan_BI_DIR.UP: - if last_top and klc.low < last_bi.low: #print(klc.end_time, "Bottom 8, 2", last_bi.start_time) #klc.klc_fx_type = Chan_KLC_FX.BOTTOM8 + #klc.fx = Chan_FX_TYPE.BOTTOM """ last_bi.set_end_klc(last_top, klc) bi = ChanBI(last_top, len(bi_list), Chan_BI_DIR.DOWN) @@ -954,6 +956,7 @@ class TF_DF(): """ else: 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_bottom: #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: bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) + klc.set_klc_fx_type(Chan_KLC_FX.TOP3) #print(klc.end_time, klc.fx, "二类卖点Sell 1") else: # A new top found @@ -973,16 +977,18 @@ class TF_DF(): #print(klc.end_time, klc.fx, "一类卖点Sell 1") bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) + # 不满足结合律的分型 else: - # 不满足结合律的分型 + #klc.set_klc_fx_type(Chan_KLC_FX.TOP0) if last_bottom.index + bi_klc_min > klc.index: if last_top.high > klc.high: #print(klc.start_time, klc.fx, "二类卖点Sell 1") #klc.set_fx(Chan_FX_TYPE.PTOP) bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) - # New TOP Found没有意义,前面的UKNOWN已经出现TOP7 + # New TOP Found前面的UKNOWN可能出现TOP7,但是这里的也可能出现TOP8分型 else: + # 顶分型在出现2之前超过前一个笔的顶 TOP8 if last_top.index + bi_klc_min < klc.index and len(bi_list) > 1: pre_last_bi = bi_list[-2] 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 bi_list[-1].add_klc(klc) 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: - klc.set_fx(Chan_FX_TYPE.PTOP) + #klc.set_fx(Chan_FX_TYPE.PTOP) bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "无效分型") + print(klc.end_time, klc.fx, "无效顶分型") # 满足结合律 else: # 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) klc.set_bi(bi_list[-1]) #print(klc.start_time, last_bottom.start_time, "Normal TOP Found, Confirm down bi 4") - # last bottom = None + # last bottom = None 初始化的时候用,其他时间不用 else: + # 初始化的时候用,其他时间不用 if last_top.high < klc.high: last_bi = bi_list[-1] 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") bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) + # 初始化的时候用,其他时间不用 else: klc.set_fx(Chan_FX_TYPE.TT) #print(klc.start_time, klc.fx, "二类卖点Sell 2") bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) + # last_top == None 初始化的时候用,其他时间不用 else: if last_bottom: # 不满足结合律的分型 @@ -1052,7 +1063,7 @@ class TF_DF(): #print(klc.start_time, klc.fx, bi_list[-1].dir, "Last Top Change 4") bi_list[-1].add_klc(klc) 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: # First temp top last_top = klc @@ -1071,6 +1082,7 @@ class TF_DF(): if last_bottom.low < klc.low: bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) + klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM3) #print(last_bottom.start_time, last_bottom.end_time, "--------------------------------1") #print(klc.end_time, klc.fx, "二类买点Buy 1") else: @@ -1082,8 +1094,9 @@ class TF_DF(): #print(klc.end_time, klc.fx, "一类买点Buy 1") bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) + # 不满足结合律的分型 else: - # 不满足结合律的分型 + #klc.set_klc_fx_type(Chan_KLC_FX.TOP0) if last_top.index + bi_klc_min > klc.index: if last_bottom.low < klc.low: #print(klc.end_time, klc.fx, "中枢买点Buy 1") @@ -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 bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) + #klc.set_klc_fx_type(Chan_KLC_FX.BOTTOM8) else: #klc.set_fx(Chan_FX_TYPE.UNKNOWN) bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) - #print(klc.start_time, klc.fx, "无效分型") + print(klc.end_time, klc.fx, "无效底分型") # 满足结合律的分型 else: # 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) klc.set_bi(bi_list[-1]) #print(klc.start_time, last_top.start_time, "Normal Bottom Found, Confirm up bi 6") - # last_top = None + # last_top = None 初始化的时候用,其他时间不用 else: if last_bottom.low > klc.low: last_bi = bi_list[-1] @@ -1149,7 +1163,7 @@ class TF_DF(): #print(klc.start_time, klc.fx, "二类买点Buy 2") bi_list[-1].add_klc(klc) klc.set_bi(bi_list[-1]) - # last_bottom = None + # last_bottom = None 初始化的时候用,其他时间不用 else: if last_top: # 不满足结合律的分型 diff --git a/config/FreqAI_Test.json b/config/FreqAI_Test.json new file mode 100644 index 0000000..755d60c --- /dev/null +++ b/config/FreqAI_Test.json @@ -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 + } +} diff --git a/config/Local_Test.json b/config/Local_Test.json index 50a2f78..8d1b9f5 100644 --- a/config/Local_Test.json +++ b/config/Local_Test.json @@ -11,9 +11,9 @@ "cancel_open_orders_on_exit": true, "trading_mode": "futures", "margin_mode": "isolated", + "timeframe": "1m", "can_short" : true, - "timeframe" : "1m", - "process_only_new_candles" : false, + "process_only_new_candles" : true, "unfilledtimeout": { "entry": 1, "exit": 1, @@ -21,7 +21,7 @@ "unit": "minutes" }, "entry_pricing": { - "price_side": "same", + "price_side": "other", "use_order_book": true, "order_book_top": 1, "price_last_balance": 0.0, @@ -31,7 +31,7 @@ } }, "exit_pricing":{ - "price_side": "same", + "price_side": "other", "use_order_book": true, "order_book_top": 1 }, diff --git a/config/SOL5m.json b/config/SOL5m.json new file mode 100644 index 0000000..789c9c0 --- /dev/null +++ b/config/SOL5m.json @@ -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 + } +} diff --git a/strategies/ChanSameLevelStrategy.py b/strategies/ChanSameLevelStrategy.py new file mode 100644 index 0000000..453ddea --- /dev/null +++ b/strategies/ChanSameLevelStrategy.py @@ -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]) diff --git a/strategies/CryptoFutures1m5mStrategy.py b/strategies/CryptoFutures1m5mStrategy.py new file mode 100644 index 0000000..ab811d7 --- /dev/null +++ b/strategies/CryptoFutures1m5mStrategy.py @@ -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 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 diff --git a/strategies/CryptoFuturesAIStrategy.py b/strategies/CryptoFuturesAIStrategy.py new file mode 100644 index 0000000..98c0fcf --- /dev/null +++ b/strategies/CryptoFuturesAIStrategy.py @@ -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 diff --git a/strategies/EMA_Cross.py b/strategies/EMA_Cross.py new file mode 100644 index 0000000..a875058 --- /dev/null +++ b/strategies/EMA_Cross.py @@ -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]) \ No newline at end of file diff --git a/strategies/PriceActionStrategy.py b/strategies/PriceActionStrategy.py new file mode 100644 index 0000000..2771121 --- /dev/null +++ b/strategies/PriceActionStrategy.py @@ -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 diff --git a/strategies/SOL15mStrategy.py b/strategies/SOL15mStrategy.py new file mode 100644 index 0000000..8033d63 --- /dev/null +++ b/strategies/SOL15mStrategy.py @@ -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 diff --git a/strategies/SOL5mStrategy.py b/strategies/SOL5mStrategy.py new file mode 100644 index 0000000..6df70b8 --- /dev/null +++ b/strategies/SOL5mStrategy.py @@ -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]) diff --git a/strategies/SOL5mStrategyV6.py b/strategies/SOL5mStrategyV6.py new file mode 100644 index 0000000..71ed043 --- /dev/null +++ b/strategies/SOL5mStrategyV6.py @@ -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 diff --git a/strategies/SOL5mStrategyV7.py b/strategies/SOL5mStrategyV7.py new file mode 100644 index 0000000..0d6281c --- /dev/null +++ b/strategies/SOL5mStrategyV7.py @@ -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 diff --git a/strategies/SOL5mStrategy_ShortTerm.py b/strategies/SOL5mStrategy_ShortTerm.py new file mode 100644 index 0000000..5d32741 --- /dev/null +++ b/strategies/SOL5mStrategy_ShortTerm.py @@ -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 diff --git a/web/templates/index.html b/web/templates/index.html index 9d169f1..20d9ec0 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -5477,7 +5477,7 @@ if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 } - displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("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 = { time: timestamp, @@ -5541,7 +5541,7 @@ if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 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 = { time: timestamp, @@ -5625,7 +5625,7 @@ if (fx.fx_strength < 1.0){ // 调整小周期阈值 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 = { time: timestamp, @@ -5681,7 +5681,7 @@ if (fx.fx_strength < 2.0){ // 调整小周期阈值 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分型标记配置 const markerConfig = { time: timestamp,