投资宇宙由SPY、QQQ、IWM、VGK、EWJ、EEM、IYR、GSG、GLD和SHY组成。计算12个月的回报动量(MOM(12)),仅选择正动量资产(MOM(12)>0),最多6个。熊市中,通过多市场广度指标计算债券比例(BF),当正动量资产少于保护因子时,BF=100%。将良好资产构成风险部分,按等权重配置,风险投资组合与债券部分按(1-BF)/BF混合,最终组合为仅做多、等权重。SHY ETF替换非正动量资产。

策略概述

投资宇宙由SPY、QQQ、IWM、VGK、EWJ、EEM、IYR、GSG、GLD和SHY组成。我们计算12个月的回报动量(回溯期),公式如(1)所示:MOM(12) = p0/SMA(12) – 1,其中p0为最近的资产价格,SMA(12)为12个月的简单移动平均线过滤器。N=10表示风险投资宇宙中的资产数量,仅具有正动量的资产,即MOM(12)>0,n<=N,将被用于投资组合中,最多为Top=6。剩下的(N-n)资产(MOM<=0)为不良资产。

在熊市中,通过多市场广度指标计算最优债券比例(BF),公式如(2)所示:BF = (N-n)/(N-n1),其中n1 = a*N/4,a=2为保护因子(a>=0),因此当n<=n1时,BF=100%(进一步计算见图2)。具有最高动量的Top (Top<=N)良好资产构成风险(类股票)部分的投资组合,并为每个风险资产设置等权重(EW)。如果n < Top,只有n个良好资产(具有正动量)将被包括在此风险EW投资组合中。将风险EW投资组合与债券部分按(1-BF)/BF的方式混合,构建最终的投资组合;SHY ETF将替换非正动量的资产。因此,我们的投资组合为仅做多、等权重,除债券部分(其比例由公式(2)决定)外。

策略合理性

趋势跟随策略也称为绝对动量。动量和反转是相反的效应,取决于回溯期的长度。除了绝对动量外,还有相对(或横截面)动量。在这种情况下,动量是在资产之间进行比较,通常使用过去12个月的回报。“双重动量”是指基于12个月回报(高于无风险利率)的绝对动量和相对动量相结合的策略。在本文中,作者将保护性动量与基于SMA(简单移动平均线)的简单双重动量模型相结合,得出保护性资产配置(PAA)策略。选择了最具保护性的PAA策略(PAA2)。

论文来源

Protective Asset Allocation (PAA): A Simple Momentum-Based Alternative for Term Deposits [点击浏览原文]

<摘要>

自2008年金融危机和最近(2015年底)的回调以来,投资者一直在寻找风险较小的投资。因此,对低风险/绝对回报投资组合的需求不断增长。在本文中,我们描述了一种简单的双重动量模型(称为保护性资产配置或PAA),它具有强有力的“崩盘保护”,可能符合这一需求。这是一种传统60/40股票/债券投资组合的战术变体,其中最佳的股票/债券组合由双重动量和多市场广度决定。我们使用全球多资产ETF代理进行了回测。从1970年12月开始,允许我们研究PAA在加息周期中的表现。最具保护性的PAA策略变体的样本内(1970年12月-1992年12月)和样本外回报满足了我们的绝对回报要求,同时也没有牺牲高回报。这使PAA成为一年期定期存款的有吸引力替代方案。

回测表现

年化收益率12.2%
波动率6.8%
Beta0.169
夏普比率1.06
索提诺比率0.133
最大回撤-8.2%
胜率75%

完整python代码

from AlgorithmImports import *
#endregion

