Long-Term Reversal Combined with a Momentum Effect in Industry Portfolios
Log in to collectAcademic paper
Industry Long-Term Return Reversal
Graham N. Bornholt; Omar Gharaibeh; Mirela Malin
- Griffith University
- ?Griffith University - Griffith Business School
- ?Griffith University - Department of Accounting, Finance and Economics
Strategy in a nutshell
This strategy trades 48 industry ETFs quarterly, buying long-term losers with strong recent performance and selling long-term winners with weak recent performance, capturing reversals within long-term performance extremes.
Economic rationale
Industries experience structural changes—like competition, consolidation, or regulatory shifts—that often go underrecognized by investors. These delayed reactions cause past losers to rebound and past winners to weaken, creating exploitable return reversals.
Backtest performance
Annualised return9.9%
Volatility19.49%
Beta0.165
Sharpe ratio0.3
Sortino ratio0.218
Win rate50%
Full Python code
from AlgorithmImports import *
class LongTermReversalCombinedWithMomentumEffectAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.tickers = [
"XLY", # Consumer Discretionary Select Sector SPDR Fund
"PBS", # Invesco Dynamic Media ETF
"PEJ", # Invesco Dynamic Leisure and Entertainment ETF
"PMR", # Invesco Dynamic Retail ETF
"XLP", # Consumer Staples Select Sector SPDR Fund
"PBJ", # Invesco Dynamic Food & Beverage ETF
"XLE", # Energy Select Sector SPDR Fund
"PBW", # Invesco WilderHill Clean Energy ETF
"PXE", # Invesco Dynamic Energy Exploration & Production ETF
"NLR", # VanEck Vectors Uranium+Nuclear Energy ETF
"AMJ", # JPMorgan Alerian MLP Index ETN
"XLF", # Financial Select Sector SPDR Fund
"KBE", # SPDR S&P Bank ETF
"KIE", # SPDR S&P Insurance ETF
"KRE", # SPDR S&P Regional Banking ETF
"PSP", # Invesco Global Listed Private Equity ETF
"XLV", # Health Care Select Sector SPDR Fund
"IBB", # iShares Nasdaq Biotechnology ETF
"IHF", # iShares U.S. Healthcare Providers ETF
"IHE", # iShares U.S. Pharmaceuticals ETF
"XLI", # Industrial Select Sector SPDR Fund
"ITA", # iShares U.S. Aerospace & Defense ETF
"IYT", # iShares Transportation Average ETF
"PHI", # Invesco Water Resources ETF
"XLB", # Materials Select Sector SPDR ETF
"MOO", # VanEck Vectors Agribusiness ETF
"GDX", # VanEck Vectors Gold Miners ETF
"XHB", # SPDR S&P Homebuilders ETF
"IGE", # iShares North American Natural Resources ETF
"XLK", # Technology Select Sector SPDR Fund
"FDN", # First Trust Dow Jones Internet Index
"SOXX", # iShares PHLX Semiconductor ETF
"IGV", # iShares Expanded Tech-Software Sector ET
"IYZ", # iShares U.S. Telecommunications ETF
"XLU", # Utilities Select Sector SPDR Fund
"IGF", # iShares Global Infrastructure ETF
]
self.data = {} # Storing closes
self.symbols = []
self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.current_month = 0
self.needed_months = 120
self.months_for_short_perf = 12
self.period = self.needed_months * 21
self.SetWarmUp(self.period)
for ticker in self.tickers:
security = self.AddEquity(ticker, Resolution.Daily)
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(5)
self.data[security.Symbol] = SymbolData(self.period)
self.symbols.append(security.Symbol)
self.selection_flag = False
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
def OnData(self, data):
for symbol in self.symbols:
if symbol in data:
if data[symbol]:
price = data[symbol].Value
self.data[symbol].update(price)
if self.IsWarmingUp: return
if not self.selection_flag:
return
self.selection_flag = False
performances = {}
for symbol in self.symbols:
if self.data[symbol].is_ready():
performances[symbol] = self.data[symbol].performance()
sorted_by_performance = [x[0] for x in sorted(performances.items(), key=lambda item: item[1])]
quartile = int(len(sorted_by_performance) / 4)
winners = sorted_by_performance[-quartile:]
losers = sorted_by_performance[:quartile]
short_term_perf_losers = {}
short_term_perf_winners = {}
for symbol in losers:
short_term_perf_losers[symbol] = self.data[symbol].short_term_performance(self.months_for_short_perf)
for symbol in winners:
short_term_perf_winners[symbol] = self.data[symbol].short_term_performance(self.months_for_short_perf)
short_term_losers_sort_by_perf = [x[0] for x in sorted(short_term_perf_losers.items(), key=lambda item: item[1])]
short_term_winners_sort_by_perf = [x[0] for x in sorted(short_term_perf_winners.items(), key=lambda item: item[1])]
quartile = int(len(short_term_winners_sort_by_perf) / 4)
short = short_term_winners_sort_by_perf[:quartile]
quartile = int(len(short_term_losers_sort_by_perf) / 4)
long = short_term_losers_sort_by_perf[-quartile:]
# Trade Execution
stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
if symbol not in long + short:
self.Liquidate(symbol)
long_length = len(long)
short_length = len(short)
for symbol in long:
if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable:
self.SetHoldings(symbol, 1 / long_length)
for symbol in short:
if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable:
self.SetHoldings(symbol, -1 / short_length)
def Selection(self):
self.current_month += 1
if self.current_month % 3 == 0: # Selection each quarter
self.selection_flag = True
class SymbolData():
def __init__(self, period):
self.Closes = RollingWindow[float](period)
def update(self, close):
self.Closes.Add(close)
def is_ready(self):
return self.Closes.IsReady
def performance(self):
closes = [x for x in self.Closes]
return (closes[0] - closes[-1]) / closes[-1]
def short_term_performance(self, months):
closes = [x for x in self.Closes][:21 * months] # Takes last months
return (closes[0] - closes[-1]) / closes[-1]
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))