Событийно-ориентированный бэктестинг на Python шаг за шагом. Часть 5 (и последняя)

от автора

В предыдущих статьях мы говорили о том, что такое событийно-ориентированная система бэктестинга, разобрали иерархию классов, необходимую для ее функционирования, обсудили то, как подобные системы используют рыночные данные, а также осуществляют отслеживание позиций и генерацию приказов на покупку. Кроме того, мы описали процесс оценки производительности тестируемых стратегий.

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

Примечание: В качестве примера автор использует API зарубежной компании Interactive Brokers, отсюда названия обсуждаемых модулей (IBExecutionHandler и т.п.). У ITinvest есть собственный API-интерфейс SmartCOM, который может быть использован при создания систем, подобных описываемой.

Идея заключается в создании класса IBexecutionHandler, который будет получать экземпляры OrderEvent из очереди событий, а затем отправлять их на биржу с через API-интерфейс брокерской системы с помощью специальной Python-библиотеки для работы с ним 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. Конструктор __init__ требует знания очереди событий. Кроме того, требуется спецификация order_routing (значение по умолчанию “SMART”). В случае необходимости описания каких-либо требований, относящихся к конкретной биржей, это также можно сделать здесь. В качестве валюты по умолчанию установлены американские доллары.

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

# ib_execution.py  class IBExecutionHandler(ExecutionHandler):     """     Получает информацию о приказе через API брокерской торговой системы для ведения счета при живой торговле.     """      def __init__(self, events,                   order_routing="SMART",                   currency="USD"):         """         Инициализация экземпляра IBExecutionHandler.         """         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() 

Брокерский API-интерфейс из примера использует систему оповещения о событиях, которая позволяет нашему классу отвечать на конкретные сообщения определенным образом — это похоже на работу самого событийно-ориентированного бэктестера. Для краткости мы не включаем код обработки ошибок, кроме вывода в термнал через метод error_method.

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

Если обнаружено сообщение “orderStatus”, в котором говорится о том, что конкретный приказ был исполнен, то вызывается create_fill для создания события FillEvent. Кроме того, в целях отладки и логирования это сообщение выводится на экран:

# ib_execution.py          def _error_handler(self, msg):         """         Отвечает за «ловлю» сообщений об ошибках.         """         # В нашей версии нет обработки ошибок         print "Server Error: %s" % msg      def _reply_handler(self, msg):         """        Отвечает за обработку ответов сервера         """         # Обработка информации о конкретном приказе orderId          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)         # Обработка исполненных приказов         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-интерфейсу с помощью объекта ibConnection. По умолчанию он использует порт 7496 и clientId равный 10. После создания объекта, вызывается метод connect для непосредственного подключения:

# ib_execution.py          def create_tws_connection(self):         """         Подключение к брокерской системе через порт 7496 с clientId 10. Этот clientId выбран нами и необходимо как-то разделять Id для потоков данных о исполненных приказах и рыночных данных, если последний где-либо используется.         """         tws_conn = ibConnection()         tws_conn.connect()         return tws_conn 

Для отслеживания отдельных приказов используется метод create_initial_order_id. В нашем примере этот id равняется 1, но в более продуманной системе можно было бы запрашивать через API брокерской системы последний доступный ID и использовать его.

# ib_execution.py          def create_initial_order_id(self):         """         Создатет начальный order ID, использующийся для отслеживания отправленных приказов.          """         # Здесь можно использовать довольно сложную    #логику, но мы просто установим значение в 1.                return 1 

Следующий метод register_handlers просто регистрирует ошибки и методы обработки ответов сервера:

# ib_execution.py          def register_handlers(self):         """         Регистрация ошибок и методов обработки ответов сервера.         ""         self.tws_conn.register(self._error_handler, 'Error')          self.tws_conn.registerAll(self._reply_handler) 

Далее необходимо создать экземпляр Contract и связать его с экземпляром Order, который будет отправляться в API брокерской системы. Метод create_contract генерирует первый компонент этой пары. Ему нужен символ тикера, тип финансового инструмента (акция, фьючерс и т.п.), биржа и валюта. Он возвращает экземпляр Contract:

# ib_execution.py          def create_contract(self, symbol, sec_type, exch, prim_exch, curr):         """         Создание объекта Contract, который определяет, что будет покупаться, на какой бирже и за какую валюту.          symbol - Символ тикера контракта         sec_type - Тип финансового инструмента ('STK' значит акция)         exch - Биржа, на которой будет осуществляться сделка         prim_exch - Основная биржа, на которой сделку совершить предпочтительнее         curr - Валюта сделки         """         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 отвечает за создания второго элемента пары — экземпляра Order. Ему нужен тип приказа (марет или лимит), количество акций для сделки и действие (покупка или продажа). Он возвращает экземпляр Order:

