Quant BuffetRelax, Not Over Thinking

Long-Term Reversal Combined with a Momentum Effect

Log in to collect

Academic paper

Long-Term Return Reversal: Evidence from International Market Indices

AuthorsMirela Malin; Graham N. Bornholt

Institute
  • Griffith University
  • ?Griffith University - Department of Accounting, Finance and Economics

Strategy in a nutshell

The strategy selects stocks from 26 emerging markets, ranking them by 60-month performance to identify late-stage winners (LW) and losers (LL). Within each group, countries are further sorted by 6-month momentum. The portfolio goes long on strong-momentum LL and short on weak-momentum LW, with equal weights, six-month holding periods, and monthly staggered rebalancing.

Economic rationale

The approach blends long-term reversal with short-term momentum. Momentum helps pinpoint losers more likely to recover and winners more likely to decline, enhancing return predictability

Backtest performance

Annualised return15.94%
Volatility28.89%
Beta-0.055
Sharpe ratio0.41
Sortino ratio-0.428
Win rate47%

Full Python code

from AlgorithmImports import *
from math import floor
class LongTermReversalCombinedwithaMomentumEffect(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2004, 1, 1)
self.SetCash(100000)     

self.symbols = [
    "EWJ",  # iShares MSCI Japan Index ETF
    "EWA",  # iShares MSCI Australia ETF
    "EWG",  # iShares MSCI Germany ETF
    "EWU",  # iShares MSCI United Kingdom ETF
    "EWW",  # iShares MSCI Mexico Inv. Mt. Idx
    "EWS",  # iShares MSCI Singapore ETF
    "ERUS", # iShares MSCI Russia ETF
    "IVV",  # iShares S&P 500 Index
    "AAXJ", # iShares MSCI All Country Asia ex Japan Index ETF
    "EWQ",  # iShares MSCI France Index ETF
    "EWH",  # iShares MSCI Hong Kong Index ETF
    "EPI",  # WisdomTree India Earnings ETF
    "EIDO"  # iShares MSCI Indonesia Investable Market Index ETF
    "EWI",  # iShares MSCI Italy Index ETF
    "ENZL", # iShares MSCI New Zealand Investable Market Index Fund
    "NORW"  # Global X FTSE Norway 30 ETF
    "EWY",  # iShares MSCI South Korea Index ETF
    "EWP",  # iShares MSCI Spain Index ETF
    "EWD",  # iShares MSCI Sweden Index ETF
    "EWL",  # iShares MSCI Switzerland Index ETF
    "GXC",  # SPDR S&P China ETF
    "EWC",  # iShares MSCI Canada Index ETF
    "EWZ",  # iShares MSCI Brazil Index ETF
    "ARGT", # Global X FTSE Argentina 20 ETF
    "EWO",  # iShares MSCI Austria Investable Mkt Index ETF
    "EWK",  # iShares MSCI Belgium Investable Market Index ETF
    "ECH",  # iShares MSCI Chile Investable Market Index ETF
    "EGPT", # Market Vectors Egypt Index ETF
]
self.holding_period = 6

self.data = {}
self.managed_queue = []

self.long_period = 60*21
self.short_period = 6*21
self.SetWarmUp(self.long_period, Resolution.Daily)

for symbol in self.symbols:
    data = self.AddEquity(symbol, Resolution.Daily)
    data.SetLeverage(10)
    data.SetFeeModel(CustomFeeModel())
    
    self.data[symbol] = SymbolData(symbol, self.long_period)

self.Schedule.On(self.DateRules.MonthStart(self.symbols[0]), self.TimeRules.AfterMarketOpen(self.symbols[0]), self.Rebalance)

def OnData(self, data):
for symbol in self.data:
    symbol_obj = self.Symbol(symbol)
    if symbol_obj in data and data[symbol_obj]:
        self.data[symbol].update(data[symbol_obj].Value)
                
def Rebalance(self):
# momentum pair - long and short period momentum.
momentum = {
    x : (self.data[x].performance(self.long_period), self.data[x].performance(self.short_period)) for x in self.symbols if self.data[x].is_ready()
}

long = []
short = []
if len(momentum) != 0:
    sorted_long_mom = sorted(momentum.items(), key = lambda x: x[1][0])
    quartile = floor(len(sorted_long_mom) / 4)
    long_winners = sorted_long_mom[-quartile:]
    long_loosers = sorted_long_mom[:quartile]
    
    short_term_n = 3
    long_winners_sorted_short_mom = sorted(long_winners, key = lambda x: x[1][1])
    short = [x[0] for x in long_winners_sorted_short_mom][:short_term_n]
    
    long_loosers_sorted_short_mom = sorted(long_loosers, key = lambda x: x[1][1])
    long = [x[0] for x in long_loosers_sorted_short_mom][-short_term_n:]

    long_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(long)
    short_w = self.Portfolio.TotalPortfolioValue / self.holding_period / len(short)
    
    # symbol/quantity collection
    long_symbol_q = [(x, floor(long_w / self.Securities[x].Price)) for x in long]
    short_symbol_q = [(x, -floor(short_w / self.Securities[x].Price)) for x in short]

    self.managed_queue.append(RebalanceQueueItem(long_symbol_q + short_symbol_q))
    
if len(self.managed_queue) == 0: return

remove_item = None

# Rebalance portfolio
for item in self.managed_queue:
    if item.holding_period == self.holding_period:
        
        # liquidate
        for symbol, quantity in item.symbol_q:
            self.MarketOrder(symbol, -quantity)
        remove_item = item
        
    elif item.holding_period == 0:
        opened_symbol_q = []
        
        for symbol, quantity in item.symbol_q:
            self.MarketOrder(symbol, quantity)
            opened_symbol_q.append((symbol, quantity))
                    
        # Only opened orders will be closed        
        item.symbol_q = opened_symbol_q
        
    item.holding_period += 1
    
# We need to remove closed part of portfolio after loop. Otherwise it will miss one item in self.managed_queue.
if remove_item:
    self.managed_queue.remove(remove_item)
class RebalanceQueueItem():
def __init__(self, symbol_q):
# symbol/quantity collections
self.symbol_q = symbol_q
self.holding_period = 0
class SymbolData():
def __init__(self, symbol, period):
self.Symbol = symbol
self.Price = RollingWindow[float](period)

def update(self, value):
self.Price.Add(value)

def is_ready(self) -> bool:
return self.Price.IsReady

def performance(self, days_to_count_in) -> float:
closes = [x for x in self.Price][:days_to_count_in]
return (closes[0] / closes[-1] - 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"))