
Introduction
In this article, we review the infamous January effect which proposes that stocks’ prices increase from December to January is the highest. We illustrate the causes of the January effect, and present a simple trading strategy to profit from this calendar effect. The project is shared on my online repository https://github.com/DinodC/january-effect.
We start off by importing packages
import numpy as np
import pandas as pd
import pickle
Pull Data
In this section, we collect S&P consittuents’ historical data from a previous project https://quant-trading.blog/2019/06/24/backtesting-a-trading-strategy-part-2/.
keys = ['sp500',
'sp400',
'sp600']
Initialize close
close = {}
Pull data
for i in keys:
# Load OHLCV data
with open(i + '_data.pickle', 'rb') as f:
data = pickle.load(f)
# Update close prices data
close[i] = data.close
f.close()
Inspect close prices of S&P 500 Index
close['sp500'].head()
Symbols | A | AAL | AAP | AAPL | ABBV |
---|---|---|---|---|---|
date | |||||
2014-06-11 | 40.1199 | 40.2868 | 125.0694 | 86.0249 | 45.0769 |
2014-06-12 | 39.7726 | 38.2958 | 123.2252 | 84.5860 | 44.6031 |
2014-06-13 | 39.8407 | 38.4672 | 123.7407 | 83.6603 | 45.0187 |
2014-06-16 | 39.7181 | 39.1150 | 124.0086 | 84.5035 | 44.8857 |
2014-06-17 | 40.0927 | 39.8867 | 124.9411 | 84.3935 | 45.1351 |
5 rows × 505 columns
close['sp500'].tail()
Symbols | A | AAL | AAP | AAPL | ABBV |
---|---|---|---|---|---|
date | |||||
2019-06-04 | 67.95 | 29.12 | 154.61 | 179.64 | 76.75 |
2019-06-05 | 68.35 | 30.36 | 154.61 | 182.54 | 77.06 |
2019-06-06 | 69.16 | 30.38 | 154.90 | 185.22 | 77.07 |
2019-06-07 | 69.52 | 30.92 | 155.35 | 190.15 | 77.43 |
2019-06-10 | 70.29 | 30.76 | 153.52 | 192.58 | 76.95 |
5 rows × 505 columns
close['sp500'].describe()
Symbols | A | AAL | AAP | AAPL | ABBV |
---|---|---|---|---|---|
count | 1258.000000 | 1258.000000 | 1258.000000 | 1258.000000 | 1258.000000 |
mean | 52.010924 | 41.207727 | 145.418612 | 135.094399 | 66.653293 |
std | 13.866577 | 6.366236 | 24.128339 | 38.127616 | 17.721243 |
min | 32.258600 | 24.539800 | 79.168700 | 82.743800 | 42.066600 |
25% | 39.393500 | 36.586125 | 132.519725 | 103.622650 | 53.170800 |
8 rows × 505 columns
close['sp500'].shape
(1258, 505)
The January Effect
A calendar effect is an economic or stock market behavior which is related to the calendar such as the day of the week or the month of the year. The most popular is the January effect which suggests that stock prices’ increase from December to January is the highest. The January effect was first observed by Sydney Wachtel in 1942, but seems to have lost its effect in recent years.
Explanations Of The January Effect:
Possible explanations of the January effect include:
- Tax-loss harvesting (or saving):
Tax-loss harvesting allows investors to save taxes on realized gains using their unrealized losses. In detail, investors sell the the losing stocks of their portfolio to generate losses. Investors’ taxes are reduced as the gains and losses are netted out. - Window dressing:
Window dressing enables investors to improve the appearance of the portfolios which they manage. In detail, investors buy (sell) winning (losing) stocks to enhance portfolio appearance. Investors’ portfolios has a better image as they now contain high-flying stocks. - Bonus:
Bonus (end-of-year) allows investors to purchase stocks at the beginning of the year. In detail, investors buying stocks in January will push the stock prices up. Investors’ bonus fuels price increase from December to January.
A Trading Strategy To Profit From The January Effect
A possible trading strategy to profit from the January effect is the following:
- Buy the losing (or off-loaded) stocks of December; and
- Sell the winning stocks of December.
We backtest the strategy using the different investment universes:
- S&P 500 Index composed of large capitalization stocks
- S&P 400 Index comprised of medium capitalization stocks
- S&P 600 Index composed of small capitalization stocks
Assume transaction cost (one-way)
tc_one_way = 0.0005
Initialize the strategy’s returns
returns = {}
Backtesting the strategy
for i in keys:
# Create today series
today = pd.Series(close[i].index)
# Create months and years series
months = pd.Series(close[i].index.month)
years = pd.Series(close[i].index.year)
# Create next day of the month and next day of the year
next_day_month = months.shift(periods=-1)
next_day_year = years.shift(periods=-1)
# Last day of December
mask_last_day_dec = (months==12) & (next_day_month==1)
last_day_dec = today[mask_last_day_dec]
# Last day of Jan
mask_last_day_jan = (months==1) & (next_day_month==2)
last_day_jan = today[mask_last_day_jan]
# Ensure that last day of January is after last day of December
assert (last_day_jan.values > last_day_dec.values).any(), 'Assertion violated'
# End of year indices
mask_eoy = (years!=next_day_year)
eoy = today[mask_eoy]
# Last item is not eoy
eoy = eoy[:-1]
# Check that eoy dates match last day of December dates
assert (last_day_dec.values==eoy.values).any(), 'Assertion violated'
# Calculate annual returns (from December of previous year to December of current year)
annual_returns = close[i][mask_eoy.values].pct_change()
# Retrieve last day of January close prices
close_last_day_jan = close[i][mask_last_day_jan.values]
# Retrieve last day of December close prices
close_last_day_dec = close[i][mask_last_day_dec.values]
# Modify the index of clost_last_day_dec
close_last_day_dec.index = close_last_day_jan.index
# Calculate January returns (from December of previous year to January of current year)
january_returns = (close_last_day_jan - close_last_day_dec) / close_last_day_dec
for j in range(1, annual_returns.shape[0]-1):
# Create a mask for stocks with returns != NaN
mask_has_data = np.isfinite(annual_returns.iloc[j, :])
has_data = list(mask_has_data[mask_has_data].index)
# Sort stocks as per annual returns
sort_tickers = annual_returns[has_data].iloc[j, :].sort_values().index
# Set the number of stocks to long (short)
top_n = round(len(has_data) / 10)
# List of stocks to long and short
longs = sort_tickers[:top_n]
shorts = sort_tickers[-top_n:]
# Calculate returns from the last day of December to the last day of January
long_returns = (january_returns.iloc[j][longs]).mean()
short_returns = (january_returns.iloc[j][shorts]).mean()
portfolio_returns = 0.5 * (long_returns - short_returns) - 2 * tc_one_way
# Update portfolio returns
returns[i] = portfolio_returns
Backtesting Results
In this section, we present the backtesting results of the trading strategy.
pd.DataFrame(returns,
index=['returns'],
columns=keys)
sp500 | sp400 | sp600 | |
---|---|---|---|
returns | 0.048986 | 0.063328 | 0.067413 |
Remarks:
- The trading strategy generated positive returns under all investment universes.
- The trading strategy produced the highest (lowest) returns using small (large) capitalization stocks.
- Note that higher spreads are usually attached to small-cap stocks which could reduce the trading strategy’s returns.
Conclusion
In this article, we reviewed the well-known January effect, and proposed a trading strategy to profit from it. The trading strategy buys (sells) the losing (winning) stocks in December, and awaits reversal in January. The trading strategy generates the most (least) returns when using small (large) capitalization stocks.