Backtesting baseado em eventos com Python - Parte VIII

Autora:Bem-estar, Criado: 2019-03-26 16:38:59, Atualizado:

Já faz algum tempo desde que consideramos o backtester orientado a eventos, que começamos a discutir neste artigo. Na parte VI, descrevi como codificar um modelo de ExecutionHandler stand-in que funcionou para uma situação de backtesting histórica.

Eu já discuti anteriormente como baixar o Trader Workstation e criar uma conta de demonstração do Interactive Brokers, bem como como como criar uma interface básica para a API do IB usando IbPy.

A ideia essencial da classe IBExecutionHandler (veja abaixo) é receber instâncias de OrderEvent da fila de eventos e, em seguida, executá-las diretamente contra a API de ordem Interactive Brokers usando a biblioteca IbPy. A classe também irá lidar com as mensagens Server Response enviadas de volta através da API. Nesta fase, a única ação tomada será criar instâncias de FillEvent correspondentes que serão enviadas de volta para a fila de eventos.

A classe em si poderia tornar-se bastante complexa, com lógica de otimização de execução, bem como tratamento de erros sofisticados.

Implementação do Python

Como sempre, a primeira tarefa é criar o arquivo Python e importar as bibliotecas necessárias.

Importamos as bibliotecas de manipulação de data/hora necessárias, os objetos IbPy e os objetos Eventos específicos que são tratados pelo IBExecutionHandler:

# ib_execution.py

import datetime
import time

from ib.ext.Contract import Contract
from ib.ext.Order import Order
from ib.opt import ibConnection, message

from event import FillEvent, OrderEvent
from execution import ExecutionHandler

Agora definimos a classe IBExecutionHandler.IniciarO constructor primeiro requer conhecimento da fila de eventos. Também requer especificação de order_routing, que eu definiu para SMART. Se você tiver requisitos específicos de troca, você pode especificá-los aqui. A moeda padrão também foi definida em dólares americanos.

Dentro do método, criamos um dicionário fill_dict, necessário mais tarde para uso na geração de instâncias do FillEvent. Também criamos um objeto de conexão tws_conn para armazenar nossas informações de conexão com a API Interactive Brokers. Também temos que criar uma ordem_id padrão inicial, que acompanha todas as ordens subsequentes para evitar duplicatas. Finalmente, registramos os manipuladores de mensagens (que definiremos com mais detalhes abaixo):

# ib_execution.py

class IBExecutionHandler(ExecutionHandler):
    """
    Handles order execution via the Interactive Brokers
    API, for use against accounts when trading live
    directly.
    """

    def __init__(self, events, 
                 order_routing="SMART", 
                 currency="USD"):
        """
        Initialises the IBExecutionHandler instance.
        """
        self.events = events
        self.order_routing = order_routing
        self.currency = currency
        self.fill_dict = {}

        self.tws_conn = self.create_tws_connection()
        self.order_id = self.create_initial_order_id()
        self.register_handlers()

A API do IB utiliza um sistema de eventos baseado em mensagens que permite que nossa classe responda de maneiras particulares a certas mensagens, de forma semelhante ao próprio backtester orientado por eventos.

O método _reply_handler, por outro lado, é usado para determinar se uma instância FillEvent precisa ser criada. O método pergunta se uma mensagem openOrder foi recebida e verifica se uma entrada em nosso fill_dict para essa particular orderId já foi definida. Se não, então uma é criada.

Se ele vê uma mensagem de orderStatus e essa mensagem específica afirma que uma ordem foi preenchida, então ele chama create_fill para criar um FillEvent.

