from datetime import datetime from functools import reduce from pandas import DataFrame from pandas import notna import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib import logging from freqtrade.persistence import Trade from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy logger = logging.getLogger(__name__) class DynamicFreqAIBotStrategy(IStrategy): """ A conservative FreqAI trend strategy. The model predicts forward average return, while hand-written filters keep entries aligned with trend, momentum, liquidity, and volatility regimes. """ INTERFACE_VERSION = 3 can_short = False timeframe = "15m" startup_candle_count = 220 process_only_new_candles = True minimal_roi = {} stoploss = -0.99 trailing_stop = False use_custom_stoploss = True use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False use_custom_roi = True buy_zscore_multiplier = DecimalParameter(0.80, 1.80, default=1.10, decimals=2, space="buy", optimize=True) sell_zscore_multiplier = DecimalParameter(0.80, 1.80, default=1.10, decimals=2, space="sell", optimize=True) min_expected_return = DecimalParameter(0.002, 0.020, default=0.006, decimals=3, space="buy", optimize=True) max_negative_return = DecimalParameter(-0.030, -0.002, default=-0.006, decimals=3, space="sell", optimize=True) buy_rsi_min = IntParameter(45, 60, default=52, space="buy", optimize=True) buy_rsi_max = IntParameter(58, 75, default=68, space="buy", optimize=True) buy_adx_min = IntParameter(15, 30, default=20, space="buy", optimize=True) sell_rsi_min = IntParameter(35, 55, default=46, space="sell", optimize=True) max_atr_pct = DecimalParameter(0.020, 0.080, default=0.055, decimals=3, space="buy", optimize=True) def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame: """ Create the features that FreqAI will use to "learn" from the market. FreqAI will automatically generate these features for all timeframes defined in config.freqai.json (e.g. 15m and 1h). """ dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period) dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period) dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period) # Bollinger Bands bollinger = qtpylib.bollinger_bands(dataframe["close"], window=period, stds=2) dataframe["%-bb_lowerband-period"] = bollinger["lower"] dataframe["%-bb_upperband-period"] = bollinger["upper"] dataframe["%-bb_width-period"] = (bollinger["upper"] - bollinger["lower"]) / dataframe["close"] dataframe["%-close-bb_lower-distance-period"] = ( dataframe["close"] / bollinger["lower"] - 1 ) dataframe["%-close-bb_upper-distance-period"] = ( dataframe["close"] / bollinger["upper"] - 1 ) # MACD macd = ta.MACD(dataframe) dataframe["%-macd-period"] = macd["macd"] dataframe["%-macdsignal-period"] = macd["macdsignal"] dataframe["%-macdhist-period"] = macd["macdhist"] dataframe["%-ema-distance-period"] = ( dataframe["close"] / ta.EMA(dataframe, timeperiod=period) - 1 ) return dataframe def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: """ Features that are independent of period. """ dataframe["%-pct-change"] = dataframe["close"].pct_change() dataframe["%-pct-change-3"] = dataframe["close"].pct_change(3) dataframe["%-pct-change-12"] = dataframe["close"].pct_change(12) dataframe["%-raw_volume"] = dataframe["volume"] dataframe["%-raw_price"] = dataframe["close"] dataframe["%-volume-ratio-24"] = dataframe["volume"] / dataframe["volume"].rolling(24).mean() dataframe["%-candle-body"] = (dataframe["close"] - dataframe["open"]) / dataframe["open"] dataframe["%-high-low-range"] = (dataframe["high"] - dataframe["low"]) / dataframe["close"] return dataframe def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: """ Standard features (e.g. day of week, hour of day). """ dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek dataframe["%-hour_of_day"] = dataframe["date"].dt.hour return dataframe def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame: """ The "Target" (what FreqAI tries to predict). We tell the AI to predict whether price goes UP or DOWN in the next N candles. """ # Read label_period_candles from config (works in both live and backtest mode) label_period_candles = self.freqai_info["feature_parameters"]["label_period_candles"] dataframe["&-s_close"] = ( dataframe["close"] .shift(-label_period_candles) .rolling(label_period_candles) .mean() / dataframe["close"] - 1 ) return dataframe def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Ask FreqAI to populate all the features and predictions dataframe = self.freqai.start(dataframe, metadata, self) dataframe["ema12"] = ta.EMA(dataframe, timeperiod=12) dataframe["ema36"] = ta.EMA(dataframe, timeperiod=36) dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["volume_mean_24"] = dataframe["volume"].rolling(24).mean() dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"] dataframe["target_roi"] = dataframe["&-s_close"].rolling(200, min_periods=50).quantile(0.65) dataframe["sell_roi"] = dataframe["&-s_close"].rolling(200, min_periods=50).quantile(0.35) if "&-s_close_mean" in dataframe and "&-s_close_std" in dataframe: dataframe["target_roi"] = ( dataframe["&-s_close_mean"] + dataframe["&-s_close_std"] * self.buy_zscore_multiplier.value ) dataframe["sell_roi"] = ( dataframe["&-s_close_mean"] - dataframe["&-s_close_std"] * self.sell_zscore_multiplier.value ) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: ai_signal = ( (dataframe["&-s_close"] > dataframe["target_roi"]) & (dataframe["&-s_close"] > self.min_expected_return.value) ) trend_filter = ( (dataframe["ema12"] > dataframe["ema36"]) & (dataframe["ema36"] > dataframe["ema200"]) & (dataframe["close"] > dataframe["ema12"]) ) momentum_filter = ( (dataframe["rsi"] >= self.buy_rsi_min.value) & (dataframe["rsi"] <= self.buy_rsi_max.value) & (dataframe["adx"] >= self.buy_adx_min.value) ) participation_filter = ( (dataframe["volume"] > 0) & (dataframe["volume"] >= dataframe["volume_mean_24"]) ) volatility_filter = ( dataframe["atr_pct"].notna() & (dataframe["atr_pct"] <= self.max_atr_pct.value) ) enter_long_conditions = [ dataframe["do_predict"] == 1, ai_signal, trend_filter, momentum_filter, participation_filter, volatility_filter, ] if enter_long_conditions: dataframe.loc[ reduce(lambda x, y: x & y, enter_long_conditions), ["enter_long", "enter_tag"] ] = (1, "freqai_trend_buy") return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: ai_exit_signal = ( (dataframe["&-s_close"] < dataframe["sell_roi"]) | (dataframe["&-s_close"] < self.max_negative_return.value) ) trend_break = ( qtpylib.crossed_below(dataframe["close"], dataframe["ema12"]) | (dataframe["ema12"] < dataframe["ema36"]) ) momentum_fade = dataframe["rsi"] < self.sell_rsi_min.value exit_long_conditions = [ dataframe["do_predict"] == 1, ai_exit_signal | trend_break | momentum_fade, ] if exit_long_conditions: dataframe.loc[ reduce(lambda x, y: x & y, exit_long_conditions), ["exit_long", "exit_tag"] ] = (1, "freqai_trend_sell") return dataframe def custom_roi(self, pair: str, trade: Trade, current_time: datetime, trade_duration: int, entry_tag: str | None, side: str, **kwargs) -> float: """ Lower profit targets over time so capital is not trapped waiting for the model to recover a stale entry. """ if trade_duration > 240: return 0.01 if trade_duration > 120: return 0.02 if trade_duration > 60: return 0.03 return 0.05 def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ Use ATR for the initial stoploss and tighten aggressively once price moves in our favor. """ dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if dataframe is None or dataframe.empty: return -0.05 last_candle = dataframe.iloc[-1].squeeze() if current_profit > 0.04: return -0.015 if current_profit > 0.02: return -0.01 atr_val = last_candle.get("atr") if atr_val is not None and notna(atr_val) and current_rate > 0: stop_distance = (atr_val * 2.5) / current_rate return max(min(-stop_distance, -0.02), -0.08) return -0.05