Requirements

  • Quandl
  • Python 3.4
  • pandas==0.17.1
  • numpy==1.10.4
In [1]:
import numpy as np
import pandas as pd
import Quandl   # Necessary for obtaining financial data easily
from abc import ABCMeta, abstractmethod

import datetime
import matplotlib.pyplot as plt

from pandas.io.data import DataReader
/usr/local/lib/python3.4/site-packages/pandas/io/data.py:33: FutureWarning: 
The pandas.io.data module is moved to a separate package (pandas-datareader) and will be removed from pandas in a future version.
After installing the pandas-datareader package (https://github.com/pydata/pandas-datareader), you can change the import ``from pandas.io import data, wb`` to ``from pandas_datareader import data, wb``.
  FutureWarning)

Random strategy

In [2]:
class Strategy(object):
    """Strategy is an abstract base class providing an interface for
    all subsequent (inherited) trading strategies.

    The goal of a (derived) Strategy object is to output a list of signals,
    which has the form of a time series indexed pandas DataFrame.

    In this instance only a single symbol/instrument is supported."""

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_signals(self):
        """An implementation is required to return the DataFrame of symbols 
        containing the signals to go long, short or hold (1, -1 or 0)."""
        raise NotImplementedError("Should implement generate_signals()!")
In [3]:
class Portfolio(object):
    """An abstract base class representing a portfolio of 
    positions (including both instruments and cash), determined
    on the basis of a set of signals provided by a Strategy."""

    __metaclass__ = ABCMeta

    @abstractmethod
    def generate_positions(self):
        """Provides the logic to determine how the portfolio 
        positions are allocated on the basis of forecasting
        signals and available cash."""
        raise NotImplementedError("Should implement generate_positions()!")

    @abstractmethod
    def backtest_portfolio(self):
        """Provides the logic to generate the trading orders
        and subsequent equity curve (i.e. growth of total equity),
        as a sum of holdings and cash, and the bar-period returns
        associated with this curve based on the 'positions' DataFrame.

        Produces a portfolio object that can be examined by 
        other classes/functions."""
        raise NotImplementedError("Should implement backtest_portfolio()!")
In [4]:
class RandomForecastingStrategy(Strategy):
    """Derives from Strategy to produce a set of signals that
    are randomly generated long/shorts. Clearly a nonsensical
    strategy, but perfectly acceptable for demonstrating the
    backtesting infrastructure!"""    
    
    def __init__(self, symbol, bars):
        """Requires the symbol ticker and the pandas DataFrame of bars"""
        self.symbol = symbol
        self.bars = bars

    def generate_signals(self):
        """Creates a pandas DataFrame of random signals."""
        signals = pd.DataFrame(index=self.bars.index)
        signals['signal'] = np.sign(np.random.randn(len(signals)))

        # The first five elements are set to zero in order to minimise
        # upstream NaN errors in the forecaster.
        signals['signal'][0:5] = 0.0
        return signals
