Multi-Asset Market Breadth Momentum
Log in to collectAcademic paper
Protective Asset Allocation (PAA): A Simple Momentum-Based Alternative for Term Deposits
Wouter J. Keller; Jan Willem Keuning
- NLVrije Universiteit Amsterdam
- ?VU University Amsterdam
- ?TrendXplorer
Strategy in a nutshell
The strategy invests in 12 ETFs across multiple asset classes, including:
U.S. equity: SPY, QQQ, IWM
Global equity: VGK, EWJ
Emerging markets: EEM
Alternative assets: GSG, GLD, IYR
Bonds: HYG, LQD, TLT, IEF
Selection is driven by a 12-month momentum indicator (MOM > 0). The process involves:
Bond fraction (BF): Calculated with a protection factor of 2 to determine defensive exposure.
Risky portfolio construction: The top 6 assets with positive momentum are chosen.
Allocation rule: Risky assets are equally weighted, with weights scaled according to (1−BF)/BF(1 - BF) / BF(1−BF)/BF.
Economic rationale
This approach builds on the dual-momentum framework but increases resilience during bear markets. By systematically shifting more capital into bonds and defensive assets during downturns, it delivers lower drawdowns and volatility, even if raw returns are slightly lower than traditional dual momentum. The result is a smoother equity curve and stronger risk-adjusted performance
Backtest performance
Full Python code
from AlgorithmImports import *
from collections import deque
class MultiAssetMarketBreadthMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2008, 1, 1)
self.SetCash(100_000)
self.symbols: List[str] = [
'SPY', 'QQQ', 'IWM', 'VGK', 'EWJ', 'EEM', 'GSG', 'GLD', 'IYR', 'HYG', 'LQD', 'TLT'
]
self.safe_bond: str = 'IEF'
period: int = 12 * 21
self.data: Dict[str, deque] = {}
self.sma: Dict[str, SimpleMovingAverage] = {}
for symbol in self.symbols:
self.AddEquity(symbol, Resolution.Daily)
self.data[symbol] = deque(maxlen = period)
self.sma[symbol] = self.SMA(symbol, period, Resolution.Daily)
history: DataFrame = self.History(self.Symbol(symbol), period, Resolution.Daily)
if not history.empty:
closes: Series = history.loc[symbol].close
for time, close in closes.items():
self.sma[symbol].Update(time, close)
self.AddEquity(self.safe_bond, Resolution.Daily)
self.last_month: int = -1
def OnData(self, slice: Slice) -> None:
if self.last_month == self.Time.month:
return
self.last_month = self.Time.month
mom: Dict[str, float] = {}
for symbol in self.symbols:
symbol_obj: Symbol = self.Symbol(symbol)
# SMA data is ready.
if self.sma[symbol].IsReady:
if symbol_obj in slice.Bars:
price: float = slice.Bars[symbol_obj].Value
if price != 0:
mom[symbol] = price / self.sma[symbol].Current.Value - 1
else:
return # Wait for every asset.
if len(mom) != 0:
# Bond fraction calc.
good_assets: List = sorted([x[0] for x in mom.items() if x[1] > 0], key = lambda x: x[1], reverse = True)[:6]
N: int = len(self.symbols)
n: int = len(good_assets)
a: int = 2
n1: float = a*N/4
BF: float = (N-n)/(N-n1)
bond_share: float = (1-BF)/BF
# Trade execution.
self.Liquidate()
# Risky part.
# "leverage" ratio in case there's two parts of portfolio - risky as well as safe part.
ratio: float = 0.5 if bond_share != 0 else 1
for symbol in good_assets:
self.SetHoldings(symbol, ratio * (1/n))
# Bond part.
self.SetHoldings(self.safe_bond, ratio * bond_share)