Recherche Environnements de test en Python avec les pandas

Auteur:La bonté, Créé: 2019-03-16 11:58:20, mis à jour:

Le backtesting est le processus de recherche d'application d'une idée de stratégie de trading à des données historiques afin de vérifier les performances passées.

Dans cet article (et ceux qui suivent), un système de backtesting orienté objet de base écrit en Python sera décrit. Ce système initial sera principalement une aide à l'enseignement, utilisée pour démontrer les différents composants d'un système de backtesting.

Vue d'ensemble des tests antérieurs

Le processus de conception d'un système de backtesting robuste est extrêmement difficile. Simuler efficacement tous les composants qui affectent les performances d'un système de trading algorithmique est difficile.

Lors du développement d'un système de backtesting, il est tentant de vouloir constamment le réécrire à partir de zéro, car de plus en plus de facteurs sont cruciaux pour évaluer les performances.

En tenant compte de ces préoccupations, le backtester présenté ici sera quelque peu simpliste.

Types de systèmes de contre-test

Il existe généralement deux types de systèmes de backtesting qui seront d'intérêt. Le premier est basé sur la recherche, utilisé principalement dans les premiers stades, où de nombreuses stratégies seront testées afin de sélectionner celles pour une évaluation plus sérieuse. Ces systèmes de backtesting de recherche sont souvent écrits en Python, R ou MatLab car la vitesse de développement est plus importante que la vitesse d'exécution dans cette phase.

Le deuxième type de système de backtesting est basé sur des événements, c'est-à-dire qu'il effectue le processus de backtesting dans une boucle d'exécution similaire (sinon identique) au système d'exécution du trading lui-même.

Ces derniers systèmes sont souvent écrits dans un langage haute performance tel que C++ ou Java, où la vitesse d'exécution est essentielle.

Backtester de recherche orientée objet en Python

La conception et la mise en œuvre d'un environnement de backtesting basé sur la recherche orientée objet seront maintenant discutées.

  • Les interfaces de chaque composant peuvent être spécifiées à l'avance, tandis que les internes de chaque composant peuvent être modifiés (ou remplacés) au fur et à mesure que le projet progresse
  • En spécifiant les interfaces à l'avance, il est possible de tester efficacement le comportement de chaque composant (via des tests unitaires)
  • Lors de l'extension du système, de nouveaux composants peuvent être construits sur d'autres ou en plus d'autres, soit par héritage, soit par composition

À ce stade, le backtester est conçu pour une facilité de mise en œuvre et un degré raisonnable de flexibilité, au détriment de la véritable précision du marché. En particulier, ce backtester ne pourra gérer que des stratégies agissant sur un seul instrument. Plus tard, le backtester sera modifié pour gérer des ensembles d'instruments. Pour le backtester initial, les composants suivants sont nécessaires:

  • Stratégie - Une classe Stratégie reçoit un Pandas DataFrame de barres, c'est-à-dire une liste de points de données Open-High-Low-Close-Volume (OHLCV) à une fréquence particulière.
  • Portfolio - La majeure partie du travail de backtesting aura lieu dans la classe Portfolio. Il recevra un ensemble de signaux (comme décrit ci-dessus) et créera une série de positions, allouées contre une composante de trésorerie.
  • Performance - L'objet Performance prend un portefeuille et produit un ensemble de statistiques sur son rendement.

Qu'est-ce qui manque?

Comme on peut le voir, ce backtester n'inclut aucune référence à la gestion de portefeuille/risque, au traitement de l'exécution (c'est-à-dire pas d'ordres limités) ni ne fournira une modélisation sophistiquée des coûts de transaction.

Mise en œuvre

Nous allons maintenant décrire les implémentations pour chaque objet.

Stratégie

L'objet Stratégie doit être assez générique à ce stade, car il gérera les stratégies de prévision, d'inversion de la moyenne, de dynamique et de volatilité. Les stratégies à considérer ici seront toujours basées sur des séries temporelles, c'est-à-dire prix driven. Une exigence précoce pour ce backtester est que les classes de stratégie dérivées acceptent une liste de barres (OHLCV) comme entrée, plutôt que des ticks (prix de transaction par transaction) ou des données du carnet d'ordres.

La classe Stratégie produira également toujours des recommandations de signaux. Cela signifie qu'elle conseillera une instance de portefeuille dans le sens d'aller long/short ou de maintenir une position. Cette flexibilité nous permettra de créer plusieurs Stratégie advisors qui fournissent un ensemble de signaux, qu'une classe de portefeuille plus avancée peut accepter afin de déterminer les positions réelles entrées.

