Timeframe
1h
Direction
Long Only
Stoploss
-4.0%
Trailing Stop
No
ROI
0m: 3.0%, 60m: 2.0%, 120m: 1.0%
Interface Version
3
Startup Candles
200
Indicators
3
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
BBRsiMeanReversionStrategy — Phase 1.4
Replaces momentum-following (RSI crossover) with mean reversion:
buy when price is genuinely oversold below the lower Bollinger Band,
sell when price recovers to the middle Band (mean) or RSI normalises.
Why this works better in bear markets:
- Bear markets have frequent oversold bounces even during downtrends.
- Entries are at beaten-down prices (better risk/reward than chasing momentum).
- Exit target is the mean (realistic 2-4%), not a wide ROI table.
Hyperopt spaces: buy, sell, roi, stoploss.
In-sample: 20250524– (full history)
Out-of-sample: TIMERANGE=20260101- bash scripts/backtest.sh
Not financial advice. Do not use with real funds without independent review.
"""
from typing import Dict, List
import talib.abstract as ta
from pandas import DataFrame
from freqtrade.optimize.space import SKDecimal
from freqtrade.strategy import IntParameter, IStrategy
class BBRsiMeanReversionStrategy(IStrategy):
INTERFACE_VERSION = 3
can_short = False
timeframe = "1h"
startup_candle_count = 200
# ------------------------------------------------------------------ #
# Exit targets — tighter than momentum strategy; mean reversion #
# captures small bounces quickly rather than riding trends. #
# ------------------------------------------------------------------ #
minimal_roi = {
"0": 0.03,
"60": 0.02,
"120": 0.01,
}
stoploss = -0.04
trailing_stop = False
process_only_new_candles = True
# ------------------------------------------------------------------ #
# Hyperopt parameters #
# ------------------------------------------------------------------ #
buy_rsi_oversold = IntParameter(15, 40, default=30, space="buy", optimize=True)
sell_rsi_recover = IntParameter(50, 75, default=60, space="sell", optimize=True)
# ------------------------------------------------------------------ #
# Constrained Hyperopt spaces #
# ------------------------------------------------------------------ #
@staticmethod
def stoploss_space() -> List:
# Cap at -8 % — mean reversion entries are at better prices so a
# wider stop is rarely justified.
return [SKDecimal(-0.08, -0.02, decimals=3, name="stoploss")]
@staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]:
roi_table: Dict[int, float] = {}
roi_table[0] = params["roi_p1"] + params["roi_p2"] + params["roi_p3"]
roi_table[params["roi_t3"]] = params["roi_p1"] + params["roi_p2"]
roi_table[params["roi_t3"] + params["roi_t2"]] = params["roi_p1"]
# Floor at 1 % — never hold to breakeven
roi_table[params["roi_t3"] + params["roi_t2"] + params["roi_t1"]] = 0.01
return roi_table
# ------------------------------------------------------------------ #
# Indicators #
# ------------------------------------------------------------------ #
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
bb = ta.BBANDS(dataframe, timeperiod=20, nbdevup=2.0, nbdevdn=2.0)
dataframe["bb_upper"] = bb["upperband"]
dataframe["bb_middle"] = bb["middleband"]
dataframe["bb_lower"] = bb["lowerband"]
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
return dataframe
# ------------------------------------------------------------------ #
# Entry — buy genuine oversold dips, not momentum breakouts #
# ------------------------------------------------------------------ #
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
# Price has pierced the lower Bollinger Band
(dataframe["close"] < dataframe["bb_lower"])
# RSI confirms oversold — not just noisy band touch
& (dataframe["rsi"] < self.buy_rsi_oversold.value)
# Not in a catastrophic downtrend (within 15 % of EMA200)
& (dataframe["close"] > dataframe["ema200"] * 0.85)
& (dataframe["volume"] > 0)
),
["enter_long", "enter_tag"],
] = [1, "bb_rsi_oversold"]
return dataframe
# ------------------------------------------------------------------ #
# Exit — take profit at the mean, not via trend signals #
# ------------------------------------------------------------------ #
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Exit 1: mean reversion complete — price back at middle BB
dataframe.loc[
(
(dataframe["close"] >= dataframe["bb_middle"])
& (dataframe["volume"] > 0)
),
["exit_long", "exit_tag"],
] = [1, "bb_middle_reached"]
# Exit 2: RSI has normalised before price reached the middle band
dataframe.loc[
(
(dataframe["rsi"] > self.sell_rsi_recover.value)
& (dataframe["close"] < dataframe["bb_middle"])
& (dataframe["volume"] > 0)
),
["exit_long", "exit_tag"],
] = [1, "rsi_recovered"]
return dataframe