Ereignisgesteuertes Backtesting mit Python - Teil I

Schriftsteller:Gutes, Erstellt: 2019-03-22 11:53:50, Aktualisiert:

Wir haben die letzten paar Monate damit verbracht, bei QuantStart verschiedene Handelsstrategien unter Verwendung von Python und Pandas zu backtesten. Die vektorisierte Natur von Pandas stellt sicher, dass bestimmte Operationen auf großen Datensätzen extrem schnell sind. Die bisher untersuchten Formen des vektorisierten Backtesters leiden jedoch unter einigen Nachteilen bei der Simulation der Handelsdurchführung. In dieser Artikelreihe werden wir einen realistischeren Ansatz zur historischen Strategie-Simulation durch den Aufbau einer ereignisgesteuerten Backtestumgebung unter Verwendung von Python diskutieren.

Ereignisgesteuerte Software

Bevor wir uns mit der Entwicklung eines solchen Backtesters beschäftigen, müssen wir das Konzept von ereignisgesteuerten Systemen verstehen. Videospiele bieten einen natürlichen Anwendungsfall für ereignisgesteuerte Software und ein einfaches Beispiel zum Erforschen. Ein Videospiele hat mehrere Komponenten, die in Echtzeit mit hohen Frameraten miteinander interagieren. Dies wird durch Ausführen der gesamten Reihe von Berechnungen innerhalb einer unendlichen Schleife, die als Ereignisschleife oder Spielschleife bekannt ist, behandelt.

Bei jedem Ticken der Spielschleife wird eine Funktion aufgerufen, um das neueste Ereignis zu empfangen, das durch eine entsprechende vorherige Aktion innerhalb des Spiels generiert wurde. Je nach Art des Ereignisses, das einen Tastendruck oder einen Mausklick umfassen könnte, wird eine nachfolgende Aktion durchgeführt, die entweder die Schleife beendet oder einige zusätzliche Ereignisse generiert. Der Prozess wird dann fortgesetzt. Hier sind einige Beispiele für Pseudo-Code:

while True:  # Run the loop forever
    new_event = get_new_event()   # Get the latest event

    # Based on the event type, perform an action
    if new_event.type == "LEFT_MOUSE_CLICK":
        open_menu()
    elif new_event.type == "ESCAPE_KEY_PRESS":
        quit_game()
    elif new_event.type == "UP_KEY_PRESS":
        move_player_north()
    # ... and many more events

    redraw_screen()   # Update the screen to provide animation
    tick(50)   # Wait 50 milliseconds

Der Code überprüft kontinuierlich nach neuen Ereignissen und führt dann Aktionen aus, die auf diesen Ereignissen basieren. Insbesondere erlaubt er die Illusion der Echtzeit-Reaktionsbehandlung, da der Code kontinuierlich geschleppt und Ereignisse nachgeprüft wird. Wie sich herausstellen wird, ist dies genau das, was wir benötigen, um eine Hochfrequenz-Handelssimulation durchzuführen.

Warum ein Ereignisgesteuerter Backtester?

Ereignisgesteuerte Systeme bieten viele Vorteile gegenüber einem vektorisierten Ansatz:

  • Code-Wiederverwendung - Ein ereignisgesteuerter Backtester kann sowohl für historische Backtests als auch für den Live-Handel mit minimalem Ausschalten von Komponenten verwendet werden.
  • Lookahead-Bias - Bei einem ereignisgesteuerten Backtester gibt es keine Lookahead-Bias, da der Empfang von Marktdaten als Ereignis behandelt wird, auf das reagiert werden muss.
  • Realismus - Ereignisgesteuerte Backtester ermöglichen eine erhebliche Anpassung der Ausführung von Aufträgen und der Transaktionskosten.

Obwohl Ereignisgesteuerte Systeme viele Vorteile bieten, leiden sie unter zwei großen Nachteilen gegenüber einfacheren vektorisierten Systemen. Erstens sind sie wesentlich komplexer zu implementieren und zu testen. Es gibt mehr bewegliche Teile, die zu einer größeren Wahrscheinlichkeit der Einführung von Fehlern führen.

Zweitens sind sie im Vergleich zu einem vektorisierten System langsamer auszuführen. Optimale vektorisierte Operationen können bei mathematischen Berechnungen nicht verwendet werden. Wir werden in späteren Artikeln über Möglichkeiten zur Überwindung dieser Einschränkungen sprechen.

Übersicht des Ereignisgesteuerten Backtesters

