“该策略涵盖S&P 500指数和美国通胀保值债券(TIPS)。首先,计算股票市场相对于TIPS的超额收益率。然后,评估波动率并确定风险厌恶程度,使用恒定相对风险厌恶系数(CRRA)。最后,依据默顿规则,最优股票投资比例与超额收益率成正比,与风险和风险厌恶程度成反比,调整资产配置。”
资产类别:债券,股票 | 区域:美国 | 频率:每月 | 市场:债券,股权 | 关键词:市场时机,默顿规则,收益率
策略概述
投资范围包括S&P 500指数和美国通胀保值债券(TIPS)。首先,计算“超额收益率”,即股票市场的收益率超过TIPS的实际收益率。随后,在资产配置决策时估算风险(波动率),并确定风险厌恶程度,使用恒定相对风险厌恶系数(CRRA)来表示。最后,利用默顿规则计算投资组合中分配给股票的最优比例,该比例与超额收益率成正比,与风险和风险厌恶程度成反比。
策略合理性
根据以往的研究,高CAPE(周期性调整市盈率)意味着股市预期回报率较低。该指标的预测能力可以用投资者心理来解释(例如,投资者对成长股的增长预期反应过度)。此外,CAPE比P/E(市盈率)指标更具优势,因为它通过平滑盈利波动提高了预测能力。最后,加入风险和风险厌恶程度的考量提高了该策略的功能性。
论文来源
Men Doth Not Invest by Earnings Yield Alone [点击浏览原文]
- James White, Victor Haghani,Elm Partners
<摘要>
股票市场吸引力的最受欢迎指标——席勒的周期性调整市盈率(CAPE)——目前在美国为39倍,高于过去120年中98%的时间水平。那么,理性的投资者该如何解读这一现象?他应该远离美国股市,还是坚持预设的战略股权配置?在这篇文章中,我们认为CAPE远非无关紧要,但单靠它并不能告诉投资者应持有多少股票头寸。我们相信,对于某些长期投资者来说,使用席勒的CAPE进行市场时机选择是有意义的。


回测表现
| 年化收益率 | 10.02% |
| 波动率 | 9.1% |
| Beta | 0.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
