Pruebas de retroceso basadas en eventos con Python - Parte VIII

El autor:La bondad, Creado: 2019-03-26 16:38:59, Actualizado:

Ha pasado un tiempo desde que consideramos el backtester basado en eventos, que comenzamos a discutir en este artículo. En la Parte VI describí cómo codificar un modelo de ejecucionHandler que funcionara para una situación de backtesting histórica. En este artículo vamos a codificar el gestor de API de Interactive Brokers correspondiente para avanzar hacia un sistema de negociación en vivo.

Anteriormente he discutido cómo descargar Trader Workstation y crear una cuenta de demostración de Interactive Brokers, así como cómo crear una interfaz básica a la API del IB utilizando IbPy.

La idea esencial de la clase IBExecutionHandler (ver más abajo) es recibir instancias de OrderEvent de la cola de eventos y luego ejecutarlas directamente contra la API de órdenes de Interactive Brokers utilizando la biblioteca IbPy. La clase también manejará los mensajes Server Response enviados a través de la API. En esta etapa, la única acción que se tomará será crear instancias de FillEvent correspondientes que luego se enviarán de vuelta a la cola de eventos.

Sin embargo, he optado por mantenerlo relativamente simple para que pueda ver las ideas principales y extenderlo en la dirección que se adapte a su estilo comercial particular.

Implementación de Python

Como siempre, la primera tarea es crear el archivo Python e importar las bibliotecas necesarias. El archivo se llama ib_execution.py y vive en el mismo directorio que los otros archivos impulsados por eventos.

Importamos las bibliotecas de manejo de fecha/hora necesarias, los objetos IbPy y los objetos Event específicos que son manejados por 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

Ahora definimos la clase IBExecutionHandler.Iniciarconstructor primero requiere conocimiento de la cola de eventos. También requiere especificación de order_routing, que he predeterminado a SMART. Si tiene requisitos de intercambio específicos, puede especificarlos aquí. La moneda predeterminada también se ha establecido en dólares estadounidenses.

Dentro del método creamos un diccionario fill_dict, necesario más adelante para su uso en la generación de instancias FillEvent. También creamos un objeto de conexión tws_conn para almacenar nuestra información de conexión a la API de Interactive Brokers. También tenemos que crear un orden_id por defecto inicial, que realiza un seguimiento de todos los pedidos posteriores para evitar duplicados. Finalmente, registramos los manipuladores de mensajes (que definiremos con más detalle a continuación):

# 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()

La API del IB utiliza un sistema de eventos basado en mensajes que permite a nuestra clase responder de maneras particulares a ciertos mensajes, de manera similar al propio backtester impulsado por eventos.

El método _reply_handler, por otro lado, se utiliza para determinar si se necesita crear una instancia de FillEvent. El método pregunta si se ha recibido un mensaje de openOrder y verifica si ya se ha establecido una entrada en nuestro fill_dict para este orderId en particular. Si no, se crea una.

Si ve un mensaje orderStatus y ese mensaje en particular indica que se ha cumplido un pedido, entonces llama create_fill para crear un FillEvent. También saca el mensaje al terminal para fines de registro / depuración:

# 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)

El siguiente método, create_tws_connection, crea una conexión a la API IB utilizando el objeto IbPy ibConnection. Utiliza un puerto predeterminado de 7496 y un ID de cliente predeterminado de 10. Una vez que se crea el objeto, se llama al método connect para realizar la conexión:

# 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 realizar un seguimiento de las órdenes separadas (con el fin de realizar un seguimiento de los rellenos) se utiliza el siguiente método create_initial_order_id. Lo he configurado por defecto a 1, pero un enfoque más sofisticado sería consultar IB para la última ID disponible y usarlo. Siempre puede restablecer el ID de pedido de la API actual a través del panel de configuración global > configuración global > configuración de 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

El siguiente método, register_handlers, simplemente registra los métodos de manipulación de errores y respuestas definidos anteriormente con la conexión 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)

Al igual que con el tutorial anterior sobre el uso de IbPy, necesitamos crear una instancia de contrato y luego emparejarla con una instancia de orden, que se enviará a la API de IB. El siguiente método, create_contract, genera el primer componente de este par. Espera un símbolo de ticker, un tipo de seguridad (por ejemplo, acciones o futuros), un intercambio / intercambio primario y una moneda. Devuelve la instancia 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

El siguiente método, create_order, genera el segundo componente del par, a saber, la instancia de orden. Espera un tipo de orden (por ejemplo, mercado o límite), una cantidad del activo para el comercio y una acción (comprar o vender).

# 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 la duplicación de instancias de FillEvent para un ID de pedido particular, utilizamos un diccionario llamado fill_dict para almacenar claves que coinciden con ID de pedido particulares. Cuando se ha generado un relleno, la clave filled de una entrada para un ID de pedido particular se establece en True. Si se recibe un mensaje posterior de Server Response de IB que indica que se ha llenado un pedido (y es un mensaje duplicado), no dará lugar a un nuevo relleno. El siguiente método create_fill_dict_entry realiza esto:

# 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
        }

El siguiente método, create_fill, en realidad crea la instancia FillEvent y la coloca en la cola 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)

Ahora que todos los métodos anteriores han sido implementados, queda por anotar el método execute_order de la clase base abstracta ExecutionHandler.

Primero comprobamos que el evento que se recibe a este método es en realidad un OrderEvent y luego preparamos los objetos Contract y Order con sus respectivos parámetros.

Es extremadamente importante llamar al método time.sleep(1) para asegurar que el orden realmente pase a IB. ¡La eliminación de esta línea conduce a un comportamiento inconsistente de la API, al menos en mi sistema!

Por último, incrementamos el ID de pedido para asegurarnos de que no duplicamos pedidos:

# 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 clase forma la base de un manipulador de ejecución de Interactive Brokers y se puede utilizar en lugar del manipulador de ejecución simulado, que solo es adecuado para backtesting.

De esta manera, estamos reutilizando tanto como sea posible de los sistemas de backtest y en vivo para garantizar que el código swap out se minimice y, por lo tanto, el comportamiento en ambos es similar, si no idéntico.


Más.