Overall Statistics
Total Trades
494
Average Win
0.32%
Average Loss
-0.34%
Compounding Annual Return
2.153%
Drawdown
3.600%
Expectancy
0.130
Net Profit
12.436%
Sharpe Ratio
0.724
Loss Rate
42%
Win Rate
58%
Profit-Loss Ratio
0.94
Alpha
0.038
Beta
-0.962
Annual Standard Deviation
0.028
Annual Variance
0.001
Information Ratio
0.058
Tracking Error
0.028
Treynor Ratio
-0.021
Total Fees
$567.69
#The investment universe consists of NYSE, AMEX and NASDAQ firms that have stock returns data in CRSP. Financial and utility firms with SIC codes from 6000 to 6999
#and from 4900 to 4949 are excluded.
#Firstly, the earnings acceleration is calculated as a difference of two fractions, where the first fraction is Earnings per share (EPS) of stock i at quarter t minus the EPS of
#stock i at quarter t-4 divided by the stock price price at the end of quarter t-1. The second fraction is a difference of EPS of stock i at quarter t-1 and EPS of stock i at
#quarter t-5 divided by the stock price at the end of quarter t-2.
#Long the highest earnings acceleration decile and short the lowest earnings acceleration decile. Holding period is one month
#Porfolio is value-weighted.

class January_Effect(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2014, 1, 1)
        self.SetEndDate(2019, 7, 1)
        self.SetCash(100000)
        
        self.quarters_count = 6
        self.num_coarse = 100
        self.num_fine = 50
        
        self.symbol_ea = {} # contain the earnings acceleration for every stock
        self.symbol_rw = {} # contain RollingWindow objects for every stock
        
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        self.AddEquity("SPY", Resolution.Daily)
        self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.AfterMarketOpen("SPY"), self.Rebalance) # update quarterly_rebalance
        self.quarterly_rebalance = True
        
    def CoarseSelectionFunction(self, coarse):
        if self.quarterly_rebalance:
            selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 5)]
            filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True) 
            self.filtered_coarse = [ x.Symbol for x in filtered[:self.num_coarse]]
            return self.filtered_coarse
        else: 
            return self.filtered_coarse
        
    def FineSelectionFunction(self, fine):
        if self.quarterly_rebalance:
            # pre-select
            selected = [x for x in fine if x.EarningReports.BasicEPS.ThreeMonths > 0]
            for stock in selected:
                if not stock.Symbol in self.symbol_rw.keys():
                    self.symbol_rw[stock.Symbol] = RollingWindow[float](self.quarters_count)
                # update rolling window for every stock
                self.symbol_rw[stock.Symbol].Add(stock.EarningReports.BasicEPS.ThreeMonths)
                
                if self.symbol_rw[stock.Symbol].IsReady:
                    rw = self.symbol_rw[stock.Symbol]
                    eps_fraction1 = (rw[0] - rw[4]) / rw[1]
                    eps_fraction2 = (rw[1] - rw[5]) / rw[2]
                    self.symbol_ea[stock.Symbol] = eps_fraction1 - eps_fraction2 # That's the earnings acceleration we want
            
            sorted_dict = sorted(self.symbol_ea.items(), key = lambda x: x[1], reverse = True)
            self.filtered_fine = [x[0] for x in sorted_dict[:self.num_fine]]
            self.quarterly_rebalance = False
            
            #Log for validate
            self.Log([x.Value for x in self.filtered_fine])
            
            return self.filtered_fine
        else:
            return self.filtered_fine

    def Rebalance(self):
        if self.Time.month % 3 == 0:
            self.quarterly_rebalance = True
        else:
            self.quarterly_rebalance = False
            
    def OnData(self, data):

        if self.quarterly_rebalance:
            
            if len(self.symbol_ea) == 0:
                return
            
            decile_len = int(len(self.symbol_ea) / 10)
            
            long_stocks = self.filtered_fine[:decile_len]
            short_stocks = self.filtered_fine[-decile_len:]
            
            # Close positions first
            stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
            for stock in stocks_invested:
                if (stock not in long_stocks) and (stock not in short_stocks):
                    self.Liquidate(stock)
                    
            for long_stock in long_stocks:
                if self.Portfolio[long_stock].IsShort:
                    self.Liquidate(long_stock)
                if not self.Portfolio[long_stock].IsLong:
                    self.SetHoldings(long_stock, 1/(2*decile_len))
                    
            for short_stock in short_stocks:
                if self.Portfolio[short_stock].IsLong:
                    self.Liquidate(short_stock)
                if not self.Portfolio[short_stock].IsShort:
                    self.SetHoldings(short_stock, -1/(2*decile_len))
                    
            self.StartTime = self.Time
        
        stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
        
        # holding period is 1 month
        if len(stocks_invested) > 0 and (self.Time - self.StartTime).days > 30:
            for stock in stocks_invested:
                self.Liquidate(stock)