Original paper
Abstract
Accruals are the non-cash component of earnings. They represent adjustments made to cash flows to generate a profit measure largely unaffected by the timing of receipts and payments of cash. Prior research uncovers two anomalies: expected returns increase in profitability and decrease in accruals. We show that cash-based operating profitability (a measure that excludes accruals) outperforms measures of profitability that include accruals. Further, cash-based operating profitability subsumes accruals in predicting the cross section of average returns. An investor can increase a strategy's Sharpe ratio more by adding just a cash-based operating profitability factor to the investment opportunity set than by adding both an accruals factor and a profitability factor that includes accruals.
Keywords:Â Operating profitability, Accruals, Cash flows, Anomalies, Asset pricing
Trading rules
- Investable stocks: Firms listed on NYSE, Amex, and NASDAQ (but omit non-standard shares).
- Concentrate on larger stocks: divide using the median market cap of NYSE companies.
- Calculate operating profitability: sales - cost of goods sold - sales, general, and administrative expenses.
- Determine cash-based operating profitability: remove accrual components from operating profitability.
- Classify stocks into deciles based on their cash-centric operational profit.
- Purchase stocks in the highest deciles and sell those in the lowest.
- Apply a strategy weighted by value.
- Adjust the portfolio once a year.
Python code
Backtrader
import backtrader as bt
import pandas as pd
class CashBasedOperatingProfitability(bt.Strategy):
params = (
('deciles', 10),
)
def __init__(self):
self.ranker = None
def next(self):
if self.ranker is not None:
self.ranker.cancel()
self.ranker = self.datas[0].close[0]
def notify_order(self, order):
if order.status in [order.Completed, order.Canceled, order.Margin]:
self.ranker = None
def prenext(self):
self.next()
def rebalance_portfolio(self):
market_caps = {data: data.market_cap[0] for data in self.datas}
big_stocks = sorted(self.datas, key=lambda d: market_caps[d], reverse=True)[:len(self.datas)//2]
op_profits = {data: data.sales[0] - data.cogs[0] - data.sga[0] for data in big_stocks}
cash_based_op_profits = {data: op_profits[data] - data.accruals[0] for data in big_stocks}
sorted_by_cbop = sorted(big_stocks, key=lambda d: cash_based_op_profits[d], reverse=True)
long_stocks = sorted_by_cbop[:len(sorted_by_cbop)//self.params.deciles]
short_stocks = sorted_by_cbop[-len(sorted_by_cbop)//self.params.deciles:]
for data in self.datas:
if data in long_stocks:
self.order_target_percent(data, target=market_caps[data] / sum(market_caps.values()))
elif data in short_stocks:
self.order_target_percent(data, target=-market_caps[data] / sum(market_caps.values()))
else:
self.order_target_percent(data, target=0.0)
def next_open(self):
self.rebalance_portfolio()
if __name__ == '__main__':
cerebro = bt.Cerebro()
cerebro.addstrategy(CashBasedOperatingProfitability)
# Add data feeds for NYSE, Amex, NASDAQ-traded firms (excluding non-common shares)
# ...
cerebro.run()
cerebro.plot()
Please note that you need to have the data feeds for NYSE, Amex, and NASDAQ-traded firms ready, with the necessary financial data (sales, cost of goods sold, sales, general, and administrative expenses, market capitalization, and accruals) for the calculations in the strategy. You should add the data feeds in the ‘Add data feeds’ section of the code.