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

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

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

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

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

Карта: сущности и три круга

Эта страница - карта библиотеки: из чего она состоит, по каким кругам ходят данные и команды и на каких правилах это держится. Подробности каждого круга лежат на своей странице, здесь только то, что нужно, чтобы не потеряться. Термины в таблице ниже - словарь всей книги: везде они значат ровно это.

Сущности и связи

Бот сам поднимает коннекторы, глобального реестра в библиотеке нет. Коннекторов два вида: MD - рынок, один на брокера, и UD - заявки и портфель, один на счет. Все пишут в один канал событий. Запускаются они одинаково: start(конфиг, счетчик_лимитов, канал_событий).

На схеме: бот, библиотека с коннекторами, брокер, общий канал событий и счетчик лимитов.

flowchart TB
  BR[Брокер<br>REST, WebSocket, gRPC]
  subgraph lib[Библиотека brokapi]
    MD[MD, рыночные<br>данные, один<br>на брокера]
    UD1[UD, данные<br>счета 1: заявки,<br>портфель]
    UD2[UD, данные<br>счета 2]
  end
  L[Счетчик лимитов<br>один на область квоты]
  Q[(Канал событий<br>один на всех)]
  Bot[Бот]
  BR <--> lib
  L -. квота .- lib
  lib -- события в конверте --> Q
  Q --> Bot
  Bot -- команды --> lib

Из схемы следует три вещи, и у каждой есть причина:

  • MD и UD разделены. Всплеск в потоке стакана не должен задерживать отмену заявки. Если бы команды и стакан шли через одно соединение, отмена стояла бы в очереди за стаканом. У MD и UD разные соединения и разные лимиты, они ничего не знают друг о друге и не делят сокет.
  • Счетчик лимитов один на ту область, где брокер считает квоту. У Т-Банка это пользователь (все счета вместе), у БКС - токен, то есть счет (по документации). Дать двум коннекторам разные счетчики на одну квоту нельзя: вместе они превысят ее, и брокер начнет отказывать. Дать один счетчик там, где лимиты независимы, тоже плохо: коннекторы будут мешать друг другу без нужды. Как устроен счетчик: Лимиты запросов, какая область у какого брокера: Несколько счетов и брокеров.
  • Канал событий один, коннектор не знает, кто его читает. Бот отдал канал при старте и может дать один всем коннекторам или по каналу каждому. Если читатель отстает, коннектор не блокируется: что тогда происходит, решает буфер подписки (Круг событий).

Словарь:

СущностьЧто это
Коннектор MDстаканы, сделки рынка, котировки, свечи. К счету не привязан
Коннектор UDзаявки, свои сделки, портфель. Ровно один счет
Счетчик лимитовнабор «ведер» с квотой. Считает запросы до отправки, при нехватке сразу RateLimited, в сеть ничего не уходит (Лимиты запросов)
Канал событийочередь mpsc, которую бот отдал при старте
Конверт Envelopeсобытие плюс метка коннектора source, номер подписки sub и время (События и возможности)
cidномер заявки. Придумывает бот, UUID, до отправки (Круг команд)
Подпискапоток событий одного вида. У каждой свой буфер (Круг событий)
Шлюз командзамок на торговые команды (CommandGate): закрыт после старта, при перегрузке и при остановке (Круг жизни)
Сверкаreconcile: сравнение списка заявок бота со счетом, после нее шлюз открывается (Круг жизни)

Слои

Крейты разложены так, чтобы логика брокера не знала про сеть, а сеть - про логику. Стрелка на схеме значит «пользуется». Что лежит в каждом крейте, до отдельного типа: Справочник API.

flowchart TB
  CONN[brokapi-bcs, brokapi-tbank, brokapi-finam,<br>brokapi-alor, brokapi-bybit<br>коннекторы: склеивают слои]
  PROTO[brokapi-bcs-protocol, brokapi-tbank-protocol,<br>brokapi-finam-protocol, brokapi-alor-protocol,<br>brokapi-bybit-protocol<br>клиент брокера: сеть]
  RT[brokapi-runtime<br>лимиты, буферы, шлюз,<br>очередь по бумаге, сверка]
  SANS[brokapi-sansio<br>ядро без сети: разбор,<br>машина состояний заявки]
  CORE[brokapi-core<br>общие типы и трейты:<br>то, что видит бот]
  CONN --> PROTO & RT & SANS
  RT --> SANS --> CORE
  MOCK[brokapi-mocks<br>имитации брокеров] -. для тестов .-> PROTO
  CONF[brokapi-conformance<br>общие сценарии] -. проверяет .-> CONN

Что из этого следует:

  • Бот видит только brokapi-core. Сменить брокера - поменять коннектор, код бота почти не меняется.
  • Ядро без сети проверяется записанными сообщениями брокера и виртуальным временем, быстро и без интернета.
  • Внизу - тестовый контур: имитация брокера с настоящим протоколом и общие сценарии, которые гоняются против каждого коннектора.

Зачем ядро без сети

Ядро коннектора (sans-io) - чистая логика: «пришло такое сообщение брокера -> вот события для бота» и «чтобы подписаться, нужно отправить вот такое сообщение». Оно не открывает сокеты, не держит таймеры, не знает про асинхронность. Сеть и время ему подает внешний слой. Интерфейс ядра (BrokerCore) - пять действий: on_frame (разобрать пришедшее сообщение), poll_transmit (что отправить брокеру), poll_timeout (когда разбудить ядро), on_timeout (обработать наступивший срок), apply (привести подписки к нужному набору).

