Лимиты запросов
Брокеры ограничивают число запросов и на превышение отвечают отказом. Чтобы не получать отказы в самый неудобный момент, библиотека считает запросы сама и до отправки: если квоты нет, запрос не уходит в сеть вообще. Счетчик один на ту область, где брокер считает квоту (Карта), а область у каждого брокера своя: Несколько счетов и брокеров.
Как устроен счетчик
Счетчик (BrokerLimiter) - это набор «ведер» (token bucket). У каждого ведра три параметра: скорость пополнения
(rate запросов за per) и размер (burst). Ведро стартует полным и пополняется равномерно, а не шагами. Один
запрос тратит одну единицу. Тихий клиент может отправить burst запросов сразу, занятой удерживается на rate.
Каждый метод API привязан к одному или нескольким ведрам (маршрут). Например, у БКС и Финама:
PlaceOrder = ["global", "orders"]
CancelOrder = ["global", "cancel"]
Постановка тратит общее ведро global и ведро заявок, отмена - общее и свое. Ведро global общее для всех методов
брокера: оно же защищает от суммарного превышения, а отдельные ведра - от перекоса внутри.
Что происходит при вызове
- Коннектор берет из ведер маршрута по одной единице. Если одного ведра не хватило, уже взятые единицы возвращаются: частично потраченная квота не пропадает.
- Ведер хватило - запрос уходит к брокеру.
- Не хватило - вызов сразу возвращает
RateLimited { retry_after }, в сеть ничего не ушло.
Счетчик никогда не ждет сам, ждать должен бот. Ожидание внутри вызова скрыло бы задержку от бота и держало бы
очередь по бумаге (Круг команд), а решение «подождать, пропустить или отказаться»
принадлежит боту. Что делать с RateLimited: Ошибки и что с ними делать.
retry_after - подсказка, сколько ждать до следующей попытки. Это не обещание, что следующий вызов пройдет: другое
ведро маршрута может пополняться дольше.
Метод без маршрута (например, вызванный через raw(), см. raw) тратит общее ведро брокера (маршрут
"*"). Бесплатных вызовов нет: иначе один неучтенный метод мог бы исчерпать настоящую квоту брокера и вызвать отказы
у всех остальных.
Почему отмена в отдельном ведре
Отмена не должна ждать за потоком постановок: если заявка ушла не туда, снять ее нужно именно тогда, когда бот
ставит много. Поэтому у отмены свое ведро (cancel у БКС и Финама, order_cancel у Т-Банка), а ведро заявок (orders, у Т-Банка
order_post) меньше общего. Поток постановок упирается
в него раньше, чем исчерпает global, и для отмены остается место в обоих ведрах.
У этой защиты есть граница: отмена тоже тратит global. Если исчерпать его чтением (стаканы, справочник, история),
отказ получит и отмена. Поэтому читайте с запасом и не гоните массовые запросы справочника в то время, когда в рынке
стоят ваши заявки.
Свои числа: формат файла
Файл лимитов - TOML: ведра (rate запросов за per, запас burst), маршруты методов к ведрам и квоты соединений. Раздел
называется по брокеру ([bcs], [tbank], [finam], [alor], [bybit]) или как угодно, если читать общей функцией.
[bcs.buckets.orders]
rate = 10 # запросов за период
per = "1s"
burst = 3 # сколько можно потратить сразу
[bcs.route]
"*" = ["orders"] # метод без своего маршрута (см. выше)
PlaceOrder = ["orders"]
[bcs.conns]
max_conns = 4
max_subs_per_conn = 50
supports_multiplex = false
Читают его brokapi::bcs::limits::limits_from_toml_str (и limits_from_toml_path, так же у АЛОРа и Bybit, у Т-Банка brokapi::tbank::limits, у Финама
brokapi::finam::limits::parse_limits), либо общая BrokerLimits::from_toml_str из brokapi::runtime, которая берет единственный
раздел файла. Дальше BrokerLimiter::new(&limits): он возвращает Arc<BrokerLimiter>, делится он через Arc (.clone() у Arc), а не копией.
Полный разбираемый файл, который дает «первая заявка прошла, вторая получила RateLimited». Лишние ключи разбор пропускает: старый файл с dedicated_command_conns читается, ключ ничего не делает (его убрали, потому что ни один коннектор его не читал).
retry_after показывает, сколько ведру нужно до одного токена (при per = "2s" порядка 1,8 секунды сразу после траты):
[bcs.buckets.orders]
rate = 1
per = "2s"
burst = 1
[bcs.route]
"*" = ["orders"]
PlaceOrder = ["orders"]
[bcs.conns]
max_conns = 4
max_subs_per_conn = 50
supports_multiplex = false
Рабочий пример малого бюджета, чтобы поймать RateLimited, - small_budget в examples/research.rs. burst = 0 не пропускает
ничего никогда, а retry_after в этом случае все равно показывает период: ведро не пополнится само.
Чтобы получить «один раз отказали, потом прошло»: burst = 1 и длинный per, первый вызов проходит, второй сразу получает RateLimited.
Откуда числа
Из файлов limits.toml в каталогах крейтов (crates/bcs, crates/tbank, crates/finam, crates/alor, crates/bybit). Каждый файл встроен в свой
крейт и доступен как limits::default_limits() (у всех одинаково). Свои числа можно загрузить из строки или
файла (limits_from_toml_str, limits_from_toml_path, у Финама parse_limits).
Это осторожные значения, и они разной надежности:
- Т-Банк публикует свои лимиты, они взяты из документации (в ней же сказано, что реальные значения динамические и
их возвращает
GetUserTariff). - Финам называет одну цифру на весь API: 200 запросов в минуту. Остальное выбрано ниже этой цифры.
- АЛОР публикует 100 HTTP-запросов в секунду на пользователя, 10 соединений WebSocket, 5000 подписок на соединение и
5000 необработанных сообщений в буфере соединения. Ведро на 100 в секунду для всех методов и мягкое ведро
placeна 60 для постановки, замены и стоп-заявки (запас для отмены). Защиту «социальный рейтинг» (баллы за действия, блокировка логина на 2 часа) брокер описывает без порогов, коннектор их не считает. - БКС публикует таблицу: 10 запросов в секунду на каждый сервис, 3 для неторговых операций (история операций), для
- Bybit публикует лимиты на пользователя и метод (10 запросов в секунду на создание, изменение, отмену и «снять все», 50 на чтения) и на адрес (600 запросов за 5 секунд, сверх этого HTTP 403 и бан адреса на 10 минут). Библиотека держит ведро на каждый вид команды и общее ведро адреса на 100 в секунду. Заголовки остатка лимита (
X-Bapi-Limit-Status) она не читает, счет ведется у нее самой. сервиса заявок еще 200000 запросов в сутки. Ведро на сервис, всплеск 10 (одна секунда), для постановки заявки еще мягкие ворота на 8 в секунду (не лимит брокера: поток заявок не съест весь бюджет сервиса и отмене остается место), суточное ведро считает только то, что отправил этот процесс. Как БКС считает секунду и сутки (окно или скольжение, календарные сутки или 24 часа), в документации не сказано.
Занижать безопаснее, чем завышать: лишнее ожидание стоит секунд, а отказ брокера приходит на боевом счете в произвольный момент.
Квоты соединений
Счетчик хранит и квоты соединений (ConnQuota): сколько соединений и подписок на соединение допускает брокер. Он их
только хранит. Коннектор решает, что с ними делать. Сейчас это использует Финам: число одновременных потоков ограничено,
и при превышении подписка получает Rejected с кодом subscription_quota.
Листать можно клавишами Влево и Вправо.