Mean reversion strategy that buys oversold conditions and sells overbought conditions in ranging markets.
Timeframe
5m
Direction
Long Only
Stoploss
-5.0%
Trailing Stop
No
ROI
0m: 6.0%, 10m: 3.0%, 20m: 2.0%, 40m: 1.0%
Interface Version
N/A
Startup Candles
N/A
Indicators
9
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
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