Revisiting The January Effect

Photo by the author

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()
SymbolsAAALAAPAAPLABBV
date
2014-06-1140.119940.2868125.069486.024945.0769
2014-06-1239.772638.2958123.225284.586044.6031
2014-06-1339.840738.4672123.740783.660345.0187
2014-06-1639.718139.1150124.008684.503544.8857
2014-06-1740.092739.8867124.941184.393545.1351

5 rows × 505 columns

close['sp500'].tail()
SymbolsAAALAAPAAPLABBV
date
2019-06-0467.9529.12154.61179.6476.75
2019-06-0568.3530.36154.61182.5477.06
2019-06-0669.1630.38154.90185.2277.07
2019-06-0769.5230.92155.35190.1577.43
2019-06-1070.2930.76153.52192.5876.95

5 rows × 505 columns

close['sp500'].describe()
SymbolsAAALAAPAAPLABBV
count1258.0000001258.0000001258.0000001258.0000001258.000000
mean52.01092441.207727145.418612135.09439966.653293
std13.8665776.36623624.12833938.12761617.721243
min32.25860024.53980079.16870082.74380042.066600
25%39.39350036.586125132.519725103.62265053.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:

  1. 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.
  2. 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.
  3. 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:

  1. Buy the losing (or off-loaded) stocks of December; and
  2. Sell the winning stocks of December.

We backtest the strategy using the different investment universes:

  1. S&P 500 Index composed of large capitalization stocks
  2. S&P 400 Index comprised of medium capitalization stocks
  3. 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)
sp500sp400sp600
returns0.0489860.0633280.067413

Remarks:

  1. The trading strategy generated positive returns under all investment universes.
  2. The trading strategy produced the highest (lowest) returns using small (large) capitalization stocks.
  3. 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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s