Круг событий: подписки, буферы, обрывы
Страница описывает путь события от брокера к боту и то, что происходит, когда бот отстает или связь рвется. Схема
пути - на Карте, здесь ее подробности: подписки, режимы буфера,
переполнение, Resync, восстановление соединений и токены.
Подписка
Метод subscribe_* (стакан, сделки, котировки, свечи, статусы заявок, исполнения, портфель) возвращает
SubscriptionId. События этой подписки приходят в общий канал в конверте с этим номером. Снять подписку можно
методом unsubscribe(sub) (для рыночных подписок).
Что важно:
-
subscribe_*возвращается, когда поток открыт (у Финама есть оговорка, см. ниже). Событие, которое случилось после возврата, до бота дойдет: заявку можно ставить сразу послеsubscribe_order_stateиsubscribe_executions. Раньше нельзя: заявка, исполненная до открытия потока, оставила бы бота без события. Поток не открылся заsubscribe_ack_timeout(3 секунды, у Т-Банкаrequest_timeout) - ошибкаTransport, подписка снята, вызов можно повторить. Отказ брокера (нет такого инструмента, нет прав) -Rejected { subscription_refused }. Что считается «поток открыт», зависит от брокера:Брокер Признак БКС, заявки, исполнения, портфель сокет orders/events/wsилиportfolio/wsоткрыт (брокер присылает данные сам, подтверждения в протоколе нет)БКС, рынок кадр подтверждения брокера ( ...Success), либо первые данные подпискиТ-Банк ответ брокера о подписке в самом потоке ( Ok), для портфеля оба потокаФинам брокер открыл поток (заголовки ответа получены). Оговорка ниже АЛОР кадр 200на подписку (guidподписки)Bybit ответ на подписку с тем же req_idиsuccess: trueВызов может идти до этого срока. Если за него поток не открылся, это
Transport(сеть, медленный брокер), а не отказ брокера. У БКС и Финама брокер подтверждения не присылает: между открытием сокета или потока и моментом, когда брокер начинает слать события, остается промежуток на его стороне, и библиотека его не видит.Финам - исключение. Финам работает по gRPC, а gRPC-сервер может отправить заголовки ответа потока только вместе с первым сообщением. Поток статусов заявок молчит, пока заявок нет, поэтому здоровый поток может выглядеть «не открытым» до первой заявки. Поэтому у Финама
subscribe_*ждет открытия потока доsubscribe_ack_timeout, и если за это время поток не открылся, все равно возвращаетOk: поток дооткрывается в фоне, подписка не снимается, а гарантии «все после возврата дойдет» до открытия потока нет (состояние покажетsub_status). Отказ брокера внутри срока по-прежнему возвращается ошибкойRejected. -
Повторная подписка. У Т-Банка подписка на статусы заявок, исполнения и портфель одна на коннектор: повторный вызов вернет тот же
SubscriptionId. Стакан и котировки одного инструмента идут одной проводной подпиской, отписка от одной не рвет другую. -
Квота. У Финама открытие потока тратит квоту, как любой запрос. Число одновременных потоков ограничено (
subscription_quota).
Режимы буфера
Между потоком брокера и общим каналом стоит буфер на каждую подписку. Поток брокера читается в любом случае
и никогда не ждет, пока бот разберет канал. Если бот отстает, события копятся в буфере подписки, а режим решает, что
с ними будет. Режим бот выбирает при подписке (SubscribeOpts).
| Режим | Что происходит, когда бот отстает | Для чего |
|---|---|---|
Conflate | старый снимок заменяется новым, бот получает самый свежий. Данные не копятся. Держится последнее событие каждого вида (у Т-Банка на одной подписке портфеля идут портфель, позиции и остатки, свежий остаток не заменяет позиции) | стакан, котировки (полные снимки) |
DropNewest { cap } | пока в буфере меньше cap событий, все идет как обычно, при заполнении новые события отбрасываются, растет счетчик | некритичное: свечи, статистика |
Lossless { cap } | ничего не теряется. Если буфер заполнился, это переполнение: см. ниже | свои заявки, свои сделки |
Для статусов своих заявок и исполнений годится только Lossless: библиотека сама ставит этот режим, размер
буфера задается в конфиге UD (order_state_buffer, по умолчанию 1024). Для портфеля режим выбирает бот.
Получается два простых правила:
- Стакан, котировки -
Conflate. Старый снимок бесполезен, нужен последний. - Все, что нельзя потерять, -
Losslessс достаточнымcapи быстрое чтение канала ботом.
Переполнение: Overflow и Degraded
Если Lossless-буфер заполнился:
- Коннектор присылает
Event::Overflow { sub, lost_from }: этой подписке не хватило места, события начиная сlost_fromмогли быть потеряны. Событие приходит один раз на подписку, раньше обычных данных. - Новые события этой подписки отбрасываются, пока не будет сверки. Тихой потери нет: бот знает, что дыра образовалась.
- Коннектор переходит в состояние
Degraded: торговые команды возвращаютError::Degraded. Продолжать торговлю при дыре в данных о своих заявках нельзя.
На схеме: как медленное чтение канала доходит до закрытой торговли.
flowchart TB A[Бот читает канал медленно] --> B[Lossless-буфер заполнен] B --> C[Event::Overflow<br>и состояние Degraded] C --> D[Команды: Error::Degraded] D --> E[Выход: бот дочитывает канал<br>и зовет reconcile]
Почему торговля закрывается, а не продолжается с пометкой «возможна дыра»: бот видит свои заявки только по событиям, и потерянное событие о частичном исполнении или снятии значит, что бот торгует по неверной картине. Закрытый шлюз заставляет сначала сверить ее со счетом. Порядок действий боту: Если что-то пошло не так.
Чтобы переполнения не было: читайте канал отдельной задачей и не делайте в ней долгих операций. Размер буфера подберите с запасом на всплеск.
У всех брокеров механизм одинаковый. Буферы подписок у Т-Банка и БКС - один и тот
же код (Emitter из brokapi-runtime), у Финама свой, с теми же правилами.
Для рыночных подписок сверки нет (сверка - это про заявки), поэтому после Overflow подписку нужно снять
(unsubscribe) и оформить заново. Для рыночных данных обычно хватает Conflate, и переполнения нет.
Resync: стакан мог потерять данные
Стакан у всех брокеров приходит полным снимком, не приращениями. Поэтому следующий снимок сам заменяет
прежний. Но бывает, что прежнему верить уже нельзя. Тогда приходит Event::Resync { sub, instrument, reason }.
Когда:
| Причина | Когда | Брокер |
|---|---|---|
SubscriptionReset | поток переоткрыт после обрыва, подписка пересоздана | Т-Банк (стакан и котировки), БКС (стакан), Финам (стакан), АЛОР (стакан), Bybit (стакан) |
SequenceGap | в данных брокера обнаружен пропуск. Т-Банк помечает так стакан, который сам брокер счел несогласованным (такой стакан не публикуется). Финам помечает приращение без базового снимка и немедленно переоткрывает поток. Bybit: изменения стакана идут подряд по номеру u, не следующий номер сбрасывает стакан, коннектор просит у биржи свежий снимок (отписка и подписка) | Т-Банк, Финам, Bybit |
У БКС и АЛОРа SequenceGap не бывает: стакан приходит полным снимком, и сверять нечего. SubscriptionReset идет по одному разу на каждую
подписку на стакан, сразу после переподключения WebSocket и до снимков восстановленных подписок. Первое подключение
Resync не дает: терять нечего. Коннектор узнает о подключении от самой сессии WebSocket, отдельным служебным сообщением в
том же потоке, что и данные, поэтому Resync не может обогнать данные и не может опоздать за ними.
Что делать: выбросить стакан, который хранится у бота, не торговать по нему, дождаться следующего
Event::OrderBook. У Финама Resync идет вне очереди склейки. У Т-Банка и БКС он проходит через буфер подписки, и если бот сильно
отстает, его может заменить более свежий снимок (он тоже свежий, так что бот в любом случае получит актуальные данные).
Reconnected: поток заявок или исполнений оборвался
Потоки своих заявок (subscribe_order_state) и своих исполнений (subscribe_executions) тоже обрываются и переоткрываются
сами. Но в отличие от стакана, здесь за время обрыва могло произойти то, о чем бот не узнает из потока: заявка исполнилась,
снялась, была отклонена. Поэтому после того как поток открылся заново, приходит
Event::Reconnected { sub, gap_from } на подписку, чей поток оборвался.
sub- подписка, у которой оборвался поток. Приходит по одному событию на подписку заявок и на подписку исполнений (если подписаны на обе).gap_from- время (локальные часы, наносекунды), когда коннектор заметил обрыв. Дыра начинается в этот момент или раньше, поэтому историю читайте с запасом назад.
Что делает коннектор: закрывает торговый шлюз, как при Overflow. Команды получают Degraded и в сеть не уходят, пока не
пройдет reconcile. Шлюз закрывается раньше, чем событие дойдет до бота, поэтому команда после события уже отказана.
Что делать боту: то же, что после Overflow. Дочитать канал до конца (события до дыры нужны как обычно), прочитать
состояние у брокера (active_orders, order_state по своим cid, executions с момента чуть раньше gap_from), вызвать
reconcile. Успешная сверка открывает шлюз. Подробно: Если что-то пошло не так.
Для портфеля такого события нет: каждое сообщение потока портфеля и так целое состояние, следующее заменит прежнее. Для
рыночных данных вместо него Resync (выше).
| Т-Банк | БКС | Финам | АЛОР | Bybit | |
|---|---|---|---|---|---|
| Когда приходит | поток заявок или поток сделок открылся не первый раз | второе и каждое следующее подключение сокета orders/events/ws (он несет и статусы, и исполнения, поэтому событие идет на обе подписки) | поток заявок или поток сделок был открыт, оборвался и открылся снова | второе и каждое следующее подключение сокета подписок (он несет и заявки, и сделки, поэтому событие идет на обе подписки) | второе и каждое следующее подключение приватного сокета (события идут на подписки заявок и исполнений) |
gap_from | момент переоткрытия (обрыв сам коннектор не видит) | момент подключения | момент обрыва | момент подключения | момент подключения |
| Что еще делает коннектор | перечитывает активные заявки и присылает изменившиеся | ничего | ничего | ничего | ничего, подписки заказываются заново |
Восстановление соединений
Все коннекторы сами переоткрывают оборванные соединения и восстанавливают подписки. Бот ничего для этого не делает. На схеме: что происходит после обрыва, пока поток снова не заработает.
flowchart TB W[Поток работает] --> X[Обрыв: молчание,<br>ошибка, закрытие] X --> P[Пауза: 0,5 с, дальше<br>удваивается до 30 с] P --> R[Переподключение] R -- не вышло --> P R -- вышло --> S[Подписки восстановлены] S --> RS[Resync по стакану:<br>старый выбросить] RS --> W
Из схемы следует, что бот не видит обрыва как отдельного события: он видит Resync по стакану, когда поток вернулся, и
Reconnected по потокам заявок и исполнений. Стакан у всех брокеров приходит полным снимком, поэтому после Resync достаточно
дождаться следующего. А после Reconnected бот обязан прочитать состояние и позвать reconcile
(Если что-то пошло не так). Параметры по брокерам:
| Т-Банк | БКС | Финам | АЛОР | Bybit | |
|---|---|---|---|---|---|
| Пауза перед повтором | 0,5 с, удваивается до 30 с, сбрасывается после первого сообщения потока (не после открытия: поток, который открылся и сразу оборвался, не пробуется на полной скорости). Не настраивается | 0,5 с, удваивается до 30 с, сбрасывается, если соединение прожило 10 с. Настраивается: reconnect_min и reconnect_max в MarketDataConfig и UserDataConfig | 0,5 с, удваивается до 30 с, сбрасывается, если поток прожил 10 с. Не настраивается | 0,5 с, удваивается до 30 с, сбрасывается, если соединение прожило 10 с. Настраивается: reconnect_min и reconnect_max в MarketDataConfig и UserDataConfig | 0,5 с, удваивается до 30 с, сбрасывается, если соединение прожило 10 с. Настраивается: reconnect_min и reconnect_max в MarketDataConfig и UserDataConfig |
| Как замечает обрыв | молчание дольше трех интервалов пинга (по умолчанию 3 x 30 с) | пинг каждые 30 с, нет ответа 60 с - переподключение | ошибка потока, закрытие сервером | ошибка чтения, закрытие сервером, пинг каждые 30 с, ответа на пинг коннектор не требует | пинг каждые 20 с, как просит биржа, молчание дольше 50 с - переподключение |
| Подписки | восстанавливаются все | восстанавливаются все | поток каждой подписки открывается заново | восстанавливаются все, каждая со свежим токеном | восстанавливаются все, приватный сокет входит заново |
Resync стакана | да | да | да | да | да |
| Заявки после обрыва | Event::Reconnected и закрытый шлюз. Активные заявки перечитываются, изменения приходят событиями. Заявки, закрытые за время обрыва, находятся через order_state и executions | Event::Reconnected и закрытый шлюз. Не перечитываются | Event::Reconnected и закрытый шлюз. Не перечитываются | Event::Reconnected и закрытый шлюз. Не перечитываются | Event::Reconnected и закрытый шлюз. Не перечитываются |
| Статус подписки | нет | нет | sub_status(sub): Connecting, Active, Reconnecting, Failed, Erroring | нет | нет |
Почему паузы одинаковые. Ни в коде, ни в документации брокеров нет причины для разных диапазонов: ни один брокер не называет, как часто можно переподключаться. Поэтому у всех один диапазон. Начало в 0,5 с не бьет по серверу в первую секунду обрыва, а потолок в 30 с не оставляет бота без данных дольше половины минуты. У БКС, АЛОРа и Bybit границы можно поменять в настройке коннектора. Сколько подключений в секунду терпит БКС, в документации не названо. У Т-Банка и Финама паузы пока зашиты в код.
Особенности:
- Финам: подписка, которую сервер отверг (нет прав, нет инструмента), помечается
Failedи пробуется снова примерно раз в 30 секунд, на случай, если права появятся.subscribe_*вернет ошибку сразу, если отказ пришел при открытии. - Т-Банк: поток с истекшим сроком жизни (ошибка 80005) обрабатывается как обычный обрыв. Отказ открыть поток, который повтор не вылечит (токен не подошел, нет права, открыто максимум потоков, 80001),
останавливает поток:
subscribe_*возвращает ошибку, а у потока заявок или сделок, оборвавшегося позже, закрывается шлюз. Новая подписка пробует открыть поток заново. - БКС: при отказе 403 при подключении токен обновляется принудительно.
Токены и их обновление
| Брокер | Что дает вход | Как обновляется |
|---|---|---|
| БКС | refresh-токен из настройки. Из него коннектор получает токен доступа | Токен доступа берется при первом запросе и обновляется заранее (после 90% срока expires_in). После ответа 401 на чтение он обновляется, и запрос повторяется один раз. Команда (заявка, отмена, правка) после 401 не повторяется: токен обновляется для следующего вызова, а 401 возвращается ошибкой (Rejected с кодом http_401). При отказе 403 на WebSocket токен обновляется принудительно. Если Keycloak присылает новый refresh-токен, коннектор принимает его, но только в памяти: на диск он не пишется, после рестарта берется токен из настройки. Документация БКС не говорит, остается ли старый refresh-токен рабочим |
| Т-Банк | токен. Он статичный, обновлять нечего | Если токен отвергнут, это ошибка настройки (Config) |
| Финам | секрет Trade API. По нему коннектор получает JWT | Срок жизни JWT читается из ответа брокера. Обновление идет заранее (за пятую часть срока, от 5 до 120 секунд, но не позже середины срока), не дожидаясь ошибки. Неудачное обновление повторяется с паузой от 1 до 30 секунд, старый токен пока работает. У Auth и TokenDetails таймаут 10 секунд, срок токена это меньшее из TokenDetails и exp - iat. Ошибка UNAUTHENTICATED вызывает обновление сразу. Чтение после этого повторяется один раз, команда не повторяется никогда, но обновление запускается и на ней |
| АЛОР | refresh-токен с портала разработчика. Из него коннектор получает токен доступа | Токен доступа живет 30 минут и берется заново через 20 (или раньше, если его собственный срок короче). После ответа 401 на чтение токен обновляется и чтение повторяется один раз, команда после 401 не повторяется. По WebSocket токен идет в каждом сообщении: при переподключении подписки восстанавливаются со свежим токеном, а ответ 401 по сокету обновляет токен до переподключения |
| Bybit | пара ключ и секрет API | Обновлять нечего: ключ статичный, каждый запрос подписывается секретом. Время для подписи берется у биржи (сдвиг часов читается при старте). Ключ, который биржа отвергла, это ошибка настройки (Config) |
Один JWT Финама можно разделить между несколькими коннекторами одного секрета (shared_tokens): Несколько
счетов и брокеров.
Симптомы, которые видит бот, и что с ними делать: Ошибки и что с ними делать.
Листать можно клавишами Влево и Вправо.