Introduction
Momentum is a well-known strategy that buys stocks with the best return over the past three to twelve months and sells stocks with the worst performances over the same time horizon. The reversal strategy buys the stocks with relatively low returns and sells stocks with high returns. In this algorithm, we will develop a long-short strategy combining the momentum/reversal effect with the realized volatility.
Method
The Universe Initial Filter
The investment universe consists of NYSE, AMEX and NASDAQ stocks with prices higher than $5 per share. In the FineSelectionFunction, we divide the universe into two equal halves by size of the company. Here size is defined as the share price times the number of shares outstanding.
def coarse_selection_function(self, coarse):
# update the price of stocks in universe everyday
for i in coarse:
if i.symbol not in self.data_dict:
self.data_dict[i.symbol] = SymbolData(i.symbol, self.lookback)
self.data_dict[i.symbol].update(i.adjusted_price)
if self.monthly_rebalance:
# drop stocks which have no fundamental data or have too low prices
filtered_coarse = [x.symbol for x in coarse if (x.has_fundamental_data) and (float(x.price) > 5)]
return filtered_coarse
else:
return []
def fine_selection_function(self, fine):
if self.monthly_rebalance:
sorted_fine = sorted(fine, key = lambda x: x.earning_reports.basic_average_shares.value * self.data_dict[x.symbol].price, reverse=True)
# select stocks with large size
top_fine = sorted_fine[:int(0.5*len(sorted_fine))]
self.filtered_fine = [x.symbol for x in top_fine]
return self.filtered_fine
else:
return []
The Realized Return and Volatility
At the beginning of each month, realized returns and realized (annualized) volatilities are calculated for each stock. The realized volatility refers to the historical volatility. The formula of the realized volatility \(\sigma\) is
\[R_{avg}=\frac{\sum_{i=1}^n R_i}{n}\] \[\sigma=\sqrt{\frac{\sum_{i=1}^n(R_i-R_{avg})^2}{n-1}}\]
To annualize the volatility, we multiply the 1-day volatility by the square root of the number of trading days in a year – in our case square root of 252.
A 6-month warm-up period is required to initialize the history price for stocks in the universe. We create the class
SymbolData
to save all required variables associated with a single stock.
One week (5 trading days) prior to the beginning of each month is skipped to avoid biases due to microstructures.
class SymbolData:
'''Contains data specific to a symbol required by this model'''
def __init__(self, symbol, lookback):
self.symbol = symbol
# self.history = RollingWindow[Decimal](lookback)
self.history = deque(maxlen=lookback)
self.price = None
def update(self, value):
# update yesterday's close price
self.price = value
# update the history price series
self.history.append(float(value))
# self.history.add(value)
def is_ready(self):
return len(self.history) == self.history.maxlen
def volatility(self):
# one week (5 trading days) prior to the beginning of each month is skipped
prices = np.array(self.history)[:-5]
returns = (prices[1:]-prices[:-1])/prices[:-1]
# calculate the annualized realized volatility
return np.std(returns)*np.sqrt(252)
def return(self):
# one week (5 trading days) prior to the beginning of each month is skipped
prices = np.array(self.history)[:-5]
# calculate the annualized realized return
return (prices[-1]-prices[0])/prices[0]
After the warm-up period, the historical price series is ready. Stocks are sorted into quintiles based on their realized volatility. Stocks in the top 20% highest volatility are further sorted into quintiles by their six-month realized returns. The algorithm goes long on stocks from the highest performing quintile from the highest volatility group and short on stocks from the lowest performing quintile from the highest volatility group.
def on_data(self, data):
if self.monthly_rebalance and self.filtered_fine:
filtered_data = {symbol: symbolData for (symbol, symbolData) in self.data_dict.items() if symbol in self.filtered_fine and symbolData.is_ready()}
self.filtered_fine = None
self.monthly_rebalance = False
if len(filtered_data) < 100: return
# sort the universe by volatility and select stocks in the top high volatility quintile
sorted_by_vol = sorted(filtered_data.items(), key=lambda x: x[1].volatility(), reverse = True)[:int(0.2*len(filtered_data))]
sorted_by_vol = dict(sorted_by_vol)
# sort the stocks in top-quintile by realized return
sorted_by_return = sorted(sorted_by_vol, key = lambda x: sorted_by_vol[x].return(), reverse = True)
long = sorted_by_return[:int(0.2*len(sorted_by_return))]
short = sorted_by_return[-int(0.2*len(sorted_by_return)):]
Portfolio Rebalance and Trade
The methodology of Jegadeesh and Titamn (1993) is used to rebalance the portfolio.
Specifically, at the beginning of each month, stocks are sorted into quintiles based on their realized returns and equally weighted portfolios are formed to be held for the next six months.
This sorting and portfolio formation procedure is performed each month. In any given month \(t\), the strategy holds 6 portfolios that are selected in the current month as well as the previous 5 months.
Therefore 1/6 of the portfolio is rebalanced every month. We save those 6 portfolios in a deque list self.portfolios
and the list is updated every month. The portfolio of the current month is added while the portfolio selected from six months ago is removed from the list.
def initialize(self):
self.portfolios = deque(maxlen=6)
def on_data(self, data):
self.portfolios.append(short+long)
# 1/6 of the portfolio is rebalanced every month
if len(self.portfolios) == self.portfolios.maxlen:
for i in list(self.portfolios)[0]:
self.liquidate(i)
# stocks are equally weighted and held for 6 months
short_weight = 1/len(short)
for i in short:
self.set_holdings(i, -1/6*short_weight)
long_weight = 1/len(long)
for i in long:
self.set_holdings(i, 1/6*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!