有两个投资宇宙:风险偏好(Risk-On)和风险规避(Risk-Off)。每月底,根据动量对13个风险偏好资产排名,结合总回报、价格与移动平均线、风险调整回报三种指标。选择前N名资产并等权重分配。如果任何资产表现出负动量,则用得分最高的风险规避资产替换。持有投资组合一个月后重复该过程。如果风险偏好宇宙中至少10个资产显示负动量,投资组合应全部转向风险规避资产。

策略概述

有两个投资宇宙:风险偏好(Risk-On)和风险规避(Risk-Off)。风险偏好宇宙包括13个资产,如上所述。每个月底,我们根据动量对这13个风险偏好资产进行排名(我们结合了三种指标【总回报、价格减去移动平均线、风险调整回报】和三种回溯期【3、6和12个月/50、100和200天】来形成子投资组合,并为每个资产计算平均值)。我们选择排名前N的资产,并为每个资产分配等权重(1/N)。如果任何资产表现出负动量(如果其中一个信号显示负动量,我们将其归类为负动量资产),则将其替换为得分最高的风险规避资产。我们持有投资组合一个月,然后重复该过程。此外,如果风险偏好宇宙中的四分之三(至少10个资产)显示负动量,则该投资组合应全部投资于风险规避资产。

策略合理性

延续如Antonacci(2016年)和Keller和Keuning(2016年)的研究,作者继续探索GTAA策略,力求实现更广泛的多元化,并减少波动和回撤,为提供60/40投资组合的可行替代方案做出了贡献。在这些年中,当多年的宽松货币政策被逆转时,股票与债券的相关性由于通胀高度不确定性而趋于正相关,这一贡献尤其受到重视。然而,进一步研究更细微的重新平衡方法可能会进一步改善已经良好的结果。

论文来源

Long-Only Multi-Asset Momentum: Searching for Absolute Returns [点击浏览原文]

<摘要>

由于推动股市的长期顺风消退,且股票与债券的相关性可能变为正相关,60/40投资组合的前景恶化。因此,分配给不相关的策略显得合理。在本文中,我们开发了一种仅做多的多资产动量策略,显示出具有吸引力的风险调整回报,并能够在滚动基础上产生正回报,同时与传统投资组合的相关性较低。该策略基于一种稳健的方法,考虑了多种动量指标和形成期。它可以独立使用,也可以与60/40投资组合结合,产生更高的回报,降低波动性并减少回撤。

回测表现

年化收益率8.02%
波动率7.61%
Beta0.282
夏普比率1.05
索提诺比率0.454
最大回撤-9.69%
胜率81%

完整python代码

from AlgorithmImports import *
from typing import List, Dict, Union
import data_tools
# endregion

class AdaptiveAssetAllocation(QCAlgorithm):

    def Initialize(self) -> None:
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)

        # set asset variables
        self.risk_assets:List[str] = ['SPY', 'QQQ', 'IWM', 'VGK', 'EWJ', 'EEM', 'VNQ', 'DBC', 'DBA', 'GLD', 'LQD', 'HYG', 'TLT']
        self.cash_assets:List[str] = ['SHV', 'IEF']

        self.roc_periods:List[int] = [3 * 21, 6 * 21, 12 * 21]
        self.sma_periods:List[int] = [50, 100, 200]
        self.warm_up_period:int = 12 * 21
        self.top_equities:int = 5

        self.roc:Dict[str, List[RateOfChange]] = {}
        self.sma:Dict[str, List[SimpleMovingAverage]] = {}
        self.ram:Dict[str, List[CustomRAM]] = {}

        # warm up of indicators
        self.SetWarmup(self.warm_up_period, Resolution.Daily)

        for ticker in self.risk_assets + self.cash_assets:
            # price data subscription
            data:Security = self.AddEquity(ticker, Resolution.Daily)
            
            # indicators subscription
            self.sma[ticker] = [self.SMA(ticker, period, Resolution.Daily) for period in self.sma_periods]
            self.roc[ticker] = [self.ROC(ticker, period, Resolution.Daily) for period in self.roc_periods]

            self.ram[ticker] = []
            for period in self.roc_periods:
                self.custom_indicator = data_tools.CustomRAM('RAM', period)
                self.RegisterIndicator(ticker, self.custom_indicator, Resolution.Daily)      
                self.ram[ticker].append(self.custom_indicator)

        self.recent_month:int = -1

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

        aggregated_momentum:Dict[str, float] = {}
        rebalance_flag:bool = False

        for ticker in self.risk_assets + self.cash_assets:
            if ticker in data and data[ticker]:
                indicators:List[Union[RateOfChange, SimpleMovingAverage, CustomRAM]] = self.roc[ticker] + self.sma[ticker] + self.ram[ticker]

                # all indicators are warmed up and ready
                if all(indicator.IsReady for indicator in indicators):
                    # calculate aggregated momentum average
                    roc:float = sum([mom.Current.Value for mom in self.roc[ticker]])
                    ram:float = sum([mom.Current.Value for mom in self.ram[ticker]])
                    sma:float = sum([((data[ticker].Price / sma.Current.Value) - 1) for sma in self.sma[ticker]])
                    aggregated_momentum[ticker] = (roc + sma + ram) / len(indicators)

                rebalance_flag = True
        
        # monthly rebalance
        if self.Time.month != self.recent_month and rebalance_flag:
            self.recent_month = self.Time.month

            weight:Dict[str, float] = {}

            if len(aggregated_momentum) >= self.top_equities:
                # sorting
                risk_asset_momentum:Dict[str, float] = {ticker: value for ticker, value in aggregated_momentum.items() if ticker in self.risk_assets}
                cash_asset_momentum:Dict[str, float] = {ticker: value for ticker, value in aggregated_momentum.items() if ticker in self.cash_assets}
                sorted_risk_by_momentum:List[str] = sorted(risk_asset_momentum, key=risk_asset_momentum.get, reverse=True)
                sorted_cash_by_momentum:List[str] = sorted(cash_asset_momentum, key=cash_asset_momentum.get, reverse=True)

                # portfolio consists of the top 5 positive momentum equities
                top_equities:List[str] = [ticker for ticker in sorted_risk_by_momentum][:self.top_equities]
                equities:List[str] = [ticker for ticker in top_equities if risk_asset_momentum[ticker] > 0]

                weight = {x: 1 / self.top_equities for x in equities}
                if len(equities) < self.top_equities and len(sorted_cash_by_momentum) == len(self.cash_assets):
                    weight[sorted_cash_by_momentum[0]] = (self.top_equities - len(equities)) / self.top_equities

            # liquidate symbols that should not be held
            invested:List[Symbol] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
            for ticker in invested:
                if ticker not in weight:
                    self.Liquidate(ticker)

            # rebalance portfolio
            for ticker, w in weight.items():
                if ticker in data and data[ticker]:
                    self.SetHoldings(ticker, w)

Leave a Reply

Discover more from Quant Buffet

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

Continue reading