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.g_a_r_r_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 coarse_selection_function(self, coarse):
if self.month_start:
self.coarse = True
coarse = [i for i in coarse if i.adjusted_price > 10]
for i in coarse:
if i.symbol not in self.symbol_price:
self.symbol_price[i.symbol] = SymbolData(i.symbol)
self.symbol_price[i.symbol].window.add(float(i.adjusted_price))
if self.symbol_price[i.symbol].window.is_ready:
price = np.array([i for i in self.symbol_price[i.symbol].window])
returns = (price[:-1]-price[1:])/price[1:]
self.symbol_price[i.symbol].yearly_return = (price[0]-price[-1])/price[-1]
GARR_12 = np.prod([(1+i)**(1/12) for i in returns])-1
GARR_1 = (1+returns[0])**(1/12)-1
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.
ReadySymbolPrice = {symbol: SymbolData for symbol, SymbolData in self.symbol_price.items() if SymbolData.window.is_ready}
if ReadySymbolPrice and len(ReadySymbolPrice)>50:
# sort stocks in coarse by last 12-month return
sorted_by_return = sorted(ReadySymbolPrice, key = lambda x: ReadySymbolPrice[x].yearly_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].g_a_r_r_ratio)[:15]
self.increase_loser = sorted(loser, key = lambda x: ReadySymbolPrice[x].g_a_r_r_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 on_data(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.set_holdings(j, -short_weight)
long_weight = 0.5/len(self.decrease_winner)
for i in self.decrease_winner:
self.set_holdings(i, long_weight)
Derek Melchin
See the attached backtest for an updated version of the algorithm with the following changes:
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
Jing Wu
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!