Introduction
This tutorial implements a seasonality strategy that trades based on historical same-calendar-month returns. The strategy is derived from the paper Common Factors in Return Seasonalities.
A great deal of research on seasonality effects in algorithmic trading exists. Seasonality patterns are well documented in stock returns across numerous countries and in commodity and country portfolios. The phenomenon’s occurrence is not isolated to specific stocks or monthly time intervals, for example, seasonality is observed at the daily frequency as well. Our implementation reflects the existing research.
In our algorithm, we will first use a coarse selection filter function to narrow down our universe to the top 100 liquid securities with a price greater than $5.
Next, for each security in the universe, we will calculate the monthly return for the same-calendar month of the previous year. For example, if we implement this strategy on a backtest for the period of August 2019, we would base our long and short positions on monthly returns from August 2018. We will long the securities with top monthly returns and short those with the bottom monthly returns.
At the end of each month we will rebalance and repeat the strategy. The following section offers further explanation of how to implement each step of the strategy.
Method
Step 1: Select our universe
We first select the top 100 liquid securities and ETFs with prices greater than $5 based on dollar volume for our universe. Research from "Common Factors" suggests that the U.S. Equity, commodity, and Index markets are all affected by seasonality patterns. Therefore, we can include any assets in our universe. Note that while this strategy does not require fundamental data for implementation, other strategies in the library do. In those cases we would need to remove ETFs from the universe because we don’t have fundamental data for ETFs.
# Sort the securities with prices > 5 in DollarVolume decendingly
selected = sorted([x for x in coarse if x.price > 5],
key=lambda x: x.dollar_volume, reverse=True)
# Get securities after coarse selection
symbols = [x.symbol for x in selected[:self.num_coarse]]
Step 2: Calculate the same-calendar month returns of the previous year
"Common Factors" indicates that taking long and short positions based on historical same-calendar month returns earns an average monthly return of 1.88%. Our implementation also selects securities to long and short based on their same-calendar month returns. For each security in the universe, we calculate the monthly return for the same-calendar month of the previous year and choose the symbols as follows:
# Get historical close data for coarse-selected symbols of the same calendar month
start = self.time.replace(day = 1, year = self.time.year-1)
end = Expiry.end_of_month(start) - timedelta(1)
history = self.history(symbols, start, end, Resolution.DAILY).close.unstack(level=0)
# Get the same calendar month returns for the symbols
MonthlyReturn = {ticker: prices.iloc[-1]/prices.iloc[0] for ticker, prices in history.iteritems()}
# Sorted the values of monthly return
sortedReturn = sorted(MonthlyReturn.items(), key=lambda x:x[1], reverse=True)
# Get the symbols to long / short
self.long_symbols = [x[0] for x in sortedReturn[:self.num_long]]
self.short_symbols = [x[0] for x in sortedReturn[-self.num_short:]]
# Note that self.long_symbols/self.short_symbols contains strings instead of symbols
return [x for x in symbols if str(x) in self.long_symbols + self.short_symbols]
Step 3: Rebalance monthly
At the end of each month, we rebalance our portfolio, liquidate the securities that are not part of the new month’s universe, and repeat step 1 and 2. Keep in mind we use equal weights for the long and short positions of securities in our portfolio.
'''
Rebalance every month based on same-calendar month returns effect
'''
# Before next rebalance, do nothing
if self.time < self.next_rebalance:
return
count = len(self.long_symbols + self.short_symbols)
# Open long positions
for symbol in self.long_symbols:
self.set_holdings(symbol, 1/count)
# Open short positions
for symbol in self.short_symbols:
self.set_holdings(symbol, -1/count)
# Rebalance at the end of every month
self.next_rebalance = Expiry.end_of_month(self.time) - timedelta(1)
Results
In backtesting our algorithm achieves a Sharpe ratio of 0.128 relative to S&P 500 (SPY) Sharpe ratio of 0.773 for the past 10 years. To improve, we can build upon this implementation by trying the following extensions:
- Using the same-calendar months of multiple years (e.g. the last 5 years), instead of using the previous year as we did in this tutorial, to get more stable monthly returns.
- Using discounting to capture time effects in the returns.
- Creating the initial universe using different criteria such as quarterly, rather than monthly, returns.
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.
Daniel Chen
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!