from freqtrade.strategy.interface import IStrategy from freqtrade.strategy import IntParameter, DecimalParameter from pandas import DataFrame import talib.abstract as ta class TrendFollowingStrategy(IStrategy): can_short: bool = True timeframe = '5m' # Basic ROI/SL - keep modest defaults; can be tuned later minimal_roi = { "0": 0.03 } stoploss = -0.08 trailing_stop = True # Ensure we have enough candles for indicators (BB 20, EMA 200, etc.) startup_candle_count = 210 # ---- Hyperoptable parameters (buy/sell) to enable default spaces ---- # These unlock hyperopt for this strategy (avoid "no parameter for this space" error) buy_rsi = IntParameter(45, 60, default=50, space='buy') buy_bb_mid_offset = DecimalParameter(0.0, 0.02, decimals=3, default=0.0, space='buy') sell_rsi = IntParameter(40, 55, default=50, space='sell') sell_bb_mid_offset = DecimalParameter(0.0, 0.02, decimals=3, default=0.0, space='sell') def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Moving averages dataframe['ema_short'] = ta.EMA(dataframe, timeperiod=9) dataframe['ema_long'] = ta.EMA(dataframe, timeperiod=21) dataframe['ema_trend'] = ta.EMA(dataframe, timeperiod=200) # Bollinger Bands (20, 2) bb_upper, bb_middle, bb_lower = ta.BBANDS( dataframe['close'], timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0 ) dataframe['bb_upper'] = bb_upper dataframe['bb_mid'] = bb_middle dataframe['bb_lower'] = bb_lower # RSI momentum dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) # Volume (ensure positive) dataframe['volume_mean_slow'] = dataframe['volume'].rolling(30).mean() return dataframe def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs) -> float: stop = abs(float(self.stoploss)) if getattr(self, "stoploss", None) is not None else 0.08 base = 0.05 / stop if stop > 0 else (proposed_leverage or 1.0) base = max(1.0, min(float(base), float(max_leverage))) return base def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Long entry: EMA9 > EMA21, close breaks above BB mid (trend confirmation), RSI > 50, volume ok dataframe.loc[ ( (dataframe['ema_short'] > dataframe['ema_long']) & (dataframe['close'] > dataframe['bb_mid'] * (1 + float(self.buy_bb_mid_offset.value))) & (dataframe['rsi'] > int(self.buy_rsi.value)) & (dataframe['close'] > dataframe['ema_trend']) & (dataframe['volume'] > 0) & (dataframe['volume'] > dataframe['volume_mean_slow']) ), 'enter_long' ] = 1 # Short entry: EMA9 < EMA21, close below BB mid, RSI < 50, price below long EMA, volume ok dataframe.loc[ ( (dataframe['ema_short'] < dataframe['ema_long']) & (dataframe['close'] < dataframe['bb_mid'] * (1 - float(self.buy_bb_mid_offset.value))) & (dataframe['rsi'] < 100 - int(self.buy_rsi.value)) & (dataframe['close'] < dataframe['ema_trend']) & (dataframe['volume'] > 0) & (dataframe['volume'] > dataframe['volume_mean_slow']) ), 'enter_short' ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Long exit: Cross back down (EMA9 < EMA21) or mean reversion (close < BB mid) or RSI loss of momentum dataframe.loc[ ( (dataframe['ema_short'] < dataframe['ema_long']) | (dataframe['close'] < dataframe['bb_mid'] * (1 - float(self.sell_bb_mid_offset.value))) | (dataframe['rsi'] < int(self.sell_rsi.value)) ), 'exit_long' ] = 1 # Short exit: Cross back up (EMA9 > EMA21) or mean reversion (close > BB mid) or RSI momentum fades dataframe.loc[ ( (dataframe['ema_short'] > dataframe['ema_long']) | (dataframe['close'] > dataframe['bb_mid'] * (1 + float(self.sell_bb_mid_offset.value))) | (dataframe['rsi'] > 100 - int(self.sell_rsi.value)) ), 'exit_short' ] = 1 return dataframe