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

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

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

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

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

Лимиты запросов

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

Как устроен счетчик

Счетчик (BrokerLimiter) - это набор «ведер» (token bucket). У каждого ведра три параметра: скорость пополнения (rate запросов за per) и размер (burst). Ведро стартует полным и пополняется равномерно, а не шагами. Один запрос тратит одну единицу. Тихий клиент может отправить burst запросов сразу, занятой удерживается на rate.

Каждый метод API привязан к одному или нескольким ведрам (маршрут). Например, у БКС и Финама:

PlaceOrder  = ["global", "orders"]
CancelOrder = ["global", "cancel"]

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

Что происходит при вызове

  1. Коннектор берет из ведер маршрута по одной единице. Если одного ведра не хватило, уже взятые единицы возвращаются: частично потраченная квота не пропадает.
  2. Ведер хватило - запрос уходит к брокеру.
  3. Не хватило - вызов сразу возвращает 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.

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

brokapi by RunInfra