# ib_execution.py          def create_order(self, order_type, quantity, action):         """         Создается объект Order (типа Market/Limit) для осуществления сделки long/short.          order_type - 'MKT', 'LMT' для приказов Market или Limit          quantity – Количество акций, которые надо купить или продать         action - 'BUY' или 'SELL'         """         order = Order()         order.m_orderType = order_type         order.m_totalQuantity = quantity         order.m_action = action         return order 

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

# ib_execution.py          def create_fill_dict_entry(self, msg):         """         Создает пометку в словаре Fill Dictionary, где перечислены orderID. Это нужно для реализации событийно-ориентированного поведения системы обработки сообщений сервера. """         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):         """         Создается FillEvent, который после исполнения ордера помещается в очередь событий           """         fd = self.fill_dict[msg.orderId]          # Подготовка данных об исполнении         symbol = fd["symbol"]         exchange = fd["exchange"]         filled = msg.filled         direction = fd["direction"]         fill_cost = msg.avgFillPrice          # Создание объекта FillEvent         fill = FillEvent(             datetime.datetime.utcnow(), symbol,              exchange, filled, direction, fill_cost         )          # Убеждаемся, что из-за многочисленных сообщений не возникли лишние события         self.fill_dict[msg.orderId]["filled"] = True          # Помещаем событие fill в очередь         self.events.put(fill_event) 

После реализации всех описанных выше методов остается только переопределить метод execute_order из абстрактного базового класса ExecutionHandler. В частности, этот метод отвечает за выставление приказов с помощью API брокерской системы.

Прежде всего, нужно проверить, что полученное методом событие — это действительно OrderEvent, а затем подготовить для него объекты Contract и Order с соответствующими параметрами. После их создания вызывается метод placeOrder из IbPy для соответствующего order_id.

Кроме того, крайне важно вызвать метод time.sleep (1), чтобы убедиться в том, что приказ действительно прошел в брокерскую систему. Удаление этого параметра может приводить к неконсистентному взаимодействию с API.

И, наконец, следует инкрементно увеличить величину ID ордера, чтобы не дублировать приказы:

# ib_execution.py          def execute_order(self, event):         """         Создание необходимы объектов приказов для отправки в брокерскую систему через API.          После этого запрашиваются результаты для генерации соответствующих событий fill, которые помещаются в очередь.          Параметры:         event – Содержит объект Event с информацией о приказе.         """         if event.type == 'ORDER':             # Подготовка параметров финансового инструмента             asset = event.symbol             asset_type = "STK"             order_type = event.order_type             quantity = event.quantity             direction = event.direction              # Создание контракта в брокерской системе с помощью прошедшего события Order                          ib_contract = self.create_contract(                 asset, asset_type, self.order_routing,                 self.order_routing, self.currency             )              # Создание приказа в системе брокера с помощью события Order  ib_order = self.create_order(                 order_type, quantity, direction             )              # Использование подключения для отправки приказа              self.tws_conn.placeOrder(                 self.order_id, ib_contract, ib_order             )              # ПРИМЕЧАНИЕ: Следующая строка очень важна             # Она позволяет убедиться в том, что приказ прошел!             time.sleep(1)              # Инкрементно увеличиваем ID приказа для текущей сессии             self.order_id += 1 

Этот класс формирует обработчик взаимодействия с брокерской системой и может быть использован для работы в режиме симулятора, что подходит исключительно для тестирования на исторических данных. Для того, чтобы использовать систему в ситуации реальной торговли, необходимо создать обработчик реального потока данных с биржи для замена потока исторических данных. Об этом мы поговорим в будущих статьях.

Как можно заметить, в ходе разработки бэктестера и модулей для реальной торговли мы где только возможно прибегали к повторному использованию кода — это позволят свести число ошибок к минимуму и убедиться в том, что поведение разных частей системы будет похожим, если не идентичным, как для торговли в режиме онлайн, так и в процессе бэктестинга.


На сегодня все, спасибо за внимание! Мы будем рады ответить на ваши вопросы и комментарии. Не забывайте подписываться на наш блог!

Все материалы цикла:

ссылка на оригинал статьи http://habrahabr.ru/post/270215/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *