Quant BuffetRelax, Not Over Thinking

UK Top-10 Momentum Long-Short Equity Strategy

Log in to collect

Academic 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

Annualised return32%
Beta0.211
Sharpe ratio-0.16
Maximum drawdown86.2%
Win rate50%

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"))