Introduction
Multiple factors can explain abnormal Equity returns which could be used to build profitable Equity long-short strategy. The most common factors are momentum, short-term reversal, market value, size factors and so on. In this tutorial, we will choose a few factors associated with earnings quality to investigate the return premium on stocks.
Method
Earnings Quality Metrics
Earnings quality refers to the ability of reported earnings to predict a company's future earnings. The earnings should be attributable to higher sales or lower costs, rather than artificial profits created by accounting anomalies or tricks such as inflation of inventories or changing depreciation. It is one of the most critical measures in financial reporting systems. High earnings quality indicates a healthy development in the firm's business and can improve the market efficiency.
The first metric of earnings quality is accruals. It is defined by cash flow relative to reported earnings. The high-quality earnings firms are characterized by low accruals while the low-quality firms are characterized by high accruals. The formula is
\[\begin{equation} \begin{aligned} A = \ & ( \Delta CA - \Delta Cash) \\ &- ( \Delta CL - \Delta STD - \Delta ITP) \\ &- Dep \end{aligned} \end{equation}\]
where
- \(\Delta CA\) = annual change in current assets
- \(\Delta Cash\) = change in cash and cash equivalents
- \(\Delta CL\) = change in current liabilities
- \(\Delta STD\) = change in debt included in current liabilities
- \(\Delta ITP\) = change in income taxes payable
- \(\Delta Dep\) = annual depreciation and amortization expense
We use an annual change reported for two consecutive fiscal years. To calculate the accruals, we save the fine fundamental object in the last year and calculate the difference in the next year. The algorithm will start to trade after one-year initialization.
def calculate_accruals(self, current, previous):
accruals = []
for stock_data in current:
#compares this and last year's fine fundamental objects
try:
prev_data = None
for x in previous:
if x.symbol == stock_data.symbol:
prev_data = x
break
#calculates the balance sheet accruals and adds the property to the fine fundamental object
delta_assets = float(stock_data.financial_statements.balance_sheet.current_assets.value)-float(prev_data.financial_statements.balance_sheet.current_assets.value)
delta_cash = float(stock_data.financial_statements.balance_sheet.cash_and_cash_equivalents.value)-float(prev_data.financial_statements.balance_sheet.cash_and_cash_equivalents.value)
delta_liabilities = float(stock_data.financial_statements.balance_sheet.current_liabilities.value)-float(prev_data.financial_statements.balance_sheet.current_liabilities.value)
delta_debt = float(stock_data.financial_statements.balance_sheet.current_debt.value)-float(prev_data.financial_statements.balance_sheet.current_debt.value)
delta_tax = float(stock_data.financial_statements.balance_sheet.income_tax_payable.value)-float(prev_data.financial_statements.balance_sheet.income_tax_payable.value)
dep = float(stock_data.financial_statements.income_statement.depreciation_and_amortization.value)
avg_total = (float(stock_data.financial_statements.balance_sheet.total_assets.value)+float(prev_data.financial_statements.balance_sheet.total_assets.value))/2
#accounts for the size difference
stock_data.accrual = ((delta_assets-delta_cash)-(delta_liabilities-delta_debt-delta_tax)-dep)/avg_total
accruals.append(stock_data)
except:
#value in current universe does not exist in the previous universe
pass
return accruals
The second metric is cash flow to assets ratio. It is calculated by dividing cash flows from operations by the average total assets. Firms with high cash flow to total assets are of high earnings quality. The other two metrics are Return on Equity (ROE) and Debt to Assets (DA) which are available properties of fine fundamental objects. The Higher the ROE ratio, the better the earnings quality. Low debt to assets means low leverage. It leads to more stable earnings and less dependence on the current financing conditions in the economy. Therefore, low debt to assets ratios is a signal of high earnings quality. Before calculating those variables, we should make sure their values are all positive.
def fine_selection_function(self, fine):
if self.yearly_rebalance:
#filters out the non-financial companies that don't contain the necessary data
fine = [x for x in fine if (x.company_reference.industry_template_code != "B")
and (x.financial_statements.balance_sheet.current_assets.value != 0)
and (x.financial_statements.balance_sheet.cash_and_cash_equivalents.value != 0)
and (x.financial_statements.balance_sheet.current_liabilities.value != 0)
and (x.financial_statements.balance_sheet.current_debt.value != 0)
and (x.financial_statements.balance_sheet.income_tax_payable.value != 0)
and (x.financial_statements.income_statement.depreciation_and_amortization.value != 0)]
if not self.previous_fine:
# will wait one year in order to have the historical fundamental data
self.previous_fine = fine
self.yearly_rebalance = False
return []
else:
# calculate the accrual for each stock
fine = self.calculate_accruals(fine, self.previous_fine)
filtered_fine = [x for x in fine if (x.financial_statements.cash_flow_statement.operating_cash_flow.value != 0)
and (x.earning_reports.basic_e_p_s.value != 0)
and (x.earning_reports.basic_average_shares.value != 0)
and (x.operation_ratios.debtto_assets.value != 0)
and (x.operation_ratios.ROE.value != 0)]
for i in filtered_fine:
# cash flow to assets
i.CFA = i.financial_statements.cash_flow_statement.operating_cash_flow.value/(i.earning_reports.basic_e_p_s.value * i.earning_reports.basic_average_shares.value)
# debt to assets
i.DA = i.operation_ratios.debtto_assets.value
# return on equity
i.ROE = i.operation_ratios.ROE.value
The Scoring System
The investment universe consists of all non-financial stocks from NYSE, Amex and Nasdaq. Next, we will build a composite scoring system to rank the stocks in the universe. The first step is sorting stocks by four factors respectively. “good” quality has a high score, so ideally a stock has low accruals, low debt to assets, high ROE, and high cash flow to assets will be allocated high score. Therefore, we sort the accruals and debt to assets ratio in descending orders and sort the ROE and cash flow to assets ratios in ascending order. Then the score of each stock is the sum of rank in four factors.
# sort stocks by four factors respectively
sortedByAccrual = sorted(filtered_fine, key=lambda x: x.accrual, reverse=True) # high score with low accrual
sortedByCFA = sorted(filtered_fine, key=lambda x: x.CFA) # high score with high CFA
sortedByDA = sorted(filtered_fine, key=lambda x: x.DA, reverse=True) # high score with low leverage
sortedByROE = sorted(filtered_fine, key=lambda x: x.ROE) # high score with high ROE
# create dict to save the score for each stock
score_dict = {}
# assign a score to each stock according to their rank with different factors
for i,obj in enumerate(sortedByAccrual):
score_accrual = i
score_c_f_a = sortedByCFA.index(obj)
score_d_a = sortedByDA.index(obj)
score_r_o_e = sortedByROE.index(obj)
score = score_accrual + score_c_f_a + score_d_a + score_r_o_e
score_dict[obj.symbol] = score
sortedByScore = sorted(score_dict, key = lambda x: score_dict[x], reverse = True)
# long stocks with the top score (> 30%) and short stocks with the bottom score (< 70%)
self.long = sortedByScore[:int(0.3*len(sortedByScore))]
self.short = sortedByScore[-int(0.3*len(sortedByScore)):]
Trade and Rebalance
Based on the composite factor score, the algorithm goes long the top 30% of high score stocks and short the bottom 30% of low score stocks. Final factor portfolio is formed at the end of each June and is rebalanced yearly.
def on_data(self, data):
if not self.yearly_rebalance: return
if self.long and self.short:
long_stocks = [x.key for x in self.portfolio if x.value.IS_LONG]
short_stocks = [x.key for x in self.portfolio if x.value.IS_SHORT]
# liquidate the stocks not in the filtered long/short list
for long in long_stocks:
if long not in self.long:
self.liquidate(long)
for short in short_stocks:
if short not in self.short:
self.liquidate(short)
long_weight = 0.8/len(self.long)
for i in self.long:
self.set_holdings(i, long_weight)
short_weight = 0.8/len(self.short)
for i in self.short:
self.set_holdings(i, -short_weight)
Derek Melchin
See the attached backtest for an updated version of the algorithm with the following changes:
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
Jing Wu
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!