Quant BuffetRelax, Not Over Thinking

Balance Sheet Accruals Strategy

Log in to collect

Academic paper

The Persistence of the Accruals Anomaly

AuthorsBaruch Lev; Doron Nissim

Institute
  • New York University
  • ?New York University - Stern School of Business
  • Columbia University
  • ?Columbia University - Columbia Business School

Strategy in a nutshell

The investment universe includes stocks from NYSE, AMEX, and NASDAQ. Accruals, a non-cash earnings component, are calculated using the formula: BS_ACC = (∆CA - ∆Cash) - (∆CL - ∆STD - ∆ITP) - Dep, with ∆CA representing the annual change in current assets, ∆Cash the change in cash equivalents, ∆CL the change in current liabilities, ∆STD the change in short-term debt, ∆ITP the change in income taxes payable, and Dep the depreciation expense. Stocks are ranked into deciles based on accruals, with investments in the lowest and shorts in the highest. Portfolios are rebalanced annually in May, post-earnings publication.

Economic rationale

The accrual anomaly, attributed to the earnings fixation hypothesis, suggests investors overly focus on earnings, neglecting to separately evaluate cash-flow and accrual elements. This oversight leads to misplaced optimism for companies with high accruals and undue pessimism for those with low accruals, causing valuation errors: high accrual companies become overvalued and underperform, while low accrual ones are undervalued but yield high returns. Recent studies, including Detzel, Schabel, and Strauss's "There are Two Very Different Accruals Anomalies," highlight the distinction between investment-related and non-investment accruals. They found that investment accruals better predict returns, are influenced by market sentiment, and have a risk-associated premium, unlike non-investment accruals. This differentiation clarifies previous mixed findings, indicating two separate phenomena within the accrual anomaly: a risk-based investment accruals premium and mispricing of non-investment accruals.

Backtest performance

Annualised return7.5%
Volatility10.26%
Beta0.125
Sharpe ratio-0.103
Sortino ratio-0.092
Maximum drawdown66.6%
Win rate49%

Full Python code

from AlgoLib import *
import numpy as np
from typing import List, Dict

class AccrualsBasedStrategy(XXX):

def Initialize(self):
self.SetStartDate(2006, 1, 1)
self.SetCash(100000)

self.UniverseSettings.Leverage = 5
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.ScreenStocks)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0

# Configuration
self.stock_exchanges: List[str] = ['NYS', 'NAS', 'ASE']
self.stock_limit: int = 3000
self.rebalance_month: int = 5
self.accruals: Dict[Symbol, float] = {}
self.buy_list: List[Symbol] = []
self.sell_list: List[Symbol] = []
self.is_selection_time: bool = False

self.spy_symbol: Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.Schedule.On(self.DateRules.MonthStart(self.spy_symbol), 
                 self.TimeRules.AfterMarketOpen(self.spy_symbol), 
                 self.DecideRebalance)

def ScreenStocks(self, fundamentals: List[Fundamental]) -> List[Symbol]:
if not self.is_selection_time:
    return Universe.Unchanged

eligible_stocks = [f for f in fundamentals if f.HasFundamentalData and
                   f.SecurityReference.ExchangeId in self.stock_exchanges and
                   all(not np.isnan(x) for x in [
                       f.FinancialStatements.BalanceSheet.CurrentAssets.TwelveMonths,
                       f.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths,
                       f.FinancialStatements.BalanceSheet.CurrentLiabilities.TwelveMonths,
                       f.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths,
                       f.FinancialStatements.BalanceSheet.IncomeTaxPayable.TwelveMonths,
                       f.FinancialStatements.IncomeStatement.DepreciationAndAmortization.TwelveMonths])]

if len(eligible_stocks) > self.stock_limit:
    eligible_stocks = sorted(eligible_stocks, key=lambda x: x.MarketCap, reverse=True)[:self.stock_limit]

for stock in eligible_stocks:
    symbol = stock.Symbol
    accruals_data = self.ComputeAccruals(stock)
    self.accruals[symbol] = accruals_data

sorted_accruals = sorted(self.accruals.items(), key=lambda item: item[1])
decile_size = len(sorted_accruals) // 10
self.buy_list = [symbol for symbol, _ in sorted_accruals[:decile_size]]
self.sell_list = [symbol for symbol, _ in sorted_accruals[-decile_size:]]

return self.buy_list + self.sell_list

def ComputeAccruals(self, stock: Fundamental) -> float:
delta_assets = stock.FinancialStatements.BalanceSheet.CurrentAssets.TwelveMonths - stock.FinancialStatements.BalanceSheet.CashAndCashEquivalents.TwelveMonths
delta_liabilities = stock.FinancialStatements.BalanceSheet.CurrentLiabilities.TwelveMonths - stock.FinancialStatements.BalanceSheet.CurrentDebt.TwelveMonths - stock.FinancialStatements.BalanceSheet.IncomeTaxPayable.TwelveMonths
depreciation = stock.FinancialStatements.IncomeStatement.DepreciationAndAmortization.TwelveMonths
total_assets = stock.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths
accruals = (delta_assets - delta_liabilities - depreciation) / total_assets
return accruals

def DecideRebalance(self):
self.is_selection_time = self.Time.month == self.rebalance_month

def OnSecuritiesChanged(self, changes: SecurityChanges):
for removed in changes.RemovedSecurities:
    if removed.Symbol in self.accruals:
        del self.accruals[removed.Symbol]

def OnData(self, data: Slice):
if not self.is_selection_time:
    return

self.is_selection_time = False
self.ExecuteTrades(data)

def ExecuteTrades(self, data: Slice):
targets = [PortfolioTarget(symbol, 1 / len(self.buy_list)) for symbol in self.buy_list if data.ContainsKey(symbol)] + \
          [PortfolioTarget(symbol, -1 / len(self.sell_list)) for symbol in self.sell_list if data.ContainsKey(symbol)]
self.SetHoldings(targets)
self.buy_list.clear()
self.sell_list.clear()

# Example custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
fee = parameters.Security.Price