""" 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