Quant BuffetRelax, Not Over Thinking

Cash Holdings Effect and Net Operating Assets

Log in to collect

Academic 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

Annualised return9.25%
Volatility17.26%
Beta-0.022
Sharpe ratio0.54
Win rate47%

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"))