#!/usr/bin/env python # coding: utf-8 # https://www.quantstart.com/articles/Research-Backtesting-Environments-in-Python-with-pandas # # 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 # # 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() # 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) # In[11]: portfolio = MarketOnOpenPortfolio(symbol, bars, signals, initial_capital=100000.0) # In[12]: pf = portfolio.backtest_portfolio() # In[13]: pf.head(10) # # BACKTESTING A MOVING AVERAGE CROSSOVER # https://www.quantstart.com/articles/Backtesting-a-Moving-Average-Crossover-in-Python-with-pandas # 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() # ## Plot two charts to assess trades and equity curve # In[19]: import matplotlib.pyplot as plt import seaborn as sns get_ipython().run_line_magic('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()