Торговый алгоритм на Python для IB.API: подключение к TWS

Это третья статья в данной серии. В ней мы создадим алгоритм, который будет подключаться к TWS и получать «первичные сообщения».

Создавая данный алгоритм, мы рассмотрим реализацию двух обязательных классов EClient и EWrapper. И в конце заглянем «за кулисы» и посмотрим, что происходит, когда мы подключаемся к TWS.



🔌Подключение к Trader Workstation

Это один из самых простых процессов, которые можно организовать с помощью IB API.

В самом простом случае для организации подключения нам понадобятся всего два класса: EWrapper и EClient. С ними мы уже познакомились в первой статье из этой серии.

EClient и EWrapper — в каждом вашем приложении

Как я уже писал, два данных класса являются обязательными участниками любого приложения на Python3, где задействован TWS. И это не просто так. Без этих двух классов вы не сможете даже подключиться к терминалу, про всё остальное можно даже не говорить.

В первой статье мы рассмотрели роли данных классов. Теперь давайте поговорим об их применени.

Все начинается с импорта модулей, содержащих эти классы:

import ibapi.wrapper
import ibapi.client

После подключения данных классов займемся созданием их объектов. Тут будьте внимательны, есть одна хитрость:

wrp = ibapi.wrapper.EWrapper()
cln = ibapi.client.EClient(wrp)

Ничего странного не увидели? Ничего страшного, читайте далее.

Обратите внимание на порядок создания объектов, он должен быть именно таким, как написано в примере: сначала объект EWrapper, потом EClient. Почему? Потому что при инициализации объекта EClient мы должны передать в его инициализатор объект класса EWrapper(!). Вот почему важно создать объект EWrapper первым.

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

🔌Организация подключения к TWS

Связь с терминалом использует сокетное подключение. Если вы знаете как работать через сокеты в Python3, то понимаете на сколько это страшная конструкция из кода (что для организации сервера, что для организации клиента). Но при использовании IB API это всего лишь одна строчка кода:

cln.connect("127.0.0.1", 7496, 1)

Разберем параметры, передоваемые в этот метод. Вот они по порядку:

  • «127.0.0.1» — host (тип str) — название хоста или IP-адресс. Можно указать IP-адрес компьютера, если TWS запущен на другом компьютере в локальной сети. Можно отправить пустую строку. Можно отправить «localhost». В данном примере используется IP-адрес «локальной петли», то есть вы обращаетесь к TWS на том же компьютере, на котором создаете своё приложение.
  • 7496 — port (тип int) — порт сокетного подключения, естественно. Не менее важная вещь для сокетного подключения. Не поленитесь, залезьте сейчас в настройки вашего TWS и посмотрите пункт Socket port. Используйте порт, который там указан.
  • 1 — clientId (тип int) — идентификационный номер клиента. В самой первой статье я писал, что один TWS способен поддерживать связь с 32 клиентами одновременно, благодаря этому номеру TWS и различает клиентов. В будущем уделяйте пристальное внимание этому номеру, когда будете писать многопоточное приложение. Ну, и разумеется, в настройках TWS указывается Master API client ID — это «главный клиент». Измените настройку на единицу, как указано в коде. Потом можете менять на любое число (что в настройках, что в коде).

Небольшая ремарка: если вы работаете с WEB-сервером или экспериментировали на вашей машине с локальной сетью, посмотрите ваш файл host. Очень важно, чтобы там была строка: «127.0.0.1 localhost», в противном случае наступите на те же грабли, что и я. У меня localhost’у соответствовал IP-адрес другого компьютера в локальной сети — API на отрез отказывался подключаться к TWS, выкидывая ошибку.

Ну, а отключение от TWS еще проще:

cln.disconnect()

Еще одна ремарка: если вы — опытный Python-разработчик, то знаете, что надо обязательно отключать сокетное подключение. Если же нет, то объясняю: при открытии сокетного подключения (равно как и при открытии файла), интерпретатор Python создает специальный дескриптор для работы с подключением, так же TWS, со своей стороны, продолжает «держать» такое подключение, ожидая по нему запросов от IB API, по-этому, чтобы не происходило накопление дескрипторов и TWS не перегружался «пустыми» подключениями, не забывайте закрывать подключение из вашего кода. Тем более, что это всего одна строчка кода.

С подключением разобрались, но как-то «жиденько» получилось… Если запустить представленный код, он просто подключиться к терминалу, а потом отключиться от него. Давайте хотя бы спросим у него «как дела», а точнее попробуем получить «первичные сообщения».

📨Получение сообщений от TWS

Любой запрос от вашего приложения к терминалу и любой ответ в обратную сторону, IB API считает «сообщениями». Они бывают разными и по сложности данных и по сложности обработки, но сейчас самое важное — это то, что они протекают в неком «канале обмена сообщениями» и, чтобы получить от терминала первичные сообщения, нам надо «подключиться» к такому «каналу». В конце статьи рассказывается про процессы, протекающие в момент подключения, поэтому сейчас давайте сосредоточемся на коде.

