El backtesting es el proceso de investigación de la aplicación de una idea de estrategia comercial a los datos históricos con el fin de determinar el rendimiento pasado. En particular, un backtester no garantiza el rendimiento futuro de la estrategia. Sin embargo, son un componente esencial del proceso de investigación de la línea de producción de estrategias, lo que permite filtrar las estrategias antes de ponerlas en producción.
En este artículo (y en los siguientes) se describirá un sistema básico de backtesting orientado a objetos escrito en Python.
El proceso de diseño de un sistema de backtesting robusto es extremadamente difícil. Simulación efectiva de todos los componentes que afectan el rendimiento de un sistema de negociación algorítmica es un reto. Poca granularidad de los datos, opacidad de la ruta de orden en un corredor, la latencia de orden y una gran cantidad de otros factores conspiran para alterar el rendimiento
Cuando se desarrolla un sistema de backtesting, es tentador querer constantemente
Con estas preocupaciones en mente, el backtester presentado aquí será algo simplista. A medida que exploremos otros temas (optimización de la cartera, gestión de riesgos, manejo de costos de transacción) el backtester se volverá más robusto.
Hay generalmente dos tipos de sistemas de backtesting que serán de interés. El primero es basado en la investigación, utilizado principalmente en las primeras etapas, donde se probarán muchas estrategias para seleccionar las que se evaluarán más seriamente.
El segundo tipo de sistema de backtesting está basado en eventos, es decir, lleva a cabo el proceso de backtesting en un bucle de ejecución similar (si no idéntico) al sistema de ejecución de operaciones en sí. Modela de forma realista los datos del mercado y el proceso de ejecución de órdenes para proporcionar una evaluación más rigurosa de una estrategia.
Estos últimos sistemas a menudo se escriben en un lenguaje de alto rendimiento como C ++ o Java, donde la velocidad de ejecución es esencial.
El diseño y la implementación de un entorno de backtesting basado en la investigación orientado a objetos se discutirá ahora.
En esta etapa, el backtester está diseñado para facilitar la implementación y un grado razonable de flexibilidad, a expensas de la verdadera precisión del mercado. En particular, este backtester solo podrá manejar estrategias que actúan en un solo instrumento. Más tarde, el backtester se modificará para manejar conjuntos de instrumentos. Para el backtester inicial, se requieren los siguientes componentes:
Como se puede ver, este backtester no incluye ninguna referencia a la gestión de cartera / riesgo, manejo de ejecución (es decir, sin órdenes límite) ni proporcionará un modelado sofisticado de los costos de transacción. Esto no es un gran problema en esta etapa. Nos permite familiarizarnos con el proceso de creación de un backtester orientado a objetos y las bibliotecas Pandas / NumPy. Con el tiempo se mejorará.
Ahora procederemos a delinear las implementaciones para cada objeto.
El objeto de Estrategia debe ser bastante genérico en esta etapa, ya que manejará estrategias de pronóstico, reversión media, impulso y volatilidad. Las estrategias que se consideran aquí siempre estarán basadas en series de tiempo, es decir,
La clase Estrategia también producirá siempre recomendaciones de señales. Esto significa que aconsejará a una instancia de cartera en el sentido de ir largo / corto o mantener una posición. Esta flexibilidad nos permitirá crear múltiples Strategy
La interfaz de las clases se aplicará utilizando una metodología de clase base abstracta. Una clase base abstracta es un objeto que no se puede instanciar y, por lo tanto, solo se pueden crear clases derivadas.backtest.py. La clase Estrategia requiere que cualquier subclase implemente el método generate_signals.
Con el fin de evitar que la clase Estrategia de ser instanciado directamente (¡ya que es abstracto!) es necesario utilizar el ABCMeta y abstractmethod objetos del módulo abc.metaclasepara ser igual a ABCMeta y luego decorar el método generate_signals con el decorador 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()!")
Si bien la interfaz anterior es sencilla, se volverá más complicada cuando esta clase sea heredada para cada tipo específico de estrategia.
La clase de cartera es donde residirá la mayor parte de la lógica de negociación. Para este backtest de investigación, la cartera está a cargo de determinar el tamaño de la posición, el análisis de riesgos, la gestión de costos de transacción y el manejo de la ejecución (es decir, órdenes de mercado abiertas, de mercado cerradas).
Esta clase hace un amplio uso de pandas y proporciona un gran ejemplo de dónde la biblioteca puede ahorrar una gran cantidad de tiempo, particularmente en lo que respecta a la manipulación de datos. Por otra parte, el truco principal con pandas y NumPy es evitar la iteración sobre cualquier conjunto de datos utilizando la sintaxis de d en... Esto se debe a que NumPy (que subyace a pandas) optimiza el bucle por operaciones vectorizadas. Por lo tanto, verá pocas (¡si alguna!) iteraciones directas al utilizar pandas.
El objetivo de la clase de cartera es producir una secuencia de operaciones y una curva de equidad, que serán analizadas por la clase de rendimiento.
La clase de cartera necesitará saber cómo se desplegará el capital para un conjunto particular de señales de negociación, cómo manejar los costos de transacción y qué formas de órdenes se utilizarán. El objeto de la estrategia opera en barras de datos y, por lo tanto, se deben hacer suposiciones con respecto a los precios alcanzados en la ejecución de una orden. Dado que el precio alto / bajo de cualquier barra es desconocido a priori, solo es posible usar los precios de apertura y cierre para la negociación. En realidad, es imposible garantizar que una orden se cumpla a uno de estos precios particulares al usar una orden de mercado, por lo que será, en el mejor de los casos, una aproximación.
Además de las suposiciones sobre el cumplimiento de las órdenes, este backtester ignorará todos los conceptos de restricciones de margen/correduría y asumirá que es posible ir largo y corto en cualquier instrumento libremente sin ninguna restricción de liquidez.
La siguiente lista continúabacktest.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()!")
En esta etapa se han introducido las clases básicas abstractas de Estrategia y Cartera, y ahora estamos en condiciones de generar algunas implementaciones concretas derivadas de estas clases, con el fin de producir una "estrategia de juguete" funcional.
Comenzaremos generando una subclase de Estrategia llamada RandomForecastStrategy, cuya única tarea es producir señales largas / cortas elegidas al azar! Si bien esta es claramente una estrategia comercial sin sentido, servirá a nuestras necesidades al demostrar el marco de backtesting orientado a objetos. Así comenzaremos un nuevo archivo llamado random_forecast.py, con la lista para el pronosticador aleatorio como sigue:
# 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
Ahora que tenemos un sistema de pronóstico
El objeto de cartera, aunque extremadamente flexible en su interfaz, requiere opciones específicas en lo que respecta a cómo manejar los costos de transacción, las órdenes de mercado, etc. En este ejemplo básico, he considerado que será posible ir largo / corto de un instrumento fácilmente sin restricciones o margen, comprar o vender directamente al precio de apertura de la barra, cero costos de transacción (incluyendo deslizamiento, comisiones e impacto en el mercado) y han especificado la cantidad de acciones directamente a comprar para cada operación.
Aquí está la continuación de la lista de 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
Esto nos da todo lo que necesitamos para generar una curva de equidad basada en tal sistema.el principalFunción:
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 salida del programa es la siguiente. La suya será diferente de la salida de abajo dependiendo del rango de fechas que seleccione y la semilla aleatoria utilizada:
SPY holdings cash total returns
Fecha en la que se produjo
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
En este caso, la estrategia perdió dinero, lo que no es sorprendente dada la naturaleza estocástica del pronosticador! Los próximos pasos son crear un objeto de rendimiento que acepte una instancia de cartera y proporcione una lista de métricas de rendimiento en las que basar una decisión para filtrar la estrategia o no.
También podemos mejorar el objeto Portfolio para tener un manejo más realista de los costos de transacción (como comisiones y deslizamiento de Interactive Brokers). También podemos incluir directamente un motor de pronóstico en un objeto de Estrategia, que (con suerte) producirá mejores resultados. En los siguientes artículos exploraremos estos conceptos con más profundidad.