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