Pesquisa Ambientes de backtesting em Python com pandas

Autora:Bem-estar, Criado: 2019-03-16 11:58:20, Atualizado:

O backtesting é o processo de pesquisa de aplicação de uma ideia de estratégia de negociação a dados históricos, a fim de verificar o desempenho passado.

Neste artigo (e os que o seguirem) um sistema básico de backtesting orientado a objetos escrito em Python será descrito. Este sistema inicial será principalmente uma "ajuda para o ensino", usada para demonstrar os diferentes componentes de um sistema de backtesting.

Análise geral dos testes de retrocesso

O processo de projetar um sistema de backtesting robusto é extremamente difícil. Simulando efetivamente todos os componentes que afetam o desempenho de um sistema de negociação algorítmica é desafiador. A granularidade de dados pobre, a opacidade do roteamento de ordens em um corretor, a latência de ordens e uma miríade de outros fatores conspiram para alterar o desempenho verdadeiro de uma estratégia em relação ao desempenho backtested.

Quando se desenvolve um sistema de backtesting, é tentador querer constantemente reescrevê-lo do zero, uma vez que mais fatores são cruciais na avaliação do desempenho.

Com estas preocupações em mente, o backtester apresentado aqui será um pouco simplista. à medida que exploramos outras questões (otimização de carteira, gestão de risco, gestão de custos de transação) o backtester vai se tornar mais robusto.

Tipos de sistemas de backtesting

Existem geralmente dois tipos de sistemas de backtesting que serão de interesse. O primeiro é baseado em pesquisa, usado principalmente nos estágios iniciais, onde muitas estratégias serão testadas a fim de selecionar aqueles para avaliação mais séria.

O segundo tipo de sistema de backtesting é baseado em eventos, ou seja, ele realiza o processo de backtesting em um ciclo de execução semelhante (se não idêntico) ao próprio sistema de execução de negociação.

Estes últimos sistemas são frequentemente escritos em uma linguagem de alto desempenho, como C ++ ou Java, onde a velocidade de execução é essencial.

Backtester de Pesquisa Orientada a Objetos em Python

A concepção e implementação de um ambiente de backtesting baseado em pesquisa orientado a objetos será agora discutida.

  • As interfaces de cada componente podem ser especificadas antecipadamente, enquanto os internos de cada componente podem ser modificados (ou substituídos) à medida que o projeto progride
  • Ao especificar as interfaces de antemão, é possível testar eficazmente o comportamento de cada componente (através de testes unitários)
  • Ao estender o sistema, novos componentes podem ser construídos sobre ou em adição a outros, seja por herança ou composição

Neste estágio, o backtester é projetado para facilitar a implementação e um grau razoável de flexibilidade, à custa da verdadeira precisão do mercado. Em particular, este backtester só será capaz de lidar com estratégias que atuam em um único instrumento. Mais tarde, o backtester será modificado para lidar com conjuntos de instrumentos. Para o backtester inicial, os seguintes componentes são necessários:

  • Estratégia - Uma classe de Estratégia recebe um Pandas DataFrame de barras, ou seja, uma lista de pontos de dados Open-High-Low-Close-Volume (OHLCV) em uma frequência particular.
  • Portfólio - A maior parte do trabalho de backtesting ocorrerá na classe Portfólio. Ele receberá um conjunto de sinais (como descrito acima) e criará uma série de posições, alocadas contra um componente de caixa.
  • Desempenho - O objeto Desempenho toma um portfólio e produz um conjunto de estatísticas sobre o seu desempenho.

O que está faltando?

Como pode ser visto, este backtester não inclui qualquer referência à gestão de carteira / risco, manipulação de execução (ou seja, sem ordens de limite) nem fornecerá modelagem sofisticada de custos de transação. Isso não é muito problema nesta fase. Ele nos permite familiarizar-se com o processo de criação de um backtester orientado a objetos e as bibliotecas Pandas / NumPy. Com o tempo, ele será melhorado.

Implementação

Vamos agora prosseguir para delinear as implementações para cada objeto.

Estratégia

O objeto de Estratégia deve ser bastante genérico nesta fase, uma vez que ele estará lidando com previsão, inversão média, impulso e estratégias de volatilidade. As estratégias que estão sendo consideradas aqui sempre serão baseadas em séries de tempo, ou seja, price driven. Um requisito inicial para este backtester é que as classes de Estratégia derivadas aceitarão uma lista de barras (OHLCV) como entrada, em vez de ticks (preços de comércio por comércio) ou dados do livro de ordens. Assim, a melhor granularidade que está sendo considerada aqui será de barras de 1 segundo.

A classe de estratégia também irá sempre produzir recomendações de sinais. Isto significa que irá aconselhar uma instância de carteira no sentido de ir longo / curto ou manter uma posição. Esta flexibilidade nos permitirá criar vários estratégias conselheiros que fornecem um conjunto de sinais, que uma classe de carteira mais avançada pode aceitar, a fim de determinar as posições reais que estão sendo inseridas.

