Overall Statistics
Total Trades
1157
Average Win
0.93%
Average Loss
-0.68%
Compounding Annual Return
6.551%
Drawdown
18.300%
Expectancy
0.175
Net Profit
84.685%
Sharpe Ratio
0.585
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.36
Alpha
0.049
Beta
0.148
Annual Standard Deviation
0.116
Annual Variance
0.014
Information Ratio
-0.344
Tracking Error
0.169
Treynor Ratio
0.461
Total Fees
$1670.52
class PriceEarningsMomentumAlgorithm(QCAlgorithm):
    '''
    A stock momentum strategy based on quarterly returns and earning growth.
    Paper: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=299107
    Online copy: https://www.trendrating.com/wp-content/uploads/dlm_uploads/2019/03/momentum.pdf
    '''
    def Initialize(self):
        self.SetStartDate(2010, 1, 1)   # Set Start Date
        self.SetEndDate(2019, 9, 1)     # Set End Date
        self.SetCash(100000)            # Set Strategy Cash

        self.UniverseSettings.Resolution = Resolution.Daily          # Resolution setting of universe selection
        self.AddUniverse(self.CoarseSelection, self.FineSelection)   # Coarse and Fine Universe Selection

        self.nextRebalance = self.Time  # Initialize next balance time
        self.rebalanceDays = 90         # Rebalance quarterly

        self.numOfCoarse = 100          # Number of coarse selected universe
        self.longSymbols = []           # Symbol list of the equities we'd like to long
        self.shortSymbols = []          # Symbol list of the equities we'd like to short
        self.epsBySymbol = {}           # Contains RollingWindow objects of EPS for every stock

    def CoarseSelection(self, coarse):
        '''
        Pick the top 100 liquid equities as the coarse-selected universe
        '''
        # Before next rebalance time, just remain the current universe
        if self.Time < self.nextRebalance:
            return Universe.Unchanged

        # Sort the equities (prices > 5) by Dollar Volume descendingly
        selectedByDollarVolume = sorted([x for x in coarse if x.Price > 5 and x.HasFundamentalData], 
                                        key = lambda x: x.DollarVolume, reverse = True)

        # Pick the top 100 liquid equities as the coarse-selected universe
        return [x.Symbol for x in selectedByDollarVolume[:self.numOfCoarse]]

    def FineSelection(self, fine):
        '''
        Select securities based on their quarterly return and their earnings growth 
        '''
        symbols = [x.Symbol for x in fine]

        # Get the quarterly returns for each symbol
        history = self.History(symbols, self.rebalanceDays, Resolution.Daily)
        history = history.drop_duplicates().close.unstack(level = 0)
        rankByQuarterReturn = self.GetQuarterlyReturn(history)

        # Get the earning growth for each symbol
        rankByEarningGrowth = self.GetEarningGrowth(fine) 

        # Get the sum of rank for each symbol and pick the top ones to long and the bottom ones to short
        rankSumBySymbol = {key: rankByQuarterReturn.get(key, 0) + rankByEarningGrowth.get(key, 0) 
                                for key in set(rankByQuarterReturn) | set(rankByEarningGrowth)}

        # Get 10 symbols to long and short respectively
        sortedDict = sorted(rankSumBySymbol.items(), key = lambda x: x[1], reverse = True)
        self.longSymbols = [x[0] for x in sortedDict[:10]]
        self.shortSymbols = [x[0] for x in sortedDict[-10:]]

        return [x for x in symbols if str(x) in self.longSymbols + self.shortSymbols]

    def GetQuarterlyReturn(self, history):
        '''
        Get the rank of securities based on their quarterly return from historical close prices
        Return: dictionary
        '''
        # Get quarterly returns for all symbols
        # (The first row divided by the last row)
        returns = history.iloc[0] / history.iloc[-1]

        # Transform them to dictionary structure
        returns = returns.to_dict()

        # Get the rank of the returns (key: symbol; value: rank)
        # (The symbol with the 1st quarterly return ranks the 1st, etc.)
        ranked = sorted(returns, key = returns.get, reverse = True)
        return {symbol: rank for rank, symbol in enumerate(ranked, 1)}

    def GetEarningGrowth(self, fine):
        '''
        Get the rank of securities based on their EPS growth
        Return: dictionary
        '''

        # Earning Growth by symbol
        egBySymbol = {}
        for stock in fine:

            # Select the securities with EPS (> 0)
            if stock.EarningReports.BasicEPS.ThreeMonths == 0:
                continue

            # Add the symbol in the dict if not exist
            if not stock.Symbol in self.epsBySymbol:
                self.epsBySymbol[stock.Symbol] = RollingWindow[float](2)

            # Update the rolling window for each stock
            self.epsBySymbol[stock.Symbol].Add(stock.EarningReports.BasicEPS.ThreeMonths)

            # If the rolling window is ready
            if self.epsBySymbol[stock.Symbol].IsReady:
                rw = self.epsBySymbol[stock.Symbol]
                # Caculate the Earning Growth
                egBySymbol[stock.Symbol] = (rw[0] - rw[1]) / rw[1]

        # Get the rank of the Earning Growth
        ranked = sorted(egBySymbol, key = egBySymbol.get, reverse = True)
        return {symbol: rank for rank, symbol in enumerate(ranked, 1)}

    def OnData(self, data):
        '''
        Rebalance quarterly
        '''
        # Do nothing until next rebalance
        if self.Time < self.nextRebalance:
            return

        # Liquidate the holdings if necessary
        for holding in self.Portfolio.Values:
            symbol = holding.Symbol
            if holding.Invested and symbol.Value not in self.longSymbols + self.shortSymbols:
                self.Liquidate(symbol, "Not Selected")

        # Open positions for the symbols with equal weights
        count = len(self.longSymbols + self.shortSymbols)
        if count == 0:
            return

        # Enter long positions
        for symbol in self.longSymbols:
            self.SetHoldings(symbol, 1 / count)

        # Enter short positions
        for symbol in self.shortSymbols:
            self.SetHoldings(symbol, -1 / count)

        # Set next rebalance time
        self.nextRebalance += timedelta(self.rebalanceDays)