Финам
Крейты: brokapi-finam-protocol (gRPC клиент по официальным .proto и менеджер токенов) и brokapi-finam (коннекторы).
Песочницы у Trade API нет: любая проверка идет на настоящем счете, см. Перед боевым счетом.
Вход
| Что | Как |
|---|---|
| Секрет | секрет Trade API (FINAM_SECRET). По нему коннектор получает JWT |
| Счет | account_id обязателен для UD (FINAM_ACCOUNT_ID). При старте проверяется, что счет принадлежит секрету (список счетов приходит вместе с токеном). Нет в списке - Config |
Загрузка из .env | api_config_from_env() сам .env не читает, загрузите его заранее (dotenvy::dotenv()) |
| Адрес | по умолчанию боевой, можно подменить FINAM_API_URL (только https://, адрес имитации задается в коде) |
| Заголовок | JWT передается как есть. Официальные SDK шлют так, часть примеров шлет с Bearer: если сервер откажет, включите FinamApiConfig::bearer. |
| Лимиты | brokapi::finam::limits::default_limits(), один счетчик на пользователя |
| Обновление токена | заранее и в фоне, подробно в Токенах. Один JWT можно отдать нескольким коннекторам (shared_tokens) |
Где взять секрет и номер счета
Секрет выпускает человек, у которого счет в Финаме (правило 0). Секрет Trade API и номер счета берутся в кабинете Финама, в разделе про Trade API (токены или ключи доступа). Пошагового описания кабинета в документации Trade API, на которую мы опираемся, нет, поэтому порядок кнопок здесь не приводится: ищите раздел API в кабинете. Что известно из самой документации:
- Секрет обменивается на короткоживущий JWT (метод
Auth), коннектор делает это сам. МетодTokenDetailsпоказывает срок токена, доступ к рыночным данным и список счетов секрета. Номер счета дляFINAM_ACCOUNT_IDберется из этого списка, в примерах документации он выглядит какA12345. Счет не из списка -Configпри старте. - В
TokenDetailsесть признакreadonly: сессия и счета секрета помечаются «только чтение». Если кабинет позволяет выпустить такой секрет, для первого запуска берите его: портфель, стакан и свечи работают, заявку брокер не примет. Секрет с правом торговли нужен, когда человек решил поставить первую боевую заявку. - Песочницы нет, поэтому после чтения идет первый день на боевом счете.
Первый день на Финаме
У Финама нет песочницы, и любая проверка идет на настоящем счете. План на первый день, от безопасного к боевому. Каждый шаг начинается, когда прошел предыдущий, а шаги 3 и 4 - только со слова владельца счета.
- Имитация. Весь код сначала проходит на
brokapi.testing.mock_finam(): быстрый старт на Python, тамfirst_orderработает с клиентом любого брокера. - Чтение боевого счета.
FINAM_SECRET(лучше только для чтения) иFINAM_ACCOUNT_ID, затемconnect("finam"),portfolio(),order_book(...). Неверный секрет или чужой счет видны наconnect(Config). Заявок на этом шаге нет. - Минимальная заявка далеко от рынка и отмена. Один лот (
trading_params(...).lot_size), цена в 90% от лучшей покупки, округленная вниз до шага. Если биржа отвергла цену как выходящую из допустимого коридора, подвиньте ее ближе к рынку на 5% и повторите с новымcid. - Проверка, что комментарий вернулся. Библиотека кладет полный
cidв комментарий заявки, а Финам должен вернуть его без изменений (Какcidуходит к брокеру). От этого зависитreconcile: если комментарий искажен, ваши заявки придут как чужие, а бот не узнает свою заявку после рестарта. Проверка: поставить заявку, закрыть клиента (заявка остается на бирже), открыть нового и сверить его с журналом из одногоcid. Заявка должна оказаться вconfirmed, аforeignостаться пустым.
async def first_day(connect: Callable[[], Awaitable[brokapi.Client]]) -> bool:
"""Places one lot far below the market, 'restarts' the bot and checks that the broker gave the order back as ours.
`connect` makes a client of the account. Returns True when the comment of the order came back and the order was
cancelled. Anything else is a reason to stop: do not trade on this account until it is clear."""
first = await connect()
sber = await first.instrument("SBER", "MISX")
params = await first.trading_params(sber)
book = await first.order_book(sber, depth=1)
price = brokapi.round_to_step(book.bids[0].price * Decimal("0.9"), params.price_step, brokapi.RoundMode.DOWN)
await first.reconcile([])
cid = brokapi.new_cid() # in a real bot it is in the journal before the next line
await first.place(cid=cid, instrument=sber, side=brokapi.Side.BUY, qty=params.lot_size, price=price)
await first.close() # the process "dies": the order stays at the exchange
# The second run knows only the journal: the cid. If Finam gave the comment back untouched, the order is recognised.
second = await connect()
try:
report = await second.reconcile([cid])
recognised = [order.cid for order in report.confirmed] == [cid]
print(f"the order {'came back as ours' if recognised else 'was NOT recognised'}: {len(report.foreign)} foreign")
for order in report.confirmed:
await second.cancel(order.cid)
print(f"orders left after the cancel: {len(await second.active_orders())}")
return recognised and not report.foreign
finally:
await second.close()
Программа печатает the order came back as ours: 0 foreign и orders left after the cancel: 0. Любой другой результат - повод остановиться и не торговать на этом
счете, пока причина не ясна: сообщите о нем через «Сообщить об ошибке» со строкой вывода (без секрета).
Заявка у Финама всегда «на день», replace нет. Если заявка осталась на счете после остановки бота, ее снимает бот: Аварийная остановка бота.
Идентификатор инструмента
NativeId::SymbolMic, строка SBER@MISX (тикер и код биржи MIC). Ключ библиотеки тот же. Построить идентификатор
без сети: brokapi::finam::util::instrument_from_symbol("SBER@MISX"). Любой другой вид идентификатора - Config.
Единицы и стакан
| Что | Как |
|---|---|
| Количество | штуки (не лоты), так отдает API. Кратность лоту проверяется на нашей стороне, если лот передан в price_grid(): некратное количество - Rejected { invalid_qty } до отправки. |
| Лот для торговли | trading_params: лот из GetAsset (lot_size). Если у токена есть счет, вторым запросом GetAssetParams (по первому счету токена) берется trade_lot_size, «размер лота для торговых операций». Он больше нуля - заменяет lot_size и min_qty. Ноль («значения нет»), отказ брокера на этот вызов - остается лот из GetAsset. Квота: два запроса справочника вместо одного. |
| Глубина стакана | до 50. Финам глубину не документирует: сервер присылает, сколько сочтет нужным, а коннектор режет до запрошенной (по умолчанию и максимум 50) |
| Стакан | Финам присылает строки с действиями «добавить, обновить, удалить» и признаком снимка. Коннектор ведет локальный стакан и всегда отдает боту полный снимок. Поэтому Conflate безопасен |
| Разрыв стакана | приращение без базового снимка: Resync { SequenceGap } и немедленное переоткрытие потока. Поток переоткрыт после обрыва: Resync { SubscriptionReset } |
| Котировка | нулевая покупка или продажа - None |
| Сделки рынка | повторы после переподключения отсекаются по номеру сделки. Сделка без стороны отбрасывается |
| Свечи | M1 M5 M15 M30 H1 H4 D1 W1 MN. 10 минут у Финама нет, коннектор собирает их из 5-минуток: история склейкой пар по границам 10 минут, поток по подписке на M5. Неполная пара отдается как есть |
| Статус торгов | вычисляется из расписания (прямого вызова нет) |
| Расписание | по инструменту: в exchange нужно передать SYMBOL@MIC, иначе Config |
Как cid уходит к брокеру
Поле Финама client_order_id вмещает 20 символов, UUID (128 бит) туда не помещается. Поэтому:
- полный номер кладется в комментарий заявки (до 128 символов) в виде
bc1:и 32 символа. Финам возвращает заявку целиком в состоянии и в сделках, и номер читается оттуда. - в
client_order_idидет метка из 20 символов, полученная из UUID. Обратно в UUID она не превращается, это запасной ключ на случай, если комментарий в ответе пропал.
На старте коннектор проверяет, что оба формата вмещаются в поля брокера: иначе это ошибка настройки, а не поломка на
ходу. Заявка, поставленная не коннектором (из приложения), получает подставной стабильный cid, и такие заявки
попадают в foreign.
Механизм держится на том, что Финам возвращает комментарий заявки без изменений, как сказано в его документации. Если комментарий придет искаженным, сверка не узнает ваши заявки: проверьте это на минимальной заявке до боевого счета.
Ответы брокера
| Ответ | Результат |
|---|---|
RESOURCE_EXHAUSTED | RateLimited (ждать предлагается 1 секунду, брокер срок не называет) |
UNAUTHENTICATED и отказанный вход по секрету | Config (как у остальных брокеров: исправить секрет, повтор не поможет) |
NOT_FOUND | Rejected с кодом not_found |
INVALID_ARGUMENT, PERMISSION_DENIED, FAILED_PRECONDITION и подобные | Rejected |
Таймаут, UNAVAILABLE, INTERNAL и все остальное на команде | Unknown |
| Статус «отклонено», «ошибка», «отказ брокера» внутри ответа | Rejected с описанием |
Чтения один раз повторяются после обновления JWT. Команды не повторяются никогда, в том числе после UNAUTHENTICATED: это Config, и новую попытку бот делает сам с новым cid. Обновление JWT при этом запускается, следующий вызов встретит свежий токен.
Заявки
- Срок обычной заявки всегда «на день» (
TIME_IN_FORCE_DAY). - Изменение заявки отсутствует.
replaceвозвращаетUnsupported("replace_order")сразу и без сети (caps().supports_replace = false). Отмена и новая заявка не атомарны, поэтому библиотека не подменяет одно другим. - Отмена и чтение заявки идут по номеру брокера. Номер коннектор запоминает после постановки, иначе ищет по списку заявок счета.
- Статусы брокера переводятся в общие: новая и ожидающие -
Acked, частично исполненная -PartiallyFilled, исполненная -Filled, снятая и истекшая -Cancelled, отклоненная -Rejected. Неизвестный статус считается рабочей заявкой.
Свои сделки, портфель
| Что | Как |
|---|---|
| Поток исполнений | SubscribeTrades, режим Lossless. cid у исполнения чужой заявки (поставленной руками, не этим процессом) остается None: придуманный cid из потока заявок исполнению не передается |
| История | Trades страницами: один ответ вмещает не больше executions_limit сделок (по умолчанию 1000). Полный ответ значит, что период может содержать больше: коннектор спрашивает остаток периода (от времени последней сделки в сторону, куда отсортирован ответ), склеивает ответы и берет каждую сделку один раз по trade_id. Каждая страница тратит квоту Trades. Если период не помещается (больше сделок с одной временной меткой, чем executions_limit, или больше 100 страниц), это Rejected { range_too_long }, а не обрезанный список. Постраничное чтение работает при любой сортировке ответа. cid читается из комментария |
| Портфель | GetAccount. Позиции включают теоретические: брокер добавляет к открытым позициям те, что создали бы активные заявки, и отличить их нельзя (в ответе нет признака). Покупка на 10 штук, которых нет на счете, может показаться позицией в 10 штук. Объем следующей заявки по этому списку при работающих заявках не считайте, берите позицию из своих исполнений. Деньги по валютам, позиции со знаком. Блокированных денег Финам по валютам не отдает (blocked = 0). Поток SubscribeAccount, каждое сообщение - полное состояние |
| Заявки в сверке | список заявок счета, стоп-заявки отфильтрованы. Из потока обычных заявок они тоже отфильтрованы |
Стоп-заявки
Стоп-лосс и стоп-лимит - обычной заявкой типа стоп (PlaceOrder). Тейк-профит - отдельным методом PlaceSLTPOrder.
Особенности: у тейк-профита нельзя задать цену после срабатывания (там защитный спред, вернется
Rejected { unsupported_limit_price }), срок «до даты» только у тейк-профита, стоп-заявки не показываются в
active_orders и reconcile, cid читается из комментария. Если у чужой SL/TP-заявки единица объема - процент от позиции (sl_qty_measure или tp_qty_measure равны PERCENT), количество в штуках неизвестно и в StopOrderSnapshot::qty стоит 0, а не процент. Свои тейк-профиты коннектор ставит в штуках. Подробно: Стоп-заявки.
Подписки и потоки
Один поток gRPC на подписку, поверх одного соединения HTTP/2 на коннектор. После обрыва поток переоткрывается с
паузой от 0,5 до 30 секунд. Подписка, которую сервер отверг (нет прав, нет инструмента), помечается Failed и
пробуется снова примерно раз в 30 секунд. Открытие потока тратит квоту, как любой запрос. sub_status(sub) показывает
состояние подписки (Connecting, Active, Reconnecting, Failed, Erroring). Когда поток заявок или поток сделок был открыт, оборвался и открылся снова, бот получает Event::Reconnected на эту подписку, и шлюз закрывается до reconcile. Поток счета (портфель) такого события не дает: каждое его сообщение целое состояние.
Чего Финам не поддерживает или делает иначе
| Что | Как |
|---|---|
| Изменение заявки | нет в API, Unsupported |
| Свечи 10 минут | нет у брокера, собираются из 5-минуток |
| Поток стоп-заявок | отдельного нет, Unsupported |
| Песочница | нет |
| Тейк-профит с ценой после срабатывания | нет, защитный спред |
| Числа | google.type.Decimal разбирается из строки как есть, уходит строкой с теми же знаками. Шаг цены - min_step и decimals целыми, без деления с плавающей точкой |
| Стоимость шага цены | не отдается: price_step_value равен шагу цены (верно для акций, не для фьючерсов) |
| Заблокированные деньги по валютам | не отдаются |
Лимиты
Единственная цифра, которую публикует Финам: 200 запросов в минуту. Отдельных лимитов на заявки и на потоки нет. Область
лимита (на секрет, пользователя или счет) брокер словами не называет. Считаем, что на пользователя, поэтому счетчик один
на программу. Значения в crates/finam/limits.toml заниженные, отмена вынесена в отдельное ведро. Если упретесь в RateLimited раньше времени, уменьшите значения.
Что учесть
- Комментарий заявки: Финам должен возвращать его без изменений (
GetOrders,GetOrder,SubscribeOrders, сделки). От этого зависитreconcile. - Повтор
client_order_id: что сделает Финам, в документации не сказано. Коннектор повторов не делает. - Заголовок JWT уходит как есть. Если сервер откажет, включите
FinamApiConfig::bearer.
Листать можно клавишами Влево и Вправо.