UK Top-10 Momentum Long-Short Equity Strategy
Log in to collectAcademic paper
Strategy in a nutshell
The investment universe consists of all UK listed companies (this is the investment universe used in the source academic study, and it could be easily changed into any other market – see Ammann, Moellenbeck, Schmid: Feasible Momentum Strategies in the US Stock Market). Stocks with the lowest market capitalization (25% of the universe) are excluded due to liquidity reasons. Momentum profits are calculated by ranking companies based on their stock market performance over the previous 12 months (the rank period). The investor goes long in the ten stocks with the highest performance and goes short in the ten stocks with the lowest performance. The portfolio is equally weighted and rebalanced yearly. We assume the investor has an account size of 10 000 pounds.
Economic rationale
Academic studies show strong support for momentum effects. The main reasons for anomaly persistence are behavioral biases like investor herding, investor over and underreaction, and confirmation bias. Another natural interpretation of momentum profits is that stocks underreact to information. For example, if a firm releases good news, and the stock price only reacts partially to the good news, then buying the stock after the initial release of the news will generate profits.
Backtest performance
Full Python code
from AlgoLib import *
class MomentumEffectinStocksinSmallPortfolios(XXX):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
self.coarse_count = 500
self.long = []
self.short = []
# Daily data.
self.data = {}
self.period = 12 * 21
self.quantile = 10
self.leverage = 5
self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.selection_flag = True
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.month = 11
self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def CoarseSelectionFunction(self, coarse):
# Update the rolling window every day.
for stock in coarse:
symbol = stock.Symbol
if symbol in self.data:
# Store daily price.
self.data[symbol].update(stock.AdjustedPrice)
# Selection once a month.
if not self.selection_flag:
return Universe.Unchanged
# selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa']
selected = [x.Symbol
for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'],
key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]]
# Warmup price rolling windows.
for symbol in selected:
if symbol in self.data:
continue
self.data[symbol] = SymbolData(symbol, self.period)
history = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
self.Log(f"Not enough data for {symbol} yet")
continue
closes = history.loc[symbol].close
for time, close in closes.iteritems():
self.data[symbol].Price.Add(close)
return [x for x in selected if self.data[x].is_ready()]
def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0]
# if len(fine) > self.coarse_count:
# sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True)
# top_by_market_cap = sorted_by_market_cap[:self.coarse_count]
# else:
# top_by_market_cap = fine
# Performance sorting.
performance = {x.Symbol : self.data[x.Symbol].performance() for x in fine}
if len(performance) >= self.quantile:
decile = int(len(performance) / self.quantile)
sorted_by_perf = sorted(performance.items(), key = lambda x: x[1], reverse = True)
self.long = [x[0] for x in sorted_by_perf[:decile]]
self.short = [x[0] for x in sorted_by_perf[-decile:]]
return self.long + self.short
def OnData(self, data):
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution.
long_count = len(self.long)
short_count = len(self.short)
invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in self.long + self.short:
self.Liquidate(symbol)
for symbol in self.long:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, 1 / long_count)
for symbol in self.short:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, -1 / short_count)
def Selection(self):
# Rebalance every 12 months.
if self.month == 12:
self.selection_flag = True
self.month += 1
if self.month > 12:
self.month = 1
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):
return self.Price.IsReady
def performance(self):
closes = [x for x in self.Price]
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"))