Pruebas de retroceso basadas en eventos con Python - Parte III

El autor:La bondad, Creado: 2019-03-23 11:22:28, Actualizado:

En los dos artículos anteriores de la serie hemos discutido lo que es un sistema de backtesting basado en eventos y la jerarquía de clases para el objeto Event.

Uno de nuestros objetivos con un sistema de negociación basado en eventos es minimizar la duplicación de código entre el elemento de backtesting y el elemento de ejecución en vivo. Idealmente sería óptimo utilizar la misma metodología de generación de señales y componentes de gestión de cartera tanto para las pruebas históricas como para el comercio en vivo. Para que esto funcione, el objeto Estrategia que genera las señales y el objeto Cartera que proporciona órdenes basadas en ellas deben utilizar una interfaz idéntica a una fuente de mercado tanto para la ejecución histórica como en vivo.

Esto motiva el concepto de una jerarquía de clases basada en un objeto DataHandler, que da a todas las subclases una interfaz para proporcionar datos de mercado a los componentes restantes dentro del sistema.

Las subclases de ejemplo específicas podrían incluir HistoricCSVDataHandler, QuandlDataHandler, SecuritiesMasterDataHandler, InteractiveBrokersMarketFeedDataHandler, etc. En este tutorial solo vamos a considerar la creación de un procesador de datos CSV histórico, que cargará datos CSV intradiarios para acciones en un conjunto de barras Open-Low-High-Close-Volume-OpenInterest. Esto luego se puede usar para gotear feed en una base de barra por barra los datos en las clases de Estrategia y Cartera en cada latido del corazón del sistema, evitando así el sesgo de lookahead.

La primera tarea es importar las bibliotecas necesarias. Específicamente vamos a importar pandas y las herramientas de clase base abstracta.event.pycomo se describe en el tutorial anterior:

# data.py

import datetime
import os, os.path
import pandas as pd

desde abc importar ABCMeta, método abstracto

desde la importación de eventos MarketEvent El DataHandler es una clase base abstracta (ABC), lo que significa que es imposible instanciar una instancia directamente. Solo se pueden instanciar subclases. La razón de esto es que el ABC proporciona una interfaz a la que todas las subclases posteriores de DataHandler deben adherirse, lo que garantiza la compatibilidad con otras clases que se comunican con ellas.

Hacemos uso de lametaclaseAdemás, usamos el decorador @abstractmethod para que Python sepa que el método será anulado en subclases (esto es idéntico a un método virtual puro en C++).

Los dos métodos de interés son get_latest_bars y update_bars. El primero devuelve los últimos N bares de la marca de tiempo de la frecuencia cardíaca actual, lo que es útil para los cálculos de rodaje necesarios en las clases de Estrategia.

# data.py

