Forschung Backtesting-Umgebungen in Python mit Pandas

Schriftsteller:Gutes, Erstellt: 2019-03-16 11:58:20, aktualisiert:

Backtesting ist der Forschungsprozess, bei dem eine Handelsstrategie-Idee auf historische Daten angewendet wird, um die vergangene Leistung zu ermitteln. Insbesondere gibt ein Backtester keine Garantie für die zukünftige Leistung der Strategie. Sie sind jedoch ein wesentlicher Bestandteil des Strategiepipeline-Forschungsprozesses und ermöglichen es, Strategien zu filtern, bevor sie in Produktion gebracht werden.

In diesem Artikel (und den folgenden) wird ein grundlegendes objektorientiertes Backtesting-System in Python beschrieben. Dieses frühe System wird in erster Linie ein "Lehrmittel" sein, das verwendet wird, um die verschiedenen Komponenten eines Backtesting-Systems zu demonstrieren.

Überblick über die Rückprüfung

Der Prozess der Entwicklung eines robusten Backtesting-Systems ist äußerst schwierig. Die effektive Simulation aller Komponenten, die die Leistung eines algorithmischen Handelssystems beeinflussen, ist eine Herausforderung. Schlechte Datengranularität, Opazität der Auftragsvermittlung bei einem Broker, Auftragsverzögerung und eine Vielzahl anderer Faktoren verschworen sich, um die "wahre" Leistung einer Strategie gegenüber der zurückgetesteten Leistung zu verändern.

Bei der Entwicklung eines Backtesting-Systems ist es verlockend, es ständig von Grund auf neu zu schreiben, da mehr Faktoren entscheidend für die Bewertung der Leistung sind.

Mit diesen Bedenken im Hinterkopf wird der hier vorgestellte Backtester etwas vereinfacht.

Arten von Backtestsystemen

Es gibt im Allgemeinen zwei Arten von Backtesting-Systemen, die von Interesse sein werden. Das erste ist forschungsbasiert, hauptsächlich in den frühen Stadien verwendet, wo viele Strategien getestet werden, um diejenigen für eine ernstere Bewertung auszuwählen.

Die zweite Art von Backtesting-System ist ereignisbasiert. Das heißt, es führt den Backtesting-Prozess in einer Ausführungsschleife aus, die dem Handelsausführungssystem selbst ähnlich (wenn nicht identisch) ist. Es wird Marktdaten und den Auftragsausführungsprozess realistisch modellieren, um eine strengere Bewertung einer Strategie bereitzustellen.

Letztere Systeme werden häufig in einer Hochleistungssprache wie C++ oder Java geschrieben, bei der die Ausführungsgeschwindigkeit unerlässlich ist.

Objektorientierter Backtester in Python

Die Entwicklung und Implementierung einer objektorientierten, forschungsbasierten Backtesting-Umgebung wird nun diskutiert.

  • Die Schnittstellen der einzelnen Komponenten können im Voraus festgelegt werden, während die Innenseiten der einzelnen Komponenten im Laufe des Projekts geändert (oder ersetzt) werden können
  • Durch die vorläufige Spezifizierung der Schnittstellen kann effektiv getestet werden, wie sich jede Komponente verhält (durch Einheitstests)
  • Bei der Erweiterung des Systems können entweder durch Vererbung oder Zusammensetzung neue Komponenten auf oder zusätzlich zu anderen konstruiert werden.

