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