Backtesting eines Moving Average Crossover in Python mit Pandas

Schriftsteller:Gutes, Erstellt: 2019-03-27 15:11:40, Aktualisiert:

In diesem Artikel werden wir die von uns eingeführten Mechanismen nutzen, um eine aktuelle Strategie zu erforschen, nämlich den gleitenden Durchschnitts-Crossover auf AAPL.

Strategie für die Verlagerung des gleitenden Durchschnitts

Die Moving Average Crossover-Technik ist eine sehr bekannte vereinfachte Momentum-Strategie.

Die Strategie, wie hier beschrieben, ist nur lang. Zwei separate einfache gleitende Durchschnittsfilter werden mit unterschiedlichen Lookback-Perioden einer bestimmten Zeitreihe erstellt. Signale zum Kauf des Vermögenswerts treten auf, wenn der kürzere Lookback-Gleibende Durchschnitt den längeren Lookback-Gleibenden Durchschnitt übersteigt. Wenn der längere Durchschnitt anschließend den kürzeren Durchschnitt übersteigt, wird der Vermögenswert zurückverkauft.

Für dieses Beispiel habe ich Apple, Inc. (AAPL) als Zeitreihe gewählt, mit einem kurzen Rückblick von 100 Tagen und einem langen Rückblick von 400 Tagen. Dies ist das Beispiel der zipline-algorithmischen Handelsbibliothek. Wenn wir also unseren eigenen Backtester implementieren möchten, müssen wir sicherstellen, dass er mit den Ergebnissen in zipline übereinstimmt, als grundlegendes Validierungsmittel.

Durchsetzung

Achten Sie darauf, dass Sie dem vorherigen Tutorial folgen, das beschreibt, wie die anfängliche Objekthierarchie für den Backtester aufgebaut wird, sonst funktioniert der folgende Code nicht.

  • Python - 2.7.3
  • NumPy - 1.8.0
  • Pandas - 0.12.0
  • Matplotlib - 1.1.0

Die Implementierung von ma_cross.py erfordertbacktest.pyDer erste Schritt besteht darin, die notwendigen Module und Objekte zu importieren:

# ma_cross.py

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

from pandas.io.data import DataReader
from backtest import Strategy, Portfolio

Wie im vorherigen Tutorial werden wir die Strategie-Abstrakte-Basisklasse unterteilen, um MovingAverageCrossStrategy zu produzieren, die alle Details enthält, wie die Signale generiert werden, wenn sich die gleitenden Durchschnitte von AAPL kreuzen.

Das Objekt benötigt ein short_window und ein long_window, auf denen es funktionieren kann. Die Werte wurden auf Standards von 100 Tagen bzw. 400 Tagen gesetzt, was die gleichen Parameter sind, die im Hauptbeispiel von zipline verwendet werden.

Die gleitenden Durchschnitte werden erstellt, indem die Funktion pandas rolling_mean auf den Balken [Close] des Schlusskurses der AAPL-Aktie verwendet wird. Sobald die einzelnen gleitenden Durchschnitte konstruiert wurden, wird die Signalreihe erzeugt, indem die Spalte auf 1,0 gesetzt wird, wenn der kurze gleitende Durchschnitt größer als der lange gleitende Durchschnitt ist, oder auf 0,0. Daraus können die Positionsbestellungen erzeugt werden, um Handelssignale darzustellen.

# 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

Das MarketOnClosePortfolio ist von Portfolio unterteilt, das inbacktest.py. Es ist fast identisch mit der im vorherigen Tutorial beschriebenen Implementierung, mit der Ausnahme, dass die Trades nun auf einer Close-to-Close-Basis statt einer Open-to-Open-Basis ausgeführt werden.

# 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

Nach der Definition der Klassen MovingAverageCrossStrategy und MarketOnClosePortfolio wird einHauptDie Ergebnisse der Strategie werden zusätzlich anhand eines Graphs der Eigenkapitalkurve untersucht.

Das Panda DataReader-Objekt lädt OHLCV-Preise von AAPL-Aktien für den Zeitraum vom 1. Januar 1990 bis zum 1. Januar 2002 herunter, woraufhin die Signale DataFrame erstellt werden, um die Long-Only-Signale zu generieren.

Der letzte Schritt besteht darin, mit matplotlib ein zweistelliges Diagramm beider AAPL-Preise zu zeichnen, überlagert mit den gleitenden Durchschnitten und Kauf-/Verkaufssignalen sowie der Eigenkapitalkurve mit den gleichen Kauf-/Verkaufssignalen.

# 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.)

    # 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(212, ylabel='Portfolio value in $')
    returns['total'].plot(ax=ax2, lw=2.)

    # Plot the "buy" and "sell" trades against the equity curve
    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()

Die grafische Ausgabe des Codes ist wie folgt. Ich habe den IPython %paste Befehl verwendet, um dies direkt in die IPython-Konsole zu setzen, während ich in Ubuntu bin, so dass die grafische Ausgabe sichtbar bleibt. Die rosa Aufsticks repräsentieren den Kauf der Aktie, während die schwarzen Absticks den Verkauf darstellen:imgAAPL gleitende durchschnittliche Crossover-Leistung von 1990-01-01 bis 2002-01-01

Wie man sehen kann, verliert die Strategie in diesem Zeitraum mit fünf Hin- und Rückgeschäften Geld. Dies ist angesichts des Verhaltens von AAPL in diesem Zeitraum nicht überraschend, das einen leichten Abwärtstrend verzeichnete, gefolgt von einem deutlichen Anstieg ab 1998. Die Rückblickperiode der gleitenden Durchschnittssignale ist ziemlich groß und beeinflusste den Gewinn des Endgeschäfts, der die Strategie sonst rentabel gemacht hätte.

In den folgenden Artikeln werden wir ein anspruchsvolleres Mittel zur Analyse der Leistung entwickeln und beschreiben, wie die Rückblickperioden der einzelnen gleitenden Durchschnittssignale optimiert werden können.


Mehr