In diesem Stadium ist der Backtester für die Einfachheit der Implementierung und ein angemessenes Maß an Flexibilität auf Kosten der echten Marktgenauigkeit konzipiert. Insbesondere wird dieser Backtester nur in der Lage sein, Strategien zu handhaben, die auf einem einzigen Instrument wirken. Später wird der Backtester modifiziert, um Instrumente zu handhaben. Für den ersten Backtester sind folgende Komponenten erforderlich:

  • Strategy - Eine Strategy-Klasse erhält einen Pandas DataFrame aus Balken, d.h. eine Liste von Open-High-Low-Close-Volume (OHLCV) Datenpunkten mit einer bestimmten Frequenz.
  • Portfolio - Der Großteil der Backtesting-Arbeit erfolgt in der Portfolio-Klasse. Es erhält eine Reihe von Signalen (wie oben beschrieben) und erstellt eine Reihe von Positionen, die gegen eine Bargeldkomponente zugewiesen werden.
  • Performance - Das Performance-Objekt nimmt ein Portfolio und erstellt eine Reihe von Statistiken über seine Performance.

Was fehlt?

Wie man sehen kann, beinhaltet dieser Backtester keine Bezugnahme auf Portfoliomanagement/Risiko-Management, Ausführungshandling (d.h. keine Limit-Orders) und wird auch keine anspruchsvolle Modellierung der Transaktionskosten liefern. Dies ist in diesem Stadium kein großes Problem. Er ermöglicht uns, uns mit dem Prozess der Erstellung eines objektorientierten Backtesters und den Pandas/NumPy-Bibliotheken vertraut zu machen. Mit der Zeit wird er verbessert.

Durchsetzung

Wir werden nun die Implementierungen für jedes Objekt skizzieren.

Strategie

Das Strategy-Objekt muss in diesem Stadium ziemlich generisch sein, da es mit Prognosen, Mittelumkehrung, Dynamik und Volatilitätsstrategien umgeht. Die hier betrachteten Strategien werden immer auf Zeitreihen basieren, d.h. preisgetrieben. Eine frühe Anforderung für diesen Backtester ist, dass abgeleitete Strategy-Klassen eine Liste von Bars (OHLCV) als Eingabe annehmen, anstatt Ticks (Trade-by-Trade-Preise) oder Auftragsbuchdaten. Die beste Granularität, die hier in Betracht gezogen wird, sind also 1-Sekunden-Bars.

Die Strategy-Klasse wird auch immer Signalempfehlungen erstellen. Dies bedeutet, dass sie eine Portfolio-Instanz im Sinne von Long/Short oder Halten einer Position berät. Diese Flexibilität ermöglicht es uns, mehrere Strategy advisors zu erstellen, die eine Reihe von Signalen bereitstellen, die eine fortgeschrittenere Portfolio-Klasse akzeptieren kann, um die tatsächlichen eingegebenen Positionen zu bestimmen.

Die Schnittstelle der Klassen wird durch die Verwendung einer abstrakten Basisklasse-Methodik durchgesetzt. Eine abstrakte Basisklasse ist ein Objekt, das nicht instanziert werden kann und somit nur abgeleitete Klassen erstellt werden können.backtest.py. Die Strategy-Klasse erfordert, dass jede Unterklasse die Methode generate_signals implementiert.

Um zu verhindern, dass die Strategy-Klasse direkt instanziiert wird (da sie abstrakt ist!), ist es notwendig, die ABCMeta- und abstractmethod-Objekte aus dem abc-Modul zu verwenden.Metaklassedie Methode generate_signals mit dem Abstraktmethod-Dekorator dekorieren.

# backtest.py

from abc import ABCMeta, abstractmethod

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()!")

Während die obige Schnittstelle einfach ist, wird sie komplizierter, wenn diese Klasse für jede spezifische Art von Strategie vererbt wird.

Portfolio

Die Portfolio-Klasse ist der Bereich, in dem sich die meisten Handelslogiken befinden. Für diesen Research-Backtester ist die Portfolio-Klasse für die Bestimmung der Positionsgröße, die Risikoanalyse, das Transaktionskostenmanagement und die Ausführung von Aufträgen (d. h. Markt-on-Open, Markt-on-Close-Orders) verantwortlich. In einem späteren Stadium werden diese Aufgaben in separate Komponenten aufgeteilt. Im Moment werden sie in eine Klasse eingebunden.