class KellersKeunigsPAA(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2000, 1, 1)  
        self.SetCash(100000)
        
        # Parameters for algorithm 
        self.lookback:int = 12 * 21                 # Lookback period (in months)
        self.protection:int = 2                     # Protection factor = 0 (low), 1, 2 (high)
        self.topM:int = 6                           # topM is the max number of equities
        self.n_levels:int = 2                       # number of discrete levels for bond_fraction (>=2)
        self.cash_universe:List[str] = ["SHY"]      # risk free asset to move into for protection 
        self.N_safe:int = int(len(self.cash_universe))

        self.risky_universe:List[str] = [
            "SPY", "QQQ", "IWM",
            "VGK", "EWJ", "EEM",
            "IYR", "GSG", "GLD"
            ]

        self.N_eq:int = len(self.risky_universe)

        sec = self.AddSecurity(SecurityType.Equity, "SHY", Resolution.Minute)
        sec.MarketPrice = self.GetLastKnownPrice(sec)
        
        self.symbol_objs = []
        
        for ticker in list(self.risky_universe):
            self.symbol_objs.append(self.AddSecurity(SecurityType.Equity, ticker, Resolution.Minute).Symbol)
        
        for symbol_obj in self.symbol_objs:
            symbol_obj.lookback_ma = self.SMA(symbol_obj, self.lookback, Resolution.Daily)
            
        self.SetWarmup(self.lookback, Resolution.Daily)
        self.recent_month:int = -1

    def OnData(self, data:Slice) -> None:
        if self.IsWarmingUp: return

        if not (self.cash_universe[0] in data and data[self.cash_universe[0]]):
            return

        if not(self.Time.hour == 9 and self.Time.minute == 45):
            return

        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
    
        # poll the Risk Universe set to determine the number of assets with positive momentum
        n = 0
        for symbol_obj in self.symbol_objs:
            if symbol_obj in data and data[symbol_obj]:
                if symbol_obj.lookback_ma.IsReady:
                    price = data[symbol_obj].Value
                    sma = symbol_obj.lookback_ma.Current.Value
                    if price > sma: n += 1
        
        # Calculate the bond fraction based on N_eq, prot, and n
        # This is the portion to be invested in safe harbor
        # Calculate equity fraction and weight per equity (frac_eq, w_eq) 
        # Limit bond_fraction to a discrete number of levels (n_levels >=2)
        
        # n1 = a*N/4
        n1:float = (self.protection * self.N_eq) / 4.0
        # BF = (N-n)/(N-n1)
        bond_fraction:float = min(1.0, (float(self.N_eq) - float(n)) / (float(self.N_eq) - n1))
        w_safe:float = bond_fraction
        
        # calculate the MOM for each equity determine the number of equities to be purchases
        N = 0
        for symbol_obj in self.symbol_objs:
            symbol_obj.MOM = 0.

            if symbol_obj in data and data[symbol_obj]:
                if symbol_obj.lookback_ma.IsReady:
                    price = data[symbol_obj].Value
                    sma = symbol_obj.lookback_ma.Current.Value
                    symbol_obj.MOM = (price / sma) - 1
                    if symbol_obj.MOM > 0.0: N+=1
        
        if N == 0:
            self.Liquidate()
            return

        frac_eq:float = 1.0 - w_safe
        n_eq:int = min(N, self.topM)
        w_eq:float = 0.
        if N > 0: w_eq = frac_eq / float(n_eq)
        mom_threshold = sorted([i.MOM for i in self.symbol_objs if i.MOM != 0.], reverse=True)[n_eq - 1]
        
        if frac_eq > 0.0:
            for symbol_obj in self.symbol_objs:
                if symbol_obj.MOM >= float(mom_threshold):
                    self.SetHoldings(symbol_obj, w_eq)
                else:
                    if self.Portfolio[symbol_obj].Invested:
                        self.Liquidate(symbol_obj)

            self.SetHoldings(self.cash_universe[0], w_safe)
        else:
            for symbol_obj in self.symbol_objs:
                if self.Portfolio[symbol_obj].Invested:
                    self.Liquidate(symbol_obj)

            self.SetHoldings(self.cash_universe[0], 1.0)

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading