Quant Buffet放轻松,别过度思虑

大盘股中的价值溢价

登录后收藏

学术论文

Is There a Value Premium Among Large Stocks?

作者Andrade

机构
  • ?Chhaochharia, 迈阿密大学 - 财务系,迈阿密大学 - 财务系
论文摘要

根据Fama和French(2012年)的市净率排序,在大型股票中没有全球价值溢价。通过两种简单的改动,可以恢复这一溢价:基于市盈率而非市净率对股票进行排序,以及使用全球而非区域性的价值断点。最终得到的全球大型股票价值溢价(衡量为前30%股票与后30%股票之间的回报差)从每月17个基点(t统计量=1.09)增加到64个基点(t统计量=2.61)。使用基于盈利预测的市盈率而非历史盈利计算的市盈率进一步加深了大型股票中的价值效应。这个价值溢价不仅限于小型股票,仍然是金融学中的一个重要话题。由于估值比率不可互换,研究人员在研究或控制价值效应时应超越市净率的范畴。

策略概要

该策略投资于发达市场股票(北美、欧盟、亚太地区),每月根据市值和收益率将其分为六个投资组合。收益率的计算方法是将每股收益(使用I/B/E/S的预测数据,分别针对年份t、t+1、t+2)除以股价。排除了负面收益预测和收益率超过100%的股票。每年根据纽约证券交易所的中位数断点,将股票按市值分为“大”股和“小”股。在每个市值范围内,股票进一步按“低”(底部30%)、“中”(中间40%)和“高”(顶部30%)的收益率分组。投资者在高收益的大市值股票上做多,在低收益的大市值股票上做空,且每月重新平衡投资组合,使用市值加权。

策略合理性

价值异常现象有很好的文献记录。最常见的解释是,投资者对成长股的增长潜力反应过度,因此,价值股被低估。对于大市值股票,更准确地计算市盈率有助于增强这一效应。市场价格往往与基于分析师预测的未来收益计算出的收益率比与基于历史收益计算的收益率更为一致。每月对股票进行排序也有助于使用更准确的信息。

回测表现

波动率14.11%
夏普比率0.56
索提诺比率-0.441
胜率48%

完整 Python 代码

from AlgorithmImports import *
from typing import List, Dict
# endregion
class ValuePremiumInLargeCapStocks(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2011, 1, 1)       # estimize dataset starts in 2011
self.SetCash(100_000)

self.years_period: int = 3
self.low_high_percentage: int = 30
self.min_stocks: int = 10           # big cap part will be split in 30, 40 and 30 percent
self.leverage: int = 5
self.min_share_price: int = 5
self.prices: Dict[Symbol, float] = {}
self.weights: Dict[Symbol, float] = {}
self.estimates: Dict[Symbol, Dict[int, List[float]]] = {}
market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.already_subscribed: List[Symbol] = []
self.fundamental_count: int = 500
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.rebalance_flag: bool = False
self.selection_flag: bool = False
self.UniverseSettings.Leverage = self.leverage
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.BeforeMarketClose(market), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
    return Universe.Unchanged
self.selection_flag = False
self.rebalance_flag = True

selected: List[Fundamental] = [
    x for x in fundamental 
    if x.HasFundamentalData 
    and x.Market == 'usa' 
    and x.Price > self.min_share_price
    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]]
# store estimize db symbol
for stock in selected:
    if stock.Symbol not in self.already_subscribed:
        self.AddData(EstimizeEstimate, stock.Symbol)
        self.already_subscribed.append(stock.Symbol)
self.prices = { stock.Symbol: stock.AdjustedPrice for stock in selected }
market_cap_median: float = np.median(list(map(lambda stock: stock.MarketCap, selected)))
large_cap: List[Fundamental] = list(filter(lambda stock: stock.MarketCap > market_cap_median, selected))
curr_year: int = self.Time.year
eps_yield_by_stock: Dict[Fundamental, float] = {}
for stock in large_cap:
    symbol: Symbol = stock.Symbol
    ticker: str = symbol.Value
    if ticker not in self.estimates or curr_year not in self.estimates[ticker]:
        continue
    eps_mean: float = self.CalcEpsMean(self.estimates[ticker], curr_year, self.years_period)
    eps_yield: float = eps_mean / self.prices[symbol]
    if eps_yield > 0:
        eps_yield_by_stock[stock] = eps_yield
if len(eps_yield_by_stock) < self.min_stocks:
    return Universe.Unchanged

# create long and short portfolios
eps_yield_values: List[float] = list(eps_yield_by_stock.values())
top_percentile_eps: float = np.percentile(eps_yield_values, self.low_high_percentage)
bottom_percentile_eps: float = np.percentile(eps_yield_values, 100 - self.low_high_percentage)
short_leg: List[Fundamental] = [x[0] for x in eps_yield_by_stock.items() if x[1] < top_percentile_eps]
long_leg: List[Fundamental] = [x[0] for x in eps_yield_by_stock.items() if x[1] > bottom_percentile_eps]
# market cap weighting
for i, portfolio in enumerate([long_leg, short_leg]):
    mc_sum: float = sum(list(map(lambda stock: stock.MarketCap, portfolio)))
    for stock in portfolio:
        self.weights[stock.Symbol] = ((-1)**i) * stock.MarketCap / mc_sum

return list(self.weights.keys())

def OnData(self, slice: Slice) -> None:
estimate: Dict[Symbol, float] = slice.Get(EstimizeEstimate)
for symbol, value in estimate.items():
    ticker: str = symbol.Value
    if ticker not in self.estimates:
        self.estimates[ticker] = {}
    fiscal_year: int = value.FiscalYear
    if fiscal_year not in self.estimates[ticker]:
        self.estimates[ticker][fiscal_year] = []
    self.estimates[ticker][fiscal_year].append(value.Eps)
# rebalance when selection was made
if not self.rebalance_flag:
    return
self.rebalance_flag = False
# trade execution
portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weights.items() if slice.contains_key(symbol) and slice[symbol]]
self.SetHoldings(portfolio, True)
self.weights.clear()

def CalcEpsMean(self, eps_by_years: Dict[int, List[float]], curr_year: int, years_period: int) -> float:
eps_values: List[float] = []
for i in range(years_period):
    year: int = curr_year + i
    if year in eps_by_years:
        eps_values += eps_by_years[year]
return np.mean(eps_values)
def Selection(self) -> None:
self.selection_flag = True
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))