Backtesting baseado em eventos com Python - Parte V

Autora:Bem-estar, Criado: 2019-03-25 15:54:16, Atualizado:

No artigo anterior sobre backtesting orientado por eventos, consideramos como construir uma hierarquia de classes de estratégia. estratégias, como definidas aqui, são usadas para gerar sinais, que são usados por um objeto de carteira para tomar decisões sobre se enviar ordens.

Este artigo descreve um objeto NaivePortfolio que acompanha as posições dentro de uma carteira e gera ordens de uma quantidade fixa de ações com base em sinais.

Monitoramento da posição e gestão de pedidos

O sistema de gerenciamento de ordens de carteira é possivelmente o componente mais complexo de um backtester orientado a eventos. Seu papel é acompanhar todas as posições atuais do mercado, bem como o valor de mercado das posições (conhecido como holdings).

Para além da gestão das posições e das participações, a carteira deve também ter conhecimento dos fatores de risco e das técnicas de dimensionamento das posições, a fim de otimizar as ordens enviadas a uma corretora ou a outra forma de acesso ao mercado.

Continuando na linha da hierarquia da classe Event, um objeto Portfolio deve ser capaz de lidar com objetos SignalEvent, gerar objetos OrderEvent e interpretar objetos FillEvent para atualizar posições.

Implementação

Criamos um novo arquivoportfolio.pyE importar as bibliotecas necessárias. Estes são os mesmos que a maioria das outras implementações de classes base abstratas. Precisamos importar a função de piso da biblioteca de matemática para gerar tamanhos de ordem de valores inteiros. Também precisamos dos objetos FillEvent e OrderEvent, uma vez que o Portfólio lida com ambos.

# portfolio.py

import datetime
import numpy as np
import pandas as pd
import Queue

de abc import ABCMeta, abstractmethod do piso de importação de matemática

da importação de eventos FillEvent, OrderEvent Como antes, criamos um ABC para Portfólio e temos dois métodos virtuais puros update_signal e update_fill. O primeiro lida com novos sinais de negociação sendo capturados da fila de eventos e o último lida com preenchimentos recebidos de um objeto de manipulação de execução.

# portfolio.py

class Portfolio(object):
    """
    The Portfolio class handles the positions and market
    value of all instruments at a resolution of a "bar",
    i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        raise NotImplementedError("Should implement update_signal()")

    @abstractmethod
    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        raise NotImplementedError("Should implement update_fill()")

O assunto principal deste artigo é a classe NaivePortfolio. Ela é projetada para lidar com o tamanho da posição e as participações atuais, mas executará ordens de negociação de uma maneira "estúpida" simplesmente enviando-as diretamente para a corretora com um tamanho fixo de quantidade predeterminado, independentemente do dinheiro mantido.

O NaivePortfolio requer um valor de capital inicial, que eu definiu como padrão de 100.000 USD.

O portfólio contém os membros de todas as posições e as posições atuais. O primeiro armazena uma lista de todas as posições anteriores registradas no momento de um evento de dados de mercado. Uma posição é simplesmente a quantidade do ativo. Posições negativas significam que o ativo foi comprado. O último membro armazena um dicionário contendo as posições atuais para a última atualização da barra de mercado.

Para além dos membros das posições, o portfólio armazena participações, que descrevem o valor de mercado actual das posições detidas. Valor de mercado actual significa neste caso o preço de encerramento obtido a partir da barra de mercado actual, que é claramente uma aproximação, mas é razoável o suficiente no momento. all_holdings armazena a lista histórica de todas as participações de símbolos, enquanto current_holdings armazena o dicionário mais atualizado de todos os valores das participações de símbolos.

# portfolio.py

class NaivePortfolio(Portfolio):
    """
    The NaivePortfolio object is designed to send orders to
    a brokerage object with a constant quantity size blindly,
    i.e. without any risk management or position sizing. It is
    used to test simpler strategies such as BuyAndHoldStrategy.
    """
    
    def __init__(self, bars, events, start_date, initial_capital=100000.0):
        """
        Initialises the portfolio with bars and an event queue. 
        Also includes a starting datetime index and initial capital 
        (USD unless otherwise stated).

        Parameters:
        bars - The DataHandler object with current market data.
        events - The Event Queue object.
        start_date - The start date (bar) of the portfolio.
        initial_capital - The starting capital in USD.
        """
        self.bars = bars
        self.events = events
        self.symbol_list = self.bars.symbol_list
        self.start_date = start_date
        self.initial_capital = initial_capital
        
        self.all_positions = self.construct_all_positions()
        self.current_positions = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )

        self.all_holdings = self.construct_all_holdings()
        self.current_holdings = self.construct_current_holdings()

O seguinte método, construct_all_positions, simplesmente cria um dicionário para cada símbolo, define o valor para zero para cada um e, em seguida, adiciona uma chave de data e hora, finalmente adicionando-a a uma lista.

# portfolio.py

    def construct_all_positions(self):
        """
        Constructs the positions list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        return [d]

