 ## 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.

class SymbolData:
def __init__(self, symbol):
self.symbol = symbol
self.window = RollingWindow[float](13)
self.GARR_ratio = None
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

$GARR_n = \prod_{i=1}^{n}(1 + r_i)^{\frac{1}{n}}-1$

where $$GARR_1$$ is the GARR of the most recent month, $$GARR_{12}$$ is the GARR of the previous 12 months, and $$r_i$$ 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= \frac{GARR_{1} }{GARR_{12}}$

def CoarseSelectionFunction(self, coarse):
if self.month_start:
self.coarse = True
coarse = [i for i in coarse if i.AdjustedPrice > 10]
for i in coarse:
if i.Symbol not in self.SymbolPrice:
self.SymbolPrice[i.Symbol] = SymbolData(i.Symbol)
price = np.array([i for i in self.SymbolPrice[i.Symbol].window])
returns = (price[:-1]-price[1:])/price[1:]
self.SymbolPrice[i.Symbol].yearly_return = (price-price[-1])/price[-1]
GARR_12 = np.prod([(1+i)**(1/12) for i in returns])-1
GARR_1 = (1+returns)**(1/12)-1
self.SymbolPrice[i.Symbol].GARR_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.

ReadySymbolPrice = {symbol: SymbolData for symbol, SymbolData in self.SymbolPrice.items() if SymbolData.window.IsReady}
# sort stocks in coarse by last 12-month return
# top 30% with the highest 12-month return goes into winner group
winner = sorted_by_return[:int(len(sorted_by_return)*0.3)]
# bottom 30% with the lowest 12-month return goes into loser group
loser = sorted_by_return[-int(len(sorted_by_return)*0.3):]
self.decrease_winner = sorted(winner, key = lambda x: ReadySymbolPrice[x].GARR_ratio)[:15]
self.increase_loser = sorted(loser, key = lambda x: ReadySymbolPrice[x].GARR_ratio)[-15:]
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.

def OnData(self, data):
if self.month_start and self.coarse:
self.month_start = False
self.coarse = False
if all([self.decrease_winner, self.increase_loser]):
stocks_invested = [x.Key for x in self.Portfolio]
for i in stocks_invested:
if i not in self.decrease_winner+self.increase_loser:
self.Liquidate(i)
short_weight = 0.5/len(self.increase_loser)
for j in self.increase_loser:
self.SetHoldings(j, -short_weight)
long_weight = 0.5/len(self.decrease_winner)
for i in self.decrease_winner:
self.SetHoldings(i, long_weight)