Introduction

In this tutorial, we will develop a strategy based on the price and earnings momentum effect of stocks. This strategy is derived from the paper "Momentum" by N. Jegadeesh and S. Titman.

N. Jegadeesh et al. describe price/return momentum as a tendency for stocks that perform well over a three to twelve month period to continue to perform well over a subsequent three to twelve month period. Similarly, stocks that perform poorly over a three to twelve month period have a tendency to continue to perform poorly. They describe earnings momentum as the tendency for stocks with high earnings per share (EPS) to continue to outperform stocks with low EPS.

Below, we will implement a quarterly-rebalanced stock strategy based on the price and earnings momentum.

Method

Step 1: Select the coarse universe

We will use both a coarse selection filter and in a later step, a fine universe filter, to narrow down our universe of assets. Our coarse universe filter creates a set of stocks based on volume, price, and whether fundamental data on the stock exists. In this step we filter for the top 100 liquid Equities with prices greater than $5. We also exclude the Equities missing fundamental data because EPS is needed in the fine selection step.

def coarse_selection(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.next_rebalance:
        return Universe.UNCHANGED

    # Sort the equities (prices > 5) by Dollar Volume descendingly
    selected_by_dollar_volume = sorted([x for x in coarse if x.price > 5 and x.has_fundamental_data], 
                                    key = lambda x: x.dollar_volume, reverse = True)

    # Pick the top 100 liquid equities as the coarse-selected universe
    return [x.symbol for x in selected_by_dollar_volume[:self.num_of_coarse]]

Step 2: Calculate quarterly return and earnings growth

N. Jegadeesh et al. state price momentum and earnings momentum can be used as two indicators for trading. We will calculate for price momentum with the GetQuarterlyReturn method and for earnings momentum with the GetEarningGrowth method.

GetQuarterlyReturn calculates price momentum for each symbol in our coarse universe and ranks each stock based on its quarterly return. First we request last quarter’s close price for all stocks. Then we calculate quarterly return by taking the delta of the first day’s close price and the last day’s close price. Finally, we store the symbols and their corresponding rankings by quarterly return in a dictionary in preparation for fine selection.

def get_quarterly_return(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)}

GetEarningGrowth calculates earnings momentum for each symbol in our coarse universe and ranks each stock based on its earnings growth. First we use a RollingWindow to store and update the BasicEPS to reflect quarterly earnings reports. A RollingWindow holds a set of the most recent entries of data. As we move from time t=0 forward, our rolling window will shuffle data further along to a different index until it leaves the window completely. The object in the window with index[0] refers to the most recent item. The length-1 in the window is the oldest object.

Our RollingWindow has a length of 2 so index[0] is the current EPS and index[1] is last quarter's EPS. We calculate earnings growth for each stock by taking the delta of this quarter’s EPS and last quarter’s EPS divided by last quarter’s EPS. Finally we rank each asset based on earnings growth.

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

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

        # Select the securities with EPS (> 0)
        if stock.earning_reports.basic_e_p_s.three_months == 0:
            continue

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

        # Update the rolling window for each stock
        self.eps_by_symbol[stock.symbol].add(stock.earning_reports.basic_e_p_s.three_months)

        # If the rolling window is ready
        if self.eps_by_symbol[stock.symbol].is_ready:
            rw = self.eps_by_symbol[stock.symbol]
            # Caculate the Earning Growth
            eg_by_symbol[stock.symbol] = (rw[0] - rw[1]) / rw[1]

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

Step 3: Select the fine universe

We use a fine selection filter in addition to a coarse selection filter to refine our asset selection based on corporate fundamental data. We can use both quarterly return and earnings growth from our two indicators to generate an average rank for each stock. Then we can go long on the top 10 and short the bottom 10.

def fine_selection(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.rebalance_days, Resolution.DAILY)
    history = history.drop_duplicates().close.unstack(level = 0)
    rank_by_quarter_return = self.get_quarterly_return(history)

    # Get the earning growth for each symbol
    rank_by_earning_growth = self.get_earning_growth(fine) 

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

    # Get 10 symbols to long and short respectively
    sorted_dict = sorted(rank_sum_by_symbol.items(), key = lambda x: x[1], reverse = True)
    self.long_symbols = [x[0] for x in sorted_dict[:10]]
    self.short_symbols = [x[0] for x in sorted_dict[-10:]]

    return [x for x in symbols if str(x) in self.long_symbols + self.short_symbols]

Step 4: Rebalance quarterly

We choose to rebalance every quarter and use equal weights for the long and short positions of securities in our portfolio.

def on_data(self, data):
    '''
    Rebalance quarterly
    '''
    # Do nothing until next rebalance
    if self.time < self.next_rebalance:
        return

    # Liquidate the holdings if necessary
    for holding in self.portfolio.values:
        symbol = holding.symbol
        if holding.invested and symbol.value not in self.long_symbols + self.short_symbols:
            self.liquidate(symbol, "Not Selected")

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

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

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

    # Set next rebalance time
    self.next_rebalance += timedelta(self.rebalance_days)

Results

Our backtest results in a Sharpe ratio of -0.268 while the SP500 Sharpe ratio is 0.758 during the same decade. This performance may be due to several factors:

  • The number of stocks in our portfolio, 100, could be too low.
  • Equal weighting for all stocks may not fully capture the strength of higher ranking stocks.
  • Rebalancing quarterly may be too frequent for a momentum strategy in Equities.

This tutorial shows us how to take advantage of the techniques of requesting historical data and using a RollingWindow. We hope the community can further develop strategies based on these techniques.



Reference

  1. Momentum, N. Jegadeesh, S.Titman