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

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

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

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

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

Финам

Крейты: brokapi-finam-protocol (gRPC клиент по официальным .proto и менеджер токенов) и brokapi-finam (коннекторы). Песочницы у Trade API нет: любая проверка идет на настоящем счете, см. Перед боевым счетом.

Вход

ЧтоКак
Секретсекрет Trade API (FINAM_SECRET). По нему коннектор получает JWT
Счетaccount_id обязателен для UD (FINAM_ACCOUNT_ID). При старте проверяется, что счет принадлежит секрету (список счетов приходит вместе с токеном). Нет в списке - Config
Загрузка из .envapi_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 - только со слова владельца счета.

  1. Имитация. Весь код сначала проходит на brokapi.testing.mock_finam(): быстрый старт на Python, там first_order работает с клиентом любого брокера.
  2. Чтение боевого счета. FINAM_SECRET (лучше только для чтения) и FINAM_ACCOUNT_ID, затем connect("finam"), portfolio(), order_book(...). Неверный секрет или чужой счет видны на connect (Config). Заявок на этом шаге нет.
  3. Минимальная заявка далеко от рынка и отмена. Один лот (trading_params(...).lot_size), цена в 90% от лучшей покупки, округленная вниз до шага. Если биржа отвергла цену как выходящую из допустимого коридора, подвиньте ее ближе к рынку на 5% и повторите с новым cid.
  4. Проверка, что комментарий вернулся. Библиотека кладет полный 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_EXHAUSTEDRateLimited (ждать предлагается 1 секунду, брокер срок не называет)
UNAUTHENTICATED и отказанный вход по секретуConfig (как у остальных брокеров: исправить секрет, повтор не поможет)
NOT_FOUNDRejected с кодом 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 раньше времени, уменьшите значения.

Что учесть

  1. Комментарий заявки: Финам должен возвращать его без изменений (GetOrders, GetOrder, SubscribeOrders, сделки). От этого зависит reconcile.
  2. Повтор client_order_id: что сделает Финам, в документации не сказано. Коннектор повторов не делает.
  3. Заголовок JWT уходит как есть. Если сервер откажет, включите FinamApiConfig::bearer.

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

brokapi by RunInfra