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.
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
Mit diesen Bedenken im Hinterkopf wird der hier vorgestellte Backtester etwas vereinfacht.
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.
Die Entwicklung und Implementierung einer objektorientierten, forschungsbasierten Backtesting-Umgebung wird nun diskutiert.
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:
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.
Wir werden nun die Implementierungen für jedes Objekt skizzieren.
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.
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
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.
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
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
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.