RSI Mean Reversion v1.1 with Bollinger Bands.
Timeframe
15m
Direction
Long Only
Stoploss
-2.5%
Trailing Stop
Yes
ROI
0m: 4.0%, 30m: 2.0%, 60m: 1.0%, 120m: 0.0%
Interface Version
3
Startup Candles
N/A
Indicators
4
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
RSI Mean Reversion v1.1 — Freqtrade Strategy (Interface V3)
Market Thesis:
Crypto assets frequently overshoot to the downside during panic events,
creating extreme oversold conditions below the lower Bollinger Band.
These overshoots tend to revert — at minimum back to the BB middle,
often to the upper BB. This strategy enters when multiple oversold
confirmations align (BB, RSI, Stochastic) and rides the reversion.
v1.1 vs v1.0 — Code Quality Improvements ONLY:
- BooleanParameter for use_ema_trend_filter (was IntParameter)
- bb_std_dev as DecimalParameter (was hardcoded 2.0)
- enter_tag / exit_tag for signal analysis
- process_only_new_candles = True
- All exit thresholds as tunable sell-space parameters
- Clear Guards vs Triggers documentation in code
- Volume > 0 as basic sanity check (standard Freqtrade practice)
NO new entry guards. NO new filters. NO exit logic changes.
v1.0 signal logic is preserved exactly.
Author: Agent Zero — Senior Algo Trading Engineer
Version: 1.1.0
Backtest (18 pairs, 253d, -42.9% market): +7.93%, 76.2% win, PF 3.29
"""
import logging
from functools import reduce
import talib.abstract as ta
from freqtrade.strategy import (
IStrategy,
IntParameter,
DecimalParameter,
BooleanParameter,
)
from pandas import DataFrame
logger = logging.getLogger(__name__)
class RSIMeanReversionV11(IStrategy):
"""
RSI Mean Reversion v1.1 with Bollinger Bands.
Buys oversold bounces with BB + RSI + Stochastic confirmation.
Exits at RSI overbought or Stochastic overbought.
Designed for 15m timeframe on EUR-paired crypto assets.
"""
INTERFACE_VERSION = 3
# ── Hyperopt Parameters ──────────────────────────────────────────────
# Defaults match v1.0's proven hyperopt results (200 epochs)
# Hyperopt spaces: buy, sell, roi, stoploss, trailing
# Bollinger Bands
bb_period = IntParameter(15, 25, default=15, space="buy", optimize=True)
bb_std_dev = DecimalParameter(1.5, 3.0, default=2.0, decimals=1,
space="buy", optimize=True)
# RSI
rsi_period = IntParameter(10, 20, default=12, space="buy", optimize=True)
rsi_buy_threshold = IntParameter(25, 45, default=35, space="buy", optimize=True)
rsi_sell_threshold = IntParameter(55, 80, default=65, space="sell", optimize=True)
# Stochastic
stoch_k_period = IntParameter(10, 20, default=15, space="buy", optimize=True)
stoch_d_period = IntParameter(2, 5, default=2, space="buy", optimize=True)
stoch_smooth = IntParameter(2, 5, default=5, space="buy", optimize=True)
stoch_buy_threshold = IntParameter(15, 35, default=25, space="buy", optimize=True)
stoch_sell_threshold = IntParameter(60, 85, default=75, space="sell", optimize=True)
# EMA Trend Filter
ema_trend_period = IntParameter(100, 250, default=197, space="buy", optimize=True)
use_ema_trend_filter = BooleanParameter(default=False, space="buy", optimize=True)
# ── Risk Management ──────────────────────────────────────────────────
stoploss = -0.025 # 3.5% hard stop — conservative for 15m crypto
trailing_stop = True
trailing_stop_positive = 0.008 # 1% trailing once triggered
trailing_stop_positive_offset = 0.012 # Activate trailing at 1.5% profit
trailing_only_offset_is_reached = True
minimal_roi = {'0': 0.04, '30': 0.02, '60': 0.01, '120': 0}
# ── Strategy Settings ────────────────────────────────────────────────
timeframe = "15m"
can_short: bool = False
process_only_new_candles: bool = True
startup_candle_count: int = 220 # EMA 197 + buffer
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": False,
}
order_time_in_force = {
"entry": "GTC",
"exit": "GTC",
}
# ── Indicator Calculation ────────────────────────────────────────────
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Compute all technical indicators.
NOTE: Hyperopt parameters use .value accessor here.
Freqtrade re-runs this per epoch during hyperopt.
"""
# Bollinger Bands
bollinger = ta.BBANDS(
dataframe,
timeperiod=self.bb_period.value,
nbdevup=self.bb_std_dev.value,
nbdevdn=self.bb_std_dev.value,
)
dataframe["bb_lower"] = bollinger["lowerband"]
dataframe["bb_middle"] = bollinger["middleband"]
dataframe["bb_upper"] = bollinger["upperband"]
# RSI
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=self.rsi_period.value)
# Stochastic Oscillator
stoch = ta.STOCH(
dataframe,
fastk_period=self.stoch_k_period.value,
slowk_period=self.stoch_d_period.value,
slowk_matype=0,
slowd_period=self.stoch_smooth.value,
slowd_matype=0,
)
dataframe["stoch_k"] = stoch["slowk"]
dataframe["stoch_d"] = stoch["slowd"]
# EMA — long-term trend filter
dataframe["ema_trend"] = ta.EMA(
dataframe, timeperiod=self.ema_trend_period.value
)
return dataframe
# ── Entry Logic ──────────────────────────────────────────────────────
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Generate long entry signals.
GUARDS (market must be suitable):
- Optional: price above EMA trend (uptrend confirmation)
TRIGGERS (entry signal — ALL must be true):
- Price <= lower Bollinger Band (oversold stretch)
- RSI < buy threshold (momentum exhausted to downside)
- Stochastic %K < buy threshold AND crossing above %D
(momentum turning bullish in oversold zone)
"""
conditions = []
# ── Guards ───────────────────────────────────────────────────
if self.use_ema_trend_filter.value:
conditions.append(dataframe["close"] > dataframe["ema_trend"])
# ── Triggers ─────────────────────────────────────────────────
# Price at or below lower Bollinger Band
conditions.append(dataframe["close"] <= dataframe["bb_lower"])
# RSI confirms oversold
conditions.append(dataframe["rsi"] < self.rsi_buy_threshold.value)
# Stochastic: in oversold zone AND bullish crossover
conditions.append(dataframe["stoch_k"] < self.stoch_buy_threshold.value)
conditions.append(dataframe["stoch_k"] > dataframe["stoch_d"])
conditions.append(
dataframe["stoch_k"].shift(1) <= dataframe["stoch_d"].shift(1)
)
# Apply conditions
dataframe.loc[
reduce(lambda a, b: a & b, conditions),
"enter_long",
] = 1
dataframe.loc[
reduce(lambda a, b: a & b, conditions),
"enter_tag",
] = "bb_rsi_stoch_oversold"
return dataframe
# ── Exit Logic ───────────────────────────────────────────────────────
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""Generate long exit signals.
Exits are independent OR conditions (any triggers exit):
1. RSI > sell threshold (momentum exhausted to upside)
2. Stochastic %K > sell threshold (overbought zone)
This is NOT the inverse of entry — it targets profit taking
at natural mean-reversion completion points.
"""
dataframe.loc[
(
(dataframe["rsi"] > self.rsi_sell_threshold.value)
| (dataframe["stoch_k"] > self.stoch_sell_threshold.value)
),
"exit_long",
] = 1
dataframe.loc[
(dataframe["rsi"] > self.rsi_sell_threshold.value),
"exit_tag",
] = "rsi_overbought"
dataframe.loc[
~(dataframe["rsi"] > self.rsi_sell_threshold.value)
& (dataframe["stoch_k"] > self.stoch_sell_threshold.value),
"exit_tag",
] = "stoch_overbought"
return dataframe