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
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
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.