Have you ever noticed the tendency for stocks that have risen to continue rising in the future? Likewise, a few down days seem to beget more losses. This is known as momentum and strategies that rely on these patterns are momentum-based strategies.

TL;DR

We develop a basic momentum strategy and test it on GameStop to yield strong returns.

Trading Momentum

Traders and investors have long known about the effects of momentum and have found that these effects appear across a wide variety of markets and time frames. Running these strategies on a single instrument is also known as trend following or time series momentum. We’ll stick with the latter name, and abbreviate it TSM from here on out.

While there are a whole host of ways to run this strategy by combining it with a series of indicators, risk management overlays (a must for a real trading system!), and diversification, we’ll start simple by showing how it works on a single instrument.

To get started, we can turn to Python and some standard libraries.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import yfinance as yf

From here, we can build our basic strategy function that we’ll call TSMStrategy. This will take the log returns of our time series, the period we're interested in, and a boolean variable that determines whether we will allow short positions or not. The function will then simulate the strategy for us and return the cumulative sum of the performance for further analysis.

def TSMStrategy(returns, period=1, shorts=False):
    if shorts:
        position = returns.rolling(period).mean().map(
            lambda x: -1 if x <= 0 else 1)
    else:
        position = returns.rolling(period).mean().map(
            lambda x: 0 if x <= 0 else 1)
    performance = position.shift(1) * returns
    return performance

It’s a simple function for a simple (but powerful) strategy. The next step requires us to provide it with some data and see how it performs. I’m going to choose a stock that his been in the news a lot lately: GameStop (GME) to see how our model performs.

ticker = 'GME'
yfObj = yf.Ticker(ticker)
data = yfObj.history(start='2000-01-01', end='2020-12-31')

Using the yfinance package, we can get the total public history of GME. It began trading in 2002, but setting the start date to 2000 will allow us to pick up the stock from the beginning without any errors.

To pass this to our strategy, we need to calculate the log returns and provide that to our function.

returns = np.log(data['Close'] / data['Close'].shift(1)).dropna()

The simplest TSM we can implement would require us to purchase the stock if it was up yesterday, and sell if it was down (if we’re holding it, otherwise we just wait). Let’s give it a shot.

performance = TSMStrategy(returns, period=1, shorts=False).dropna()
years = (performance.index.max() - performance.index.min()).days / 365
perf_cum = np.exp(performance.cumsum())
tot = perf_cum[-1] - 1
ann = perf_cum[-1] ** (1 / years) - 1
vol = performance.std() * np.sqrt(252)
rfr = 0.02
sharpe = (ann - rfr) / vol
print(f"1-day TSM Strategy yields:" +
      f"\n\t{tot*100:.2f}% total returns" + 
      f"\n\t{ann*100:.2f}% annual returns" +
      f"\n\t{sharpe:.2f} Sharpe Ratio")
gme_ret = np.exp(returns.cumsum())
b_tot = gme_ret[-1] - 1
b_ann = gme_ret[-1] ** (1 / years) - 1
b_vol = returns.std() * np.sqrt(252)
b_sharpe = (b_ann - rfr) / b_vol
print(f"Baseline Buy-and-Hold Strategy yields:" + 
      f"\n\t{b_tot*100:.2f}% total returns" + 
      f"\n\t{b_ann*100:.2f}% annual returns" +
      f"\n\t{b_sharpe:.2f} Sharpe Ratio")
1-day TSM Strategy yields:
	225.03% total returns
	6.44% annual returns
	0.12 Sharpe Ratio
Baseline Buy-and-Hold Strategy yields:
	184.63% total returns
	5.70% annual returns
	0.07 Sharpe Ratio

The 1-Day TSM strategy beats the buy-and-hold with a reasonable annual return (ignoring transaction costs, which may be high given such a short term strategy). The 1-day lookback is likely fraught with a lot of false trends, so we can run a variety of different time periods to see how they stack up. We’ll just run our model in a loop with 3, 5, 15, 30, and 90-day time periods.

import matplotlib.gridspec as gridspec
periods = [3, 5, 15, 30, 90]
fig = plt.figure(figsize=(12, 10))
gs = fig.add_gridspec(4, 4)
ax0 = fig.add_subplot(gs[:2, :4])
ax1 = fig.add_subplot(gs[2:, :2])
ax2 = fig.add_subplot(gs[2:, 2:])
ax0.plot((np.exp(returns.cumsum()) - 1) * 100, label=ticker, linestyle='-')
perf_dict = {'tot_ret': {'buy_and_hold': (np.exp(returns.sum()) - 1)}}
perf_dict['ann_ret'] = {'buy_and_hold': b_ann}
perf_dict['sharpe'] = {'buy_and_hold': b_sharpe}
for p in periods:
    log_perf = TSMStrategy(returns, period=p, shorts=False)
    perf = np.exp(log_perf.cumsum())
    perf_dict['tot_ret'][p] = (perf[-1] - 1)
    ann = (perf[-1] ** (1/years) - 1)
    perf_dict['ann_ret'][p] = ann
    vol = log_perf.std() * np.sqrt(252)
    perf_dict['sharpe'][p] = (ann - rfr) / vol
    ax0.plot((perf - 1) * 100, label=f'{p}-Day Mean')
    
ax0.set_ylabel('Returns (%)')
ax0.set_xlabel('Date')
ax0.set_title('Cumulative Returns')
ax0.grid()
ax0.legend()
_ = [ax1.bar(i, v * 100) for i, v in enumerate(perf_dict['ann_ret'].values())]
ax1.set_xticks([i for i, k in enumerate(perf_dict['ann_ret'])])
ax1.set_xticklabels([f'{k}-Day Mean' 
    if type(k) is int else ticker for 
    k in perf_dict['ann_ret'].keys()],
    rotation=45)
ax1.grid()
ax1.set_ylabel('Returns (%)')
ax1.set_xlabel('Strategy')
ax1.set_title('Annual Returns')
_ = [ax2.bar(i, v) for i, v in enumerate(perf_dict['sharpe'].values())]
ax2.set_xticks([i for i, k in enumerate(perf_dict['sharpe'])])
ax2.set_xticklabels([f'{k}-Day Mean' 
    if type(k) is int else ticker for 
    k in perf_dict['sharpe'].keys()],
    rotation=45)
ax2.grid()
ax2.set_ylabel('Sharpe Ratio')
ax2.set_xlabel('Strategy')
ax2.set_title('Sharpe Ratio')
plt.tight_layout()
plt.show()
gme_momentum_charts.png

Looking at the above, the 15-day momentum indicator gives us the best absolute and risk-adjusted returns. However, there is a lot of spread in these results. This indicates that we don’t have a very robust strategy (which should be no surprise). We could build on this basic strategy by incorporating other indicators, such as moving averages or exponentially weighted moving averages to indicate momentum. We could manage our risk better by incorporating stop losses or trailing stops to get out of trades closer to the top rather then when we have a down or flat 15-days. We could also allocate capital across multiple securities thereby benefiting from diversification and jumping on trends wherever they appear.

Some of these improvements require a more sophisticated, event-driven backtest system to properly simulate. This is something we’ve been working on for some time at Raposa Technologies and we’re making available to the public. To stay up to date with the latest developments and features as we roll out a professional quality, no-code backtesting system, subscribe below and be among the first to get access and updates to our newest offerings.