
“该策略投资于按账面市值比(B/M)股票比率排序的价值加权十分位投资组合。如果CRSP价值加权指数的24个月回报为负,则做多最高十分位,做空最低十分位。”
资产类别: ETF、股票 | 地区: 美国 | 周期: 每月 | 市场: 股票 | 关键词: 价值因子
I. 策略概要
投资范围包括来自Kenneth French数据库的按账面市值比(B/M)股票比率排序的价值加权十分位投资组合。每个月,根据CRSP价值加权指数的24个月回报来确定市场状态。如果回报为负,则策略做多最高十分位投资组合,做空最低十分位投资组合。持仓六个月。该策略使用账面市值比(B/M)比率作为排序标准,并持仓半年。
II. 策略合理性
价值溢价通常用这样一种观点来解释,即价值投资者押注于容易对新闻反应过度、推断过去的增长并追随股价趋势的幼稚投资者。行为理论认为,当动量交易减少时,价值投资者会受益,因为这减少了对过去表现不佳的定价过低的股票的抛售压力。相反,动量交易的增加会推高定价过高的股票的价格,从而使避免这些股票的价值策略受益。此外,在市场下跌之后,定价过低的股票通常会获得更高的回报,因为过去的过度反应导致这些股票未来表现更加有利。
III. 来源论文
Value Bubbles [点击查看论文]
- Chibane, Messaoud 和 Ouzan, Samuel,诺马商学院,金融学院,俄罗斯高等经济学院,诺马商学院
<摘要>
该研究揭示,价值策略的历史表现主要由偶尔出现的泡沫驱动。行为理论认为,价值溢价应随长期滞后的市场回报或其他投资者情绪的总体代理变量而变化——我们的横截面测试支持了这一假设。从1926年到2022年,我们发现,在市场回报连续两年为负之后,美国价值溢价约为其无条件对应物的3倍,而在市场回报连续两年为正之后,价值溢价似乎消失了。此外,动量策略出现重大亏损的时期(动量崩盘)往往与价值策略出现重大盈利的时期(价值泡沫)相吻合。利用这些见解,我们开发了一种正偏斜、可实施的动态投资策略,将标准价值和动量策略的夏普比率分别提高了三倍以上和60%。我们的研究结果具有国际稳健性。


IV. 回测表现
| 年化回报 | 3.68% |
| 波动率 | 3.45% |
| β值 | 0.104 |
| 夏普比率 | 1.07 |
| 索提诺比率 | -0.113 |
| 最大回撤 | N/A |
| 胜率 | 52% |
V. 完整的 Python 代码
from AlgorithmImports import *
from numpy import floor
#endregion
class ValueFactorAfterNegativeMarketReturn(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.coarse_count = 500
self.period = 24 * 21
self.SetWarmUp(self.period, Resolution.Daily)
self.quantile = 10
# Trenching
self.holding_period = 6
self.managed_queue = []
self.symbol = self.AddEquity('VTI', Resolution.Daily).Symbol
self.data = RollingWindow[float](self.period)
self.selection_flag = False
self.recent_price = {} # recent stock prices
self.UniverseSettings.Resolution = Resolution.Daily
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.settings.daily_precise_end_time = False
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(10)
def CoarseSelectionFunction(self, coarse):
for stock in coarse:
symbol = stock.Symbol
# append recent price to market symbol
if symbol == self.symbol:
self.data.Add(stock.AdjustedPrice)
# store monthly stock prices
if self.selection_flag:
self.recent_price[symbol] = stock.AdjustedPrice
if not self.selection_flag:
return Universe.Unchanged
selected = sorted([x for x in coarse if x.HasFundamentalData and x.Price > 5],
key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count]
return [x.Symbol for x in selected]
def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0 and x.ValuationRatios.PBRatio != 0]
# market data is ready
if self.data.IsReady:
market_ret:float = self.data[0] / self.data[self.period-1] - 1
if market_ret >= 0:
return Universe.Unchanged
else:
return Universe.Unchanged
bm_ratio = {}
market_cap = {}
for stock in fine:
symbol = stock.Symbol
market_cap[symbol] = stock.MarketCap
bm_ratio[symbol] = 1 / stock.ValuationRatios.PBRatio
long:list = []
short:list = []
if len(bm_ratio) >= self.quantile:
# BM ratio sorting
sorted_by_bm = sorted(bm_ratio.items(), key=lambda x: x[1], reverse=True)
decile = int(len(sorted_by_bm) / self.quantile)
# Long the highest decile portfolio
# Short the lowest decile portolio
long = [x[0] for x in sorted_by_bm[:decile]]
short = [x[0] for x in sorted_by_bm[-decile:]]
# Market cap weighting
equity = self.Portfolio.TotalPortfolioValue / self.holding_period
weights = {}
total_market_cap_long = sum([market_cap[sym] for sym in long if sym in market_cap])
for symbol in long:
if symbol in market_cap:
weights[symbol] = market_cap[symbol] / total_market_cap_long
total_market_cap_short = sum([market_cap[sym] for sym in short if sym in market_cap])
for symbol in short:
if symbol in market_cap:
weights[symbol] = -market_cap[symbol] / total_market_cap_short
symbol_q = [(symbol, floor((equity*symbol_w) / self.recent_price[symbol])) for symbol,symbol_w in weights.items()]
self.managed_queue.append(RebalanceQueueItem(symbol_q))
return long + short
def OnData(self, data):
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution
remove_item = None
# Rebalance portfolio
for item in self.managed_queue:
if item.holding_period == self.holding_period:
for symbol, quantity in item.symbol_q:
self.MarketOrder(symbol, -quantity)
remove_item = item
elif item.holding_period == 0:
open_symbol_q = []
for symbol, quantity in item.symbol_q:
if symbol in data and data[symbol]:
self.MarketOrder(symbol, quantity)
open_symbol_q.append((symbol, quantity))
# Only opened orders will be closed
item.symbol_q = open_symbol_q
item.holding_period += 1
# We need to remove closed part of portfolio after loop. Otherwise it will miss one item in self.managed_queue.
if remove_item:
self.managed_queue.remove(remove_item)
def Selection(self):
self.selection_flag = True
class RebalanceQueueItem():
def __init__(self, symbol_q):
# symbol/quantity collections
self.symbol_q = symbol_q
self.holding_period = 0
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))