该策略涵盖S&P 500指数和美国通胀保值债券(TIPS)。首先,计算股票市场相对于TIPS的超额收益率。然后,评估波动率并确定风险厌恶程度,使用恒定相对风险厌恶系数(CRRA)。最后,依据默顿规则,最优股票投资比例与超额收益率成正比,与风险和风险厌恶程度成反比,调整资产配置。

策略概述

投资范围包括S&P 500指数和美国通胀保值债券(TIPS)。首先,计算“超额收益率”,即股票市场的收益率超过TIPS的实际收益率。随后,在资产配置决策时估算风险(波动率),并确定风险厌恶程度,使用恒定相对风险厌恶系数(CRRA)来表示。最后,利用默顿规则计算投资组合中分配给股票的最优比例,该比例与超额收益率成正比,与风险和风险厌恶程度成反比。

策略合理性

根据以往的研究,高CAPE(周期性调整市盈率)意味着股市预期回报率较低。该指标的预测能力可以用投资者心理来解释(例如,投资者对成长股的增长预期反应过度)。此外,CAPE比P/E(市盈率)指标更具优势,因为它通过平滑盈利波动提高了预测能力。最后,加入风险和风险厌恶程度的考量提高了该策略的功能性。

论文来源

Men Doth Not Invest by Earnings Yield Alone [点击浏览原文]

<摘要>

股票市场吸引力的最受欢迎指标——席勒的周期性调整市盈率(CAPE)——目前在美国为39倍,高于过去120年中98%的时间水平。那么,理性的投资者该如何解读这一现象?他应该远离美国股市,还是坚持预设的战略股权配置?在这篇文章中,我们认为CAPE远非无关紧要,但单靠它并不能告诉投资者应持有多少股票头寸。我们相信,对于某些长期投资者来说,使用席勒的CAPE进行市场时机选择是有意义的。

回测表现

年化收益率10.02%
波动率9.1%
Beta0.177
夏普比率0.97
索提诺比率0.03
最大回撤N/A
胜率71%

完整python代码

from AlgorithmImports import *
from typing import List, Dict
import data_tools
# endregion
class MarketTimingWithMertonRuleForEarningsYield(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2000, 1, 1) 
        self.SetCash(100000)
        self.risk_aversion_coefficient: int = 2
        self.long_period: int = 10 * 12 + 1      # 10 years of daily data
        self.short_period: int = 2 * 12 + 1      # 2 years of daily data
        self.SetWarmup(self.long_period, Resolution.Daily)
        self.long_term_weight: float = 0.75
        self.short_term_weight: float = 0.25
        self.min_market_allocation: int = 0
        self.max_market_allocation: int = 1
        self.leverage: int = 5
        self.market_prices: RollingWindow = RollingWindow[float](self.long_period)
        self.sp_earnings_yield_value: Union[None, float] = None
        self.real_yield_value: Union[None, float] = None
        # traded symbols
        security: Securities = self.AddEquity('SPY', Resolution.Daily)
        security.SetLeverage(self.leverage)
        self.market:Symbol = security.Symbol
        
        security: Securities = self.AddEquity('TIP', Resolution.Daily)
        security.SetLeverage(self.leverage)
        self.bonds:Symbol = security.Symbol
        self.sp_earnings_yield: Symbol = self.AddData(data_tools.MonthlyCustomData, 'SP500_EARNINGS_YIELD_MONTH', Resolution.Daily).Symbol
        
        us_treasury_long_term: bool = True
        if us_treasury_long_term:
            self.real_yield: Symbol = self.AddData(data_tools.DailyCustomData, 'DLTIIT', Resolution.Daily).Symbol
            self.real_yield_property = 'LT Real Average (>10Yrs)'
        else:
            self.real_yield: Symbol = self.AddData(data_tools.DailyCustomData, 'DFII10', Resolution.Daily).Symbol
            self.real_yield_property = '10 YR'
        self.custom_data: List[Symbol] = [self.sp_earnings_yield, self.real_yield]
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
        self.recent_month: int = -1
    def OnData(self, data: Slice) -> None:
        custom_data_last_update_date: Dict[Symbol, datetime.date] = data_tools.LastDateHandler.get_last_update_date()
        # rebalance monthly
        if self.recent_month != self.Time.month:
            self.recent_month = self.Time.month
            if all(self.Securities[symbol].GetLastData() for symbol in self.custom_data) and any(self.Time.date() > custom_data_last_update_date[symbol] for symbol in self.custom_data):
                self.Liquidate()
                return
            # update market prices
            if self.market in data and data[self.market]:
                price:float = data[self.market].Value
                self.market_prices.Add(price)
            if self.IsWarmingUp:
                return
            if self.sp_earnings_yield_value != None and self.real_yield_value != None and self.market_prices.IsReady \
                and self.market in data and self.bonds in data and data[self.market] and data[self.bonds]:
                excess_yield: float = self.sp_earnings_yield_value - self.real_yield_value
                vollatility_forecast: float = self.VolatilityForecast()
                
                # Merton Rule calculation
                market_allocation: float = excess_yield / (self.risk_aversion_coefficient * vollatility_forecast)
                
                # cap market allocation
                market_allocation: float = min(max(market_allocation, self.min_market_allocation), self.max_market_allocation)
                self.SetHoldings(self.market, market_allocation)
                self.SetHoldings(self.bonds, 1 - market_allocation)
            else:
                self.Liquidate()
            # reset data collected from previous month
            self.sp_earnings_yield_value = None
            self.real_yield_value = None
        if self.sp_earnings_yield in data and data[self.sp_earnings_yield] and data[self.sp_earnings_yield].Value != 0:
            self.sp_earnings_yield_value = data[self.sp_earnings_yield].Value / 100
        if self.real_yield in data and data[self.real_yield] and data[self.real_yield].Price != 0:
            self.real_yield_value = data[self.real_yield].Value / 100
    def VolatilityForecast(self) -> float:
        # volatility measures, both expressed as variances, and then take the square root of that weighted average to arrive at the spot estimate of equity volatility
        prices: List[float] = list(self.market_prices)
        long_term_prices: np.ndarray = np.array(prices[:self.long_period])
        short_term_prices: np.ndarray = np.array(prices[:self.short_period])
        long_term_variance: float = self.Variance(long_term_prices)
        short_term_variance: float = self.Variance(short_term_prices)
        
        weighted_average: float = (self.long_term_weight * long_term_variance + self.short_term_weight * short_term_variance) / (self.long_term_weight + self.short_term_weight)
        
        # take the square root of the weighted average
        return np.sqrt(weighted_average)
    def Variance(self, prices: np.ndarray) -> float:
        returns: np.ndarray = (prices[:-1] - prices[1:]) / prices[1:]
        ann_volatility: float = np.std(returns) * np.sqrt(12)
        variance: float = ann_volatility ** 2
        return variance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading