Quant BuffetRelax, Not Over Thinking

Quality Factor Long-Short Portfolio by Market Cap

Log in to collect

Academic paper

o Open PDF]

AuthorsNadja Guenster; Erik Kole; Ben Jacobsen

Institute
  • University of California, Berkeley
  • ?University of Muenster - Finance Center Muenster
  • NLTinbergen Institute
  • NLErasmus University Rotterdam
  • Environmental Research Institute of Michigan
  • ?ERIM
  • ?Erasmus University Rotterdam - Erasmus School of Economics - Econometric Institute
  • NLTilburg University
  • NZMassey University
  • NLTIAS School for Business and Society
  • ?Tilburg University - TIAS School for Business and Society

Strategy in a nutshell

The investment universe consists of all non-financial stocks from NYSE, Amex and Nasdaq.

Big stocks are defined as the largest stocks that make up 90% of the total market cap within the region, while small stocks make up the remaining 10% of the market cap. Investor defines breakpoints by the 30th and 70th percentiles of the multiple “Earnings Quality” ratios between large caps and small caps.

The first “Earnings Quality” ratio is defined by cash flow relative to reported earnings. The high-quality earnings firms are characterized by high cash flows (relative to reported earnings) while the low-quality firms are characterized by high reported earnings (relative to cash flow).

The second factor is based on return on equity (ROE) to exploit the well-documented “profitability anomaly” by going long high-ROE firms (top 30%) and short low-ROE firms (bottom 30%).

The third ratio – CF/A (cash flow to assets) factor goes long firms with high cash flow to total assets.

The fourth ratio – D/A (debt to assets) factor goes long firms with low leverage and short firms with high leverage.

The investor builds a scored composite quality metric by computing the percentile score of each stock on each of the four quality metrics (where “good” quality has a high score, so ideally a stock has low accruals, low leverage, high ROE, and high cash flow) and then add up the percentiles to get a score for each stock from 0 to 400. He then forms the composite factor by going long the top 30% of small-cap stocks and also large-cap stocks and short the bottom 30% of the small-cap stocks and also large-cap stocks and cap-weighting individual stocks within the portfolios.

The final factor portfolio is formed at the end of each June and is rebalanced yearly.

Economic rationale

The effect is explained mainly by investors’ behavioural defects. The majority of investors usually overly fixate on actual earnings, and they do not investigate the quality of earnings scrutinizingly. The in-depth analysis, therefore, allows exploiting this inefficiency.

Backtest performance

Annualised return7.95%
Volatility5.91%
Beta0.021
Sharpe ratio-0.18
Maximum drawdown94.9%
Win rate49%

Full Python code

class EarningsQualityFactor(XXX):

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

self.coarse_count = 3000

self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

self.accruals_data = {}

self.long = []
self.short = []

self.data = {}

self.selection_flag = True
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)

self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.Selection)

def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
    security.SetLeverage(10)
    security.SetFeeModel(CustomFeeModel(self))

def CoarseSelectionFunction(self, coarse):
if not self.selection_flag:
    return Universe.Unchanged

selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa']

return selected

def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0 and      \
        x.CompanyReference.IndustryTemplateCode != "B" and \
        ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE")) and   \
        x.FinancialStatements.BalanceSheet.CurrentAssets.Value != 0 and  \
        x.FinancialStatements.BalanceSheet.CashAndCashEquivalents.Value != 0 and    \
        x.FinancialStatements.BalanceSheet.CurrentLiabilities.Value != 0 and    \
        x.FinancialStatements.BalanceSheet.CurrentDebt.Value != 0 and   \
        x.FinancialStatements.IncomeStatement.DepreciationAndAmortization.Value != 0 and    \
        x.FinancialStatements.BalanceSheet.GrossPPE.Value != 0 and  \
        x.FinancialStatements.IncomeStatement.TotalRevenueAsReported.Value != 0 and \
        x.FinancialStatements.CashFlowStatement.OperatingCashFlow.Value != 0 and    \
        x.EarningReports.BasicEPS.Value != 0 and    \
        x.EarningReports.BasicAverageShares.Value != 0 and  \
        x.OperationRatios.DebttoAssets.Value != 0 and   \
        x.OperationRatios.ROE.Value != 0
        ]
        
if len(fine) > self.coarse_count:
    sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True)
    top_by_market_cap = [x for x in sorted_by_market_cap[:self.coarse_count]]
else:
    top_by_market_cap = fine

for stock in top_by_market_cap:
    symbol = stock.Symbol

    if symbol not in self.accruals_data:
        # Data for previous year.
        self.accruals_data[symbol] = None
        
    # Accrual calc.
    current_accruals_data = AcrrualsData(stock.FinancialStatements.BalanceSheet.CurrentAssets.Value, stock.FinancialStatements.BalanceSheet.CashAndCashEquivalents.Value,
                                        stock.FinancialStatements.BalanceSheet.CurrentLiabilities.Value, stock.FinancialStatements.BalanceSheet.CurrentDebt.Value, stock.FinancialStatements.BalanceSheet.IncomeTaxPayable.Value,
                                        stock.FinancialStatements.IncomeStatement.DepreciationAndAmortization.Value, stock.FinancialStatements.BalanceSheet.TotalAssets.Value,
                                        stock.FinancialStatements.IncomeStatement.TotalRevenueAsReported.Value)
    
    # There is not previous accruals data.
    if not self.accruals_data[symbol]:
        self.accruals_data[symbol] = current_accruals_data
        continue
    
    current_accruals = self.CalculateAccruals(current_accruals_data, self.accruals_data[symbol])
    
    # cash flow to assets
    CFA = stock.FinancialStatements.CashFlowStatement.OperatingCashFlow.Value / (stock.EarningReports.BasicEPS.Value * stock.EarningReports.BasicAverageShares.Value)
    # debt to assets
    DA = stock.OperationRatios.DebttoAssets.Value
    # return on equity
    ROE = stock.OperationRatios.ROE.Value
    
    if symbol not in self.data:
        self.data[symbol] = None

    self.data[symbol] = StockData(current_accruals, CFA, DA, ROE)
    self.accruals_data[symbol] = current_accruals_data

# Remove not updated symbols.
updated_symbols = [x.Symbol for x in top_by_market_cap]
not_updated = [x for x in self.data if x not in updated_symbols]
for symbol in not_updated:
    del self.data[symbol]
    del self.accruals_data[symbol]
    
return [x[0] for x in self.data.items()]

def OnData(self, data):
if not self.selection_flag:
    return
self.selection_flag = False

# Sort stocks by four factors respectively.
sorted_by_accruals = sorted(self.data.items(), key=lambda x: x[1].Accruals, reverse=True) # high score with low accrual 
sorted_by_CFA = sorted(self.data.items(), key=lambda x: x[1].CFA)                       # high score with high CFA
sorted_by_DA = sorted(self.data.items(), key=lambda x: x[1].DA, reverse=True)           # high score with low leverage
sorted_by_ROE = sorted(self.data.items(), key=lambda x: x[1].ROE)                       # high score with high ROE

score = {}

# Assign a score to each stock according to their rank with different factors.
for i, obj in enumerate(sorted_by_accruals):
    score_accruals = i
    score_CFA = sorted_by_CFA.index(obj)
    score_DA = sorted_by_DA.index(obj)
    score_ROE = sorted_by_ROE.index(obj)
    score[obj[0]] = score_accruals + score_CFA + score_DA + score_ROE
        
sorted_by_score = sorted(score.items(), key = lambda x: x[1], reverse = True)
tercile = int(len(sorted_by_score) / 3)
long = [x[0] for x in sorted_by_score[:tercile]]
short = [x[0] for x in sorted_by_score[-tercile:]]

# Trade execution.
# NOTE: Skip year 2007 due to data error.
if self.Time.year == 2007:
    self.Liquidate()
    return

stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
    if symbol not in long + short:
        self.Liquidate(symbol)

for symbol in long:
    if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable:  # Prevent error message.
        self.SetHoldings(symbol, 1 / len(long))
for symbol in short:
    if self.Securities[symbol].Price != 0 and self.Securities[symbol].IsTradable:  # Prevent error message.
        self.SetHoldings(symbol, -1 / len(short))

# Source: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3188172                
def CalculateAccruals(self, current_accrual_data, prev_accrual_data):
delta_assets = current_accrual_data.CurrentAssets - prev_accrual_data.CurrentAssets
delta_cash = current_accrual_data.CashAndCashEquivalents - prev_accrual_data.CashAndCashEquivalents
delta_liabilities = current_accrual_data.CurrentLiabilities - prev_accrual_data.CurrentLiabilities
delta_debt = current_accrual_data.CurrentDebt - prev_accrual_data.CurrentDebt
dep = current_accrual_data.DepreciationAndAmortization
total_assets_prev_year = prev_accrual_data.TotalAssets

acc = (delta_assets - delta_liabilities - delta_cash + delta_debt - dep) / total_assets_prev_year
return acc

def Selection(self):
if self.Time.month == 7:
    self.selection_flag = True

class AcrrualsData():
def __init__(self, current_assets, cash_and_cash_equivalents, current_liabilities, current_debt, income_tax_payable, 
                depreciation_and_amortization, total_assets, sales):
self.CurrentAssets = current_assets
self.CashAndCashEquivalents = cash_and_cash_equivalents
self.CurrentLiabilities = current_liabilities
self.CurrentDebt = current_debt
self.IncomeTaxPayable = income_tax_payable
self.DepreciationAndAmortization = depreciation_and_amortization
self.TotalAssets = total_assets

self.Sales = sales

class StockData():
def __init__(self, accruals, cfa, da, roe):
self.Accruals = accruals
self.CFA = cfa
self.DA = da
self.ROE = roe

def MultipleLinearRegression(x, y):
x = np.array(x).T
x = sm.add_constant(x)
result = sm.OLS(endog=y, exog=x).fit()
return result

# Custom fee model
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))