In [5]:
class MarketOnOpenPortfolio(Portfolio):
    """Inherits Portfolio to create a system that purchases 100 units of 
    a particular symbol upon a long/short signal, assuming the market 
    open price of a bar.

    In addition, there are zero transaction costs and cash can be immediately 
    borrowed for shorting (no margin posting or interest requirements). 

    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):
        """Creates a 'positions' DataFrame that simply longs or shorts
        100 of the particular symbol based on the forecast signals of
        {1, 0, -1} from the signals DataFrame."""
        positions = pd.DataFrame(index=self.signals.index).fillna(0.0)
        positions[self.symbol] = 100 * self.signals['signal']
        return positions
                    
    def backtest_portfolio(self):
        """Constructs a portfolio from the positions DataFrame by 
        assuming the ability to trade at the precise market open price
        of each bar (an unrealistic assumption!). 

        Calculates the total of cash and the holdings (market price of
        each position per bar), in order to generate an equity curve
        ('total') and a set of bar-based returns ('returns').

        Returns the portfolio object to be used elsewhere."""

        # Construct the portfolio DataFrame to use the same index
        # as 'positions' and with a set of 'trading orders' in the
        # 'pos_diff' object, assuming market open prices.
        pf = pd.DataFrame(index=self.bars.index)
        # Price of current operation
        pf['holdings'] = self.positions.mul(self.bars['Open'], axis='index')
        pf['cash'] = self.initial_capital - pf['holdings'].cumsum()
        pf['total'] = pf['cash'] + self.positions[self.symbol].cumsum() * self.bars['Open']
        pf['returns'] = pf['total'].pct_change()
        return pf
In [6]:
symbol = 'SPY'
bars = Quandl.get("GOOG/NYSE_%s" % symbol, collapse="daily")
In [7]:
bars.head()
Out[7]:
Open High Low Close Volume
Date
1997-08-21 0 94.25 92.09 92.59 5392600
1997-08-22 0 92.73 90.56 92.56 7172900
1997-08-25 0 93.41 91.84 92.22 3888000
1997-08-26 0 92.56 90.70 90.86 4290000
1997-08-27 0 91.97 90.41 91.41 5484300
In [8]:
# Fix open 0
bars = bars[bars.Open > 0]
In [9]:
# Create a set of random forecasting signals for SPY
rfs = RandomForecastingStrategy(symbol, bars)
signals = rfs.generate_signals()
In [10]:
signals.head(10)
Out[10]:
signal
Date
2000-01-03 0
2000-01-04 0
2000-01-05 0
2000-01-06 0
2000-01-07 0
2000-01-10 1
2000-01-11 -1
2000-01-12 -1
2000-01-13 -1
2000-01-14 -1
In [11]:
portfolio = MarketOnOpenPortfolio(symbol, bars, signals, initial_capital=100000.0)
In [12]:
pf = portfolio.backtest_portfolio()
In [13]:
pf.head(10)
Out[13]:
holdings cash total returns
Date
2000-01-03 0 100000 100000 NaN
2000-01-04 0 100000 100000 0.000000
2000-01-05 0 100000 100000 0.000000
2000-01-06 0 100000 100000 0.000000
2000-01-07 0 100000 100000 0.000000
2000-01-10 14625 85375 100000 0.000000
2000-01-11 -14581 99956 99956 -0.000440
2000-01-12 -14459 114415 99956 0.000000
2000-01-13 -14447 128862 99968 0.000120
2000-01-14 -14653 143515 99556 -0.004121
In [14]:
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(self.bars['Close'], self.short_window, min_periods=1)
        signals['long_mavg'] = pd.rolling_mean(self.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
In [15]:
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=self.signals.index).fillna(0.0)
        positions[self.symbol] = 100 * self.signals['positions']   # This strategy buys 100 shares
        return positions
                    
    def backtest_portfolio(self):
        pf = pd.DataFrame(index=self.bars.index)
        pf['holdings'] = self.positions.mul(self.bars['Close'], axis='index')
        pf['cash'] = self.initial_capital - pf['holdings'].cumsum()
        pf['total'] = pf['cash'] + self.positions[self.symbol].cumsum() * self.bars['Close']
        pf['returns'] = pf['total'].pct_change()
        return pf
In [16]:
# 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)
pf = portfolio.backtest_portfolio()
In [17]:
bars.head()
Out[17]:
Open High Low Close Volume Adj Close
Date
1990-01-02 35.249999 37.500000 35.000000 37.250001 45799600 1.151017
1990-01-03 37.999999 37.999999 37.500000 37.500000 51998800 1.158742
1990-01-04 38.250001 38.749999 37.250001 37.625000 55378400 1.162604
1990-01-05 37.750000 38.250001 36.999999 37.750000 30828000 1.166467
1990-01-08 37.500000 37.999999 36.999999 37.999999 25393200 1.174191

Plot two charts to assess trades and equity curve

In [19]:
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
In [102]:
fig = plt.figure(figsize=(15, 20))
#fig = plt.figure()
fig.patch.set_facecolor('white')     # Set the outer colour to white
ax1 = fig.add_subplot(411,  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.)

# Plot the "buy" trades against AAPL
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(412, ylabel='Portfolio value in $')
pf['total'].plot(ax=ax2, lw=2.)

# Plot the "buy" and "sell" trades against the equity curve
ax2.plot(pf.ix[signals.positions == 1.0].index, 
         pf.total[signals.positions == 1.0],
         '^', markersize=10, color='m')
ax2.plot(pf.ix[signals.positions == -1.0].index, 
         pf.total[signals.positions == -1.0],
         'v', markersize=10, color='k')

# signal
ax3 = fig.add_subplot(413, ylabel='signal')
signals.signal.plot(ax=ax3)

# signal
ax4 = fig.add_subplot(414, ylabel='signal')
signals.positions.plot(ax=ax4)

# Plot the figure
fig.show()
/usr/local/lib/python3.4/site-packages/matplotlib/figure.py:397: UserWarning: matplotlib is currently using a non-GUI backend, so cannot show the figure
  "matplotlib is currently using a non-GUI backend, "