
The dataset consists of all of the firms listed on the NYSE, Amex, and NASDAQ. Firms’ annual financial statements are sourced from Compustat. Monthly stock information come from the Center for Research in Security Prices (CRSP). As for firm characteristics, cash holdings is the proportion of total assets that the firm holds in cash and cash equivalents. Net operating assets are calculated as a difference between operating assets and liabilities at the end of fiscal year t, scaled by lagged total assets.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Cash Holdings Effect, Net Operating Assets
I. STRATEGY IN A NUTSHELL
The strategy invests in U.S. stocks listed on the NYSE, AMEX, and NASDAQ, using firm financials from Compustat and stock returns from CRSP.
Portfolio Construction: Stocks are sorted by cash holdings into deciles and by net operating assets (NOAs) into terciles.
Trading Rule: Go long on high cash-holding firms with low NOAs, and short low cash-holding firms with high NOAs.
Execution: Portfolios are equally weighted and rebalanced monthly.
II. ECONOMIC RATIONALE
The cash holding effect is primarily behavioral and linked to accrual-related anomalies and mispricing. Investors tend to over-focus on accounting profitability and neglect cash-based profitability, which is negatively related to NOAs. Low-NOA firms with high cash holdings are often undervalued due to past poor accounting performance perceptions, leading to higher subsequent returns when the undervaluation corrects. This pattern reflects the limited attention and behavioral biases affecting the cross-section of stock returns.
III. SOURCE PAPER
What is the Real Relationship between Cash Holdings and Stock Returns? [Click to Open PDF]
Mebane
<Abstract>
The literature has provided mixed evidence on the relationship between cash holdings and average stock returns. We empirically verify that the relationship is positive and robust to the adjustment of risk, the construction of cash holdings portfolios, and the weighting scheme of portfolio returns. We further examine a battery of potential channels that can explain the positive relationship. We find that the cash holding effect can be subsumed by accruals-related anomalies and it mainly comes from stocks with low net operating assets. It is stronger among stocks with high limits to arbitrage. Overall, our results indicate that the cash holding effect does not present a new asset-pricing regularity, but that it is a manifestation of existing anomalies closely related to mispricing.


IV. BACKTEST PERFORMANCE
| Annualised Return | 9.25% |
| Volatility | 17.26% |
| Beta | -0.022 |
| Sharpe Ratio | 0.54 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 47% |
V. FULL PYTHON CODE
from AlgorithmImports import *
# endregion
class CashHoldingsEffectAndNetOperatingAssets(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.leverage:int = 5
self.cash_holdings_quantile:int = 5
self.net_operating_assets_quantile:int = 2
self.three_months_flag:bool = True
self.weights:Dict[Symbol, float] = {}
self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.coarse_count:int = 3000
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())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
fundamental = [x for x in fundamental if not np.isnan(x.FinancialStatements.BalanceSheet.Cash.ThreeMonths) and x.FinancialStatements.BalanceSheet.Cash.ThreeMonths != 0 and \
not np.isnan(x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths) and x.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths != 0 and \
not np.isnan(x.FinancialStatements.BalanceSheet.FinancialAssets.ThreeMonths) and x.FinancialStatements.BalanceSheet.FinancialAssets.ThreeMonths != 0 and \
not np.isnan(x.FinancialStatements.BalanceSheet.TotalLiabilitiesAsReported.ThreeMonths) and x.FinancialStatements.BalanceSheet.TotalLiabilitiesAsReported.ThreeMonths != 0
]
if self.coarse_count <= 1000:
selected:List = sorted([x for x in fundamental if x.HasFundamentalData and x.Market == 'usa'],
key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count]
else:
selected:List = list(filter(lambda stock: stock.MarketCap != 0, [x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.AdjustedPrice >= 5]))[-self.coarse_count:]
cash_holdings:Dict[Symbol, float] = {}
net_operating_assets:Dict[Symbol, float] = {}
for stock in selected:
symbol:Symbol = stock.Symbol
cash:float = stock.FinancialStatements.BalanceSheet.Cash.ThreeMonths if self.three_months_flag \
else stock.FinancialStatements.BalanceSheet.Cash.TwelveMonths
total_assets:float = stock.FinancialStatements.BalanceSheet.TotalAssets.ThreeMonths if self.three_months_flag \
else stock.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths
financial_assets:float = stock.FinancialStatements.BalanceSheet.FinancialAssets.ThreeMonths if self.three_months_flag \
else stock.FinancialStatements.BalanceSheet.FinancialAssets.TwelveMonths
total_liabilities:float = stock.FinancialStatements.BalanceSheet.TotalLiabilitiesAsReported.ThreeMonths if self.three_months_flag \
else stock.FinancialStatements.BalanceSheet.TotalLiabilitiesAsReported.TwelveMonths
if all([cash, total_assets, financial_assets, total_liabilities]):
cash_holdings_value:float = cash / total_assets
cash_holdings[symbol] = cash_holdings_value
operating_assets:float = total_assets - financial_assets
net_operating_assets_value:float = (operating_assets - total_liabilities) / total_assets
net_operating_assets[symbol] = net_operating_assets_value
if len(cash_holdings) < self.cash_holdings_quantile or len(net_operating_assets) < self.net_operating_assets_quantile:
return Universe.Unchanged
cash_holdings_quantile:int = int(len(cash_holdings) / self.cash_holdings_quantile)
sorted_by_cash_holdings:List[Symbol] = [x[0] for x in sorted(cash_holdings.items(), key=lambda item: item[1])]
high_cash_holdings:List[Symbol] = sorted_by_cash_holdings[-cash_holdings_quantile:]
low_cash_holdings:List[Symbol] = sorted_by_cash_holdings[:cash_holdings_quantile]
net_operating_assets_quantile:int = int(len(net_operating_assets) / self.net_operating_assets_quantile)
sorted_by_net_op_assets:List[Symbol] = [x[0] for x in sorted(net_operating_assets.items(), key=lambda item: item[1])]
low_net_operating_assets:List[Symbol] = sorted_by_net_op_assets[:net_operating_assets_quantile]
high_net_operating_assets:List[Symbol] = sorted_by_net_op_assets[-net_operating_assets_quantile:]
long:List[Symbol] = [symbol for symbol in high_cash_holdings if symbol in low_net_operating_assets]
short:List[Symbol] = [symbol for symbol in low_cash_holdings if symbol in high_net_operating_assets]
for i, portfolio in enumerate([long, short]):
for symbol in portfolio:
self.weights[symbol] = ((-1) ** i) / len(portfolio)
return list(self.weights.keys())
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# trade execution
invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in self.weights:
self.Liquidate(symbol)
for symbol, w in self.weights.items():
if symbol in data and data[symbol]:
self.SetHoldings(symbol, w)
self.weights.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"))