Зачем так:

  • Ядро проверяется быстро и без сети. Тест кормит его записанными сообщениями брокера и проверяет события. Время в таких тестах виртуальное.
  • Ошибки разбора и перевода ищутся отдельно от ошибок сети. Если смешать, сбой в сети похож на ошибку разбора, и причину ищут не там.
  • Записанные настоящие сообщения (когда будут живые прогоны) становятся тестами без правок.

У Т-Банка и Финама запросы типизированы (gRPC), поэтому вместо poll_transmit у ядра Т-Банка есть свой метод выдачи запросов. На работу бота это не влияет.

Три круга

Вся работа - три круга, которые крутятся одновременно и почти не мешают друг другу: события (брокер -> бот), команды (бот -> брокер) и жизнь коннектора (шлюз команд и сверка). Бот общается с коннекторами двумя путями: команды - вызовы методов, ответ приходит как результат вызова, события - все подписки всех коннекторов складываются в один канал.

Круг событий: брокер -> бот

Внутри коннектора сообщение брокера проходит три ступени. Ядро только разбирает и подсказывает, что отправить и когда его разбудить, а сокеты и таймеры держит внешний слой. На схеме: путь сообщения от брокера до бота и служебные сигналы ядра.

flowchart TB
  BR[Брокер] -- сообщение --> P[Протокол<br>сокет, поток gRPC]
  P -- кадр --> C[Ядро без сети<br>on_frame: разбор]
  C -- события в общих типах --> B[Буфер подписки<br>Conflate, DropNewest<br>или Lossless]
  B --> Q[(Канал событий)]
  Q --> Bot[Бот читает<br>отдельной задачей]
  C -. poll_transmit:<br>что отправить .-> P
  C -. poll_timeout:<br>когда разбудить .-> T[Таймер]
  T -. on_timeout .-> C

Поток брокера читается всегда и никогда не ждет бота. Если бот отстает, решает режим буфера:

  • Conflate - старый снимок заменяется свежим. Для стакана и котировок.
  • DropNewest - лишнее отбрасывается со счетчиком. Для некритичного.
  • Lossless - ничего не теряется. Для своих заявок и сделок. Переполнился - Overflow и закрытый шлюз. Оборвался и открылся заново поток заявок или исполнений - Reconnected и тоже закрытый шлюз (круг жизни ниже).

Почему режимов три: стакану нужен только последний снимок, а своим сделкам нужны все, и один режим на все подписки либо потерял бы деньги, либо копил бы устаревший стакан. Подробно, с обрывами и восстановлением соединений: Круг событий.

Круг команд: бот -> брокер

Команда проходит три проверки без сети, и только потом уходит к брокеру. На схеме: путь place и все его исходы.

flowchart TB
  A[place с cid] --> G{Шлюз открыт}
  G -- нет --> E1[NotReconciled, Degraded<br>или ShuttingDown]
  G -- да --> V{Вход верный}
  V -- нет --> E2[Rejected]
  V -- да --> L{Квота есть}
  L -- нет --> E3[RateLimited]
  L -- да --> O[Очередь по бумаге:<br>ждет прежние команды]
  O --> M[cid помечен использованным<br>повтор cid: Rejected]
  M --> S[Отправка брокеру]
  S --> R{Ответ}
  R -- принял --> OK[OrderAck]
  R -- отказал --> RJ[Rejected]
  R -- нет ответа --> U[Unknown<br>без повтора]

Что из этого следует:

  • Все, что выше «Отправки», до брокера не доходит. Заявки точно нет, а при отказе на первых трех проверках cid не потрачен: после устранения причины его можно использовать снова. cid помечается только после очереди, поэтому пока вызов ждет в очереди, номер еще чистый.
  • Unknown не повторяется никогда. Заявка без ответа могла встать, повтор дал бы две заявки. Бот выясняет судьбу через order_state(cid): Если что-то пошло не так.
  • Очередь по бумаге: отмена не обгонит постановку той же бумаги. Разные бумаги идут параллельно. Отмена тратит отдельное ведро квоты, поэтому поток постановок ее не вытесняет (Лимиты запросов).

Исходы, коды отказов и порядок проверок подробно: Круг команд.

Круг жизни: шлюз торговли

У каждого UD есть шлюз торговых команд. Чтение и подписки работают всегда, закрывается только торговля. На схеме: три состояния шлюза и переходы между ними.

stateDiagram-v2
  state "Не сверен<br>NotReconciled" as NR
  state "Торговля открыта" as OPEN
  state "Перегрузка<br>Degraded" as DEG
  state "Остановка<br>ShuttingDown" as SD
  [*] --> NR: start
  NR --> OPEN: reconcile успешен
  OPEN --> DEG: Overflow в Lossless-буфере<br>или Reconnected потока заявок
  DEG --> OPEN: бот дочитал канал и reconcile
  OPEN --> SD: shutdown
  NR --> SD: shutdown
  DEG --> SD: shutdown
  SD --> [*]

Из обоих закрытых состояний выход один - сверка. Это и есть ответ на вопрос «можно ли торговать»: только когда список заявок бота совпал со счетом и в данных о своих заявках нет дыры. Остановка необратима: коннектор закрывается, торговать дальше нельзя.

Сверка, ее результат и причины порядка действий: Круг жизни. Что делать по шагам: Запуск, рестарт и остановка бота.

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

brokapi by RunInfra