Betting Against Alpha
Log in to collectAcademic paper
Alex R. Horenstein
- University of Miami
- ?University of Miami - School of Business Administration - Department of Economics
Strategy in a nutshell
Targets large-cap stocks on NYSE, NASDAQ, and AMEX (excluding REITs/ADRs). Uses five-year CAPM alphas to construct portfolios: short high-alpha, long low-alpha. Rebalanced annually with alpha-weighted positions.
Economic rationale
High-beta and high non-market-beta stocks are often overpriced by leverage-constrained investors. Fund managers tilt toward high-alpha assets to attract flows and enhance information ratios, exploiting long-term alpha while mitigating tracking error.
Backtest performance
Annualised return0.24%
Volatility36.48%
Beta0.039
Sharpe ratio0.08
Sortino ratio0.056
Win rate56%
Full Python code
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"))