from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter from pandas import DataFrame import talib.abstract as ta from freqtrade.persistence import Trade from datetime import datetime class MeanReversionStrategy(IStrategy): """ Mean reversion strategy that buys oversold conditions and sells overbought conditions in ranging markets. """ minimal_roi = { "0": 0.06, "10": 0.03, "20": 0.02, "40": 0.01, } stoploss = -0.05 timeframe = '5m' trailing_stop = False can_short: bool = False # Hyperparameters (optimized via hyperopt) buy_rsi = IntParameter(15, 30, default=21, space='buy') sell_rsi = IntParameter(70, 85, default=78, space='sell') buy_bb_width = DecimalParameter(0.015, 0.04, default=0.02, space='buy') cci_buy = IntParameter(-150, -80, default=-101, space='buy') adx_max = IntParameter(15, 30, default=28, space='buy') # Max ADX for ranging market def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Indicators for mean reversion. """ # RSI dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) # Bollinger Bands bollinger = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) dataframe['bb_lower'] = bollinger['lowerband'] dataframe['bb_middle'] = bollinger['middleband'] dataframe['bb_upper'] = bollinger['upperband'] # BB Width (for volatility) dataframe['bb_width'] = ( (dataframe['bb_upper'] - dataframe['bb_lower']) / dataframe['bb_middle'] ) # Price position in BB dataframe['bb_percent'] = ( (dataframe['close'] - dataframe['bb_lower']) / (dataframe['bb_upper'] - dataframe['bb_lower']) ) # Moving averages dataframe['sma_20'] = ta.SMA(dataframe, timeperiod=20) dataframe['sma_50'] = ta.SMA(dataframe, timeperiod=50) dataframe['ema_200'] = ta.EMA(dataframe, timeperiod=200) # Trend detection - price vs 200 EMA dataframe['trend'] = (dataframe['close'] - dataframe['ema_200']) / dataframe['ema_200'] # Stochastic stoch = ta.STOCH(dataframe) dataframe['slowk'] = stoch['slowk'] dataframe['slowd'] = stoch['slowd'] # Williams %R dataframe['willr'] = ta.WILLR(dataframe, timeperiod=14) # CCI (Commodity Channel Index) dataframe['cci'] = ta.CCI(dataframe, timeperiod=20) # Volume dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean() # ATR for volatility dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) # ADX for trend strength (lower = ranging = better for mean reversion) dataframe['adx'] = ta.ADX(dataframe, timeperiod=14) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Buy on oversold conditions with mean reversion signals. Only in ranging or slight uptrend markets. """ dataframe.loc[ ( # Multiple oversold indicators - more extreme (dataframe['rsi'] < self.buy_rsi.value) & (dataframe['slowk'] < 15) & (dataframe['slowd'] < 15) & (dataframe['willr'] < -85) & (dataframe['cci'] < self.cci_buy.value) & # Price near lower BB - more extreme (dataframe['bb_percent'] < 0.1) & # Not in high volatility (better for mean reversion) (dataframe['bb_width'] < self.buy_bb_width.value) & # Price below moving average (oversold) (dataframe['close'] < dataframe['sma_20']) & # Trend filter: avoid strong downtrends (dataframe['trend'] > -0.15) & # Not more than 15% below 200 EMA # ADX filter: only ranging markets (mean reversion works in ranging) (dataframe['adx'] < self.adx_max.value) & # Volume confirmation - stronger requirement (dataframe['volume'] > dataframe['volume_mean'] * 1.2) & # Stochastic oversold cross (dataframe['slowk'] > dataframe['slowd']) & # Starting to turn up (dataframe['volume'] > 0) ), 'enter_long'] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Exit ONLY on strong overbought - let ROI handle most exits. Be very conservative to lock in profits. """ dataframe.loc[ ( # Only exit on VERY strong overbought conditions ( (dataframe['rsi'] > 80) & (dataframe['slowk'] > 85) & (dataframe['bb_percent'] > 0.95) ) & (dataframe['volume'] > 0) ), 'exit_long'] = 1 return dataframe