class DataHandler(object):
    """
    DataHandler is an abstract base class providing an interface for
    all subsequent (inherited) data handlers (both live and historic).

    The goal of a (derived) DataHandler object is to output a generated
    set of bars (OLHCVI) for each symbol requested. 

    This will replicate how a live strategy would function as current
    market data would be sent "down the pipe". Thus a historic and live
    system will be treated identically by the rest of the backtesting suite.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def get_latest_bars(self, symbol, N=1):
        """
        Returns the last N bars from the latest_symbol list,
        or fewer if less bars are available.
        """
        raise NotImplementedError("Should implement get_latest_bars()")

    @abstractmethod
    def update_bars(self):
        """
        Pushes the latest bar to the latest symbol structure
        for all symbols in the symbol list.
        """
        raise NotImplementedError("Should implement update_bars()")

Con el DataHandler ABC especificado, el siguiente paso es crear un manejador para archivos CSV históricos.

El procesador de datos requiere unos pocos parámetros, a saber, una cola de eventos a la que enviar la información de MarketEvent, la ruta absoluta de los archivos CSV y una lista de símbolos.

# data.py

class HistoricCSVDataHandler(DataHandler):
    """
    HistoricCSVDataHandler is designed to read CSV files for
    each requested symbol from disk and provide an interface
    to obtain the "latest" bar in a manner identical to a live
    trading interface. 
    """

    def __init__(self, events, csv_dir, symbol_list):
        """
        Initialises the historic data handler by requesting
        the location of the CSV files and a list of symbols.

        It will be assumed that all files are of the form
        'symbol.csv', where symbol is a string in the list.

        Parameters:
        events - The Event Queue.
        csv_dir - Absolute directory path to the CSV files.
        symbol_list - A list of symbol strings.
        """
        self.events = events
        self.csv_dir = csv_dir
        self.symbol_list = symbol_list

        self.symbol_data = {}
        self.latest_symbol_data = {}
        self.continue_backtest = True       

        self._open_convert_csv_files()

Implicitamente intentará abrir los archivos con el formato de SYMBOL.csv donde el símbolo es el símbolo del ticker. El formato de los archivos coincide con el proporcionado por el proveedor de DTN IQFeed, pero se modifica fácilmente para manejar formatos de datos adicionales. La apertura de los archivos se maneja mediante el método _open_convert_csv_files a continuación.

Uno de los beneficios de usar pandas como almacén de datos internamente dentro del HistoricCSVDataHandler es que los índices de todos los símbolos que se están rastreando pueden fusionarse. Esto permite que los puntos de datos que faltan puedan ser acolchados hacia adelante, hacia atrás o interpolados dentro de estos huecos para que los tickers puedan compararse de una barra a otra. Esto es necesario para estrategias de inversión de la media, por ejemplo.

# data.py

    def _open_convert_csv_files(self):
        """
        Opens the CSV files from the data directory, converting
        them into pandas DataFrames within a symbol dictionary.

        For this handler it will be assumed that the data is
        taken from DTN IQFeed. Thus its format will be respected.
        """
        comb_index = None
        for s in self.symbol_list:
            # Load the CSV file with no header information, indexed on date
            self.symbol_data[s] = pd.io.parsers.read_csv(
                                      os.path.join(self.csv_dir, '%s.csv' % s),
                                      header=0, index_col=0, 
                                      names=['datetime','open','low','high','close','volume','oi']
                                  )

            # Combine the index to pad forward values
            if comb_index is None:
                comb_index = self.symbol_data[s].index
            else:
                comb_index.union(self.symbol_data[s].index)

            # Set the latest symbol_data to None
            self.latest_symbol_data[s] = []

        # Reindex the dataframes
        for s in self.symbol_list:
            self.symbol_data[s] = self.symbol_data[s].reindex(index=comb_index, method='pad').iterrows()

El método _get_new_bar crea un generador para proporcionar una versión formateada de los datos de barras. Esto significa que las llamadas posteriores al método darán una nueva barra hasta que se alcance el final de los datos de símbolos:

# data.py

    def _get_new_bar(self, symbol):
        """
        Returns the latest bar from the data feed as a tuple of 
        (sybmbol, datetime, open, low, high, close, volume).
        """
        for b in self.symbol_data[symbol]:
            yield tuple([symbol, datetime.datetime.strptime(b[0], '%Y-%m-%d %H:%M:%S'), 
                        b[1][0], b[1][1], b[1][2], b[1][3], b[1][4]])

El primer método abstracto de DataHandler que se implementará es get_latest_bars. Este método simplemente proporciona una lista de las últimas N barras de la estructura de datos latest_symbol_data.

# data.py

    def get_latest_bars(self, symbol, N=1):
        """
        Returns the last N bars from the latest_symbol list,
        or N-k if less available.
        """
        try:
            bars_list = self.latest_symbol_data[symbol]
        except KeyError:
            print "That symbol is not available in the historical data set."
        else:
            return bars_list[-N:]

El método final, update_bars, es el segundo método abstracto de DataHandler. Simplemente genera un MarketEvent que se agrega a la cola a medida que añade las últimas barras a los últimos_symbol_data:

# data.py

    def update_bars(self):
        """
        Pushes the latest bar to the latest_symbol_data structure
        for all symbols in the symbol list.
        """
        for s in self.symbol_list:
            try:
                bar = self._get_new_bar(s).next()
            except StopIteration:
                self.continue_backtest = False
            else:
                if bar is not None:
                    self.latest_symbol_data[s].append(bar)
        self.events.put(MarketEvent())

Por lo tanto, tenemos un objeto derivado de DataHandler, que es utilizado por los componentes restantes para realizar un seguimiento de los datos de mercado.

En el siguiente artículo consideraremos la jerarquía de la clase Estrategia y describiremos cómo se puede diseñar una estrategia para manejar múltiples símbolos, generando así múltiples Eventos de señal para el objeto Portfolio.


Más.