Um einen ereignisgesteuerten Ansatz für ein Backtesting-System anzuwenden, ist es notwendig, unsere Komponenten (oder Objekte), die spezifische Aufgaben bewältigen, zu definieren:

  • Ereignis - Das Ereignis ist die grundlegende Klasseneinheit des ereignisgesteuerten Systems. Es enthält einen Typ (wie MARKET, SIGNAL, ORDER oder FILL), der bestimmt, wie es innerhalb der Ereignisschleife behandelt wird.
  • Ereigniswarteschlange - Die Ereigniswarteschlange ist ein Python-Warteschlangeobjekt im Speicher, das alle Objekte der Event-Unterklasse speichert, die vom Rest der Software generiert werden.
  • DataHandler - Der DataHandler ist eine abstrakte Basisklasse (ABC), die eine Schnittstelle für den Umgang mit historischen oder Live-Marktdaten bietet. Dies bietet erhebliche Flexibilität, da die Strategie- und Portfolio-Module zwischen beiden Ansätzen wiederverwendet werden können. Der DataHandler generiert bei jedem Herzschlag des Systems ein neues MarketEvent (siehe unten).
  • Strategie - Die Strategie ist auch ein ABC, das eine Schnittstelle für die Erfassung von Marktdaten und die Erstellung entsprechender SignalEvents bietet, die letztendlich vom Portfolio-Objekt verwendet werden.
  • Portfolio - Dies ist ein ABC, das das mit aktuellen und nachfolgenden Positionen für eine Strategie verbundene Auftragsmanagement verwaltet. Es führt auch das Risikomanagement im gesamten Portfolio durch, einschließlich Sektor-Exposition und Positionsgröße. In einer komplexeren Implementierung könnte dies einer RiskManagement-Klasse übertragen werden. Das Portfolio nimmt SignalEvents aus der Warteschlange und generiert OrderEvents, die der Warteschlange hinzugefügt werden.
  • ExecutionHandler - Der ExecutionHandler simuliert eine Verbindung zu einer Brokerage. Die Aufgabe des Handlers besteht darin, OrderEvents aus der Warteschlange zu nehmen und sie entweder über einen simulierten Ansatz oder eine tatsächliche Verbindung zu einer Leberbrokerage auszuführen. Sobald die Aufträge ausgeführt wurden, erstellt der Handler FillEvents, die beschreiben, was tatsächlich abgewickelt wurde, einschließlich Gebühren, Provisionen und Slippage (falls modelliert).
  • Die Schleife - Alle diese Komponenten sind in eine Ereignisschleife eingewickelt, die alle Ereignistypen korrekt verarbeitet und zu der entsprechenden Komponente weiterleitet.

Dies ist ein ziemlich grundlegendes Modell einer Handelsmaschine. Es gibt erheblichen Spielraum für die Erweiterung, insbesondere in Bezug auf die Verwendung des Portfolios. Darüber hinaus können unterschiedliche Transaktionskostenmodelle auch in ihre eigene Klassenhierarchie abstrahiert werden. In diesem Stadium führt es unnötige Komplexität in diese Artikelreihe ein, so dass wir es derzeit nicht weiter diskutieren werden. In späteren Tutorials werden wir das System wahrscheinlich erweitern, um zusätzlichen Realismus einzubeziehen.

Hier ist ein Auszug aus Python-Code, der zeigt, wie der Backtester in der Praxis funktioniert. Es gibt zwei Schleifen im Code. Die äußere Schleife wird verwendet, um dem Backtester einen Herzschlag zu geben. Für den Live-Handel ist dies die Häufigkeit, mit der neue Marktdaten erfasst werden. Für Backtesting-Strategien ist dies nicht unbedingt notwendig, da der Backtester die Marktdaten in Tropf-Feed-Form verwendet (siehe die Zeile bars.update_bars)).

Die Innenschleife verarbeitet tatsächlich die Ereignisse aus dem Event-Warteschlange-Objekt. Spezifische Ereignisse werden an die jeweilige Komponente delegiert und anschließend werden neue Ereignisse in die Warteschlange hinzugefügt. Wenn die Ereignis-Warteschlange leer ist, geht die Herzschlagschleife weiter:

# Declare the components with respective parameters
bars = DataHandler(..)
strategy = Strategy(..)
port = Portfolio(..)
broker = ExecutionHandler(..)

while True:
    # Update the bars (specific backtest code, as opposed to live trading)
    if bars.continue_backtest == True:
        bars.update_bars()
    else:
        break
    
    # Handle the events
    while True:
        try:
            event = events.get(False)
        except Queue.Empty:
            break
        else:
            if event is not None:
                if event.type == 'MARKET':
                    strategy.calculate_signals(event)
                    port.update_timeindex(event)

                elif event.type == 'SIGNAL':
                    port.update_signal(event)

                elif event.type == 'ORDER':
                    broker.execute_order(event)

                elif event.type == 'FILL':
                    port.update_fill(event)

    # 10-Minute heartbeat
    time.sleep(10*60)

Dies ist der grundlegende Überblick, wie ein ereignisgesteuerter Backtester entwickelt wird.


Mehr