Problem Description
Solution
Here is a complete, fully functional Python script for a Relative Strength Index (RSI) Mean Reversion trading bot on QuantConnect.
Strategy Overview
This strategy implements a classic Mean Reversion logic:
- Indicator: Uses a 14-period RSI on the daily timeframe.
- Entry Signal (Long): When the RSI drops below 30 (Oversold), the asset is considered undervalued, and the bot buys 100% of the portfolio.
- Exit Signal: When the RSI rises above 70 (Overbought), the asset is considered overvalued, and the bot liquidates the position.
- Asset: SPY (S&P 500 ETF).
QuantConnect Strategy Code
# region imports
from AlgorithmImports import *
# endregion
class RsiMeanReversionBot(QCAlgorithm):
def initialize(self):
"""
Initialize the algorithm settings, assets, and indicators.
"""
# 1. Set Backtest Parameters
self.set_start_date(2018, 1, 1) # Start Date
self.set_end_date(2023, 1, 1) # End Date
self.set_cash(100000) # Starting Cash
# 2. Add Assets
# We use Resolution.DAILY for this example, but Resolution.MINUTE is also common.
self.ticker = "SPY"
self.equity = self.add_equity(self.ticker, Resolution.DAILY)
self.symbol = self.equity.symbol
# 3. Define Strategy Parameters
self.rsi_period = 14
self.oversold_threshold = 30
self.overbought_threshold = 70
# 4. Initialize Indicators
# We use the built-in RSI helper method.
# MovingAverageType.WILDERS is the standard smoothing for RSI.
self.rsi_indicator = self.rsi(self.symbol, self.rsi_period, MovingAverageType.WILDERS, Resolution.DAILY)
# 5. Set WarmUp
# Automatically feed historical data to ready the indicator before the start date
self.set_warm_up(self.rsi_period)
# 6. Setup Charts (Optional but recommended for visualization)
chart = Chart("RSI Analysis")
chart.add_series(Series("RSI", SeriesType.LINE, 0))
chart.add_series(Series("Overbought", SeriesType.LINE, 0))
chart.add_series(Series("Oversold", SeriesType.LINE, 0))
self.add_chart(chart)
def on_data(self, data: Slice):
"""
Event handler for new data packets.
"""
# Ensure the indicator is ready before trading
if self.is_warming_up or not self.rsi_indicator.is_ready:
return
# Ensure data exists for the symbol in the current slice
if self.symbol not in data:
return
# Get current RSI value
current_rsi = self.rsi_indicator.current.value
# Plotting for visualization
self.plot("RSI Analysis", "RSI", current_rsi)
self.plot("RSI Analysis", "Overbought", self.overbought_threshold)
self.plot("RSI Analysis", "Oversold", self.oversold_threshold)
# --- Trading Logic ---
# Check if we are not currently invested
if not self.portfolio.invested:
# Buy Signal: RSI is Oversold (< 30)
if current_rsi < self.oversold_threshold:
self.set_holdings(self.symbol, 1.0) # Allocate 100% of portfolio
self.debug(f"BUY {self.ticker} at {self.time} | Price: {data[self.symbol].close} | RSI: {current_rsi:.2f}")
# Check if we are currently invested
else:
# Sell Signal: RSI is Overbought (> 70)
if current_rsi > self.overbought_threshold:
self.liquidate(self.symbol) # Sell all holdings
self.debug(f"SELL {self.ticker} at {self.time} | Price: {data[self.symbol].close} | RSI: {current_rsi:.2f}")
def on_end_of_algorithm(self):
"""
Called at the end of the backtest.
"""
self.log(f"Final Portfolio Value: {self.portfolio.total_portfolio_value}")
Code Breakdown
-
initializeMethod:set_warm_up: This is crucial for indicator-based strategies. It processes data prior to theStartDateso that on the very first day of the backtest, the RSI indicator already has a valid value (is "ready").self.rsi(...): This helper creates the indicator and automatically registers it to receive data updates. We useMovingAverageType.WILDERSbecause the standard RSI formula uses Wilder's Smoothing.
-
on_dataMethod:self.is_warming_up: Prevents the algorithm from trading while it is still processing the warm-up data.self.portfolio.invested: A boolean property that checks if you currently hold any quantity of the asset.set_holdings(symbol, 1.0): A helper function that calculates the number of shares required to allocate 100% of your buying power to this specific asset.liquidate(symbol): Closes all positions (long or short) for the specified symbol.
-
Visualization:
- The code includes
self.plotcommands. When you run this backtest, you will see a custom chart below the equity curve showing the RSI oscillating between the Overbought (70) and Oversold (30) lines.
- The code includes
Frequently Asked Questions (Q&A)
Q: How can I change the timeframe from Daily to Minute?
A: In the initialize method, change Resolution.DAILY to Resolution.MINUTE in both the add_equity call and the self.rsi indicator definition. Note that RSI signals on minute data will be much more frequent and may result in higher transaction costs.
Q: Can I use this strategy for Shorting?
A: Yes. To short the market, you would modify the logic to self.set_holdings(self.symbol, -1.0) when the RSI is Overbought (> 70) and liquidate when it returns to neutral or becomes Oversold.
Q: Why is MovingAverageType.WILDERS used?
A: The original Relative Strength Index developed by J. Welles Wilder uses Wilder's Smoothing (a specific type of Exponential Moving Average). While you can use Simple or Exponential moving averages, Wilder's is the standard calculation found on most trading platforms (like TradingView).
Q: How do I add a Stop Loss?
A: You can add a stop loss by using self.stop_market_order immediately after placing your entry trade, or by checking the price in on_data and calling self.liquidate if the price drops below a certain percentage relative to your average entry price (self.portfolio[self.symbol].average_price).