O método construct_all_holdings é semelhante ao anterior, mas adiciona chaves adicionais para dinheiro, comissão e total, que representam, respectivamente, o dinheiro sobressalente na conta após quaisquer compras, a comissão acumulada e o patrimônio total da conta, incluindo dinheiro e quaisquer posições abertas.

# portfolio.py

    def construct_all_holdings(self):
        """
        Constructs the holdings list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return [d]

O seguinte método, construct_current_holdings é quase idêntico ao método acima, exceto que não envolve o dicionário em uma lista:

# portfolio.py

    def construct_current_holdings(self):
        """
        This constructs the dictionary which will hold the instantaneous
        value of the portfolio across all symbols.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return d

Em cada "batimento cardíaco", isto é, cada vez que novos dados de mercado são solicitados do objeto DataHandler, a carteira deve atualizar o valor de mercado atual de todas as posições mantidas.

Infelizmente, não existe tal coisa como o valor de mercado atual devido a spreads de compra/venda e questões de liquidez. Assim, é necessário estimá-lo multiplicando a quantidade do ativo detido por um preço. A abordagem que tomei aqui é usar o preço de fechamento da última barra recebida. Para uma estratégia intradiária, isso é relativamente realista. Para uma estratégia diária, isso é menos realista, pois o preço de abertura pode diferir substancialmente do preço de fechamento.

O método update_timeindex lida com o rastreamento de novas posições. Primeiro, obtém os preços mais recentes do manipulador de dados de mercado e cria um novo dicionário de símbolos para representar as posições atuais, definindo as posições new iguais às posições current. Estes só são alterados quando um FillEvent é obtido, que é tratado mais tarde no portfólio. O método então anexa esse conjunto de posições atuais à lista de todas as_posições. Em seguida, as posições são atualizadas de maneira semelhante, com a exceção de que o valor de mercado é recalculado multiplicando as posições atuais com o preço de fechamento da última barra (self.current_positions[s] * barras[s][0][5]). Finalmente, as novas posições são anexadas a todas as_posições:

# portfolio.py

    def update_timeindex(self, event):
        """
        Adds a new record to the positions matrix for the current 
        market data bar. This reflects the PREVIOUS bar, i.e. all
        current market data at this stage is known (OLHCVI).

        Makes use of a MarketEvent from the events queue.
        """
        bars = {}
        for sym in self.symbol_list:
            bars[sym] = self.bars.get_latest_bars(sym, N=1)

        # Update positions
        dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dp['datetime'] = bars[self.symbol_list[0]][0][1]

        for s in self.symbol_list:
            dp[s] = self.current_positions[s]

        # Append the current positions
        self.all_positions.append(dp)

        # Update holdings
        dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dh['datetime'] = bars[self.symbol_list[0]][0][1]
        dh['cash'] = self.current_holdings['cash']
        dh['commission'] = self.current_holdings['commission']
        dh['total'] = self.current_holdings['cash']

        for s in self.symbol_list:
            # Approximation to the real value
            market_value = self.current_positions[s] * bars[s][0][5]
            dh[s] = market_value
            dh['total'] += market_value

        # Append the current holdings
        self.all_holdings.append(dh)

O método update_positions_from_fill determina se um FillEvent é um Buy ou um Sell e, em seguida, atualiza o dicionário current_positions em conformidade adicionando/subtraindo a quantidade correta de ações:

