Overall Statistics
Total Trades
607
Average Win
0.70%
Average Loss
-0.96%
Compounding Annual Return
9.393%
Drawdown
26.700%
Expectancy
0.423
Net Profit
255.214%
Sharpe Ratio
0.65
Probabilistic Sharpe Ratio
5.074%
Loss Rate
18%
Win Rate
82%
Profit-Loss Ratio
0.74
Alpha
0.022
Beta
0.463
Annual Standard Deviation
0.108
Annual Variance
0.012
Information Ratio
-0.29
Tracking Error
0.116
Treynor Ratio
0.152
Total Fees
$1602.61
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(2009, 1, 1)  # Set Start 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']
        # Add assets
        self.eqs = list(set(self.selO+self.selD+self.selP))
        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)
        # % equity offensive 
        pct_in = 1-n_canary

        # =====================================
        # get weights for offensive and defensive universes
        # =====================================
        # determine weights of offensive universe
        if pct_in > 0:
            # price / SMA
            mom_in = mom_avg.loc[self.selO].sort_values(ascending=False)
            # equal weightings to top relative momentum securities
            in_wts = pd.Series(pct_in/self.TO,index=mom_in.index[:self.TO])
            wts = pd.concat([wts,in_wts])
        # determine weights of defensive universe
        if pct_in < 1:
            # price / SMA
            mom_out = mom_avg.loc[self.selD].sort_values(ascending=False)
            # equal weightings to top relative momentum securities
            out_wts = pd.Series((1-pct_in)/self.TD,index=mom_out.index[:self.TD])
            wts = pd.concat([wts,out_wts])     
        
        wts = wts.groupby(wts.index).sum()

        return wts