forked from paperswithbacktest/awesome-systematic-trading
-
Notifications
You must be signed in to change notification settings - Fork 1
/
earnings-quality-factor.py
277 lines (232 loc) · 11.2 KB
/
earnings-quality-factor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# https://quantpedia.com/strategies/earnings-quality-factor/
#
# 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.
#
# QC implementation changes:
# - Universe consists of top 3000 US non-financial stocks by market cap from NYSE, AMEX and NASDAQ.
class EarningsQualityFactor(QCAlgorithm):
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"))