押注阿尔法
登录后收藏学术论文
Betting Against Alpha [点击查看论文]
- University of Miami
- ?University of Miami - School of Business Administration - Department of Economics
策略概要
该策略的目标是纽约证券交易所、纳斯达克和美国证券交易所的普通股,不包括房地产投资信托基金(REITs)和美国存托凭证(ADRs)。股票按纽约证券交易所的断点分为三组:小盘股(市值≤第30百分位)、中盘股(市值>第30至≤第70百分位)和大盘股(市值>第70百分位)。仅使用大盘股数据集。
在每年的年底,使用五年数据的CAPM回归估计阿尔法和贝塔。中值阿尔法决定投资组合的构建:做空高阿尔法股票,做多低阿尔法股票。投资组合每年重新平衡。
资产权重基于阿尔法排名,确保两个投资组合的权重总和等于1。个别资产的阿尔法通过资产和市场回报之间的相关性计算,乘以资产的波动率,再除以市场波动率,并使用加权因子(个别阿尔法为0.6,加上常数0.4)进行调整。
低阿尔法投资组合的权重随着阿尔法排名的升高而降低,而高阿尔法投资组合的权重与排名成正比。最终投资组合的阿尔法计算为个别市场阿尔法的加权总和。虽然权重类似于BAA、BAB和BAAB因子,但该策略强调实用性,而不是最大化夏普比率。这种系统性的方法为区分大盘股投资组合中的高阿尔法和低阿尔法股票提供了一个稳健的框架。
II. 策略合理性
受到杠杆约束的投资者高估高市场贝塔股票,同样,他们抬高具有高非市场因子贝塔的资产,将其视为正的CAPM阿尔法。研究表明,投资者优先考虑市场风险,并将非市场风险因子解释为优异的表现。基金流动对高CAPM阿尔法做出积极反应,促使基金经理将投资组合倾向于高非市场贝塔资产,以吸引关注市场风险的投资者。旨在最大化相对于指数的信息比率的经理人,有动机选择高阿尔法资产,因为这些资产可以提高比率分子,而不会显著增加跟踪误差。
该策略与买入高阿尔法股票和卖出低阿尔法股票的行业轮动策略形成对比,主要在方法上有所不同。行业轮动使用36个月的回溯期和一个月的持有期,而这种方法依赖于五年回溯期和一年持有期。对非市场贝塔的关注提供了独特的视角,利用长期阿尔法生成机会,同时解决投资组合管理中的套利限制和基准激励问题。
回测表现
完整 Python 代码
from AlgorithmImports import *
from scipy import stats
from typing import List, Dict
class BettingAgainstAlpha(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.leverage:int = 10
self.period:int = 5 * 12 * 21
self.selection_month_count:int = 12
# Market data and consolidator.
self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
# Daily price data.
self.data:Dict[Symbol, RollingWindow] = {}
# Market monthly data.
self.data[self.symbol] = RollingWindow[float](self.period)
self.weight:Dict[Symbol, float] = {}
self.fundamental_count:int = 1000
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.month:int = 12
self.selection_flag:bool = True
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(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]:
# Update the rolling window every day.
for stock in fundamental:
symbol:Symbol = stock.Symbol
# Store monthly price.
if symbol in self.data:
self.data[symbol].Add(stock.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
# selected = [x.Symbol for x in fundamental if x.HasFundamentalData and x.Market == 'usa']
selected:List[Fundamental] = [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.MarketCap != 0 \
and x.CompanyReference.IsREIT != 1 and x.SecurityReference.ExchangeId in self.exchange_codes]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
# Warmup price rolling windows.
for stock in selected:
symbol:Symbol = stock.Symbol
if symbol in self.data:
continue
self.data[symbol] = RollingWindow[float](self.period)
history:DataFrame = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
self.Log(f"Not enough data for {symbol} yet.")
continue
closes:Series = history.loc[symbol].close
for time, close in closes.items():
self.data[symbol].Add(close)
market_returns:List[float] = []
if self.data[self.symbol].IsReady:
market_closes:np.ndarray = np.array([x for x in self.data[self.symbol]])
market_returns = (market_closes[:-1] - market_closes[1:]) / market_closes[1:]
alpha_data:Dict[Symbol, float] = {}
if len(market_returns) != 0:
for stock in selected:
symbol:Symbol = stock.Symbol
if not self.data[symbol].IsReady:
continue
stock_closes:np.ndarray = np.array([x for x in self.data[symbol]])
stock_returns:np.ndarray = (stock_closes[:-1] - stock_closes[1:]) / stock_closes[1:]
beta, alpha, r_value, p_value, std_err = stats.linregress(market_returns, stock_returns)
alpha_data[symbol] = alpha
if len(alpha_data) != 0:
# Alpha diff calc.
alpha_median:float = np.median([x[1] for x in alpha_data.items()])
high_alpha_diff:List[List[Symbol, float]] = [[x[0], x[1] - alpha_median] for x in alpha_data.items() if x[1] > alpha_median]
low_alpha_diff:List[List[Symbol, float]] = [[x[0], alpha_median - x[1]] for x in alpha_data.items() if x[1] < alpha_median]
# Alpha diff weighting.
for i, portfolio in enumerate([low_alpha_diff, high_alpha_diff]):
diff_sum:float = sum(list(map(lambda x: x[1], portfolio)))
for symbol, diff in portfolio:
self.weight[symbol] = ((-1)**i) * (diff / diff_sum)
return list(self.weight.keys())
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution.
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, w) for symbol, w in self.weight.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)
self.weight.clear()
def Selection(self) -> None:
if self.month == self.selection_month_count:
self.selection_flag = True
self.month += 1
if self.month > 12:
self.month = 1
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))