Quant BuffetRelax, Not Over Thinking

Momentum Stock Picking Strategy Using RSI Indicator

Log in to collect

Academic paper

Strategy in a nutshell

The strategy selects S&P 500 stocks using a 14-day RSI to identify those in a bull range (RSI 40–100 over 25 days). A momentum filter requires the highest 14-day RSI to exceed 70. Qualified stocks are equally weighted in the portfolio, which is rebalanced daily.

Economic rationale

RSI, a momentum oscillator, reliably indicates trend strength: above 50 signals uptrends, below 50 signals downtrends. Maintaining RSI above 40 during pullbacks confirms trend consistency, while drops below 40 indicate weakening. This makes RSI effective for trend-following strategies, identifying stocks with stable upward momentum.

Backtest performance

Annualised return2.29%
Beta0.972
Sortino ratio0.502
Win rate72%

Full Python code

from AlgorithmImports import *
class MomentumStockPickingRSIIndicator(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.UniverseSettings.Resolution = Resolution.Daily
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
self.AddUniverse(self.Universe.QC500)   # monthly selected proxy SP500 universe

self.rsi = {}
self.rsi_history = {}
self.period = 25

# monthly updated stock selection
self.selection = []

symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
    symbol = security.Symbol
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(10)
    
    if symbol not in self.selection:
        self.selection.append(symbol)

for security in changes.RemovedSecurities:
    symbol = security.Symbol
    if symbol in self.selection:
        self.selection.remove(symbol)

def OnData(self, data):
long = []

# calculate signal
for symbol in self.selection:
    if not symbol in self.rsi:
        self.rsi[symbol] = self.RSI(symbol, 14, MovingAverageType.Simple, Resolution.Daily)
        self.rsi_history[symbol] = RollingWindow[float](self.period)

    if not self.rsi[symbol].IsReady: continue

    self.rsi_history[symbol].Add(self.rsi[symbol].Current.Value)
    
    if self.rsi_history[symbol].IsReady:
        rsi_values = [x for x in self.rsi_history[symbol]]
        max_rsi = max(rsi_values)
        bull_range = sum([x > 40 for x in rsi_values]) == self.period
        
        if max_rsi > 70 and bull_range:
            long.append(symbol)

# liquidate
invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
    if symbol not in long:
        self.Liquidate(symbol)
# open new trades
long_count = len(long)

for symbol in long:
    if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable:
        self.SetHoldings(symbol, 1 / long_count)
    
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))