
The strategy selects top 5 ETFs by 6-month momentum, optimizes weights via minimum variance, targets 8% volatility, and rebalances weekly using historical volatilities and correlations.
ASSET CLASS: ETFs | REGION: Global | FREQUENCY:
Monthly | MARKET: bonds, commodities, equities | KEYWORD: Adaptive, Asset, Allocation
I. STRATEGY IN A NUTSHELL
A diversified ETF strategy selecting top 5 asset classes by momentum, weighted via minimum variance optimization, and volatility-targeted at 8%, rebalanced weekly for adaptive risk-adjusted performance.
II. ECONOMIC RATIONALE
The strategy harnesses the momentum anomaly and enhances risk-adjusted returns using short-term variance optimization, exploiting more stable short-term correlations and volatilities for efficient portfolio construction.
III. SOURCE PAPER
Adaptive Asset Allocation: A Primer [Click to Open PDF]
Adam Butler, Mike Philbrick, Rodrigo Gordillo, ReSolve Asset Management
<Abstract>
The paper addresses flaws in the traditional application of Modern Portfolio Theory related to Strategic Asset Allocation. Estimates of parameters for portfolio optimization based on long-term observed average values are shown to be inferior to alternative estimates based on observations over much shorter time frames. An Adaptive Asset Allocation portfolio assembly framework is then proposed to coherently integrate portfolio parameters in a way that delivers substantially improved performance relative to SAA over the testing horizon.


IV. BACKTEST PERFORMANCE
| Annualised Return | 15.1% |
| Volatility | 9.4% |
| Beta | 0.611 |
| Sharpe Ratio | 1.61 |
| Sortino Ratio | 0.228 |
| Maximum Drawdown | -8.8% |
| Win Rate | 54% |
V. FULL PYTHON CODE
from AlgorithmImports import *
# The investment universe consists of 10 ETFs which are proxy for the main asset classes (US Stocks, European Stocks, Japanese Stocks, EM Stocks, US REITs,
# International REITs, US Intermediate Treasuries, US Long-term Treasuries, Commodities, and Gold). Each week, the asset classes are sorted based on their
# 6-month momentum. Only the top 5 assets are then used in the next step, when the investor uses the minimum variance calculation to compute the weights
# for each asset class for the next week. 60-day historical volatilities and correlations are used in the minimum variance computation. The overall portfolio
# volatility prediction is then estimated (based on the weights from the min. variance algorithm and on historical volatilities and correlations)
# and the portfolio is rescaled to target 8% volatility. These steps are performed every week and the portfolio is rebalanced accordingly.
import pandas as pd
import numpy as np
from scipy.optimize import minimize
class AdaptiveAssetAllocation(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2008, 1, 1)
self.SetCash(100000)
self.symbols = ['SPY', 'VGK', 'EWJ', 'EEM', 'VNQ', 'RWX', 'IEI', 'IEF', 'DBC', 'GLD']
self.period = 6 * 21
self.data = {}
for symbol in self.symbols:
self.AddEquity(symbol, Resolution.Daily)
self.data[symbol] = SymbolData(self.period)
self.Schedule.On(self.DateRules.WeekStart(self.symbols[0]), self.TimeRules.AfterMarketOpen(self.symbols[0]), self.Rebalance)
def OnData(self, data):
for symbol in self.data:
symbol_obj = self.Symbol(symbol)
if symbol_obj in data and data[symbol_obj]:
self.data[symbol].update(data[symbol_obj].Value)
def Rebalance(self):
self.Liquidate()
ret_data = { x : self.data[x].performance() for x in self.symbols if self.data[x].is_ready()}
# Performance sorting.
if len(ret_data) == 0: return
sorted_by_ret = sorted(ret_data.items(), key = lambda x: x[1], reverse = True)
top_symbols = [x[0] for x in sorted_by_ret[:5]]
# Optimalization
data = {}
for symbol in top_symbols:
closes = [x for x in self.data[symbol].price]
data[symbol] = closes[:60]
df_price = pd.dataframe(data, columns=data.keys())
daily_return = (df_price / df_price.shift(1)).dropna()
a = PortfolioOptimization(daily_return, 0, len(data))
opt_weight = a.opt_portfolio()
for i in range(len(data)):
# Weight is higher than minumum QC supported weight.
if opt_weight[i] > 0.001:
self.SetHoldings(df_price.columns[i], opt_weight[i])
class PortfolioOptimization(object):
def __init__(self, df_return, risk_free_rate, num_assets):
self.daily_return = df_return
self.risk_free_rate = risk_free_rate
self.n = num_assets # numbers of risk assets in portfolio
self.target_vol = 0.08
def annual_port_return(self, weights):
# calculate the annual return of portfolio
return np.sum(self.daily_return.mean() * weights) * 252
def annual_port_vol(self, weights):
# calculate the annual volatility of portfolio
return np.sqrt(np.dot(weights.T, np.dot(self.daily_return.cov() * 252, weights)))
def min_func(self, weights):
# method 1: maximize sharp ratio
#return - self.annual_port_return(weights) / self.annual_port_vol(weights)
# method 2: maximize the return with target volatility
# return - self.annual_port_return(weights) / self.target_vol
# method 3: minimize variance with target volatility
return (1 / self.annual_port_vol(weights)) / self.target_vol
def opt_portfolio(self):
# maximize the sharpe ratio to find the optimal weights
cons = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bnds = tuple((0, 1) for x in range(2)) + tuple((0, 0.25) for x in range(self.n - 2))
opt = minimize(self.min_func, # object function
np.array(self.n * [1. / self.n]), # initial value
method='SLSQP', # optimization method
bounds=bnds, # bounds for variables
constraints=cons) # constraint conditions
opt_weights = opt['x']
return opt_weights
class SymbolData():
def __init__(self, period):
self.price = RollingWindow[float](period)
def update(self, value):
self.price.Add(value)
def is_ready(self) -> bool:
return self.price.IsReady
def performance(self, values_to_skip = 0) -> float:
closes = [x for x in self.price][values_to_skip:]
return (closes[0] / closes[-1] - 1)
VI. Backtest Performance