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.
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
Quando se desenvolve um sistema de backtesting, é tentador querer constantemente
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.
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.
A concepção e implementação de um ambiente de backtesting baseado em pesquisa orientado a objetos será agora discutida.
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:
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.
Vamos agora prosseguir para delinear as implementações para cada objeto.
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,
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
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.
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
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
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.