Карта: сущности и три круга
Эта страница - карта библиотеки: из чего она состоит, по каким кругам ходят данные и команды и на каких правилах это держится. Подробности каждого круга лежат на своей странице, здесь только то, что нужно, чтобы не потеряться. Термины в таблице ниже - словарь всей книги: везде они значат ровно это.
Сущности и связи
Бот сам поднимает коннекторы, глобального реестра в библиотеке нет. Коннекторов два вида: 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 --> [*]
Из обоих закрытых состояний выход один - сверка. Это и есть ответ на вопрос «можно ли торговать»: только когда список заявок бота совпал со счетом и в данных о своих заявках нет дыры. Остановка необратима: коннектор закрывается, торговать дальше нельзя.
Сверка, ее результат и причины порядка действий: Круг жизни. Что делать по шагам: Запуск, рестарт и остановка бота.
Листать можно клавишами Влево и Вправо.