# portfolio.py

    def update_positions_from_fill(self, fill):
        """
        Takes a FilltEvent object and updates the position matrix
        to reflect the new position.

        Parameters:
        fill - The FillEvent object to update the positions with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update positions list with new quantities
        self.current_positions[fill.symbol] += fill_dir*fill.quantity

O correspondente update_holdings_from_fill é semelhante ao método acima, mas atualiza os valores das participações em vez disso. A fim de simular o custo de um preenchimento, o método a seguir não usa o custo associado do FillEvent. Por que é isso? Simplificando, em um ambiente de backtesting o custo de preenchimento é realmente desconhecido e, portanto, deve ser estimado. Assim, o custo de preenchimento é definido como o preço de mercado atual (o preço de fechamento da última barra).

Uma vez conhecido o custo de reabastecimento, podem ser actualizadas as participações correntes, o numerário e os valores totais.

# portfolio.py

    def update_holdings_from_fill(self, fill):
        """
        Takes a FillEvent object and updates the holdings matrix
        to reflect the holdings value.

        Parameters:
        fill - The FillEvent object to update the holdings with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update holdings list with new quantities
        fill_cost = self.bars.get_latest_bars(fill.symbol)[0][5]  # Close price
        cost = fill_dir * fill_cost * fill.quantity
        self.current_holdings[fill.symbol] += cost
        self.current_holdings['commission'] += fill.commission
        self.current_holdings['cash'] -= (cost + fill.commission)
        self.current_holdings['total'] -= (cost + fill.commission)

O método virtual puro update_fill do Portfolio ABC é implementado aqui. Ele simplesmente executa os dois métodos anteriores, update_positions_from_fill e update_holdings_from_fill, que já foram discutidos acima:

# portfolio.py

    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        if event.type == 'FILL':
            self.update_positions_from_fill(event)
            self.update_holdings_from_fill(event)

Embora o objeto Portfólio deva lidar com FillEvents, ele também deve cuidar de gerar OrderEvents após o recebimento de um ou mais SignalEvents. O método generate_naive_order simplesmente leva um sinal para longo ou curto de um ativo e, em seguida, envia uma ordem para fazê-lo para 100 ações de tal ativo. Claramente, 100 é um valor arbitrário. Em uma implementação realista, esse valor será determinado por uma sobreposição de gerenciamento de risco ou tamanho de posição. No entanto, este é um NaivePortfolio e, portanto, naively envia todas as ordens diretamente dos sinais, sem um sistema de risco.

O método lida com o desejo, curto prazo e saída de uma posição, com base na quantidade atual e símbolo particular.

# portfolio.py

    def generate_naive_order(self, signal):
        """
        Simply transacts an OrderEvent object as a constant quantity
        sizing of the signal object, without risk management or
        position sizing considerations.

        Parameters:
        signal - The SignalEvent signal information.
        """
        order = None

        symbol = signal.symbol
        direction = signal.signal_type
        strength = signal.strength

        mkt_quantity = floor(100 * strength)
        cur_quantity = self.current_positions[symbol]
        order_type = 'MKT'

        if direction == 'LONG' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
        if direction == 'SHORT' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')   
    
        if direction == 'EXIT' and cur_quantity > 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
        if direction == 'EXIT' and cur_quantity < 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
        return order

O método update_signal simplesmente chama o método acima e adiciona a ordem gerada à fila de eventos:

# portfolio.py

    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        if event.type == 'SIGNAL':
            order_event = self.generate_naive_order(event)
            self.events.put(order_event)

O método final no NaivePortfolio é a geração de uma curva de patrimônio. Isso simplesmente cria um fluxo de retornos, útil para cálculos de desempenho e, em seguida, normaliza a curva de patrimônio para ser baseada em porcentagem. Assim, o tamanho inicial da conta é igual a 1.0:

# portfolio.py

    def create_equity_curve_dataframe(self):
        """
        Creates a pandas DataFrame from the all_holdings
        list of dictionaries.
        """
        curve = pd.DataFrame(self.all_holdings)
        curve.set_index('datetime', inplace=True)
        curve['returns'] = curve['total'].pct_change()
        curve['equity_curve'] = (1.0+curve['returns']).cumprod()
        self.equity_curve = curve

O objeto Portfólio é o aspecto mais complexo de todo o sistema de backtest baseado em eventos. A implementação aqui, embora intrincada, é relativamente elementar em seu manuseio de posições. Versões posteriores considerarão gerenciamento de risco e dimensionamento de posição, o que levará a uma ideia muito mais realista do desempenho da estratégia.

No próximo artigo, consideraremos a peça final do backtester orientado a eventos, ou seja, um objeto ExecutionHandler, que é usado para tomar objetos OrderEvent e criar objetos FillEvent a partir deles.


Mais.