Investigación Entornos de pruebas de retroceso en Python con pandas

El autor:La bondad, Creado: 2019-03-16 11:58:20, Actualizado:

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.

Resumen de las pruebas de retroceso

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 verdadero de una estrategia en comparación con el rendimiento backtested.

Cuando se desarrolla un sistema de backtesting, es tentador querer constantemente reescribirlo desde cero ya que se encuentra que más factores son cruciales para evaluar el rendimiento.

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.

Tipos de sistemas de pruebas de retroceso

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.

Backtester de investigación orientada a objetos en Python

El diseño y la implementación de un entorno de backtesting basado en la investigación orientado a objetos se discutirá ahora.

  • Las interfaces de cada componente se pueden especificar de antemano, mientras que los internos de cada componente se pueden modificar (o reemplazar) a medida que avanza el proyecto
  • Al especificar las interfaces de antemano, es posible probar eficazmente el comportamiento de cada componente (mediante pruebas unitarias)
  • Cuando se amplía el sistema, se pueden construir nuevos componentes sobre o además de otros, ya sea por herencia o composición.

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:

  • Estrategia - Una clase de Estrategia recibe un Marco de Datos de Pandas de barras, es decir, una lista de puntos de datos de volumen cercano abierto-alto-bajo-bajo (OHLCV) a una frecuencia particular.
  • Portfolio - La mayor parte del trabajo de backtesting ocurrirá en la clase Portfolio. Recibirá un conjunto de señales (como se describe anteriormente) y creará una serie de posiciones, asignadas frente a un componente de efectivo.
  • Rendimiento - El objeto Rendimiento toma una cartera y produce un conjunto de estadísticas sobre su rendimiento. En particular, emitirá características de riesgo/rendimiento (Sharpe, Sortino e índices de información), métricas de comercio/beneficio e información sobre el aprovechamiento.

¿Qué te falta?

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

Aplicación

Ahora procederemos a delinear las implementaciones para cada objeto.

Estrategia

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, conducidas por el precio. Un requisito inicial para este backtester es que las clases de Estrategia derivadas acepten una lista de barras (OHLCV) como entrada, en lugar de ticks (precios de comercio por comercio) o datos del libro de órdenes. Por lo tanto, la granularidad más fina que se considera aquí será de barras de 1 segundo.

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 advisors que proporcionan un conjunto de señales, que una clase de cartera más avanzada puede aceptar para determinar las posiciones reales que se ingresan.

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.

Cartera de activos

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 concreto, debemos crear una implementación de un objeto de cartera. Este objeto abarcará la mayoría del código de backtesting. Está diseñado para crear dos DataFrames separados, el primero de los cuales es un marco de posiciones, utilizado para almacenar la cantidad de cada instrumento mantenido en cualquier barra particular. El segundo, cartera, en realidad contiene el precio de mercado de todas las tenencias para cada barra, así como un recuento del efectivo, asumiendo un capital inicial. Esto finalmente proporciona una curva de equidad en la que evaluar el rendimiento de la estrategia.

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.


Más.