Backtesting einer Prognosestrategie für den S&P500 in Python mit Pandas

Schriftsteller:Gutes, Erstellt: 2019-03-29 14:24:50, Aktualisiert:

Reife Python-Bibliotheken wie matplotlib, pandas und scikit-learn reduzieren auch die Notwendigkeit, Boilerplate-Code zu schreiben oder eigene Implementierungen bekannter Algorithmen zu entwickeln.

Die Vorhersagestrategie

Die Prognose-Strategie selbst basiert auf einer maschinellen Lerntechnik, die als quadratischer Diskriminanten-Analysator bekannt ist und eng mit einem linearen Diskriminanten-Analysator verwandt ist.

Der Prognostiker verwendet die beiden vorherigen Tagesrenditen als eine Reihe von Faktoren, um die heutige Richtung des Aktienmarktes vorherzusagen. Wenn die Wahrscheinlichkeit, dass der Tag up übersteigt 50%, kauft die Strategie 500 Aktien des SPY ETF und verkauft sie am Ende des Tages. Wenn die Wahrscheinlichkeit eines Down-Day übersteigt 50%, verkauft die Strategie 500 Aktien des SPY ETF und kauft dann beim Schließen zurück. So ist es unser erstes Beispiel für eine Intraday-Handelsstrategie.

Beachten Sie, dass dies keine besonders realistische Handelsstrategie ist! Es ist unwahrscheinlich, dass wir jemals einen Eröffnungs- oder Schlusskurs aufgrund vieler Faktoren wie übermäßiger Eröffnungsvolatilität, Bestellvermittlung durch die Makler und potenzieller Liquiditätsprobleme rund um das Öffnen / Schließen erzielen. Darüber hinaus haben wir die Transaktionskosten nicht berücksichtigt. Dies wäre wahrscheinlich ein erheblicher Prozentsatz der Renditen, da täglich ein Hin- und Rückhandel durchgeführt wird. Daher muss unser Prognoseur relativ genau bei der Vorhersage der täglichen Renditen sein, sonst werden die Transaktionskosten alle unsere Handelsrenditen fressen.

Durchsetzung

Wie bei den anderen Python/Pandas-Tutorials habe ich folgende Bibliotheken verwendet:

  • Python - 2.7.3
  • NumPy - 1.8.0
  • Pandas - 0.12.0
  • Matplotlib - 1.1.0
  • SIKIT-learn - 0,14.1

Die Implementierung von snp_forecast.py benötigtbacktest.pyDas ist eine sehr schwierige Aufgabe.forecast.py(die hauptsächlich die Funktion create_lagged_series enthält) wird aus diesem vorherigen Tutorial erstellt.

# snp_forecast.py

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

from pandas.io.data import DataReader
from sklearn.qda import QDA

from backtest import Strategy, Portfolio
from forecast import create_lagged_series

Sobald alle relevanten Bibliotheken und Module eingeschlossen sind, ist es an der Zeit, die Strategie-Abstrakte-Basisklasse zu unterteilen, wie wir es in früheren Tutorials durchgeführt haben. SNPForecastingStrategy wurde entwickelt, um einen Quadratischen Diskriminationsanalysator an den S&P500-Aktienindex anzupassen, um seinen zukünftigen Wert vorherzusagen. Die Anpassung des Modells erfolgt in der nachstehenden fit_model-Methode, während die tatsächlichen Signale aus der generate_signals-Methode generiert werden. Dies entspricht der Schnittstelle einer Strategie-Klasse.

Die Details, wie ein quadratischer Diskriminanten-Analysator funktioniert, sowie die Python-Implementierung unten, werden im vorherigen Artikel zur Prognose von Finanzzeitreihen ausführlich beschrieben.

# snp_forecast.py

class SNPForecastingStrategy(Strategy):
    """    
    Requires:
    symbol - A stock symbol on which to form a strategy on.
    bars - A DataFrame of bars for the above symbol."""

    def __init__(self, symbol, bars):
        self.symbol = symbol
        self.bars = bars
        self.create_periods()
        self.fit_model()

    def create_periods(self):
        """Create training/test periods."""
        self.start_train = datetime.datetime(2001,1,10)
        self.start_test = datetime.datetime(2005,1,1)
        self.end_period = datetime.datetime(2005,12,31)

    def fit_model(self):
        """Fits a Quadratic Discriminant Analyser to the
        US stock market index (^GPSC in Yahoo)."""
        # Create a lagged series of the S&P500 US stock market index
        snpret = create_lagged_series(self.symbol, self.start_train, 
                                      self.end_period, lags=5) 

        # Use the prior two days of returns as 
        # predictor values, with direction as the response
        X = snpret[["Lag1","Lag2"]]
        y = snpret["Direction"]

        # Create training and test sets
        X_train = X[X.index < self.start_test]
        y_train = y[y.index < self.start_test]

        # Create the predicting factors for use 
        # in direction forecasting
        self.predictors = X[X.index >= self.start_test]

        # Create the Quadratic Discriminant Analysis model
        # and the forecasting strategy
        self.model = QDA()
        self.model.fit(X_train, y_train)

    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       

        # Predict the subsequent period with the QDA model
        signals['signal'] = self.model.predict(self.predictors)

        # Remove the first five signal entries to eliminate
        # NaN issues with the signals DataFrame
        signals['signal'][0:5] = 0.0
        signals['positions'] = signals['signal'].diff() 

        return signals