# ib_execution.py
    
    def _error_handler(self, msg):
        """
        Handles the capturing of error messages
        """
        # Currently no error handling.
        print "Server Error: %s" % msg

    def _reply_handler(self, msg):
        """
        Handles of server replies
        """
        # Handle open order orderId processing
        if msg.typeName == "openOrder" and \
            msg.orderId == self.order_id and \
            not self.fill_dict.has_key(msg.orderId):
            self.create_fill_dict_entry(msg)
        # Handle Fills
        if msg.typeName == "orderStatus" and \
            msg.status == "Filled" and \
            self.fill_dict[msg.orderId]["filled"] == False:
            self.create_fill(msg)      
        print "Server Response: %s, %s\n" % (msg.typeName, msg)

O seguinte método, create_tws_connection, cria uma conexão com a API do IB usando o objeto IbPy ibConnection. Ele usa uma porta padrão de 7496 e um ID de cliente padrão de 10. Uma vez que o objeto é criado, o método connect é chamado para realizar a conexão:

# ib_execution.py
    
    def create_tws_connection(self):
        """
        Connect to the Trader Workstation (TWS) running on the
        usual port of 7496, with a clientId of 10.
        The clientId is chosen by us and we will need 
        separate IDs for both the execution connection and
        market data connection, if the latter is used elsewhere.
        """
        tws_conn = ibConnection()
        tws_conn.connect()
        return tws_conn

Para acompanhar ordens separadas (para fins de rastreamento de preenchimentos) o seguinte método create_initial_order_id é usado. Eu o definiu para 1, mas uma abordagem mais sofisticada seria a consulta IB para o ID mais recente disponível e usá-lo. Você sempre pode redefinir o ID de ordem atual da API através do painel de Configuração Global > Configuração Global > Configurações da API:

# ib_execution.py
    
    def create_initial_order_id(self):
        """
        Creates the initial order ID used for Interactive
        Brokers to keep track of submitted orders.
        """
        # There is scope for more logic here, but we
        # will use "1" as the default for now.
        return 1

O seguinte método, register_handlers, simplesmente registra os métodos de manipulação de erros e respostas definidos acima com a conexão TWS:

# ib_execution.py
    
    def register_handlers(self):
        """
        Register the error and server reply 
        message handling functions.
        """
        # Assign the error handling function defined above
        # to the TWS connection
        self.tws_conn.register(self._error_handler, 'Error')

        # Assign all of the server reply messages to the
        # reply_handler function defined above
        self.tws_conn.registerAll(self._reply_handler)

Como no tutorial anterior sobre o uso do IbPy, precisamos criar uma instância de contrato e emparelhá-la com uma instância de ordem, que será enviada para a API do IB. O seguinte método, create_contract, gera o primeiro componente deste par. Ele espera um símbolo de ticker, um tipo de segurança (por exemplo, ações ou futuros), uma troca / troca primária e uma moeda. Retorna a instância de contrato:

# ib_execution.py
    
    def create_contract(self, symbol, sec_type, exch, prim_exch, curr):
        """
        Create a Contract object defining what will
        be purchased, at which exchange and in which currency.

        symbol - The ticker symbol for the contract
        sec_type - The security type for the contract ('STK' is 'stock')
        exch - The exchange to carry out the contract on
        prim_exch - The primary exchange to carry out the contract on
        curr - The currency in which to purchase the contract
        """
        contract = Contract()
        contract.m_symbol = symbol
        contract.m_secType = sec_type
        contract.m_exchange = exch
        contract.m_primaryExch = prim_exch
        contract.m_currency = curr
        return contract

O seguinte método, create_order, gera o segundo componente do par, ou seja, a instância de ordem. Ele espera um tipo de ordem (por exemplo, mercado ou limite), uma quantidade do ativo para negociar e uma ação (comprar ou vender). Retorna a instância de ordem:

# ib_execution.py
    
    def create_order(self, order_type, quantity, action):
        """
        Create an Order object (Market/Limit) to go long/short.

        order_type - 'MKT', 'LMT' for Market or Limit orders
        quantity - Integral number of assets to order
        action - 'BUY' or 'SELL'
        """
        order = Order()
        order.m_orderType = order_type
        order.m_totalQuantity = quantity
        order.m_action = action
        return order