L'interface des classes sera appliquée en utilisant une méthodologie de classe de base abstraite. Une classe de base abstraite est un objet qui ne peut pas être instancié et donc seulement des classes dérivées peuvent être créées.backtest.py. La classe Strategy exige que toute sous-classe implémente la méthode generate_signals.

Afin d'éviter que la classe Strategy ne soit instanciée directement (car elle est abstraite!), il est nécessaire d'utiliser les objets ABCMeta et abstractmethod du module abc.méta-classepour être égal à ABCMeta, puis décorer la méthode generate_signals avec le décorateur abstractmethod.

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

Bien que l'interface ci-dessus soit simple, elle deviendra plus compliquée lorsque cette classe est héritée pour chaque type spécifique de stratégie.

Portfolio

La classe de portefeuille est celle où résidera la majorité de la logique de négociation. Pour ce backtest de recherche, le portefeuille est chargé de déterminer la taille des positions, l'analyse des risques, la gestion des coûts de transaction et le traitement de l'exécution (c'est-à-dire les ordres d'ouverture et de clôture du marché).

Cette classe utilise largement les pandas et fournit un excellent exemple de la façon dont la bibliothèque peut économiser beaucoup de temps, en particulier en ce qui concerne le " boilerplate " des données.

L'objectif de la classe Portfolio est de produire une séquence de transactions et une courbe d'équité, qui seront analysées par la classe Performance.

La classe de portefeuille devra être informée de la façon dont le capital doit être déployé pour un ensemble particulier de signaux de trading, de la façon de gérer les coûts de transaction et des formes d'ordres qui seront utilisées. L'objet de la stratégie fonctionne sur des barres de données et doit donc faire des hypothèses concernant les prix atteints lors de l'exécution d'un ordre.

En plus des hypothèses sur le remplissage des ordres, ce backtester ignorera tous les concepts de contraintes de marge/de courtage et supposera qu'il est possible d'aller long et short dans n'importe quel instrument librement sans aucune contrainte de liquidité.

La liste suivante se poursuitbacktest.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()!")

A ce stade, les classes de base abstraites Stratégie et Portfolio ont été introduites. Nous sommes maintenant en mesure de générer des implémentations dérivées concrètes de ces classes, afin de produire une "stratégie de jouet" fonctionnelle.

Nous allons commencer par générer une sous-classe de stratégie appelée RandomForecastStrategy, dont la seule tâche est de produire des signaux longs/courts choisis au hasard! Bien qu'il s'agisse clairement d'une stratégie de trading absurde, elle répondra à nos besoins en démontrant le framework de backtesting orienté objet. Nous allons donc commencer un nouveau fichier appelé random_forecast.py, avec la liste pour le prévisionniste aléatoire comme suit:

# 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

Maintenant que nous avons un système de prévision concrète, nous devons créer une implémentation d'un objet de portefeuille. Cet objet englobera la majorité du code de backtesting. Il est conçu pour créer deux DataFrames séparés, dont le premier est un cadre de positions, utilisé pour stocker la quantité de chaque instrument détenu à n'importe quelle barre particulière. Le second, le portefeuille, contient en fait le prix du marché de toutes les participations pour chaque barre, ainsi qu'un décompte de l'argent liquide, en supposant un capital initial. Cela fournit finalement une courbe d'équité sur laquelle évaluer la performance de la stratégie.

L'objet Portefeuille, bien que très flexible dans son interface, nécessite des choix spécifiques en ce qui concerne la façon de gérer les coûts de transaction, les ordres de marché, etc. Dans cet exemple de base, j'ai considéré qu'il sera possible de long/short un instrument facilement sans restrictions ni marge, acheter ou vendre directement au prix d'ouverture de la barre, zéro coût de transaction (comprenant le glissement, les frais et l'impact sur le marché) et j'ai spécifié la quantité de stock à acheter directement pour chaque transaction.

Voici la suite de la 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

Cela nous donne tout ce dont nous avons besoin pour générer une courbe d'équité basée sur un tel système.le principalfonction:

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)

La sortie du programme est la suivante: la vôtre sera différente de la sortie ci-dessous selon la plage de dates sélectionnée et la graine aléatoire utilisée:

          SPY  holdings    cash  total   returns

La date
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

Dans ce cas, la stratégie a perdu de l'argent, ce qui n'est pas surprenant étant donné la nature stochastique du prévisionniste! Les prochaines étapes consistent à créer un objet Performance qui accepte une instance de portefeuille et fournit une liste de mesures de performance sur lesquelles baser une décision de filtrer la stratégie ou non.

Nous pouvons également améliorer l'objet Portfolio pour avoir une gestion plus réaliste des coûts de transaction (comme les commissions et les glissements des Interactive Brokers). Nous pouvons également inclure un moteur de prévision dans un objet Strategy, ce qui (espérons-le) produira de meilleurs résultats.


Plus de