Jetzt, da die Prognose-Engine die Signale erzeugt hat, können wir ein MarketIntradayPortfolio erstellen.

Das Portfolio ist so konzipiert, dass long (kaufen) 500 SPY-Aktien zum Eröffnungspreis, wenn das Signal besagt, dass ein Aufstiegstag eintritt und dann beim Schließen verkauft wird. Umgekehrt ist das Portfolio so konzipiert, dass short (verkaufen) 500 SPY-Aktien, wenn das Signal besagt, dass ein Abstiegstag eintritt und anschließend zum Schlusskurs geschlossen wird.

Um dies zu erreichen, wird täglich die Preisdifferenz zwischen den Marktöffnungs- und den Marktschlusskurs bestimmt, was zu einer Berechnung des täglichen Gewinns für die 500 gekauften oder verkauften Aktien führt. Dies führt dann natürlich zu einer Eigenkapitalkurve, indem die Gewinn-/Verlustsumme für jeden Tag kumuliert wird.

Hier ist die Liste für das MarketIntradayPortfolio:

# snp_forecast.py

class MarketIntradayPortfolio(Portfolio):
    """Buys or sells 500 shares of an asset at the opening price of
    every bar, depending upon the direction of the forecast, closing 
    out the trade at the close of the bar.

    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):
        """Generate the positions DataFrame, based on the signals
        provided by the 'signals' DataFrame."""
        positions = pd.DataFrame(index=self.signals.index).fillna(0.0)

        # Long or short 500 shares of SPY based on 
        # directional signal every day
        positions[self.symbol] = 500*self.signals['signal']
        return positions
                    
    def backtest_portfolio(self):
        """Backtest the portfolio and return a DataFrame containing
        the equity curve and the percentage returns."""

        # Set the portfolio object to have the same time period
        # as the positions DataFrame
        portfolio = pd.DataFrame(index=self.positions.index)
        pos_diff = self.positions.diff()

        # Work out the intraday profit of the difference
        # in open and closing prices and then determine
        # the daily profit by longing if an up day is predicted
        # and shorting if a down day is predicted        
        portfolio['price_diff'] = self.bars['Close']-self.bars['Open']
        portfolio['price_diff'][0:5] = 0.0
        portfolio['profit'] = self.positions[self.symbol] * portfolio['price_diff']

        # Generate the equity curve and percentage returns
        portfolio['total'] = self.initial_capital + portfolio['profit'].cumsum()
        portfolio['returns'] = portfolio['total'].pct_change()
        return portfolio

Der letzte Schritt besteht darin, die Ziele der Strategie und des PortfoliosHauptDie Funktion erhält die Daten für das SPY-Instrument und erstellt dann die Signalgenerierungsstrategie für den S&P500-Index selbst. Dies wird durch den ^GSPC-Ticker bereitgestellt. Anschließend wird ein MarketIntradayPortfolio mit einem Anfangskapital von 100.000 USD (wie in früheren Tutorials) generiert. Schließlich werden die Renditen berechnet und die Eigenkapitalkurve gezeichnet.

Beachten Sie, wie wenig Code in diesem Stadium benötigt wird, da alle schweren Berechnungen in den Unterklassen Strategie und Portfolio durchgeführt werden.

if __name__ == "__main__":
    start_test = datetime.datetime(2005,1,1)
    end_period = datetime.datetime(2005,12,31)

    # Obtain the bars for SPY ETF which tracks the S&P500 index    
    bars = DataReader("SPY", "yahoo", start_test, end_period)
    
    # Create the S&P500 forecasting strategy
    snpf = SNPForecastingStrategy("^GSPC", bars)
    signals = snpf.generate_signals()

    # Create the portfolio based on the forecaster
    portfolio = MarketIntradayPortfolio("SPY", bars, signals,              
                                        initial_capital=100000.0)
    returns = portfolio.backtest_portfolio()

    # Plot results
    fig = plt.figure()
    fig.patch.set_facecolor('white')

    # Plot the price of the SPY ETF
    ax1 = fig.add_subplot(211,  ylabel='SPY ETF price in $')
    bars['Close'].plot(ax=ax1, color='r', lw=2.)

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

    fig.show()

Der Ausgang des Programms ist unten angegeben. In diesem Zeitraum brachte die Börse 4% zurück (vorausgesetzt, eine vollständig investierte Buy-and-Hold-Strategie), während der Algorithmus selbst auch 4% zurückbrachte. Beachten Sie, dass Transaktionskosten (wie Provisionsgebühren) diesem Backtesting-System nicht hinzugefügt wurden. Da die Strategie einen Hin- und Rückverkehr täglich durchführt, werden diese Gebühren wahrscheinlich die Rendite erheblich reduzieren.

S&P500 Prognose-Strategie-Performance von 2005-01-01 bis 2006-12-31

In den folgenden Artikeln werden wir realistische Transaktionskosten hinzufügen, zusätzliche Prognose-Engines verwenden, Leistungsindikatoren ermitteln und Werkzeuge zur Optimierung des Portfolios bereitstellen.


Mehr