Introduction

Momentum effect is an anomaly in nearly every market. However, if a stock in the winner group is in the final stages of overreaction, it is not the best long opportunity because of the high probability of its reversal, and therefore profit reduction. Similarly, in the loser group, a stock which is in the final stages of overreaction is also not the best short opportunity because its reversal would lower the profit of short selling within a short time. Based on this logic, this momentum-reversal strategy seeks to buy winners and sell losers that are less likely to be in the final stages of overreaction.

Method

All stocks on NYSE and NASDAQ are used as the investment universe. We create a class to save all variables for each Symbol.

  1. class SymbolData:
  2. def __init__(self, symbol):
  3. self.symbol = symbol
  4. self.window = RollingWindow[float](13)
  5. self.g_a_r_r_ratio = None
  6. self.yearly_return = None

To get the value for the above variables, we save the stock price at the start of each month in the RollingWindow for the last 12 months and calculate the monthly return and the yearly return. Then we can compute the last month's geometric average rate of return (GARR) and the previous 12-month GARR. The formulas is

GARRn=i=1n(1+ri)1n1

where GARR1 is the GARR of the most recent month, GARR12 is the GARR of the previous 12 months, and ri is the monthly return of month i. We assign the value of two returns to each symbol in SymbolData class. All stocks are sorted based on their past 12-month return. Stocks are then divided into 3 portfolios (top 30% - winner group, middle 40% and bottom 30% - loser group). In the winner group, stocks are further classified into two categories: the return-increasing winner and return-decreasing winner using the ratio of last month's geometric average rate of return (GARR) over the last 12 month GARR.

GARR Ratio=GARR1GARR12

  1. def coarse_selection_function(self, coarse):
  2. if self.month_start:
  3. self.coarse = True
  4. coarse = [i for i in coarse if i.adjusted_price > 10]
  5. for i in coarse:
  6. if i.symbol not in self.symbol_price:
  7. self.symbol_price[i.symbol] = SymbolData(i.symbol)
  8. self.symbol_price[i.symbol].window.add(float(i.adjusted_price))
  9. if self.symbol_price[i.symbol].window.is_ready:
  10. price = np.array([i for i in self.symbol_price[i.symbol].window])
  11. returns = (price[:-1]-price[1:])/price[1:]
  12. self.symbol_price[i.symbol].yearly_return = (price[0]-price[-1])/price[-1]
  13. GARR_12 = np.prod([(1+i)**(1/12) for i in returns])-1
  14. GARR_1 = (1+returns[0])**(1/12)-1
  15. self.symbol_price[i.symbol].g_a_r_r_ratio = GARR_1 / GARR_12

The decreasing-return winner group contains 13 stocks in winner group with the lowest GARR Ratio and vice-versa for the increasing-return winner group. The loser group is divided into the increasing-return loser and the decreasing-return loser groups using a similar methodology. The return-increasing loser group contains 15 stocks in loser group with the highest GARR Ratio.

  1. ReadySymbolPrice = {symbol: SymbolData for symbol, SymbolData in self.symbol_price.items() if SymbolData.window.is_ready}
  2. if ReadySymbolPrice and len(ReadySymbolPrice)>50:
  3. # sort stocks in coarse by last 12-month return
  4. sorted_by_return = sorted(ReadySymbolPrice, key = lambda x: ReadySymbolPrice[x].yearly_return)
  5. # top 30% with the highest 12-month return goes into winner group
  6. winner = sorted_by_return[:int(len(sorted_by_return)*0.3)]
  7. # bottom 30% with the lowest 12-month return goes into loser group
  8. loser = sorted_by_return[-int(len(sorted_by_return)*0.3):]
  9. self.decrease_winner = sorted(winner, key = lambda x: ReadySymbolPrice[x].g_a_r_r_ratio)[:15]
  10. self.increase_loser = sorted(loser, key = lambda x: ReadySymbolPrice[x].g_a_r_r_ratio)[-15:]
  11. return self.decrease_winner+self.increase_loser

The algorithm goes long stocks from the decreasing-return winner group and short stocks from the increasing-return loser group. The portfolio is created as equally weighted and rebalanced on a monthly basis.

  1. def on_data(self, data):
  2. if self.month_start and self.coarse:
  3. self.month_start = False
  4. self.coarse = False
  5. if all([self.decrease_winner, self.increase_loser]):
  6. stocks_invested = [x.key for x in self.portfolio]
  7. for i in stocks_invested:
  8. if i not in self.decrease_winner+self.increase_loser:
  9. self.liquidate(i)
  10. short_weight = 0.5/len(self.increase_loser)
  11. for j in self.increase_loser:
  12. self.set_holdings(j, -short_weight)
  13. long_weight = 0.5/len(self.decrease_winner)
  14. for i in self.decrease_winner:
  15. self.set_holdings(i, long_weight)


Reference

  1. Quantpedia Premium - Momentum - Short Term Reversal Strategy

Author

Jing Wu

July 2018