Keller’s & Keunig’s Defensive Asset Allocation
Log in to collectAcademic paper
Breadth Momentum and the Canary Universe: Defensive Asset Allocation (DAA)
Wouter J. Keller; Jan Willem Keuning
- NLVrije Universiteit Amsterdam
- ?VU University Amsterdam
- ?TrendXplorer
Strategy in a nutshell
DAA invests across three universes: risky (12 global ETFs), protective/canary (VWO and BND), and cash (BIL, IEF, LQD). Using 13612W momentum, the number of “bad” canary assets (b) determines cash allocation: CF = b/B (max 100%). Depending on canary signals, the portfolio splits between top T risky assets and cash, with equal-weighting. Rebalancing is monthly.
Economic rationale
Canary assets serve as early warning indicators. Bad momentum in VWO/BND signals potential market downturns, likely reflecting emerging market sensitivity, currency effects, or broader market stress. The approach reduces downside risk while maintaining exposure to top-performing risky assets
Backtest performance
Full Python code
from AlgorithmImports import *
from typing import List, Dict
import numpy as np
# endregion
class DefensiveAssetAllocation(QCAlgorithm):
def Initialize(self):
self.SetCash(100000)
self.SetStartDate(2008, 1, 1)
self.risky_topT:int = 6
self.half_risky_topT:int = 3
self.risky_universe:List[str] = [
"SPY", "IWM",
"QQQ", "VGK",
"EWJ", "VWO",
"VNQ", "GSG",
"GLD", "TLT",
"HYG", "LQD",
]
self.cash_universe:List[str] = ["BIL", "IEF", "LQD"]
self.canary_universe:List[str] = ["VWO", "BND"]
self.all_tickers:List[str] = self.risky_universe + self.cash_universe + self.canary_universe
self.period_list:List[int] = [21, 63, 126, 252]
self.objective_score_weights:np.ndarray = [12., 4., 2., 1.]
self.SetWarmUp(max(self.period_list), Resolution.Daily)
self.momp_data_by_ticker:Dict[str, List[MomentumPercent]] = {}
# subscribe data
for ticker in self.all_tickers:
self.AddEquity(ticker, Resolution.Daily)
self.momp_data_by_ticker[ticker] = [self.MOMP(ticker, period, Resolution.Daily) for period in self.period_list]
self.recent_month:int = -1
def OnData(self, data:Slice) -> None:
if self.IsWarmingUp:
return
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
# rank all the risky and cash symbol groups by momentum score
sorted_risky:List = sorted([item for item in self.momp_data_by_ticker.items() if item[0] in self.risky_universe and
all(indicator.IsReady for indicator in item[1])], # all indicators for ticker are ready
key=lambda x: np.dot(np.array([momentum.Current.Value for momentum in x[1]]), self.objective_score_weights),
reverse=True)
best_growth:List[str] = [x[0] for x in sorted_risky[:self.risky_topT]]
second_best_growth:List[str] = best_growth[:self.half_risky_topT]
sorted_cash:List = sorted([item for item in self.momp_data_by_ticker.items() if item[0] in self.cash_universe and
all(indicator.IsReady for indicator in item[1])], # all indicators for ticker are ready
key=lambda x: np.dot(np.array([momentum.Current.Value for momentum in x[1]]), self.objective_score_weights),
reverse=True)
if len(sorted_cash) < 1:
self.Liquidate()
return
best_cash:str = sorted_cash[0][0]
# calculate the momentum score of the canary symbols
canary_scores:List[float] = [np.dot(np.array([momentum.Current.Value for momentum in item[1]]), self.objective_score_weights) \
for item in self.momp_data_by_ticker.items() if item[0] in self.canary_universe and \
all(indicator.IsReady for indicator in item[1])]
if len(canary_scores) != 2:
self.Liquidate()
return
weight:Dict[str, float] = {}
if all(x < 0 for x in canary_scores):
weight[best_cash] = 1.
elif all(x > 0 for x in canary_scores):
weight = { x : 1. / float(len(best_growth)) for x in best_growth}
elif any(x < 0 for x in canary_scores):
weight[best_cash] = .5
for x in second_best_growth:
weight[x] = .5 / float(len(second_best_growth))
# liquidate
invested:List[str] = [x.Symbol.Value for x in self.Portfolio.Values if x.Invested]
for ticker in invested:
if ticker not in weight:
self.Liquidate(ticker)
# new trade execution / rebalance
for ticker, w in weight.items():
if ticker in data and data[ticker]:
self.SetHoldings(ticker, w)