
The strategy invests in large-cap stocks, shorting high-alpha and longing low-alpha portfolios based on CAPM-estimated alphas, with weighted ranks and annual rebalancing to differentiate performance systematically.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Yearly | MARKET: equities | KEYWORD: Alpha
I. 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.
II. 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.
III. SOURCE PAPER
Betting Against Alpha [Click to Open PDF]
Alex R. Horenstein, University of Miami – School of Business Administration – Department of Economics
<Abstract>
I sort stocks based on realized alphas estimated from the CAPM, Carhart (1997), and Fama-French Five Factor (FF5, 2015) models and find that realized alphas are negatively related with future stock returns, future alpha, and Sharpe Ratios. Thus, I construct a Betting Against Alpha (BAA) factor that buys a portfolio of low-alpha stocks and sells a portfolio of high-alpha stocks. Using rank estimation methods, I show that the BAA factor spans a dimension of stock returns different than Frazzini and Pedersen’s (2014) Betting Against Beta (BAB) factor. Additionally, the BAA factor captures information about the cross-section of stock returns missed by the CAPM, Carhart, and FF5 models. The performance of the BAA factor further improves if the low alpha portfolio is calculated from low beta stocks and the high alpha portfolio from high beta stocks. I call this factor Betting Against Alpha and Beta (BAAB). I discuss several reasons that support the existence of this counter-intuitive low-alpha anomaly.


IV. BACKTEST PERFORMANCE
| Annualised Return | 0.24% |
| Volatility | 36.48% |
| Beta | 0.039 |
| Sharpe Ratio | 0.08 |
| Sortino Ratio | 0.056 |
| Maximum Drawdown | N/A |
| Win Rate | 56% |
V. 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"))
VI. Backtest Performance