| Overall Statistics |
|
Total Trades 652 Average Win 0.64% Average Loss -0.81% Compounding Annual Return 7.620% Drawdown 26.700% Expectancy 0.399 Net Profit 204.004% Sharpe Ratio 0.559 Probabilistic Sharpe Ratio 1.834% Loss Rate 22% Win Rate 78% Profit-Loss Ratio 0.79 Alpha 0.035 Beta 0.297 Annual Standard Deviation 0.103 Annual Variance 0.011 Information Ratio -0.132 Tracking Error 0.149 Treynor Ratio 0.194 Total Fees $1578.27 Estimated Strategy Capacity $1700000.00 Lowest Capacity Asset BIL TT1EBZ21QWKL |
'''
Hybrid Asset Allocation by Wouter Keller
See: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4346906
This is the "balanced" version from the paper
'''
# region imports
from AlgorithmImports import *
import numpy as np
# endregion
class HAA(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2008, 1, 1) # Set Start Date
# self.SetEndDate(2022, 12, 31) # Set End Date
self.SetCash(100000) # Set Strategy Cash
# Asset lists
self.selO = ['SPY','IWM','VWO','VEA','VNQ','DBC','IEF','TLT']
self.selD = ['BIL', 'IEF']
self.selP = ['TIP']
self.safe = ['BIL']
# Add assets
self.eqs = list(set(self.selO+self.selD+self.selP+self.safe))
for eq in self.eqs:
self.AddEquity(eq,Resolution.Minute)
# Algo Parameters
self.TO, self.TD = [4,1]
self.mom_prds = [1,3,6,12] #1, 3, 6, and 12 months.
self.hprd = np.max(self.mom_prds)*35
# Scheduler
self.Schedule.On(self.DateRules.MonthStart(self.selO[0]),
self.TimeRules.AfterMarketOpen(self.selO[0],30),
self.rebal)
self.Trade = True
def rebal(self):
self.Trade = True
def OnData(self, data):
if self.Trade:
# Get price data and trading weights
h = self.History(self.eqs,self.hprd,Resolution.Daily)['close'].unstack(level=0)
wts = self.trade_wts(h)
# trade
port_tgt = [PortfolioTarget(x,y) for x,y in zip(wts.index,wts.values)]
self.SetHoldings(port_tgt)
self.Trade = False
def trade_wts(self,hist):
# initialize wts Series
wts = pd.Series(0,index=hist.columns)
# end of month values
h_eom = (hist.loc[hist.groupby(hist.index.to_period('M')).apply(lambda x: x.index.max())]
.iloc[:-1,:])
# =====================================
# check if canary universe is triggered
# =====================================
# build dataframe of momentum values
mom = h_eom.iloc[-1,:].div(h_eom.iloc[[-p-1 for p in self.mom_prds],:],axis=0)-1
mom_avg = mom.mean(axis=0)
# Determine number of canary securities with negative momentum
n_canary = np.sum((mom_avg[self.selP]<0)*1)/len(self.selP)
# % equity offensive
pct_in = 1-n_canary
# cash return
pct_safe = 0
# cash_r = mom_avg[self.safe][0]
# =====================================
# get weights for offensive and defensive universes
# =====================================
# determine weights of offensive universe
if pct_in > 0:
# Avg MOM
mom_in = mom_avg.loc[self.selO].sort_values(ascending=False).iloc[:self.TO]
mom_in = mom_in[mom_in>0]
# equal weightings to top relative momentum securities
in_wts = pd.Series(pct_in/self.TO,index=mom_in.index)
pct_safe = 1-mom_in.shape[0]/self.TO
wts = pd.concat([wts,in_wts])
# determine weights of defensive universe
if pct_in < 1:
# Avg MOM
mom_out = mom_avg.loc[self.selD].sort_values(ascending=False).iloc[:self.TD]
# equal weightings to top relative momentum securities
out_wts = pd.Series((1-pct_in)/self.TD,index=mom_out.index)
wts = pd.concat([wts,out_wts])
# cash portion
wts[wts.index.str.startswith(self.safe[0])] += pct_safe
wts = wts.groupby(wts.index).sum()
return wts