该策略投资于商品、股票、固定收益、外汇等多个资产类别,使用搬运溢价、动量、尾部风险、价值和波动率信号。通过等风险贡献法分配权重,并对极值进行限制。投资组合包括多头和空头头寸,按等权重构建,并每季度再平衡,以追求多重风险溢价的稳定回报。

策略概述

<投资范围>

GSCI Proxy(24个子指数[包括CO COMDTY, GC COMDTY])(代表商品),16个发达市场和9个新兴市场(股票指数[例如,SPX,NK,SX5E]的成分股),单只股票为市值最大的500只美国股票,G10政府债券[期货](代表固定收益),G10货币(代表外汇)。

信号为:

SigTech的多重风险溢价策略应用了等风险贡献方法,考虑了波动率和相关性。为限制极值的影响,应用了对每个单独的风险溢价策略的最小和最大权重。此外,在所有策略中,采用了针对排名前25%的多头和排名后25%的空头的投资组合构建方法。

所有投资组合(包括最终的组合)均以等权重方式构建,并每季度再平衡。

策略合理性

分配到多个风险溢价策略的关键原因在于从有利的分散特性中获益,这在相关矩阵中有详细说明。1. 搬运:交易的基本原理是高收益资产(例如股息收益率或利率衡量的资产)表现优于低收益资产。在学术界,对搬运的研究主要集中在货币市场的影响上。但在2017年,Pedersen等人发表了他们的广泛引用的论文《搬运》,这是对所有流动资产类别的搬运溢价的全面研究。2. 动量:这些策略押注价格趋势的延续。它们的基本原理可以在行为金融学中找到,该学科研究心理学对市场参与者行为及其对资产价格的影响。动量策略自市场形成以来就存在,自20世纪70年代以来一直在投资界享有较高地位,尽管直到Jegadeesh和Titman在1993年发表的研究后才在学术界得到广泛认可。3. 尾部风险:这不是传统的风险溢价策略。通过系统性地寻求下行保护,尾部风险策略通常与试图捕捉波动率风险溢价的投资者持相反的立场。首先,从投资组合构建的角度来看,尾部风险在大幅且突然的抛售期间提供了宝贵的分散效应。其次,尾部风险策略的受欢迎程度正在上升,机构投资者正越来越多地将资金分配到这些策略中。4. 价值:这些策略的经济原理在于资产价格回归其公允价值(“均值回归”)。交易价格低于其公允价值的资产预计会在回归均值时表现优异,而高估值的资产由于相同的动态预计表现不佳。价值投资是一种久经考验的高评价投资方法,已被知名投资者如沃伦·巴菲特(伯克希尔·哈撒韦)、乔·格林布拉特(哥谭基金)和塞斯·克拉曼(包普斯特基金)成功实施。5. 波动率策略通过向希望限制不利市场事件影响的投资者出售保险,系统地收获溢价。这些系统主要通过期权实施,传统上基于隐含波动率高于实际波动率的信念。波动率溢价的存在可以通过投资者的损失规避倾向(即损失的痛苦大于相等的收益)以及他们倾向于高估大规模抛售的发生来解释。Fallon等人于2015年发表的论文证明了波动率溢价在广泛资产类别中的稳健性。单一策略之间的平均相关性为0.05,这表明多策略方法分配到广泛策略可以预期比平均单一风险溢价策略带来更高的夏普比率。

论文来源

Alternative Risk Premia Prime [点击浏览原文]

<摘要>

去年金融市场的大规模抛售、充满挑战的宏观经济环境以及日益增加的波动性促使机构投资者重新评估其战略资产配置。这些重新评估的核心问题是如何在实现有吸引力的回报的同时,为投资组合提供下行保护。对冲基金是机构投资者资产配置中的重要组成部分。事实上,几项最新的调查显示,预计2023年对对冲基金和其他另类投资策略的配置将增加,投资者越来越多地采用另类风险溢价(ARP)策略来替代传统对冲基金。在全球内化趋势的推动下,机构投资者正在内部定制ARP策略,以从提高的成本效率和透明度中获利。该白皮书探讨了ARP策略的理论基础及其历史发展。在讨论这些基本原则之后,报告还提供了跨所有主要流动资产类别的ARP的实证研究。

回测表现

年化收益率8.53%
波动率6.62%
Beta0.178
夏普比率1.23
索提诺比率N/A
最大回撤-11.1%
胜率55%

完整python代码

