Overall Statistics
Total Trades
394
Average Win
2.97%
Average Loss
-3.04%
Compounding Annual Return
2.136%
Drawdown
46.600%
Expectancy
0.019
Net Profit
37.833%
Sharpe Ratio
0.219
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
0.98
Alpha
0.02
Beta
0.514
Annual Standard Deviation
0.138
Annual Variance
0.019
Information Ratio
0.077
Tracking Error
0.138
Treynor Ratio
0.059
Total Fees
$7940.38
 
 
class EarningsQualityFactor(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2003,6,1)   #Set Start Date
        self.SetEndDate(2018,8,1)     #Set End Date
        self.SetCash(1000000)         #Set Strategy Cash
        self.UniverseSettings.Resolution = Resolution.Daily
        self.previous_fine = None
        self.long = None
        self.short = None
        self.AddUniverse(self.CoarseSelectionFunction,self.FineSelectionFunction)
        self.AddEquity("SPY", Resolution.Daily)
        # monthly scheduled event but will only rebalance once a year
        self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.At(23, 0), self.rebalance)
        self.yearly_rebalance = False
        
    def CoarseSelectionFunction(self, coarse):
        if self.yearly_rebalance:
            # drop stocks which have no fundamental data
            filtered_coarse = [x.Symbol for x in coarse if (x.HasFundamentalData) and (x.Market == "usa")]
            return filtered_coarse
        else: 
            return []      
    
    def FineSelectionFunction(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.CompanyReference.IndustryTemplateCode != "B")
                                    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.BalanceSheet.IncomeTaxPayable.Value != 0)
                                    and (x.FinancialStatements.IncomeStatement.DepreciationAndAmortization.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.CalculateAccruals(fine, self.previous_fine)
                filtered_fine = [x for x in fine if (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)]
                for i in filtered_fine:
                    # cash flow to assets
                    i.CFA = i.FinancialStatements.CashFlowStatement.OperatingCashFlow.Value/(i.EarningReports.BasicEPS.Value * i.EarningReports.BasicAverageShares.Value)
                    # debt to assets
                    i.DA = i.OperationRatios.DebttoAssets.Value
                    # return on equity
                    i.ROE = i.OperationRatios.ROE.Value

                # 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):
                    scoreAccrual = i
                    scoreCFA = sortedByCFA.index(obj)
                    scoreDA = sortedByDA.index(obj)
                    scoreROE = sortedByROE.index(obj)
                    score = scoreAccrual + scoreCFA + scoreDA + scoreROE
                    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)):]

                # save the fine data for the next year's analysis
                self.previous_fine = fine
                
                return self.long + self.short
        else:
            return []
    
    def CalculateAccruals(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.FinancialStatements.BalanceSheet.CurrentAssets.Value)-float(prev_data.FinancialStatements.BalanceSheet.CurrentAssets.Value)
                delta_cash = float(stock_data.FinancialStatements.BalanceSheet.CashAndCashEquivalents.Value)-float(prev_data.FinancialStatements.BalanceSheet.CashAndCashEquivalents.Value)
                delta_liabilities = float(stock_data.FinancialStatements.BalanceSheet.CurrentLiabilities.Value)-float(prev_data.FinancialStatements.BalanceSheet.CurrentLiabilities.Value)
                delta_debt = float(stock_data.FinancialStatements.BalanceSheet.CurrentDebt.Value)-float(prev_data.FinancialStatements.BalanceSheet.CurrentDebt.Value)
                delta_tax = float(stock_data.FinancialStatements.BalanceSheet.IncomeTaxPayable.Value)-float(prev_data.FinancialStatements.BalanceSheet.IncomeTaxPayable.Value)
                dep = float(stock_data.FinancialStatements.IncomeStatement.DepreciationAndAmortization.Value)
                avg_total = (float(stock_data.FinancialStatements.BalanceSheet.TotalAssets.Value)+float(prev_data.FinancialStatements.BalanceSheet.TotalAssets.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 OnData(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.IsLong]
            short_stocks = [x.Key for x in self.Portfolio if x.Value.IsShort]
            # 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.SetHoldings(i, long_weight)
            short_weight = 0.8/len(self.short)
            for i in self.short:
                self.SetHoldings(i, -short_weight)            


            self.yearly_rebalance = False
            self.long = False
            self.short = False