Diese Klasse nutzt Pandas ausgiebig und bietet ein großartiges Beispiel dafür, wo die Bibliothek eine riesige Menge an Zeit sparen kann, insbesondere in Bezug auf boilerplate-Daten-Ringing. Nebenbei ist der Haupttrieb mit Pandas und NumPy, die Iteration über jeden Datensatz mit der For d in... Syntax zu vermeiden. Dies liegt daran, dass NumPy (die Pandas zugrunde liegt) die Schleife durch vektorisierte Operationen optimiert. Daher werden Sie bei der Verwendung von Pandas nur wenige (wenn überhaupt!)

Das Ziel der Portfolio-Klasse ist es, letztendlich eine Abfolge von Trades und eine Eigenkapitalkurve zu erzeugen, die von der Performance-Klasse analysiert wird. Um dies zu erreichen, muss sie mit einer Liste von Handelsempfehlungen aus einem Strategieobjekt versorgt werden.

Die Portfolio-Klasse muss darüber informiert werden, wie Kapital für einen bestimmten Satz von Handelssignalen eingesetzt werden soll, wie mit Transaktionskosten umzugehen ist und welche Formen von Aufträgen verwendet werden. Das Strategieobjekt arbeitet auf Datenbalken und muss daher Annahmen hinsichtlich der bei der Ausführung einer Bestellung erzielten Preise machen. Da der hohe/niedrige Preis eines beliebigen Balkens a priori unbekannt ist, ist es nur möglich, die offenen und schließenden Preise für den Handel zu verwenden. In Wirklichkeit ist es unmöglich zu garantieren, dass eine Bestellung bei einem dieser speziellen Preise ausgeführt wird, wenn eine Marktorder verwendet wird, so dass es bestenfalls eine Annäherung sein wird.

Neben Annahmen über die Auftragserfüllung ignoriert dieser Backtester alle Konzepte von Margin-/Brokerage-Beschränkungen und geht davon aus, dass es möglich ist, in jedem Instrument frei und ohne Liquiditätsbeschränkungen lang und kurz zu gehen.

Die folgende Liste wird fortgesetzt:backtest.py:

# backtest.py

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 diesem Stadium sind die abstrakten Basisklassen Strategie und Portfolio eingeführt worden. Wir sind nun in der Lage, einige konkrete abgeleitete Implementierungen dieser Klassen zu erzeugen, um eine funktionierende Spielzeugstrategie zu erstellen.

Wir beginnen mit der Erstellung einer Unterklasse der Strategie namens RandomForecastStrategy, deren einzige Aufgabe es ist, zufällig ausgewählte Long/Short-Signale zu erzeugen! Obwohl dies eindeutig eine sinnlose Handelsstrategie ist, wird sie unseren Bedürfnissen dienen, indem sie das objektorientierte Backtesting-Framework demonstriert. So beginnen wir eine neue Datei namens random_forecast.py, mit der Auflistung für den zufälligen Prognosen wie folgt:

# random_forecast.py

import numpy as np
import pandas as pd
import Quandl   # Necessary for obtaining financial data easily

from backtest import Strategy, Portfolio

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

Jetzt, da wir ein konkretes Prognose-System haben, müssen wir eine Implementierung eines Portfolio-Objekts erstellen. Dieses Objekt umfasst den Großteil des Backtesting-Codes. Es ist entworfen, um zwei separate DataFrames zu erstellen, von denen der erste ein Positionsrahmen ist, der zur Speicherung der Menge jedes Instruments verwendet wird, das an einem bestimmten Balken gehalten wird. Das zweite, das Portfolio, enthält tatsächlich den Marktpreis aller Bestände für jeden Balken sowie eine Zählung des Bargelds unter Annahme eines Anfangskapitals. Dies liefert letztendlich eine Eigenkapitalkurve, auf der die Strategieleistung bewertet werden kann.

