Quant BuffetRelax, Not Over Thinking

Multi-Asset Market Breadth Momentum

Log in to collect

Academic paper

Protective Asset Allocation (PAA): A Simple Momentum-Based Alternative for Term Deposits

AuthorsWouter J. Keller; Jan Willem Keuning

Institute
  • 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

Annualised return10.5%
Volatility7.9%
Beta0.465
Sharpe ratio1
Sortino ratio0.28
Maximum drawdown-8.8%
Win rate66%

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)