From 5e7a29a76fc18254b72b70b97d88dacc12c52bee Mon Sep 17 00:00:00 2001 From: "zhangkun9038@dingtalk.com" Date: Mon, 19 May 2025 10:54:46 +0000 Subject: [PATCH] =?UTF-8?q?=E4=B8=8D=E5=B0=8F=E5=BF=83=E6=8A=8Afreqaiprime?= =?UTF-8?q?r.py=E5=BC=84=E6=8E=89=E4=BA=86,=E8=A1=A5=E5=9B=9E=E6=9D=A5?= =?UTF-8?q?=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/templates/freqaiprimer.py | 277 ++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 freqtrade/templates/freqaiprimer.py diff --git a/freqtrade/templates/freqaiprimer.py b/freqtrade/templates/freqaiprimer.py new file mode 100644 index 00000000..ac1d2d24 --- /dev/null +++ b/freqtrade/templates/freqaiprimer.py @@ -0,0 +1,277 @@ +import logging +import numpy as np +from functools import reduce +import talib.abstract as ta +from pandas import DataFrame +from technical import qtpylib +from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter + +logger = logging.getLogger(__name__) + +class FreqaiPrimer(IStrategy): + minimal_roi = { + 0: 0.135, + 9: 0.052, + 15: 0.007, + 60: 0 + } + stoploss = -0.263 + trailing_stop = True + trailing_stop_positive = 0.324 + trailing_stop_positive_offset = 0.411 + trailing_only_offset_is_reached = False + max_open_trades = 4 + process_only_new_candles = True + use_exit_signal = True + startup_candle_count: int = 40 + can_short = False + + buy_rsi = IntParameter(low=10, high=50, default=30, space="buy", optimize=False, load=True) + sell_rsi = IntParameter(low=50, high=90, default=70, space="sell", optimize=False, load=True) + roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.135, space="roi", optimize=True, load=True) + roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.052, space="roi", optimize=True, load=True) + roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.007, space="roi", optimize=True, load=True) + stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.263, space="stoploss", optimize=True, load=True) + trailing_stop_positive_param = DecimalParameter(low=0.1, high=0.5, default=0.324, space="trailing", optimize=True, load=True) + trailing_stop_positive_offset_param = DecimalParameter(low=0.2, high=0.6, default=0.411, space="trailing", optimize=True, load=True) + + freqai_info = { + "model": "LightGBMRegressor", + "feature_parameters": { + "include_timeframes": ["5m", "15m", "1h"], + "include_corr_pairlist": [], + "label_period_candles": 12, + "include_shifted_candles": 3, + }, + "data_split_parameters": { + "test_size": 0.2, + "shuffle": False, + }, + "model_training_parameters": { + "n_estimators": 200, + "learning_rate": 0.05, + "num_leaves": 31, + "verbose": -1, + }, + } + + plot_config = { + "main_plot": {}, + "subplots": { + "&-buy_rsi": {"&-buy_rsi": {"color": "green"}}, + "&-sell_rsi": {"&-sell_rsi": {"color": "red"}}, + "&-stoploss": {"&-stoploss": {"color": "purple"}}, + "&-roi_0": {"&-roi_0": {"color": "orange"}}, + "do_predict": {"do_predict": {"color": "brown"}}, + }, + } + + def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame: + dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) + dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) + dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period) + dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period) + dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period) + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=period, stds=2.2) + dataframe["bb_lowerband-period"] = bollinger["lower"] + dataframe["bb_middleband-period"] = bollinger["mid"] + dataframe["bb_upperband-period"] = bollinger["upper"] + dataframe["%-bb_width-period"] = ( + dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"] + ) / dataframe["bb_middleband-period"] + dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"] + dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period) + dataframe["%-relative_volume-period"] = ( + dataframe["volume"] / dataframe["volume"].rolling(period).mean() + ) + dataframe = dataframe.replace([np.inf, -np.inf], 0) + dataframe = dataframe.ffill() + dataframe = dataframe.fillna(0) + 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"] + dataframe = dataframe.replace([np.inf, -np.inf], 0) + dataframe = dataframe.ffill() + dataframe = dataframe.fillna(0) + return dataframe + + def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: + if len(dataframe["close"]) < 20: + logger.warning(f"数据不足 {len(dataframe)} 根 K 线,%-volatility 可能不完整") + dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek + dataframe["%-hour_of_day"] = dataframe["date"].dt.hour + dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() + dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0) + dataframe["%-volatility"] = dataframe["%-volatility"].ffill() + dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0) + return dataframe + + def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: + logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}") + if "close" not in dataframe.columns: + logger.error("数据框缺少必要的 'close' 列") + raise ValueError("数据框缺少必要的 'close' 列") + + try: + label_period = self.freqai_info["feature_parameters"]["label_period_candles"] + if "%-volatility" not in dataframe.columns: + logger.warning("缺少 %-volatility 列,强制重新生成") + dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() + dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0) + dataframe["%-volatility"] = dataframe["%-volatility"].ffill() + dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0) + + dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14) + dataframe["&-buy_rsi"] = dataframe["&-buy_rsi"].shift(-label_period).ffill().bfill() + + for col in ["&-buy_rsi", "%-volatility"]: + dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0) + dataframe[col] = dataframe[col].ffill() + dataframe[col] = dataframe[col].fillna(0) + if dataframe[col].isna().any(): + logger.warning(f"目标列 {col} 仍包含 NaN,数据预览:\n{dataframe[col].tail(10)}") + except Exception as e: + logger.error(f"创建 FreqAI 目标失败:{str(e)}") + raise + + logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}") + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + logger.info(f"处理交易对:{metadata['pair']}") + logger.debug(f"输入特征列:{list(dataframe.columns)}") + dataframe = self.freqai.start(dataframe, metadata, self) + logger.debug(f"FreqAI 输出特征列:{list(dataframe.columns)}") + + dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe["bb_lowerband"] = bollinger["lower"] + dataframe["bb_middleband"] = bollinger["mid"] + dataframe["bb_upperband"] = bollinger["upper"] + dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9) + + label_period = self.freqai_info["feature_parameters"]["label_period_candles"] + dataframe["up_or_down"] = np.where( + dataframe["close"].shift(-label_period) > dataframe["close"], 1, 0 + ) + + if "&-buy_rsi" in dataframe.columns: + if "%-volatility" not in dataframe.columns: + logger.warning("缺少 %-volatility 列,强制重新生成") + dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std() + dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0) + dataframe["%-volatility"] = dataframe["%-volatility"].ffill() + dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0) + + dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30 + dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05) + dataframe["&-roi_0"] = (dataframe["close"].shift(-label_period) / dataframe["close"] - 1).clip(0, 0.2) + + for col in ["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0"]: + dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0) + dataframe[col] = dataframe[col].ffill() + dataframe[col] = dataframe[col].fillna(0) + + dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5).mean().clip(10, 50) + dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5).mean().clip(50, 90) + dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.35, -0.1) + dataframe["roi_0_pred"] = dataframe["&-roi_0"].clip(0.01, 0.2) + + for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred"]: + if dataframe[col].isna().any(): + logger.warning(f"列 {col} 包含 NaN,填充为默认值") + dataframe[col] = dataframe[col].ffill() + dataframe[col] = dataframe[col].fillna(dataframe[col].mean()) + + dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3) + dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.75).clip(0.02, 0.4) + + self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1]) + self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1]) + self.stoploss = float(self.stoploss_param.value) + self.minimal_roi = { + 0: float(self.roi_0.value), + 15: float(self.roi_15.value), + 30: float(self.roi_30.value), + 60: 0 + } + self.trailing_stop_positive = float(self.trailing_stop_positive_param.value) + self.trailing_stop_positive_offset = float(self.trailing_stop_positive_offset_param.value) + + logger.info(f"动态参数:buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, " + f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}") + else: + logger.warning(f"&-buy_rsi 列缺失,跳过 FreqAI 预测逻辑,检查 freqai.start 输出") + + dataframe = dataframe.replace([np.inf, -np.inf], 0) + dataframe = dataframe.ffill() + dataframe = dataframe.fillna(0) + + logger.info(f"up_or_down 值统计:\n{dataframe['up_or_down'].value_counts().to_string()}") + logger.info(f"do_predict 值统计:\n{dataframe['do_predict'].value_counts().to_string()}") + logger.debug(f"最终特征列:{list(dataframe.columns)}") + + return dataframe + + def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + enter_long_conditions = [ + qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]), + df["tema"] > df["tema"].shift(1), + df["volume"] > 0, + df["do_predict"] == 1, + df["up_or_down"] == 1 + ] + if enter_long_conditions: + df.loc[ + reduce(lambda x, y: x & y, enter_long_conditions), + ["enter_long", "enter_tag"] + ] = (1, "long") + return df + + def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: + exit_long_conditions = [ + qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"]), + (df["close"] < df["close"].shift(1) * 0.97), + df["volume"] > 0, + df["do_predict"] == 1, + df["up_or_down"] == 0 + ] + if exit_long_conditions: + df.loc[ + reduce(lambda x, y: x & y, exit_long_conditions), + "exit_long" + ] = 1 + return df + + def confirm_trade_entry( + self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time, entry_tag, side: str, **kwargs + ) -> bool: + try: + df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + if df is None or df.empty: + logger.warning(f"无法获取 {pair} 的分析数据,拒绝交易") + return False + + last_candle = df.iloc[-1].squeeze() + if "close" not in last_candle or np.isnan(last_candle["close"]): + logger.warning(f"{pair} 的最新 K 线缺少有效 close 价格,拒绝交易") + return False + + if side == "long": + max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值 + if rate > max_rate: + logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}") + return False + elif side == "short": + logger.warning(f"{pair} 尝试做空,但策略不支持做空 (can_short={self.can_short})") + return False + + logger.debug(f"确认 {pair} 的交易:side={side}, rate={rate}, close={last_candle['close']}") + return True + except Exception as e: + logger.error(f"确认 {pair} 交易时出错:{str(e)}") + return False