События, обусловленные обратным тестированием с Python - Часть VIII

Автор:Доброта, Создано: 2019-03-26 16:38:59, Обновлено:

Прошло некоторое время с тех пор, как мы рассмотрели событие-ориентированный backtester, который мы начали обсуждать в этой статье. В части VI я описал, как кодировать stand-in ExecutionHandler модель, которая работала для исторической ситуации backtesting. В этой статье мы собираемся кодировать соответствующий Interactive Brokers API обработчика, чтобы перейти к живой торговой системе.

Ранее я обсуждал, как загрузить рабочую станцию трейдера и создать демо-аккаунт Interactive Brokers, а также как создать базовый интерфейс к API IB с использованием IbPy.

Основная идея класса IBExecutionHandler (см. ниже) заключается в получении экземпляров OrderEvent из очереди событий, а затем в их выполнении непосредственно против интерфейса интерфейса заказов Interactive Brokers с использованием библиотеки IbPy. Класс также будет обрабатывать сообщения Server Response, отправленные через API. На этом этапе единственным действием будет создание соответствующих экземпляров FillEvent, которые затем будут отправлены обратно в очередь событий.

Сам класс может стать довольно сложным, с логикой оптимизации выполнения, а также сложной обработкой ошибок.

Использование Python

Как всегда, первой задачей является создание файла Python и импорт необходимых библиотек. Файл называется ib_execution.py и находится в том же каталоге, что и другие файлы, управляемые событиями.

Мы импортируем необходимые библиотеки обработки даты/времени, объекты IbPy и конкретные объекты событий, которые обрабатываются 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

Теперь мы определяем класс IBExecutionHandler.ИнитКонструктор требует знания очереди событий. Он также требует спецификации order_routing, которую я поставил по умолчанию на SMART. Если у вас есть конкретные требования обмена, вы можете указать их здесь. Валюта по умолчанию также установлена на доллары США.

В рамках метода мы создаем словарь fill_dict, необходимый позже для использования при создании экземпляров FillEvent. Мы также создаем объект связи tws_conn для хранения информации о нашем соединении с API Interactive Brokers. Мы также должны создать первоначальный по умолчанию order_id, который отслеживает все последующие заказы, чтобы избежать дублирования. Наконец, мы регистрируем обработчики сообщений (которые мы определим более подробно ниже):

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

IB API использует систему событий на основе сообщений, которая позволяет нашему классу реагировать определенными способами на определенные сообщения, аналогично самому обратному тестеру, управляемому событиями.

Метод _reply_handler, с другой стороны, используется для определения необходимости создания экземпляра FillEvent. Метод спрашивает, было ли получено сообщение openOrder и проверяет, уже ли установлена запись в нашем fill_dict для этого конкретного orderId. Если нет, то она создается.

Если он видит сообщение orderStatus и это конкретное сообщение указывает, что заказ был выполнен, то он вызовет create_fill для создания 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)

Следующий метод, create_tws_connection, создает соединение с API IB с использованием объекта IbPy ibConnection. Он использует порт по умолчанию 7496 и ID клиента по умолчанию 10. После создания объекта метод подключения вызван для выполнения соединения:

# 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

Для отслеживания отдельных заказов (для целей отслеживания заполнения) используется следующий метод create_initial_order_id. Я поставил его по умолчанию на 1, но более сложным подходом будет запрос IB на последний доступный ID и использовать его. Вы всегда можете сбросить текущий ID заказа API через рабочую станцию трейдера > Глобальная конфигурация > панель настроек 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

Следующий метод, register_handlers, просто регистрирует методы обработки ошибок и ответов, определенные выше с подключением 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)

Как и в предыдущем учебном пособии по использованию IbPy, нам нужно создать экземпляр контракта, а затем соединить его с экземпляром заказа, который будет отправлен в API IB. Следующий метод, create_contract, генерирует первый компонент этой пары. Он ожидает символ тикера, тип ценной бумаги (например, акции или фьючерс), обмен / первичный обмен и валюту. Он возвращает экземпляр контракта:

# 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

Следующий метод, create_order, генерирует второй компонент пары, а именно экземпляр ордера. Он ожидает тип ордера (например, рынок или лимит), количество актива для торговли и action (купить или продать). Он возвращает экземпляр ордера:

# 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

Чтобы избежать дублирования экземпляров FillEvent для определенного идентификатора заказа, мы используем словарь под названием fill_dict для хранения ключей, которые соответствуют определенным идентификаторам заказа. Когда создается заполнение, ключ filled записи для определенного идентификатора заказа устанавливается на True. Если последующее сообщение Server Response будет получено от IB, в котором указывается, что заказ был заполнен (и является дублированным сообщением), это не приведет к новому заполнению.

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

Следующий метод, create_fill, фактически создает инстанцию FillEvent и помещает ее в очередь событий:

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

Теперь, когда все предыдущие методы были реализованы, остается заменять метод execute_order из абстрактного базового класса ExecutionHandler. Этот метод фактически выполняет размещение заказов с помощью API IB.

Мы сначала проверяем, что событие, получаемое этим методом, на самом деле является OrderEvent, а затем готовим объекты Contract и Order с их соответствующими параметрами.

Это чрезвычайно важно, чтобы вызвать время. сон ((1) метод, чтобы убедиться, что заказ действительно идет через IB. Удаление этой строки приводит к несоответствующему поведению API, по крайней мере, на моей системе!

Наконец, мы увеличиваем идентификатор заказа, чтобы не дублировать заказы:

# 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

Этот класс является основой обработчика исполнения Interactive Brokers и может использоваться вместо обработчика выполнения модели, который подходит только для обратного тестирования.

Таким образом, мы используем как можно больше данных из бэкстеста и живых систем, чтобы гарантировать, что код swap out будет сведен к минимуму, и, таким образом, поведение в обоих системах будет аналогичным, если не идентичным.


Больше