Para evitar a duplicação de instâncias de FillEvent para um determinado ID de ordem, utilizamos um dicionário chamado fill_dict para armazenar chaves que correspondem a IDs de ordem particulares. Quando um preenchimento foi gerado, a chave filled de uma entrada para um ID de ordem particular é definida como True. Se uma mensagem subsequente Server Response for recebida do IB afirmando que uma ordem foi preenchida (e é uma mensagem duplicada), não levará a um novo preenchimento. O seguinte método create_fill_dict_entry realiza isso:

# ib_execution.py
    
    def create_fill_dict_entry(self, msg):
        """
        Creates an entry in the Fill Dictionary that lists 
        orderIds and provides security information. This is
        needed for the event-driven behaviour of the IB
        server message behaviour.
        """
        self.fill_dict[msg.orderId] = {
            "symbol": msg.contract.m_symbol,
            "exchange": msg.contract.m_exchange,
            "direction": msg.order.m_action,
            "filled": False
        }

O seguinte método, create_fill, realmente cria a instância FillEvent e a coloca na fila de eventos:

# ib_execution.py
    
    def create_fill(self, msg):
        """
        Handles the creation of the FillEvent that will be
        placed onto the events queue subsequent to an order
        being filled.
        """
        fd = self.fill_dict[msg.orderId]

        # Prepare the fill data
        symbol = fd["symbol"]
        exchange = fd["exchange"]
        filled = msg.filled
        direction = fd["direction"]
        fill_cost = msg.avgFillPrice

        # Create a fill event object
        fill = FillEvent(
            datetime.datetime.utcnow(), symbol, 
            exchange, filled, direction, fill_cost
        )

        # Make sure that multiple messages don't create
        # additional fills.
        self.fill_dict[msg.orderId]["filled"] = True

        # Place the fill event onto the event queue
        self.events.put(fill_event)

Agora que todos os métodos anteriores foram implementados, resta substituir o método execute_order da classe base abstrata ExecutionHandler.

Primeiro, verificamos se o evento que está sendo recebido para este método é realmente um OrderEvent e, em seguida, preparamos os objetos Contract e Order com seus respectivos parâmetros.

É extremamente importante chamar o time.sleep(1) método para garantir que a ordem realmente passa para o IB. A remoção desta linha leva a um comportamento inconsistente da API, pelo menos no meu sistema!

Por fim, aumentamos o ID de encomenda para garantir que não duplicamos encomendas:

# ib_execution.py
    
    def execute_order(self, event):
        """
        Creates the necessary InteractiveBrokers order object
        and submits it to IB via their API.

        The results are then queried in order to generate a
        corresponding Fill object, which is placed back on
        the event queue.

        Parameters:
        event - Contains an Event object with order information.
        """
        if event.type == 'ORDER':
            # Prepare the parameters for the asset order
            asset = event.symbol
            asset_type = "STK"
            order_type = event.order_type
            quantity = event.quantity
            direction = event.direction

            # Create the Interactive Brokers contract via the 
            # passed Order event
            ib_contract = self.create_contract(
                asset, asset_type, self.order_routing,
                self.order_routing, self.currency
            )

            # Create the Interactive Brokers order via the 
            # passed Order event
            ib_order = self.create_order(
                order_type, quantity, direction
            )

            # Use the connection to the send the order to IB
            self.tws_conn.placeOrder(
                self.order_id, ib_contract, ib_order
            )

            # NOTE: This following line is crucial.
            # It ensures the order goes through!
            time.sleep(1)

            # Increment the order ID for this session
            self.order_id += 1

Esta classe forma a base de um manipulador de execução Interactive Brokers e pode ser usado no lugar do manipulador de execução simulado, que é apenas adequado para backtesting.

Desta forma, estamos reutilizando o máximo possível dos sistemas backtest e live para garantir que o código swap out seja minimizado e, assim, o comportamento em ambos seja semelhante, se não idêntico.


Mais.