from AlgorithmImports import *
from pandas.core.frame import DataFrame
#endregion
class MultiRiskPremiaStrategy(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        # Carry Quantpedia Strategies:
        # ID 234 - Carry Factor within Asset Classes - 4 different universes
        # Momentum Quantpedia Strategies:
        # Commodities - ID 21
        # Equity Index - ID 15
        # Equity Single Stocks - ID 14
        # Bonds - ID 426
        # FX - ID 8
        # Volatility Risk Strategies:
        # Commodities - ID 506
        # Equity Single Stocks - ID 20
        # FX - ID 507
        # Value Strategies:
        # Commodities - ID 424
        # FX - ID 9
        # Equity Index - ID 26
        # Bonds - ID 6
        self.tickers:List[str] = [
            '234_commodity', '234_equity',
            '234_fx', '234_bonds',
            '21', '15',
            '14', '426',
            '8', '506',
            '20', '507',
            '424', '6',
            '9', '26',
        ]
        self.volatility_period:int = 21
        self.volatility_target:float = 0.1
        self.leverage_cap:float = 5.
        for ticker in self.tickers:
            data:Security = self.AddData(QuantpediaEquity, ticker, Resolution.Daily)
            data.SetLeverage(self.leverage_cap * 3)
            data.SetFeeModel(CustomFeeModel())
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.recent_month:int = -1
    
    def OnData(self, data:Slice) -> None:
        if self.IsWarmingUp:
            return
        # quarterly rebalance
        if self.Time.month == self.recent_month:
            return
        self.recent_month = self.Time.month
        if self.Time.month % 3 != 0: return
        
        # rebalance
        _last_update_date:Dict[str, datetime.date] = QuantpediaEquity.get_last_update_date()
        tickers_to_trade:List[str] = [ticker for ticker in self.tickers if \
                ticker in _last_update_date and \
                self.Time.date() < _last_update_date[ticker] and \
                ticker in data and data[ticker]]
        
        # inverse volatility weighting
        long_count:int = len(tickers_to_trade)
        price_df:DataFrame = self.History(tickers_to_trade, self.volatility_period, Resolution.Daily).unstack(level=0)
        
        if not price_df.empty:
            price_df = price_df['close']
            daily_returns:DataFrame = price_df.pct_change().iloc[1:]
            daily_returns = daily_returns.loc[:, (daily_returns != 0).any(axis=0)]  # drop 0 columns
            tickers_to_trade = list(map(lambda x: self.Symbol(x).Value, list(daily_returns.columns)))    # updated valid columns
            std:pd.Series = daily_returns.std()
            weights:np.ndarray = ((1 / std) / (1 / std).sum()).values
            # volatility target
            portfolio_vol:float = np.sqrt(np.dot(weights.T, np.dot(daily_returns.cov() * self.volatility_period, weights.T)))
            leverage:float = min(self.volatility_target / portfolio_vol, self.leverage_cap)
        # trade execution
        invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
        for ticker in invested:
            if ticker not in tickers_to_trade:
                self.Liquidate(ticker)
        for i, ticker in enumerate(tickers_to_trade):
            self.SetHoldings(ticker, leverage * weights[i])
        
# Quantpedia strategy equity curve data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaEquity(PythonData):
    def GetSource(self, config:SubscriptionDataConfig, date:datetime, isLiveMode:bool) -> SubscriptionDataSource:
        return SubscriptionDataSource("data.quantpedia.com/backtesting_data/equity/quantpedia_strategies/911_related/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
    _last_update_date:Dict[str, datetime.date] = {}
    @staticmethod
    def get_last_update_date() -> Dict[str, datetime.date]:
       return QuantpediaEquity._last_update_date
    def Reader(self, config: SubscriptionDataConfig, line: str, date: datetime, isLive: bool) -> BaseData:
        data:config = QuantpediaEquity()
        data.Symbol = config.Symbol
        
        if not line[0].isdigit(): return None
        split:List[str] = line.split(';')
        
        data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
        data['close'] = float(split[1])
        data.Value = float(split[1])
        
        # store last update date
        if config.Symbol.Value not in QuantpediaEquity._last_update_date:
            QuantpediaEquity._last_update_date[config.Symbol.Value] = datetime(1,1,1).date()
        if data.Time.date() > QuantpediaEquity._last_update_date[config.Symbol.Value]:
            QuantpediaEquity._last_update_date[config.Symbol.Value] = data.Time.date()
        
        return data
        
# Custom fee model.
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

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

Continue reading