Abstract
In this tutorial, we implement a version of the short-term reversal strategy published by De Groot, Huij, & Zhou (2012). The strategy works by observing the returns of each security in the universe over the previous month. Every week, the algorithm longs the worst performers and shorts the top performers. The original strategy outlined in the literature considers the entire universe of stocks when trading. To reduce trading costs, we limit our universe to the most liquid large cap stocks. Our analysis shows the strategy underperforms the S&P 500 index during all our backtest periods except the 2020 market crash.
Method
The strategy code mainly consists of four parts: Initialization, Universe Selection, OnData, and OnSecuritiesChanged.
Algorithm Initialization
When initializing the algorithm, we add a coarse universe selection method and specify several parameters for selecting securities.
def initialize(self):
# ...
self.universe_settings.resolution = Resolution.DAILY
self.add_universe(self.select_coarse)
self.dollar_volume_selection_size = 100
self.roc_selection_size = int(0.1 * self.dollar_volume_selection_size)
self.lookback = 22
self.roc_by_symbol = {}
self.week = 0
Universe Selection
The coarse universe selection method creates a RateOfChange indicator for each of the top 100 most liquid securities in the market. Upon creation, the indicator is manually warmed-up with historical closing prices. After the indicators are ready, the universe selects the securities with the 10 best and 10 worst RateOfChange values.
def select_coarse(self, coarse):
# We should keep a dictionary for all securities that have been selected
for cf in coarse:
symbol = cf.symbol
if symbol in self.roc_by_symbol:
self.roc_by_symbol[symbol].update(cf.end_time, cf.adjusted_price)
# Refresh universe each week
week_number = self.time.date().isocalendar()[1]
if week_number == self.week:
return Universe.UNCHANGED
self.week = week_number
# sort and select by dollar volume
sorted_by_dollar_volume = sorted(coarse, key=lambda x: x.dollar_volume, reverse=True)
selected = {cf.symbol: cf for cf in sorted_by_dollar_volume[:self.dollar_volume_selection_size]}
# New selections need a history request to warm up the indicator
symbols = [k for k in selected.keys()
if k not in self.roc_by_symbol or not self.roc_by_symbol[k].is_ready]
if symbols:
history = self.history(symbols, self.lookback+1, Resolution.DAILY)
if history.empty:
self.log(f'No history for {", ".join([x.value for x in symbols])}')
history = history.close.unstack(0)
for symbol in symbols:
symbol_id = symbol.id.to_string()
if symbol_id not in history:
continue
# Create and warm-up the RateOfChange indicator
roc = RateOfChange(self.lookback)
for time, price in history[symbol_id].dropna().iteritems():
roc.update(time, price)
if roc.is_ready:
self.roc_by_symbol[symbol] = roc
# Sort the symbols by their ROC values
selected_rate_of_change = {}
for symbol in selected.keys():
if symbol in self.roc_by_symbol:
selected_rate_of_change[symbol] = self.roc_by_symbol[symbol]
sorted_by_rate_of_change = sorted(selected_rate_of_change.items(), key=lambda kv: kv[1], reverse=True)
# Define the top and the bottom to buy and sell
self.roc_top = [x[0] for x in sorted_by_rate_of_change[:self.roc_selection_size]]
self.roc_bottom = [x[0] for x in sorted_by_rate_of_change[-self.roc_selection_size:]]
return self.roc_top + self.roc_bottom
The OnData Method
As new data is passed to the OnData method, we issue orders to form a long-short portfolio. We long the securities with the lowest RateOfChange values and short those with the largest values. After rebalancing, we clear the rocTop and rocBottom lists to ensure we don’t trade again until the universe is refreshed.
def on_data(self, data):
# Rebalance
for symbol in self.roc_top:
self.set_holdings(symbol, -0.5/len(self.roc_top))
for symbol in self.roc_bottom:
self.set_holdings(symbol, 0.5/len(self.roc_bottom))
# Clear the list of securities we have placed orders for
# to avoid new trades before the next universe selection
self.roc_top.clear()
self.roc_bottom.clear()
The OnSecuritiesChanged Method
We are rebalancing the portfolio weekly, but securities can leave our defined universe between rebalance days. To accommodate this, we liquidate any securities removed from the universe in the OnSecuritiesChanged method.
def on_securities_changed(self, changes):
for security in changes.removed_securities:
self.liquidate(security.symbol, 'Removed from Universe')
Relative Performance
Period Name | Start Date | End Date | Strategy | Sharpe | Variance |
---|---|---|---|---|---|
5 Year Backtest | 1/1/2016 | 1/1/2021 | Strategy | 0.287 | 0.047 |
Benchmark | 0.754 | 0.024 | |||
2020 Crash | 2/19/2020 | 3/23/2020 | Strategy | -1.075 | 0.798 |
Benchmark | -1.467 | 0.416 | |||
2020 Recovery | 3/23/2020 | 6/8/2020 | Strategy | 1.987 | 0.132 |
Benchmark | 7.942 | 0.101 |
Mykola Dobrochynskyy
Hi,
here is a little bit calmer strategy variant
Regards
Mykola
Derek Melchin
See the attached backtest for an updated version of the algorithm in PEP8 style.
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!