Cash Hedged Momentum
Log in to collectAcademic paper
Chase P. Ross; Landon Ross; Sharon Y. Ross
- Federal Reserve
- Federal Reserve Board of Governors
- ?Board of Governors of the Federal Reserve System
- Washington University in St. Louis
- ?Washington University in St. Louis - John M. Olin Business School
- United States Department of the Treasury
- HUFinancial Research (Hungary)
- ?Office of Financial Research, US Department of the Treasury
Strategy in a nutshell
The strategy invests in U.S. stocks (excluding REITs, ADRs, preferred shares, and financial firms) with prices above $1, using cash-hedged momentum returns. Stocks are sorted based on their cash-adjusted returns, and a long-only value-weighted portfolio is constructed and rebalanced monthly. By separating cash and non-cash components of returns, the portfolio reduces the influence of cash bias and improves the covariance structure across stocks.
Economic rationale
Corporate cash can distort traditional stock return measures. By hedging out the cash portion, the strategy isolates the performance of the firm’s core operations, reducing correlation among portfolio holdings, enhancing diversification, and creating a more risk-efficient portfolio that captures pure momentum effects.
Backtest performance
Full Python code
from AlgorithmImports import *
from pandas.core.frame import DataFrame
# endregion
class CashHedgedMomentum(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.weight:dict[Symbol, float] = {}
self.price_data:dict[Symbol, RollingWindow] = {}
self.period:int = 12 * 21
self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.momentum_quantile:int = 10
self.cash_hedged_quantile:int = 25
self.percentage_traded:float = .9
self.min_share_price:float = 1.
self.fundamental_count:int = 3000
self.fundamental_sorting_key = lambda x: x.MarketCap
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.BeforeMarketClose(self.market, 0), self.Selection)
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# update the rolling window
for stock in fundamental:
symbol = stock.Symbol
# Store monthly price.
if symbol in self.price_data:
self.price_data[symbol].Add(stock.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
selected:List[Fundamental] = sorted([x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.MarketCap != 0 and x.AdjustedPrice >= self.min_share_price and \
((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE")) and \
x.FinancialStatements.BalanceSheet.CashAndCashEquivalents.ThreeMonths != 0 and not np.isnan(x.FinancialStatements.BalanceSheet.CashAndCashEquivalents.ThreeMonths) and \
x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths != 0 and not np.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths)],
key=lambda x: x.DollarVolume, reverse=True)
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
momentum:dict[Symbol, float] = {}
cash_hedged:dict[Symbol, float] = {}
# warmup price rolling windows
for stock in selected:
symbol:Symbol = stock.Symbol
if symbol not in self.price_data:
self.price_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:pd.Series = history.loc[symbol].close
for time, close in closes.items():
self.price_data[symbol].Add(close)
if self.price_data[symbol].IsReady:
cash_hedged[stock] = 1 - (stock.FinancialStatements.BalanceSheet.CashAndCashEquivalents.ThreeMonths / stock.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths)
momentum[stock] = self.price_data[symbol][0] / self.price_data[symbol][self.period - 1] - 1
if len(momentum) < self.momentum_quantile * self.cash_hedged_quantile:
return Universe.Unchanged
# sort by momentum and cash hedged
momentum_quantile:int = int(len(momentum) / self.momentum_quantile)
sorted_by_momentum:List[FineFundamental] = [x[0] for x in sorted(momentum.items(), key=lambda item: item[1])]
top_by_momentum:List[FineFundamental] = sorted_by_momentum[-momentum_quantile:]
cash_hedged_quantile:int = int(len(top_by_momentum) / self.cash_hedged_quantile)
sorted_by_cash_hedged:List[FineFundamental] = [x for x in sorted(top_by_momentum, key=lambda item: cash_hedged[item])]
top_by_cash_hedged:List[FineFundamental] = sorted_by_cash_hedged[-cash_hedged_quantile:]
total_market_cap:float = sum([x.MarketCap for x in top_by_cash_hedged])
for stock in top_by_cash_hedged:
self.weight[stock.Symbol] = (stock.MarketCap / total_market_cap) * self.percentage_traded
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:
self.selection_flag = True
# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))