БКС
Крейты: brokapi-bcs-protocol (REST и WebSocket клиент) и brokapi-bcs (коннекторы).
Вход
| Что | Как |
|---|---|
| Токен | refresh-токен БКС (BCS_REFRESH_TOKEN). Из него коннектор получает токен доступа. Счет определяет сам токен: account_id в настройке нет |
| Чтение и запись | токены бывают на чтение и на запись. Для торговли нужен токен на запись и BCS_IS_WRITE=true (в коде BcsConfig::with_write(true)). Без этого брокер торговые команды не примет |
| Песочница | нет. Торговля идет на настоящем счете |
| Обновление | токен доступа обновляется заранее и после ответа 401 на чтение (чтение повторяется один раз). Команда после 401 не повторяется. Подробно: Токены |
| Лимиты | brokapi::bcs::limits::default_limits(): встроенный файл limits.toml крейта, как у Т-Банка и Финама. Свой файл: limits_from_toml_str и limits_from_toml_path. Числа из таблицы ограничений брокера (10 запросов в секунду на сервис, 3 для неторговых операций, 200000 заявок в сутки) |
Где взять токен
Токен выпускает человек, чей это счет (правило 0). Нужен хотя бы один брокерский счет в БКС Мир инвестиций. Шаги по документации БКС:
- Войти в веб-версию БКС Мир инвестиций (
lk.bcs.ru). - Открыть «Профиль», под заголовком «Счета и тарифы» нажать на брокерский счет, к которому нужен доступ.
- В настройках счета выбрать «Токены API», нажать «Выпустить токен» и следовать инструкциям.
- Скопировать токен и сохранить сразу: брокер показывает его один раз.
Какой токен выбрать. Токены бывают двух видов: «для торговли и чтения данных» и «только для чтения». Для первого запуска возьмите «только для чтения»:
им работают портфель, стакан и свечи, а заявку брокер не примет. Токен для торговли нужен, когда человек решил поставить первую боевую заявку, и тогда же
включается BCS_IS_WRITE=true.
Сколько живет и как отозвать. Refresh-токен действует 90 суток, токен доступа из него 24 часа (его получает коннектор). Токен привязан к одному счету, одновременно действующих токенов можно выпустить сколько угодно. Удалить токен можно в тех же «Токенах API». После удаления токена для торговли его токен доступа перестает работать сразу, у токена «только для чтения» может работать до 24 часов. Номер счета в настройках не нужен: счет определяет токен.
Идентификатор инструмента
NativeId::TickerClass { ticker, class_code }, например SBER и TQBR. Ключ библиотеки - SBER@TQBR. Любой другой
вид идентификатора даст ошибку настройки (Config): справочник для БКС не нужен, идентификатор собирается сам без
сети. Расписание торгов у БКС по инструменту: в schedule передается "SBER@TQBR", иначе Config.
Единицы и стакан
| Что | Как |
|---|---|
| Числа | цены, количества и суммы читаются из JSON в Decimal по тексту ответа, 244.30 остается 244.30. |
| Количество | штуки, целое не меньше 1: в документации БКС orderQuantity это «Количество в заявке (шт.)». Коннектор передает число как есть. Дробное, нулевое и отрицательное это Rejected { invalid_qty } до отправки, ничего не округляется. Кратность лоту проверяется, если лот передан в price_grid(). Лот и шаг цены коннектор сам не знает: price_grid().learn(¶ms). Что orderQuantity в штуках, сказано в документации БКС |
| Глубина стакана | до 20. Больший запрос молча режется до 20 |
| Стакан | полный снимок. Поток идет по WebSocket, снимок по REST |
| Котировки | отдельный поток WebSocket |
| Свечи | таймфреймы M1 M5 M15 M30 H1 H4 D W MN. D1 и W1 библиотека шлет как D и W. 10-минуток нет, Unsupported |
| Лента сделок | last_trades отдает сделки текущей торговой сессии, оставляя новейшие limit штук |
Как устроены команды
БКС работает синхронными методами: POST /orders/sync, /orders/cancel/sync, /orders/edit/sync. Они ждут
решения брокера и отвечают состоянием заявки. Из этого следует:
- Отказ известен сразу как
Rejected, а не через минуту из потока. Отказ может прийти кодом HTTP 4xx или внутри ответа 200 со статусом заявки 8 и причиной. Оба случая даютRejected. - В
OrderAckприходит настоящий номер брокера (orderId), тот же, что в списке заявок и в потоке. - Отмена объявляется успешной, только если брокер ответил, что заявка отменена (статус 4). Заявка исполнилась раньше -
Rejected { already_filled }. Ответ оставил заявку живой -Unknown. Статус 6 «Отменяется (в процессе отмены)» это не отмена: заявка еще в стакане и может исполниться. В потоке и в ответах он приходит как состояниеUnknown(неCancelled),cancel()возвращаетUnknown, сверка считает такую заявку живой.Cancelledпридет со статусом 4,Filled- если заявка исполнилась. - Прежние формы запросов (с номером заявки в пути) коннектор не вызывает. Они остались в
brokapi-bcs-protocolи доступны черезraw().
Как cid уходит к брокеру
В поле clientOrderId как UUID с дефисами. Обратимо. В списке активных заявок БКС поля clientOrderId нет, только
номер брокера. Поэтому active_orders и reconcile определяют cid так: по тому, что процесс видел сам, иначе
одним запросом на каждую неизвестную заявку (по ее номеру), иначе дают заявке подставной cid. Если на счете много
чужих заявок, сверка тратит много запросов, учитывайте квоту.
Порядок как у Т-Банка и Финама: шлюз команд, проверка входа, квота, очередь по бумаге, шлюз еще раз и только потом cid помечается использованным.
Поэтому после RateLimited (и после ошибки входа вроде missing_price) тот же cid годен: заявка никуда не ушла. То же для
place_stop. Общее правило: Круг команд.
Сообщение канала orders/events/ws несет два идентификатора: clientOrderId (по документации идентификатор заявки, в примере
для исполнения он пустой) и originalClientOrderId (идентификатор измененной заявки). cid берется так: если один из них
не UUID, то другой. Если оба UUID и сообщение об отмене, замене или отмене в процессе (статусы 4, 5, 6), то originalClientOrderId,
иначе clientOrderId. Так сообщение об отмене, где clientOrderId это номер запроса на отмену, попадает на ту заявку, которую
отменили, а не на чужой cid. Какой идентификатор в каком поле приходит для каждого вида сообщения, взято из документации БКС. Исправление (executionType 13) не создает новое исполнение: оно пересказывает прежнее сообщение, настоящую
поправку сделок дает история (executions). Ошибка подписки на рыночные данные не содержит подписки, только код и текст: она
валит ту подписку, чей тикер назван в тексте, а если подписка одна в ожидании, то ее. Если по тексту не понять, ошибка валит
все ожидающие.
Изменение заявки
Есть: POST /orders/edit/sync, брокер снимает исходную заявку и ставит новую под новым cid. Количество и цена,
которых вы не указали, берутся от исходной (один запрос). Если заявка исполнена частично, количество нужно указать
явно, иначе Rejected { qty_required }.
Свои сделки, портфель
| Что | Как |
|---|---|
| Поток исполнений | из единого канала статусов orders/events/ws: сообщения с lastQuantity > 0, каждый executionId один раз. Режим Lossless. Цена исполнения считается по приросту накопленных величин: сообщения несут итоги по заявке (executedQuantity, executionValue, averagePrice), а не числа одного исполнения. Цена = (стоимость после минус стоимость до) / (количество после минус количество до) между двумя сообщениями той же заявки. Два исполнения по разным ценам дают две разные цены, не среднюю. Если предыдущего сообщения коннектор не видел (подключился посреди заявки), цена это averagePrice заявки: верно для одного исполнения, приблизительно для нескольких. Точная цена каждой сделки в истории (executions) |
| История | POST /trades/search, все страницы: по 100 записей (size = 100), пока не прочитан totalPages, каждая страница тратит квоту SearchTrades. Список БКС содержит не больше 1000 последних сделок: если ответ говорит о 1000 и больше (totalRecords), это Rejected { range_too_long }, а не список, который выглядит полным. Запись плоская, без cid и без комиссии (cid пуст, комиссия 0) |
| Портфель | снимок REST. Поток по WebSocket. Деньги и позиции берутся из одного ответа |
| Список активных заявок | POST /orders/search, только заявки с 26 января 2026 года и не больше 1000 последних (ограничение документации) |
Стоп-заявки
Условные заявки (orderType = 5). Принимается только срок «до отмены» (срок жизни условной заявки документация
не описывает), поле срока не отправляется. Направление срабатывания выбирает брокер. Пара «стоп-лосс плюс
тейк-профит» не создается. Потока статусов нет. Подробно: Стоп-заявки.
Чего БКС не поддерживает или делает иначе
| Что | Как |
|---|---|
| Свечи 10 минут | Unsupported("candles_m10") |
Справочник по виду: опционы и Other | Unsupported. Остальные виды работают, список читается целиком: страницами по 100, пока страница полная. Каждая страница тратит квоту, а список длиннее 200 страниц - ошибка, не обрезок |
| Поток стоп-заявок | Unsupported |
| Песочница | нет |
| Кадры между WebSocket и разбором | между сессией WebSocket и задачей разбора стоит канал на 512 кадров. Если он заполнится (задача разбора встала), кадр отбрасывается со счетчиком и записью в лог, без Overflow. |
| Перечитывание заявок после обрыва | нет. Но обрыв сокета orders/events/ws виден боту: Event::Reconnected на подписки заявок и исполнений и закрытый шлюз до reconcile (Круг событий) |
Лимиты
crates/bcs/limits.toml, по таблице «Ограничения» из документации брокера: у каждого сервиса свое ведро на 10 запросов
в секунду (портфель, лимиты, справочник, заявки, статус заявки, сделки, дисконты, рыночные данные), у неторговых операций
(история операций) 3 в секунду, у заявок еще суточное ведро на 200000. Заявка, отмена, замена, стоп-заявка и список заявок
тратят общее ведро сервиса заявок. До 4 соединений (канал заявок). Всплеск 10, то есть одна секунда. Постановка, замена и стоп-заявка проходят еще через ведро place на 8 в секунду
(всплеск 4): это не лимит брокера, а запас, чтобы поток заявок не занял весь бюджет сервиса и отмене осталось место. Как БКС считает секунду (фиксированное окно или скользящее) и сутки (календарные или 24 часа), в документации не сказано:
если упретесь в RateLimited раньше времени, уменьшите значения в limits.toml. Суточное ведро считает только этот процесс: на одном счете из двух процессов оно не видит общего числа.
Что учесть
- Количество в уровнях стакана и ленты в документации БКС не названо. Для расчета объема сверяйтесь с количеством в своей заявке.
- Если на запрос заявки по неизвестному
cidБКС ответит неNOT_FOUND, сверка отнесет заявку к неопределенным. Проверьте это на минимальной заявке. - Суточный лимит на заявки считает только ваш процесс. Два процесса на одном счете его не видят совместно.
Листать можно клавишами Влево и Вправо.