Das Portfolio-Objekt ist zwar äußerst flexibel in seiner Schnittstelle, erfordert aber spezifische Entscheidungen, wenn es darum geht, wie mit Transaktionskosten, Marktordern usw. umzugehen ist. In diesem Grundbeispiel habe ich in Betracht gezogen, dass es möglich sein wird, ohne Einschränkungen oder Marge leicht ein Instrument zu long/short zu gehen, direkt zum offenen Preis der Bar zu kaufen oder zu verkaufen, ohne Transaktionskosten (einschließlich Slippage, Gebühren und Marktwirkung) und die Menge an Aktien, die für jeden Handel direkt zu kaufen ist, angegeben.

Hier ist die Fortsetzung der Liste random_forecast.py:

# random_forecast.py

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=signals.index).fillna(0.0)
        positions[self.symbol] = 100*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.
        portfolio = self.positions*self.bars['Open']
        pos_diff = self.positions.diff()

        # Create the 'holdings' and 'cash' series by running through
        # the trades and adding/subtracting the relevant quantity from
        # each column
        portfolio['holdings'] = (self.positions*self.bars['Open']).sum(axis=1)
        portfolio['cash'] = self.initial_capital - (pos_diff*self.bars['Open']).sum(axis=1).cumsum()

        # Finalise the total and bar-based returns based on the 'cash'
        # and 'holdings' figures for the portfolio
        portfolio['total'] = portfolio['cash'] + portfolio['holdings']
        portfolio['returns'] = portfolio['total'].pct_change()
        return portfolio

Dies gibt uns alles, was wir brauchen, um eine Eigenkapitalkurve auf der Grundlage eines solchen Systems zu erzeugen.HauptFunktion:

if __name__ == "__main__":
    # Obtain daily bars of SPY (ETF that generally 
    # follows the S&P500) from Quandl (requires 'pip install Quandl'
    # on the command line)
    symbol = 'SPY'
    bars = Quandl.get("GOOG/NYSE_%s" % symbol, collapse="daily")

    # Create a set of random forecasting signals for SPY
    rfs = RandomForecastingStrategy(symbol, bars)
    signals = rfs.generate_signals()

    # Create a portfolio of SPY
    portfolio = MarketOnOpenPortfolio(symbol, bars, signals, initial_capital=100000.0)
    returns = portfolio.backtest_portfolio()

    print returns.tail(10)

Die Ausgabe des Programms ist wie folgt: Ihre Ausgabe unterscheidet sich von der nachstehenden abhängig von dem von Ihnen ausgewählten Datumsbereich und dem verwendeten zufälligen Samen:

          SPY  holdings    cash  total   returns

Datum
2014-01-02 -18398 -18398 111486 93088 0.000097 2014-01-03 18321 18321 74844 93165 0.000827 2014-01-06 18347 18347 74844 93191 0.000279 2014-01-07 18309 18309 74844 93153 -0.000408 2014-01-08 -18345 -18345 111534 93189 0.000386 2014-01-09 -18410 -18410 111534 93124 -0.000698 2014-01-10 -18395 -18395 111534 93139 0.000161 2014-01-13 -18371 -18371 111534 93163 0.000258 2014-01-14 -18228 -18228 111534 93306 0.001535 2014-01-15 18410 18410 74714 93124 -0.001951

In diesem Fall verlor die Strategie Geld, was angesichts der stochastischen Natur des Prognostikers nicht überrascht! Die nächsten Schritte sind die Erstellung eines Performance-Objekts, das eine Portfolio-Instanz akzeptiert und eine Liste von Performance-Metriken bereitstellt, auf denen eine Entscheidung zur Filterung der Strategie oder nicht basiert.

Wir können auch das Portfolio-Objekt verbessern, um die Transaktionskosten realistischer zu handhaben (z. B. Interactive Brokers Provisionen und Slippage). Wir können auch einfach eine Prognose-Engine in ein Strategie-Objekt integrieren, das (hoffentlich) bessere Ergebnisse liefert. In den folgenden Artikeln werden wir diese Konzepte eingehender untersuchen.


Mehr