Cash Holdings Effect and Net Operating Assets
Log in to collectAcademic paper
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.
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.
Backtest performance
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"))