A interface das classes será aplicada utilizando uma metodologia de classe base abstrata. Uma classe base abstrata é um objeto que não pode ser instanciado e, portanto, apenas classes derivadas podem ser criadas. O código Python é dado abaixo em um arquivo chamadobacktest.py. A classe Estratégia requer que qualquer subclasse implemente o método generate_signals.

Para evitar que a classe Strategy seja instanciada diretamente (já que é abstrata!) é necessário usar os objetos ABCMeta e abstractmethod do módulo abc.metaclassepara ser igual a ABCMeta e, em seguida, decorar o método generate_signals com o 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()!")

Embora a interface acima seja direta, ela se tornará mais complicada quando esta classe for herdada para cada tipo específico de estratégia.

Portfólio

A classe de carteira é onde a maioria da lógica de negociação residirá. Para este backtester de pesquisa, a carteira é responsável por determinar o tamanho da posição, análise de risco, gerenciamento de custos de transação e manuseio de execução (ou seja, ordens de mercado abertas e fechadas).

Esta classe faz amplo uso de pandas e fornece um ótimo exemplo de onde a biblioteca pode economizar uma enorme quantidade de tempo, particularmente no que diz respeito a boilerplate wrangling de dados. Além disso, o truque principal com pandas e NumPy é evitar iteração em qualquer conjunto de dados usando a sintaxe de d em.... Isso ocorre porque o NumPy (que está na base dos pandas) otimiza o looping por operações vetorizadas. Assim, você verá poucas (se houver!) iterações diretas ao utilizar pandas.

O objetivo da classe Portfólio é, em última análise, produzir uma sequência de negociações e uma curva de capital, que será analisada pela classe Desempenho. Para conseguir isso, deve ser fornecida uma lista de recomendações de negociação de um objeto de Estratégia.

A classe de carteira precisará saber como o capital deve ser implantado para um determinado conjunto de sinais de negociação, como lidar com os custos de transação e quais formas de ordens serão utilizadas. O objeto da estratégia está operando em barras de dados e, portanto, devem ser feitas suposições em relação aos preços alcançados na execução de uma ordem. Uma vez que o preço alto/baixo de qualquer barra é desconhecido a priori, apenas é possível usar os preços de abertura e fechamento para negociação. Na realidade, é impossível garantir que uma ordem será preenchida a um desses preços específicos ao usar uma ordem de mercado, por isso será, na melhor das hipóteses, uma aproximação.

Além de pressupostos sobre o preenchimento de ordens, este backtester ignorará todos os conceitos de restrições de margem/corretagem e assumirá que é possível fazer long ou short em qualquer instrumento livremente, sem quaisquer restrições de liquidez.

A seguinte lista continua:backtest.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()!")

Neste estágio, foram introduzidas as classes básicas abstractas de Estratégia e Portfólio, e estamos agora em condições de gerar algumas implementações concretas derivadas destas classes, a fim de produzir uma "estratégia de brinquedo" funcional.

Começaremos gerando uma subclasse de Estratégia chamada RandomForecastStrategy, cuja única tarefa é produzir sinais longos / curtos escolhidos aleatoriamente! Embora esta seja claramente uma estratégia de negociação sem sentido, ela servirá às nossas necessidades demonstrando o framework de backtesting orientado a objetos. Assim, começaremos um novo arquivo chamado random_forecast.py, com a listagem para o prognosticador aleatório da seguinte forma:

# 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

Agora que temos um sistema de previsão concreto, devemos criar uma implementação de um objeto de carteira. Este objeto abrangerá a maioria do código de backtesting. Ele é projetado para criar dois DataFrames separados, o primeiro dos quais é um quadro de posições, usado para armazenar a quantidade de cada instrumento mantido em qualquer barra particular. O segundo, carteira, realmente contém o preço de mercado de todas as participações para cada barra, bem como uma contagem do dinheiro, assumindo um capital inicial. Isso, finalmente, fornece uma curva de ações sobre a qual avaliar o desempenho da estratégia.

O objeto Portfólio, embora extremamente flexível na sua interface, requer escolhas específicas no que diz respeito à forma de lidar com custos de transacção, ordens de mercado, etc. Neste exemplo básico, considerei que será possível fazer long/short de um instrumento facilmente, sem restrições ou margem, comprar ou vender diretamente ao preço de abertura da barra, custos de transacção zero (incluindo deslizamento, taxas e impacto no mercado) e especifiquei a quantidade de estoque diretamente a comprar para cada negociação.

Aqui está a continuação da lista 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

Isto dá-nos tudo o que precisamos para gerar uma curva de equidade baseada num tal sistema.principalFunção:

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)

A saída do programa é a seguinte: a sua será diferente da saída abaixo, dependendo do intervalo de datas selecionado e da semente aleatória usada:

          SPY  holdings    cash  total   returns

Data
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

Neste caso, a estratégia perdeu dinheiro, o que não é surpreendente dada a natureza estocástica do prognosticador!

Também podemos melhorar o objeto Portfólio para ter um manuseio mais realista dos custos de transação (como comissões e slippage de Interactive Brokers). Também podemos incluir diretamente um mecanismo de previsão em um objeto de Estratégia, o que (esperamos) produzir melhores resultados.


Mais.