
“该策略投资于大盘股,使用标准化历史价值利差对价值和成长型投资组合进行择时,根据信号动态调整头寸,并每月重新平衡以优化回报。”
资产类别: 股票 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 价值、增长
I. 策略概要
该策略专注于CRSP文件中占总市值75%的最大股票(约263只股票)。它使用行业调整后的账面市值比(BM),通过从每只股票的BM中减去行业的价值加权平均BM来计算。股票每月按价值加权分为十分位投资组合,其中十分位10为价值股,十分位1为成长股。
价值利差是高(价值)投资组合和低(成长)投资组合的平均BM之差。线性择时策略使用标准化价值利差(VSt,HisVSt, His),基于过去12个月的历史信息计算。标准化价值利差是过去利差的两个总和之差除以它们的标准差,上限为±2,以限制极端值。该信号表明价值利差是否在历史上较大。
该策略根据信号做多或做空VSt,HisVSt, His美元,并每月重新平衡。这种方法通过利用历史价值利差对市场进行择时,并通过对价值股和成长股头寸的动态调整来优化回报。
II. 策略合理性
价值利差反映了价值股相对于成长股的相对便宜程度。零价值利差表明价值股的价格相对于成长股处于其历史平均水平。正利差表明价值股比正常情况便宜,而负利差则表明相反的情况。价值利差在单独和跨价值策略中都能强烈预测回报,预期价值回报变化显著。共同和资产类别特定成分对这种可预测性做出了同等贡献,与风险补偿相关。价值利差对价值回报的预测能力强于总股票回报与股息收益率之间的关系,突显了其有效性。
III. 来源论文
Value Timing: Risk and Return Across Asset Classes [点击查看论文]
- 法希兹·巴巴·亚拉(Fahiz Baba Yara)、马丁·布恩斯(Martijn Boons)和安德里亚·塔莫尼(Andrea Tamoni)。印第安纳大学凯利商学院,诺瓦商学院与经济学院,圣母大学门多萨商学院。
<摘要>
个别股票、商品、货币、全球政府债券和股票指数中的价值策略回报可以通过价值利差来预测。价值利差捕捉了价值策略中相对于空头投资组合的多头投资组合中价值信号的强度。我们表明,价值利差的共同成分和资产类别特定成分对这种可预测性做出了同等贡献。由共同价值引起的回报变化与风险溢价的标准预测因子密切相关,但与仅在股票中产生价值溢价的模型相悖。由特定价值引起的回报变化对资产定价模型提出了另一个挑战。许多价值择时和轮动策略表明,投资者可以实时从价值利差中获益。


IV. 回测表现
| 年化回报 | 6.42% |
| 波动率 | 20.62% |
| β值 | -0.051 |
| 夏普比率 | 0.31 |
| 索提诺比率 | 0.333 |
| 最大回撤 | N/A |
| 胜率 | 47% |
V. 完整的 Python 代码
from AlgorithmImports import *
from numpy import average, std, isnan
from typing import List, Dict, Tuple
#endregion
class ValueGrowthTiming(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.fundamental_count:int = 3000
self.fundamental_sorting_key = lambda x: x.DollarVolume
# Monthly spread data.
self.period:int = 12
self.spread:RollinWindow = RollingWindow[float](self.period)
self.standardized_spread:RollingWindow = RollingWindow[float](self.period)
self.quantile:int = 10
self.leverage:int = 10
self.long:List[Symbol] = []
self.short:List[Symbol] = []
self.weight:Union[None, int] = None
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol), self.Selection)
self.settings.daily_precise_end_time = False
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
selected = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' \
and not isnan(x.AssetClassification.MorningstarIndustryGroupCode) and x.AssetClassification.MorningstarIndustryGroupCode != 0 \
and not isnan(x.ValuationRatios.PBRatio) and x.ValuationRatios.PBRatio != 0 and x.MarketCap != 0]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
group:Dict[str, Tuple[Symbol, float, float]] = {}
# Stocks returns calc.
for stock in selected:
symbol = stock.Symbol
industry_group_code:float = stock.AssetClassification.MorningstarIndustryGroupCode
book_market:float = 1 / stock.ValuationRatios.PBRatio
# Adding stock in group.
if not industry_group_code in group:
group[industry_group_code] = []
group[industry_group_code].append((symbol, book_market, stock.MarketCap))
# Group's value weighted average calc.
group_bm_average:Dict[str, float] = {}
for industry_group_code, symbols in group.items():
total_market_cap:float = sum([x[2] for x in symbols])
weighted_items:Dict[Symbol, float] = {x[0] : (x[1] * (x[2] / total_market_cap)) for x in symbols}
group_bm_average[industry_group_code] = average([x[1] for x in weighted_items.items()])
# Symbol's adjusted bm calc.
adjusted_bm:Dict[Symbol, float] = {}
for industry_group_code, symbols in group.items():
for symbol in symbols:
symbol_bm:float = symbol[1]
adjusted_bm[symbol] = symbol_bm - group_bm_average[industry_group_code]
if len(adjusted_bm) < self.quantile:
return Universe.Unchanged
sorted_by_adjusted_bm:List[Tuple[Symbol, float]] = sorted(adjusted_bm.items(), key = lambda x: x[1], reverse = True)
quantile:int = int(len(sorted_by_adjusted_bm) / self.quantile)
value_portfolio:List[Symbol] = sorted_by_adjusted_bm[:quantile]
growth_portfolio:List[Symbol] = sorted_by_adjusted_bm[-quantile:]
spread:float = average([x[1] for x in value_portfolio]) - average([x[1] for x in growth_portfolio])
self.spread.Add(spread)
if self.spread.IsReady:
spread_values:List[float] = [x for x in self.spread]
standardized_spread:float = average(spread_values) / std(spread_values)
self.standardized_spread.Add(standardized_spread)
if self.standardized_spread.IsReady:
standardized_spread_values:List[float] = [x for x in self.standardized_spread]
self.weight = abs(standardized_spread_values[0] / max(standardized_spread_values[1:]))
if self.weight > 1:
self.weight = 1
self.long = [x[0][0] for x in value_portfolio]
self.short = [x[0][0] for x in growth_portfolio]
return self.long + self.short
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution.
targets:List[PortfolioTarget] = []
if self.weight:
for i, portfolio in enumerate([self.long, self.short]):
for symbol in portfolio:
if symbol in data and data[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) * (self.weight / len(portfolio))))
self.SetHoldings(targets, True)
self.long.clear()
self.short.clear()
self.weight = None
def Selection(self) -> None:
self.selection_flag = True
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))