← Back to Home
Enhancing RSI Mean-Reversion with ATR and ADX from $48000 to $131000 Profits

Enhancing RSI Mean-Reversion with ATR and ADX from $48000 to $131000 Profits

Mean-reversion strategies hinge on the idea that prices will revert to an average level over time. One popular indicator for such a strategy is the Relative Strength Index (RSI). In this article, we start with a basic RSI mean-reversion strategy, then describe how we can improve performance by filtering out trades during trending conditions using the Average Directional Movement Index (ADX) and the Average True Range (ATR). Generally, you should always think of the market conditions that your strategy works well in and then try to reduce false signals by filtering out undesirable markets.


The Base RSI Mean-Reversion Strategy

Strategy Concept

The basic RSI mean-reversion strategy is built around these core ideas:

Base Strategy Code

Below is the essential code for the basic RSI mean-reversion strategy written using Backtrader and yfinance libraries:

import backtrader as bt
import yfinance as yf
import datetime
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib qt5

class RsiStrategy(bt.Strategy):
    params = (
        ('rsi_period', 14),
        ('rsi_overbought', 70),
        ('rsi_oversold', 30),
        ('printlog', True),
    )

    def __init__(self):
        # Reference to the close price series
        self.dataclose = self.datas[0].close
        # For tracking orders
        self.order = None
        # Add RSI indicator
        self.rsi = bt.indicators.RSI(self.datas[0], period=self.params.rsi_period)

    def log(self, txt, dt=None, doprint=False):
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print(f'{dt.isoformat()} - {txt}')
            
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return  # Do nothing for pending orders

        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'BUY EXECUTED, Price: {order.executed.price:.2f}, '
                         f'Cost: {order.executed.value:.2f}, Comm {order.executed.comm:.2f}')
            elif order.issell():
                self.log(f'SELL EXECUTED, Price: {order.executed.price:.2f}, '
                         f'Cost: {order.executed.value:.2f}, Comm {order.executed.comm:.2f}')
            self.bar_executed = len(self)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
        self.order = None

    def next(self):
        # Skip new orders if an order is pending.
        if self.order:
            return

        # If we are not in the market, check for oversold condition for buy entry.
        if not self.position:
            if self.rsi < self.params.rsi_oversold:
                self.log(f'BUY CREATE, {self.dataclose[0]:.2f}')
                self.order = self.buy()
        else:
            # If already in the market, check for overbought condition for exit.
            if self.rsi > self.params.rsi_overbought:
                self.log(f'SELL CREATE, {self.dataclose[0]:.2f}')
                self.order = self.sell()

if __name__ == '__main__':
    cerebro = bt.Cerebro()
    cerebro.addstrategy(RsiStrategy, printlog=False)

    ticker = 'BTC-USD'
    start_date = datetime.datetime(2020, 1, 1)
    end_date = datetime.datetime(2025, 12, 31)

    data_df = yf.download(ticker, start=start_date, end=end_date, progress=False)
    if data_df.empty:
        raise ValueError("No data fetched. Check ticker symbol or date range.")

    # Adjust DataFrame for Backtrader if needed
    data_df.columns = data_df.columns.droplevel(1).str.lower()
    data_df.index = pd.to_datetime(data_df.index)
    data = bt.feeds.PandasData(dataname=data_df)
    cerebro.adddata(data)

    cerebro.broker.set_cash(100000.0)
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addsizer(bt.sizers.PercentSizer, percents=95)

    print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
    cerebro.run()
    print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')

    plt.rcParams["figure.figsize"] = (10, 6)
    figure = cerebro.plot(style='candlestick', barup='green', bardown='red', iplot=False, show=False)[0][0]
    plt.tight_layout()
    figure.show()

Pasted image 20250412235745.png ### Performance of the Base Strategy

Let’s take a look at backtest performance for bitcoin from 2020 onward:

Starting Portfolio Value: 100000.00
Final Portfolio Value: 147939.08
Sharpe Ratio: 0.020
Total Return: 39.16%
Annualized Return: 5.25%
Max Drawdown: 62.21%
Total Trades: 8
Winning Trades: 5
Losing Trades: 2
Win Rate: 62.50%
Avg Winning Trade: 26152.38
Avg Losing Trade: -42397.02
Profit Factor: 1.5421118842358195

These numbers indicate moderate growth, though the high drawdown signals the strategy’s vulnerability during trending markets where mean reversion assumptions fail.


Mean reversion strategies work best in range-bound markets. However, during trending phases, prices often continue in one direction rather than reverting, which leads to false RSI signals. To address this, we can use:

Improved Strategy Concept

For a mean-reversion strategy, the logic now becomes:

Enhanced Strategy Code

The following code integrates ADX and ATR filters to prevent entry or exit signals during trending conditions:

