Original paper
Abstract
We use the seasonal patterns in momentum returns to provide insight into investor preferences. We find the momentum factor return is much greater prior to the calendar quarter-end, especially after a stock market decline. This pattern holds more strongly for larger stocks, for both winners and losers, for the US and internationally, and especially in recent years. The established year-end seasonality is consistent with the quarterly pattern, rather than tax-loss selling. The time-series momentum of markets follows the same pattern, primarily after a market decline. The patterns imply investors prefer well-performing stocks/markets at the quarter-end, particularly in a declining market.
Keywords:Â Momentum, Seasonality, Time-Series Predictability
Trading rules
- Focus on stocks from NASDAQ, Amex, and NYSE.
- Every month, sort stocks using their returns from days 21 to 251.
- Divide these stocks into 10 groups according to their performance.
- At the end of each quarter (March, June, September, December), invest in the top 10% and short the bottom 10%.
- Portfolio weighting: equal weight for each stock
Python code
Backtrader
import backtrader as bt
from datetime import datetime
class MomentumStrategy(bt.Strategy):
params = (
('momentum_period_min', 21),
('momentum_period_max', 251),
('rebalance_months', [3, 6, 9, 12]),
('lookback_days', 30),
)
def __init__(self):
self.inds = {}
for d in self.datas:
self.inds[d] = {}
self.inds[d]['momentum'] = bt.indicators.Momentum(d.close, period=self.p.momentum_period_min, maxperiod=self.p.momentum_period_max)
def next(self):
if self.data.datetime.date().month not in self.p.rebalance_months or self.data.datetime.date().day != self.p.lookback_days:
return
rankings = list(self.datas)
rankings.sort(key=lambda x: self.inds[x]['momentum'][0], reverse=True)
decile_size = len(rankings) // 10
long_stocks = rankings[:decile_size]
short_stocks = rankings[-decile_size:]
for i, d in enumerate(self.datas):
if d in long_stocks:
self.order_target_percent(d, target=1.0/decile_size)
elif d in short_stocks:
self.order_target_percent(d, target=-1.0/decile_size)
else:
self.order_target_percent(d, target=0)
cerebro = bt.Cerebro()
# Add the stocks from NASDAQ, Amex, NYSE (Here, you should add the stock data feeds)
# cerebro.adddata(data1)
# cerebro.adddata(data2)
# ...
cerebro.addstrategy(MomentumStrategy)
cerebro.broker.setcash(100000.0)
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
cerebro.run()
print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')
Please note that you need to add the stock data feeds from NASDAQ, Amex, and NYSE to the cerebro
instance in the code.