Backtesting a Moving Average Crossover in Python with pandas

In the previous article on Research Backtesting Environments In Python With Pandas we created an object-oriented research-based backtesting environment and tested it on a random forecasting strategy. In this article we will make use of the machinery we introduced to carry out research on an actual strategy, namely the Moving Average Crossover on AAPL.

## Moving Average Crossover Strategy

The Moving Average Crossover technique is an extremely well-known simplistic momentum strategy. It is often considered the "Hello World" example for quantitative trading.

The strategy as outlined here is long-only. Two separate simple moving average filters are created, with varying lookback periods, of a particular time series. Signals to purchase the asset occur when the shorter lookback moving average exceeds the longer lookback moving average. If the longer average subsequently exceeds the shorter average, the asset is sold back. The strategy works well when a time series enters a period of strong trend and then slowly reverses the trend.

For this example, I have chosen Apple, Inc. (AAPL) as the time series, with a short lookback of 100 days and a long lookback of 400 days. This is the example provided by the zipline algorithmic trading library. Thus if we wish to implement our own backtester we need to ensure that it matches the results in zipline, as a basic means of validation.

## Implementation

Make sure to follow the previous tutorial here, which describes how the initial object hierarchy for the backtester is constructed, otherwise the code below will not work. For this particular implementation I have used the following libraries:

• Python - 2.7.3
• NumPy - 1.8.0
• pandas - 0.12.0
• matplotlib - 1.1.0

The implementation of ma_cross.py requires backtest.py from the previous tutorial. The first step is to import the necessary modules and objects:

# ma_cross.py

import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from backtest import Strategy, Portfolio


As in the previous tutorial we are going to subclass the Strategy abstract base class to produce MovingAverageCrossStrategy, which contains all of the details on how to generate the signals when the moving averages of AAPL cross over each other.

The object requires a short_window and a long_window on which to operate. The values have been set to defaults of 100 days and 400 days respectively, which are the same parameters used in the main example of zipline.

The moving averages are created by using the pandas rolling_mean function on the bars['Close'] closing price of the AAPL stock. Once the individual moving averages have been constructed, the signal Series is generated by setting the colum equal to 1.0 when the short moving average is greater than the long moving average, or 0.0 otherwise. From this the positions orders can be generated to represent trading signals.

# ma_cross.py

class MovingAverageCrossStrategy(Strategy):
"""
Requires:
symbol - A stock symbol on which to form a strategy on.
bars - A DataFrame of bars for the above symbol.
short_window - Lookback period for short moving average.
long_window - Lookback period for long moving average."""

def __init__(self, symbol, bars, short_window=100, long_window=400):
self.symbol = symbol
self.bars = bars

self.short_window = short_window
self.long_window = long_window

def generate_signals(self):
"""Returns the DataFrame of symbols containing the signals
to go long, short or hold (1, -1 or 0)."""
signals = pd.DataFrame(index=self.bars.index)
signals['signal'] = 0.0

# Create the set of short and long simple moving averages over the
# respective periods
signals['short_mavg'] = pd.rolling_mean(bars['Close'], self.short_window, min_periods=1)
signals['long_mavg'] = pd.rolling_mean(bars['Close'], self.long_window, min_periods=1)

# Create a 'signal' (invested or not invested) when the short moving average crosses the long
# moving average, but only for the period greater than the shortest moving average window
signals['signal'][self.short_window:] = np.where(signals['short_mavg'][self.short_window:]
> signals['long_mavg'][self.short_window:], 1.0, 0.0)

# Take the difference of the signals in order to generate actual trading orders
signals['positions'] = signals['signal'].diff()

return signals


The MarketOnClosePortfolio is subclassed from Portfolio, which is found in backtest.py. It is almost identical to the implementation described in the prior tutorial, with the exception that the trades are now carried out on a Close-to-Close basis, rather than an Open-to-Open basis. For details on how the Portfolio object is defined, see the previous tutorial. I've left the code in for completeness and to keep this tutorial self-contained:

# ma_cross.py

class MarketOnClosePortfolio(Portfolio):
"""Encapsulates the notion of a portfolio of positions based
on a set of signals as provided by a Strategy.

Requires:
symbol - A stock symbol which forms the basis of the portfolio.
bars - A DataFrame of bars for a symbol set.
signals - A pandas DataFrame of signals (1, 0, -1) for each symbol.
initial_capital - The amount in cash at the start of the portfolio."""

