← Back to Home
RSI Trend Confirmation Strategy in Backtester

RSI Trend Confirmation Strategy in Backtester

Trading strategies often rely on technical indicators to generate buy or sell signals. However, individual indicators can sometimes produce “false” signals, especially in choppy or ranging markets. A common technique to improve signal quality is to use multiple indicators in conjunction, where one indicator confirms the signal generated by another.

The RSITrendConfirmationStrategy is a perfect example of this approach. It uses a primary trend-following signal, such as a Moving Average (MA) Crossover, but only acts on that signal if the momentum, measured by the Relative Strength Index (RSI), confirms the direction of the potential trade.

This article serves as a demonstration, breaking down the Python code for this strategy to show how you can create your own modular, parameter-driven strategies. The goal is to build code suitable for testing within the backtrader framework itself, or for easy integration into the “Backtester” application. We’ll focus on creating a self-contained, parameterizable strategy class using backtrader conventions.

Strategy Logic Explained

The core idea is simple yet powerful:

  1. Primary Signal: The strategy first identifies a potential trend direction using a primary indicator. In the provided code, this is a Dual Moving Average Crossover.
    • Bullish Signal: The shorter-term MA crosses above the longer-term MA.
    • Bearish Signal: The shorter-term MA crosses below the longer-term MA.
  2. Momentum Confirmation (RSI Filter): Before entering a trade based on the primary signal, the strategy checks the RSI:
    • For a long entry (after a bullish MA crossover), the RSI must be above a predefined midline (typically 50), indicating bullish momentum.
    • For a short entry (after a bearish MA crossover), the RSI must be below the midline, indicating bearish momentum.
  3. Entry: If both the primary signal and the RSI confirmation align, a trade is entered (buy for bullish, sell for bearish). If the RSI does not confirm the primary signal, the signal is ignored, potentially filtering out trades against the prevailing momentum.
  4. Exit: The exit signal is based solely on the reversal of the primary indicator.
    • A long position is closed when the shorter-term MA crosses back below the longer-term MA.
    • A short position is closed when the shorter-term MA crosses back above the longer-term MA.
    • Crucially, the RSI filter is not applied to exit signals. This ensures that the position is closed based on the trend change indicated by the MAs, regardless of the RSI reading at that specific moment.

Implementation

Let’s break down the Python code which implements this strategy using the popular backtrader library.

Python

import backtrader as bt

class RSITrendConfirmationStrategy(bt.Strategy):
    """
    Uses RSI > 50 (or < 50) to confirm momentum before taking signals
    from another indicator (e.g., MA crossover).
    - Enters long on primary bullish signal only if RSI > rsi_midline.
    - Enters short on primary bearish signal only if RSI < rsi_midline.
    - Primary Signal Example: Dual Moving Average Crossover.
    - Exit based on the reverse crossover of the primary signal.
    """
    params = (
        # RSI Filter Params
        ('rsi_period', 14),      # Period for RSI calculation
        ('rsi_midline', 50.0),   # RSI level to confirm trend direction

        # Primary Signal Params (Example: Dual MA Crossover)
        ('short_ma_period', 50), # Period for the short-term MA
        ('long_ma_period', 200), # Period for the long-term MA
        ('ma_type', 'SMA'),      # Type of Moving Average: 'SMA' or 'EMA'

        ('printlog', True),      # Enable/Disable logging
    )

    def __init__(self):
        # Keep a reference to the closing price
        self.dataclose = self.datas[0].close

        # To keep track of pending orders
        self.order = None
        self.buyprice = None
        self.buycomm = None

        # --- Indicators ---
        # RSI Indicator
        self.rsi = bt.indicators.RelativeStrengthIndex(
            period=self.p.rsi_period
        )

        # Primary Signal Indicators (MA Crossover)
        # Choose MA type based on parameter
        ma_indicator = bt.indicators.SimpleMovingAverage
        if self.p.ma_type == 'EMA':
            ma_indicator = bt.indicators.ExponentialMovingAverage

        # Instantiate the Moving Averages
        self.short_ma = ma_indicator(self.datas[0], period=self.p.short_ma_period)
        self.long_ma = ma_indicator(self.datas[0], period=self.p.long_ma_period)

        # Crossover indicator for the primary signal
        # This indicator returns:
        # +1 if short_ma crosses above long_ma
        # -1 if short_ma crosses below long_ma
        #  0 otherwise
        self.ma_crossover = bt.indicators.CrossOver(self.short_ma, self.long_ma)

    def log(self, txt, dt=None, doprint=False):
        ''' Logging function for this strategy'''
        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):
        # Standard order notification logic
        if order.status in [order.Submitted, order.Accepted]:
            return # Await completion

        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    f'BUY EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm {order.executed.comm:.2f}'
                )
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            elif order.issell():
                self.log(f'SELL EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm {order.executed.comm:.2f}')
            self.bar_executed = len(self) # Record bar number when order was executed

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        # Reset order status
        self.order = None

    def notify_trade(self, trade):
        # Standard trade notification logic (when a position is closed)
        if not trade.isclosed:
            return
        self.log(f'OPERATION PROFIT, GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}')

    def next(self):
        # Core strategy logic executed on each bar
        # 1. Check if an order is pending; if so, can't send another
        if self.order:
            return

        # 2. Get the current RSI value
        current_rsi = self.rsi[0]

        # 3. Check if we are currently *not* in the market
        if not self.position:
            # Check for primary long entry signal (MA crossover bullish)
            if self.ma_crossover[0] > 0:
                # Confirm with RSI momentum
                if current_rsi > self.p.rsi_midline:
                    self.log(f'BUY CREATE, {self.dataclose[0]:.2f} (RSI {current_rsi:.2f} > {self.p.rsi_midline:.1f}, MA Cross)')
                    # Place the buy order
                    self.order = self.buy()
                # else: # Optional log for ignored signals
                #    self.log(f'Long signal ignored, RSI {current_rsi:.2f} <= {self.p.rsi_midline:.1f}')

            # Check for primary short entry signal (MA crossover bearish)
            elif self.ma_crossover[0] < 0:
                 # Confirm with RSI momentum
                if current_rsi < self.p.rsi_midline:
                    self.log(f'SELL CREATE, {self.dataclose[0]:.2f} (RSI {current_rsi:.2f} < {self.p.rsi_midline:.1f}, MA Cross)')
                    # Place the sell order (short entry)
                    self.order = self.sell()
                # else: # Optional log for ignored signals
                #    self.log(f'Short signal ignored, RSI {current_rsi:.2f} >= {self.p.rsi_midline:.1f}')

        # 4. Else (if we *are* in the market), check for exit conditions
        else:
            # Exit Long: Primary signal reverses (MA crossover bearish)
            # RSI filter is NOT used for exit
            if self.position.size > 0 and self.ma_crossover[0] < 0:
                   self.log(f'CLOSE LONG CREATE, {self.dataclose[0]:.2f} (MA Cross)')
                   # Place the closing order
                   self.order = self.close()

            # Exit Short: Primary signal reverses (MA crossover bullish)
            # RSI filter is NOT used for exit
            elif self.position.size < 0 and self.ma_crossover[0] > 0:
                   self.log(f'CLOSE SHORT CREATE, {self.dataclose[0]:.2f} (MA Cross)')
                   # Place the closing order
                   self.order = self.close()

    def stop(self):
        # Executed at the end of the backtest
        self.log(f'(RSI Period {self.p.rsi_period}, RSI Mid {self.p.rsi_midline:.1f}, MA {self.p.short_ma_period}/{self.p.long_ma_period}) Ending Value {self.broker.getvalue():.2f}', doprint=True)