Для «подключения к каналу» нам потребуются еще два метода класса EClient:

  • .isConnected() — возвращает True, если IB API подтвердит, что подключен к TWS.
  • .run() — отвечает за прием и накопление сообщений от TWS.

Отдельного внимания заслуживает метод .run(). Вообще, метод .run() как и метод .connect() является самым сложным по коду и самым важным в классе EClient. Без этих двоих все остальное лишено практического смысла. Они две главные шестеренки, которые запускают и вращают весь механизм под названием «IB API». В следующих статьях я постараюсь детально описать, что делают эти методы, а пока возврашаемся к коду.

if cln.isConnected():
    print("Успешно подключились к TWS")
    tws.th = threading.Thread(target=tws.run)
    tws.th.start()
    tws.th.join(timeout=5)

Первая строка — метод .isConnected() — проверяет подключены ли мы к TWS, и, если подключение установлено, исполнится блок кода далее.

Во второй строке мы «принтуем» обычное сообщение. Я эту строку всегда держу в коде — потом легче анализировать результат работы: сразу видно подключился мой алгоритм к TWS или нет.

А вот с третьей строки начинается самая магия! Здесь мы запускаем параллельный поток с помощью модуля threading и класса Thread() внутри него. Данный модуль входит в стандартную библиотеку Python, поэтому он всегда доступен.

В четвертой строке мы запускаем только что созданный поток.

В пятой строке мы заставляем основной поток подождать, пока выполнится организованный нами параллельный поток. Обратите внимание на параметр, который передается в метод .join(), — это timeout, в нашем примере равный 5 секундам. Данный параметр ограничивает время ожидания параллельного потока до 5 секунд, то есть, у нашего параллельного потока, работающего с методом .run(), есть 5 секунд, чтобы сделать все свои дела. Если метод .run() затянет выполнение на большее время, поток принудительно завершится. На досуге вы можете поэкспериментировать с timeout, задавая другое значение. Только не ставьте одну секунду, по своему опыту скажу — это слишком мало, чтобы получить от TWS сообщения.

🎁Код в студию!

А вот и весь код целиком:

# Импортируем необходимые библиотеки
import ibapi.wrapper                             # Wrapper - класс, обрабатывающий сообщения от TWS
import ibapi.client                              # Client - класс, подключающий приложение к TWS
import threading                                 # Модуль по работе с потоками

# Создаем необходимые объекты для работы с IB API
wrp = ibapi.wrapper.EWrapper()                   # Объект класса Wrapper
cln = ibapi.client.EClient(wrp)                  # Объект класса Сlient, передаем wrapper в инициализатор

# Подключаемся к Trader Workstation
cln.connect("127.0.0.1", 7496, 1)                # Передаем хост, номер порта и ID клиента

# Работаем с TWS, подключаем стандартные сообщения
if cln.isConnected():                            # В случе успешного подключения к TWS
    print("Успешно подключились к TWS")          # Печатаем статус подключения
    cln.th = threading.Thread(target=cln.run)    # Организовываем поток
    cln.th.start()                               # Запускаем поток
    cln.th.join(timeout=5)                       # Ожидаем окончание выполнения потока или выходим через 5 сек.

# Отключаемся от Trader Workstation
cln.disconnect()                                 # Подключение закрывать обязательно!

При запущенном TWS код выше должен отобразить в панели вот это:

Успешно подключились к TWS
ERROR:root:ERROR -1 2104 Market data farm connection is OK:usfuture
ERROR:root:ERROR -1 2104 Market data farm connection is OK:cashfarm
ERROR:root:ERROR -1 2104 Market data farm connection is OK:usfarm
ERROR:root:ERROR -1 2106 HMDS data farm connection is OK:ushmds

Если у вас так же — примите мои поздравления! Вы создали алгоритм, который подключился к TWS и получил от него сообщения! УРА вам!

Спешу вас успокоить!

  • Порядок сообщений не важен, мы же работаем с асинхронными потоками в нашем коде! Не пугайтесь если у вас сообщения вышли в другом порядке.
  • Не пугайтесь 4 сообщения про ошибки — это не «настоящие» ошибки! По крайней мере так пишут в мануале. Но на самом деле их идентификатор (-1) и коды 2104 и 2106 говорят о том, что терминал успешно подключен к дата-центрам и серверам Interactive Brokers и мы можем запрашивать разные данные и отправлять разные торговые приказы на рынок.
  • Не пугайтесь, если в конце увидете исключение: AttributeError: ‘NoneType’ object has no attribute ‘close’. Это специфика нашего кода. В следующих статьях наш код будет другим и ошибка уйдет.

Подтвержденное подключение API

Когда вы запустите код, приведенный выше, в TWS можете увидеть вот такое окно:

conn_prompt.png

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

Что бы TWS не надоедал своими «принять входящее подключение?», посмотрите в настройках пункт «Allow connection from localhost only«. Поставьте галочку на против этого пункта. Более TWS не будет запрашивать ваше подтверждение при каждом подключении.

👀Заглянем за кулисы

Этот раздел для самых любопытных. Здесь описывается процесс взаимодействия между API и TWS. Чтобы создавать алгоритмы, вам не обязательно знать, что тут написано, поэтому можете его пропустить.

