Overall Statistics |
Total Orders 5535 Average Win 0.10% Average Loss -0.12% Compounding Annual Return -2.159% Drawdown 31.400% Expectancy -0.120 Start Equity 1000000 End Equity 719183.91 Net Profit -28.082% Sharpe Ratio -0.91 Sortino Ratio -0.847 Probabilistic Sharpe Ratio 0.000% Loss Rate 51% Win Rate 49% Profit-Loss Ratio 0.78 Alpha -0.034 Beta -0.009 Annual Standard Deviation 0.038 Annual Variance 0.001 Information Ratio -0.585 Tracking Error 0.153 Treynor Ratio 3.861 Total Fees $8043.43 Estimated Strategy Capacity $2000.00 Lowest Capacity Asset AMS R735QTJ8XC9X Portfolio Turnover 0.22% |
#region imports from AlgorithmImports import * #endregion class EarningsQualityFactor(QCAlgorithm): def initialize(self): self.set_start_date(2003, 6, 30) self.set_end_date(2018, 8, 1) self.set_cash(1_000_000) self.settings.minimum_order_margin_portfolio_percentage = 0 self.set_security_initializer(BrokerageModelSecurityInitializer( self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))) self.universe_settings.resolution = Resolution.DAILY self._previous_fine = None self._long = None self._short = None self.add_universe(self._coarse_selection_function, self._fine_selection_function) self.add_equity("SPY", Resolution.DAILY) # monthly scheduled event but will only rebalance once a year self.schedule.on(self.date_rules.month_start("SPY"), self.time_rules.at(23, 0), self._rebalance) self._yearly_rebalance = False self.set_warmup(timedelta(365)) def _coarse_selection_function(self, coarse): if self._yearly_rebalance: # drop stocks which have no fundamental data return [x.symbol for x in coarse if x.has_fundamental_data] else: return [] 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_eps.value != 0) and (x.earning_reports.basic_average_shares.value != 0) and (x.operation_ratios.debt_to_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_eps.value * i.earning_reports.basic_average_shares.value) # debt to assets i.da = i.operation_ratios.debt_to_assets.value # return on equity i.roe = i.operation_ratios.roe.value # sort stocks by four factors respectively sorted_by_accrual = sorted(filtered_fine, key=lambda x: x.accrual, reverse=True) # high score with low accrual sorted_by_cfa = sorted(filtered_fine, key=lambda x: x.cfa) # high score with high CFA sorted_by_da = sorted(filtered_fine, key=lambda x: x.da, reverse=True) # high score with low leverage sorted_by_roe = 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(sorted_by_accrual): score_accrual = i score_cfa = sorted_by_cfa.index(obj) score_da = sorted_by_da.index(obj) score_roe = sorted_by_roe.index(obj) score = score_accrual + score_cfa + score_da + score_roe score_dict[obj.symbol] = score sorted_by_score = sorted(score_dict, key=lambda x: score_dict[x], reverse=True) # long stocks with the top score (>10%) and short stocks with the bottom score (<10%) self._long = sorted_by_score[:int(0.10*len(sorted_by_score))] self._short = sorted_by_score[-int(0.10*len(sorted_by_score)):] # save the fine data for the next year's analysis self._previous_fine = fine return self._long + self._short else: return [] 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 def _rebalance(self): #yearly rebalance at the end of June (start of July) if self.time.month == 7: self._yearly_rebalance = True def on_data(self, data): if not self._yearly_rebalance or self.is_warming_up: return if self._long and self._short: longs = [s for s in self._long if s in data.Bars] shorts = [s for s in self._short if s in data.Bars] long_weight = 0.5/len(longs) short_weight = -0.5/len(shorts) targets = [PortfolioTarget(i, long_weight) for i in longs] targets.extend([PortfolioTarget(i, short_weight) for i in shorts]) self.set_holdings(targets, True) self._yearly_rebalance = False self._long = False self._short = False