В предыдущих статьях мы говорили о том, что такое событийно-ориентированная система бэктестинга, разобрали иерархию классов, необходимую для ее функционирования, обсудили то, как подобные системы используют рыночные данные, а также осуществляют отслеживание позиций и генерацию приказов на покупку. Кроме того, мы описали процесс оценки производительности тестируемых стратегий.
В сегодняшнем материале будет рассмотрен процесс создания обработчика 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
Этот класс формирует обработчик взаимодействия с брокерской системой и может быть использован для работы в режиме симулятора, что подходит исключительно для тестирования на исторических данных. Для того, чтобы использовать систему в ситуации реальной торговли, необходимо создать обработчик реального потока данных с биржи для замена потока исторических данных. Об этом мы поговорим в будущих статьях.
Как можно заметить, в ходе разработки бэктестера и модулей для реальной торговли мы где только возможно прибегали к повторному использованию кода — это позволят свести число ошибок к минимуму и убедиться в том, что поведение разных частей системы будет похожим, если не идентичным, как для торговли в режиме онлайн, так и в процессе бэктестинга.
На сегодня все, спасибо за внимание! Мы будем рады ответить на ваши вопросы и комментарии. Не забывайте подписываться на наш блог!
Все материалы цикла:
- Событийно-ориентированный бэктестинг на Python шаг за шагом. Часть 1
- Событийно-ориентированный бэктестинг на Python шаг за шагом. Часть 2
- Событийно-ориентированный бэктестинг на Python шаг за шагом. Часть 3
- Событийно-ориентированный бэктестинг на Python шаг за шагом. Часть 4
- Событийно-ориентированный бэктестинг на Python шаг за шагом. Часть 5 (и последняя)
Комментарии (3)
BOOTor
05.11.2015 18:54Скажите, пожалуйста, где можно найти оригинал цикла статей?
alexlash
06.11.2015 13:43Там в конце ссылка есть на оригинал конкретно этой статьи, все материалы цикла (там правда по номерам разбивка несколько другая, тут на хабре некоторые топики объединяют по две статьи) можно найти там в разделе со статьями www.quantstart.com/articles (раздел backtesting)
alinatestova
Неплохая серия. С питоном мало общалась, но общую картину можно уловить достаточно быстро.