Итак, связь с TWS устанавливается с помощью сокетов. В данном подключении TWS выполняет роль сервера, который принимает все запросы от API, обрабатывает их и направляет обратно ответы. Один экземпляр TWS способен поддерживать до 32 сокетных подключений (то есть способен вести до 32 ваших приложений) одновременно. При этом один экземпляр TWS способен принимать до 50 запросов в секунду от всех подключенных в эту секунду клиентов.

В свою очередь, API в данном сокетном подключении выполняет роль клиента, который «запускает» подключение к TWS, направляет ему запросы и принимает ответы. Один API способен принять бесконечное количество ответов от сервера, но из-за ограничений последнего, это количество не может быть больше 50 в секунду.

В момент вызова метода .connect() IB API запрашивает у операционной системы подключение по параметрам, указанным в методе. Если по каким-то причинам подключение невозможно, операционная система вернёт ошибку. В свою очередь IB API вернёт в ваше приложение ошибку 502, что будет означать, что TWS не запущен или ждёт подключение по другому порту и/или хосту.

После установки подключения сервер (TWS) и клиент (IB API) обмениваются важной информацией: сначала они обмениваются версиями друг друга для того, чтобы понять, какими методами будут направляться сообщения в терминал и какими методами будут обрабатываться ответы в IB API. Далее терминал направляет важные данные по установленной сессии: это логин (под которым он сейчас запущен), следующий идентификационный номер запроса (об этом в следующих статьях) и время подключения к нему. Все эти сообщения не приходят в ваше приложение, так как являются техническими и нужны только IB API и TWS для сверки.

Далее устанавливается «канал обмена сообщениями», организованный на двух потоках Python (модуль threading). Через первый поток IB API направляет запросы в TWS. Через второй поток читает сообщения из сокета, декодирует их и принимает дальнейшие действия, в зависимости от типа сообщения. Более подробно механизм приема сообщений я опишу в следующих статьях.

И наконец, терминал присылает свои первые сообщения через установленный канал. Это я и называл «первичными сообщениями» в тексте выше. Данные сообщения, как и все другие, проходящие через поток получения сообщений, IB API передает в наше приложение. Их мы и видим в панели терминала или IDE. Это просто статусы подключения TWS к серверам Interactive Brokers в интернете. Те самые 4 сообщения-ошибки, которые не являются ошибками. Это просто TWS рапортует, что подключился к своим источникам данных и готов к дальнейшей работе.

☝Осторожно грабли!

Напоследок, расскажу про один эксперимент с методом .run().

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

Я так и сделал. Создал код, запустил его и… ничего не произошло… Немного посмотрел на панель, там ничего не было. И с мыслью «Ну, и хрен с тобой!» пошел заваривать себе кофе (шёл третий час ночи, нужно было топливо, чтобы не уснуть).

Через несколько минут я уже летел голопом к ноутбуку, потому что гул его кулера (из комнаты) смог «переорать» шум закипающего чайника (производитель ноутбука уверял, что сделал кулер на 30% тише).

Подлетев к монитору, я снова ничего не увидел на панели, но гул кулера меня сильно напрягал. Поэтому я аварийно завершил процесс Python, закрыл IDE, закрыл TWS и перезагрузил операционную систему. После того, как кулер успокоился, я решил разобраться «в чем дело».

Дальнейшее (уже внимательное) чтение мануала и анализ кода метода .run() показали, что данный метод управляет всеми входящими от терминала сообщениями в бесконечном цикле(!), при этом цикл завершается только если очередь сообщений полностью опустошается или, если происходит отключение от TWS. Поэтому для нормальной работы приложения требуется организовать отдельный поток, в котором и надо запускать метод .run(). Разумеется я этого не знал и запустил его в основном потоке своего кода. В ответ на это, метод .run() «подвесил» выполнение всей моей программы в бесконечном цикле, на каждой итерации которого проверял очередь на предмет входящих сообщений от TWS.

Будьте внимательны, не наступайте на грабли, на которые наступил я. Если вы не знаете про процессы (process) и потоки (threading) в Python — изучите их. Если вы не разбираетесь в очередях (queue), как способе синхронизации данных между потоками — так же изучите их. В противном случае, вам будет очень сложно разобраться в коде IB API самостоятельно.

🏁Заключение

Ух! Получилось много слов и мало кода! Понимаю вас, однако ничего сделать не могу. Слишком большой пласт информации пришлось изучить, осознать и подать.

Но несмотря на это в данной статье мы начали знакомство с IB API: узнали про два обязательных класса — EWrapper и EClient (центр всего внутри IB API), создали с их помощью скрипт, который подключается к терминалу и получает от него сообщения со статусом подключения. Рассмотрели сам процесс подключения, а в конце я рассказал, как нагрузил «боевого товарища».

И это всё только про подключение. Далее мы поговорим про контракты. К своему удивлению, вы узнаете, что это не торговые приказы терминалу. Про закачку истории, отправку торговых приказов через терминал и многое другое.

До встречи в следующих статьях!

Автор: Олег Минаев