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
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.
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
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
Si ve un mensaje
# 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
# 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
# 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
# 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