def __init__(self, symbol, bars, signals, initial_capital=100000.0):
self.symbol = symbol
self.bars = bars
self.signals = signals
self.initial_capital = float(initial_capital)
self.positions = self.generate_positions()

def generate_positions(self):
positions = pd.DataFrame(index=signals.index).fillna(0.0)
positions[self.symbol] = 100*signals['signal']   # This strategy buys 100 shares
return positions

def backtest_portfolio(self):
portfolio = self.positions*self.bars['Close']
pos_diff = self.positions.diff()

portfolio['holdings'] = (self.positions*self.bars['Close']).sum(axis=1)
portfolio['cash'] = self.initial_capital - (pos_diff*self.bars['Close']).sum(axis=1).cumsum()

portfolio['total'] = portfolio['cash'] + portfolio['holdings']
portfolio['returns'] = portfolio['total'].pct_change()
return portfolio


Now that the MovingAverageCrossStrategy and MarketOnClosePortfolio classes have been defined, a __main__ function will be called to tie all of the functionality together. In addition the performance of the strategy will be examined via a plot of the equity curve.

The pandas DataReader object downloads OHLCV prices of AAPL stock for the period 1st Jan 1990 to 1st Jan 2002, at which point the signals DataFrame is created to generate the long-only signals. Subsequently the portfolio is generated with a 100,000 USD initial capital base and the returns are calculated on the equity curve.

The final step is to use matplotlib to plot a two-figure plot of both AAPL prices, overlaid with the moving averages and buy/sell signals, as well as the equity curve with the same buy/sell signals. The plotting code is taken (and modified) from the zipline implementation example.

# ma_cross.py

if __name__ == "__main__":
# Obtain daily bars of AAPL from Yahoo Finance for the period
# 1st Jan 1990 to 1st Jan 2002 - This is an example from ZipLine
symbol = 'AAPL'
bars = DataReader(symbol, "yahoo", datetime.datetime(1990,1,1), datetime.datetime(2002,1,1))

# Create a Moving Average Cross Strategy instance with a short moving
# average window of 100 days and a long window of 400 days
mac = MovingAverageCrossStrategy(symbol, bars, short_window=100, long_window=400)
signals = mac.generate_signals()

# Create a portfolio of AAPL, with $100,000 initial capital portfolio = MarketOnClosePortfolio(symbol, bars, signals, initial_capital=100000.0) returns = portfolio.backtest_portfolio() # Plot two charts to assess trades and equity curve fig = plt.figure() fig.patch.set_facecolor('white') # Set the outer colour to white ax1 = fig.add_subplot(211, ylabel='Price in$')

# Plot the AAPL closing price overlaid with the moving averages
bars['Close'].plot(ax=ax1, color='r', lw=2.)
signals[['short_mavg', 'long_mavg']].plot(ax=ax1, lw=2.)

ax1.plot(signals.ix[signals.positions == 1.0].index,
signals.short_mavg[signals.positions == 1.0],
'^', markersize=10, color='m')

# Plot the "sell" trades against AAPL
ax1.plot(signals.ix[signals.positions == -1.0].index,
signals.short_mavg[signals.positions == -1.0],
'v', markersize=10, color='k')

# Plot the equity curve in dollars
ax2 = fig.add_subplot(212, ylabel='Portfolio value in \$')
returns['total'].plot(ax=ax2, lw=2.)

ax2.plot(returns.ix[signals.positions == 1.0].index,
returns.total[signals.positions == 1.0],
'^', markersize=10, color='m')
ax2.plot(returns.ix[signals.positions == -1.0].index,
returns.total[signals.positions == -1.0],
'v', markersize=10, color='k')

# Plot the figure
fig.show()


The graphical output of the code is as follows. I made use of the IPython %paste command to put this directly into the IPython console while in Ubuntu, so that the graphical output remained in view. The pink upticks represent purchasing the stock, while the black downticks represent selling it back:

AAPL Moving Average Crossover Performance from 1990-01-01 to 2002-01-01

As can be seen the strategy loses money over the period, with five round-trip trades. This is not surprising given the behaviour of AAPL over the period, which was on a slight downward trend, followed by a significant upsurge beginning in 1998. The lookback period of the moving average signals is rather large and this impacted the profit of the final trade, which otherwise may have made the strategy profitable.

In subsequent articles we will create a more sophisticated means of analysing performance, as well as describing how to optimise the lookback periods of the individual moving average signals.

# Just Getting Started with Quantitative Trading?

## 3 Reasons to Subscribe to the QuantStart Email List:

No Thanks, I'll Pass For Now