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.
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. 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.
Methods
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.
# Sort the securities with prices > 5 in DollarVolume decendingly
selected = sorted([x for x in coarse if x.Price > 5],
key=lambda x: x.DollarVolume, 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.EndOfMonth(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.longSymbols = [x[0] for x in sortedReturn[:self.num_long]]
self.shortSymbols = [x[0] for x in sortedReturn[-self.num_short:]]
# Note that self.longSymbols/self.shortSymbols contains strings instead of symbols
return [x for x in symbols if str(x) in self.longSymbols + self.shortSymbols]
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.
# Before next rebalance, do nothing
if self.Time < self.nextRebalance:
return
count = len(self.longSymbols + self.shortSymbols)
# Open long positions
for symbol in self.longSymbols:
self.SetHoldings(symbol, 1/count)
# Open short positions
for symbol in self.shortSymbols:
self.SetHoldings(symbol, -1/count)
# Rebalance at the end of every month
self.nextRebalance = Expiry.EndOfMonth(self.Time) - timedelta(1)
Results
In backtesting, our algorithm achieves a Sharpe ratio of 0.332 relative to S&P 500 (SPY) Sharpe ratio of 0.893 for the past 10 years. The performance indicates using the idea of same-calendar month returns makes sense. Interested users 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.
Jason Mark
Hi Daniel,
That is an interesting strategy. I wonder if having the short positions actually hurt the returns. Seeing the implementation is insightful. It helps us new quants learn how to use the platform.
Thanks!
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!