import backtrader as bt
import yfinance as yf
import datetime
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib qt5

class RsiStrategyWithTrendFilters(bt.Strategy):
    params = (
        ('rsi_period', 14),
        ('rsi_overbought', 70),
        ('rsi_oversold', 30),
        ('adx_period', 14),
        ('adx_threshold', 25),         # A high ADX indicates trending conditions.
        ('atr_period', 14),
        ('atr_ratio_threshold', 0.02),   # ATR-to-Price ratio threshold.
        ('printlog', True),
    )

    def __init__(self):
        self.dataclose = self.datas[0].close
        self.order = None
        self.rsi = bt.indicators.RSI(self.datas[0], period=self.params.rsi_period)
        self.adx = bt.indicators.AverageDirectionalMovementIndex(self.datas[0], period=self.params.adx_period)
        self.atr = bt.indicators.ATR(self.datas[0], period=self.params.atr_period)

    def log(self, txt, dt=None, doprint=False):
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print(f'{dt.isoformat()} - {txt}')

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return

        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'BUY EXECUTED, Price: {order.executed.price:.2f}, '
                         f'Cost: {order.executed.value:.2f}, Comm {order.executed.comm:.2f}')
            elif order.issell():
                self.log(f'SELL EXECUTED, Price: {order.executed.price:.2f}, '
                         f'Cost: {order.executed.value:.2f}, Comm {order.executed.comm:.2f}')
            self.bar_executed = len(self)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
        self.order = None

    def next(self):
        if self.order:
            return

        # Calculate the ATR-to-Price ratio for the current bar
        atr_ratio = self.atr[0] / self.dataclose[0]
        
        # Determine trending conditions:
        trend_confirmed = self.adx[0] >= self.params.adx_threshold
        volatile_enough = atr_ratio >= self.params.atr_ratio_threshold

        # For a mean-reversion strategy, skip trading in trending markets.
        if trend_confirmed and volatile_enough:
            return

        # If we're not in the market, consider buying when RSI is oversold.
        if not self.position:
            if self.rsi < self.params.rsi_oversold:
                self.log(f'BUY CREATE at {self.dataclose[0]:.2f} | RSI: {self.rsi[0]:.2f}')
                self.order = self.buy()
        else:
            # If in the market, consider selling when RSI is overbought.
            if self.rsi > self.params.rsi_overbought:
                self.log(f'SELL CREATE at {self.dataclose[0]:.2f} | RSI: {self.rsi[0]:.2f}')
                self.order = self.sell()

if __name__ == '__main__':
    cerebro = bt.Cerebro()
    cerebro.addstrategy(RsiStrategyWithTrendFilters, printlog=False)

    ticker = 'BTC-USD'
    start_date = datetime.datetime(2020, 1, 1)
    end_date = datetime.datetime(2025, 12, 31)
    data_df = yf.download(ticker, start=start_date, end=end_date, progress=False)
    if data_df.empty:
        raise ValueError("No data fetched. Check ticker symbol or date range.")
    
    data_df.columns = data_df.columns.droplevel(1).str.lower()
    data_df.index = pd.to_datetime(data_df.index)
    data = bt.feeds.PandasData(dataname=data_df)
    cerebro.adddata(data)

    cerebro.broker.set_cash(100000.0)
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addsizer(bt.sizers.PercentSizer, percents=95)

    print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
    cerebro.run()
    print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')

    plt.rcParams["figure.figsize"] = (10, 6)
    figure = cerebro.plot(style='candlestick', barup='green', bardown='red', iplot=False, show=False)[0][0]
    plt.tight_layout()
    figure.show()

Pasted image 20250413000051.png ### Improvements Observed

Now let’s see the performance of the enhanced strategy:

Starting Portfolio Value: 100000.00
Final Portfolio Value: 231071.06
Sharpe Ratio: 0.038
Total Return: 83.76%
Annualized Return: 11.56%
Max Drawdown: 33.76%
Total Trades: 4
Winning Trades: 3
Losing Trades: 1
Win Rate: 75.00%
Avg Winning Trade: 49901.91
Avg Losing Trade: -18634.66
Profit Factor: 8.03372435292996

The improved strategy increases returns and lowers risk by filtering out trades during trending and volatile conditions. It takes fewer trades, but they are more selective and in environments where mean reversion is more likely to be successful.


Conclusion

By starting with a basic RSI mean-reversion strategy and then improving it with ADX and ATR filters, we’ve demonstrated how to better adapt the strategy to market conditions. This approach helps avoid false signals in trending markets, leading to higher returns and lower drawdown. Such enhancements underscore the importance of tailoring your trading logic to the prevailing market environment for more robust strategy performance.

Whether you’re refining an existing system or building a new one, understanding the interplay between indicators, market regimes, and risk management is key to optimizing trading strategies. Happy trading!