Горячие клавиши

← и →: предыдущая и следующая глава

S или /: поиск по книге

?: показать эту подсказку

Esc: закрыть подсказку или поиск

Круг событий: подписки, буферы, обрывы

Страница описывает путь события от брокера к боту и то, что происходит, когда бот отстает или связь рвется. Схема пути - на Карте, здесь ее подробности: подписки, режимы буфера, переполнение, 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-буфер заполнился:

  1. Коннектор присылает Event::Overflow { sub, lost_from }: этой подписке не хватило места, события начиная с lost_from могли быть потеряны. Событие приходит один раз на подписку, раньше обычных данных.
  2. Новые события этой подписки отбрасываются, пока не будет сверки. Тихой потери нет: бот знает, что дыра образовалась.
  3. Коннектор переходит в состояние 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 и UserDataConfig0,5 с, удваивается до 30 с, сбрасывается, если поток прожил 10 с. Не настраивается0,5 с, удваивается до 30 с, сбрасывается, если соединение прожило 10 с. Настраивается: reconnect_min и reconnect_max в MarketDataConfig и UserDataConfig0,5 с, удваивается до 30 с, сбрасывается, если соединение прожило 10 с. Настраивается: reconnect_min и reconnect_max в MarketDataConfig и UserDataConfig
Как замечает обрывмолчание дольше трех интервалов пинга (по умолчанию 3 x 30 с)пинг каждые 30 с, нет ответа 60 с - переподключениеошибка потока, закрытие серверомошибка чтения, закрытие сервером, пинг каждые 30 с, ответа на пинг коннектор не требуетпинг каждые 20 с, как просит биржа, молчание дольше 50 с - переподключение
Подпискивосстанавливаются всевосстанавливаются всепоток каждой подписки открывается занововосстанавливаются все, каждая со свежим токеномвосстанавливаются все, приватный сокет входит заново
Resync стаканададададада
Заявки после обрываEvent::Reconnected и закрытый шлюз. Активные заявки перечитываются, изменения приходят событиями. Заявки, закрытые за время обрыва, находятся через order_state и executionsEvent::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): Несколько счетов и брокеров.

Симптомы, которые видит бот, и что с ними делать: Ошибки и что с ними делать.

Листать можно клавишами Влево и Вправо.

brokapi by RunInfra