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.
The core idea is simple yet powerful:
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)
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.__init__(self)
: The constructor sets
up the necessary components:
self.dataclose
).self.order
,
etc.).RelativeStrengthIndex
indicator using
the specified period.SimpleMovingAverage
or
ExponentialMovingAverage
indicators based on the
ma_type
parameter.CrossOver
indicator, which directly
compares the short and long MAs. This indicator simplifies checking for
crossovers in the next
method.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.next(self)
: This is the heart of the
strategy, executed for each bar of data:
if self.order:
).current_rsi = self.rsi[0]
).if not self.position:
):
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.< 0
) and
confirms with current_rsi
below the
rsi_midline
before creating a sell order.else:
):
self.position.size > 0
),
it checks only for a bearish MA crossover
(self.ma_crossover[0] < 0
) to trigger a close
order.self.position.size < 0
), it checks only for a
bullish MA crossover (self.ma_crossover[0] > 0
) to
trigger a close order.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.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.