Code Breakdown:

  1. params: This tuple defines the strategy’s configurable parameters:
    • rsi_period and rsi_midline control the RSI filter.
    • short_ma_period, long_ma_period, and ma_type control the primary MA Crossover signal.
    • printlog toggles detailed logging.
  2. __init__(self): The constructor sets up the necessary components:
    • It stores a reference to the closing price (self.dataclose).
    • Initializes variables for order tracking (self.order, etc.).
    • Instantiates the RelativeStrengthIndex indicator using the specified period.
    • Selects and instantiates the SimpleMovingAverage or ExponentialMovingAverage indicators based on the ma_type parameter.
    • Instantiates the CrossOver indicator, which directly compares the short and long MAs. This indicator simplifies checking for crossovers in the next method.
  3. log, notify_order, notify_trade: These are standard backtrader methods for handling logging, order status updates, and closed trade notifications. They provide visibility into the strategy’s execution during a backtest.
  4. next(self): This is the heart of the strategy, executed for each bar of data:
    • It first checks if an order is already pending (if self.order:).
    • It retrieves the latest RSI value (current_rsi = self.rsi[0]).
    • Entry Logic (if not self.position:):
      • Checks if the ma_crossover indicator shows a bullish cross (> 0). If yes, it then checks if current_rsi is above the rsi_midline. Only if both are true does it create a buy order.
      • Similarly, it checks for a bearish cross (< 0) and confirms with current_rsi below the rsi_midline before creating a sell order.
    • Exit Logic (else:):
      • If holding a long position (self.position.size > 0), it checks only for a bearish MA crossover (self.ma_crossover[0] < 0) to trigger a close order.
      • If holding a short position (self.position.size < 0), it checks only for a bullish MA crossover (self.ma_crossover[0] > 0) to trigger a close order.
  5. stop(self): This method is called when the backtest finishes, typically used to print final results or summaries, like the final portfolio value and the parameters used.

Why Use This Strategy?

Pasted image 20250503154553.png

Considerations

Conclusion

The RSITrendConfirmationStrategy offers a methodical way to enhance trend signals using momentum confirmation. As implemented here, it serves as a clear template for combining indicators within the backtrader framework.

Also, its structure, relying on backtrader’s conventions (inheriting bt.Strategy, using params, standard methods) and clear parameterization, makes it an excellent example of how to code strategies for straightforward integration and efficient testing within the “Backtester” application. Once coded this way, the strategy code can be simply added, allowing users to leverage the easy GUI for configuration, execution, and analysis.