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

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

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

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

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

Любой метод брокера, Python

Инструкция. Общий интерфейс закрывает то, что умеют все брокеры. Но у каждого брокера есть много своего: купоны и дивиденды, отчеты, избранное, корпоративные действия, свежие методы. Чтобы они не оказались закрыты, у клиента есть await client.raw(...). Он вызывает любой метод API брокера с теми же токеном, адресом и счетчиком лимитов, что у самого клиента. Общая часть, одна для Rust и Python: Любой метод брокера: raw. Там примеры на Rust и правила квоты. Здесь то, как это выглядит в Python. Код ниже - куски программы raw.py, она запускается тестом против имитаций.

reply = await client.raw(method, args=None, *, command=False)   # reply: dict (или list, как ответит брокер)

В Rust метод вызывается замыканием над типизированным клиентом. В Python замыкания нет, поэтому метод называют строкой, а аргументы передают словарем. Ответ приходит словарем.

Как называть метод и что передавать

Брокерmethodargs
Т-Банкимя метода из описания API: "GetLastPrices", "GetUserTariff". Если у двух сервисов одно имя, "UsersService/GetAccounts"сообщение запроса словарем, имена полей как в .proto (snake_case, camelCase тоже принимается)
Финамто же: "LastQuote", "Clock"то же
БКСимя из файла лимитов: "GetQuotes", "GetPortfolio", "GetOperationHistory", или "GET /путь", "POST /путь" для любого другого методаquery (параметры URL), body (JSON для POST), для GetOrderStatus еще path ({"id": ...})
АЛОРглагол и путь из описания API: "GET /md/v2/time", "GET /md/v2/Clients/MOEX/D12345/summary", "POST /commandapi/...". Глаголы GET, POST, PUT, DELETE. Каждый вызов тратит квоту по умолчаниюquery (параметры URL), body (JSON), request_id (X-REQID команды: если не дать, коннектор сделает новый)
Bybitглагол и путь из описания API V5: "GET /v5/market/time", "GET /v5/account/wallet-balance", "POST /v5/position/set-leverage". Глаголы GET и POST. Пути /v5/market/... читаются без подписи, остальные подписываются ключомquery (параметры GET), body (JSON для POST)

Т-Банк и Финам говорят по gRPC. Описание всех сервисов и сообщений вшито в библиотеку, метод ищется в нем по имени, словарь становится сообщением запроса, а ответ снова словарем. Поэтому годится любой метод API, в том числе тот, которого нет в общем интерфейсе. Формат как у протокола брокера: 64-разрядные числа записаны строками ("units": "274"), время - строка RFC 3339, деньги Т-Банка - {"units": "274", "nano": 50000000}, число Финама - {"value": "100.05"}. Имена полей в ответе - как в .proto. Лишнее поле в запросе дает ошибку, а не молча пропадает.

БКС говорит по REST. Методы, которые библиотека знает по имени, лежат в таблице (глагол и путь), остальные вызываются как "GET /путь" или "POST /путь". Числа с дробной частью в ответе БКС приходят как Decimal, float в Python не попадает нигде.

async def tbank_prices(client: brokapi.Client) -> dict[str, str]:
    """T-Bank (gRPC): the method is named as in the API description, the arguments are the request message."""
    reply = await client.raw("GetLastPrices", {"instrument_id": ["SBER_TQBR", "GAZP_TQBR"]})
    # int64 is text, money is {"units": "274", "nano": 50000000}: the wire form of the broker.
    return {p["ticker"]: f'{p["price"]["units"]}.{p["price"]["nano"]:09d}' for p in reply["last_prices"]}
async def bcs_quotes(client: brokapi.Client) -> dict[str, Any]:
    """BCS (REST): a name from its rate limit file, `query` for the URL and `body` for the JSON of a POST."""
    reply = await client.raw("GetQuotes", {"body": {"instruments": [{"ticker": "SBER", "classCode": "TQBR"}]}})
    return {record["ticker"]: record["last"] for record in reply["records"]}  # prices are Decimal
async def finam_quote(client: brokapi.Client) -> str:
    """Finam (gRPC): the same as T-Bank. `google.type.Decimal` is {"value": "100.05"}."""
    reply = await client.raw("LastQuote", {"symbol": "SBER@MISX"})
    return reply["quote"]["last"]["value"]

АЛОР говорит по REST, как БКС, но методы называются глаголом и путем из описания API брокера: "GET /md/v2/time". Числа с дробной частью приходят как Decimal. Команде заявки передают request_id: брокер отвечает на повторный идентификатор копией первого ответа и второй заявки не создает (по его документации).

async def alor_summary(client: brokapi.Client) -> Any:
    """ALOR (REST): the verb and the path of the API description. `query` is for the URL, `body` for a POST or PUT."""
    reply = await client.raw("GET /md/v2/Clients/MOEX/D00001/summary")  # your own portfolio in the path
    return reply["portfolioEvaluation"]  # prices are Decimal

В аргументах raw Decimal уходит текстом, UUID текстом, datetime с поясом строкой RFC 3339, float отвергается (TypeError).

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

  1. Берется квота. Имя метода ищется в файле лимитов брокера. Нет такого имени - тратится общее ведро (маршрут "*"). Бесплатно не уходит ни один вызов. Квоты нет - вызов не отправляется, и это RateLimited с retry_after. Счетчик общий с клиентом и с клиентами на том же limiter=: raw и portfolio() тратят одно ведро, если у них один маршрут.
  2. Идет вызов с токеном клиента и его таймаутом.
  3. Ошибка брокера переводится в те же исключения, что везде (Ошибки).
async def raw_read(client: brokapi.Client, method: str, args: dict[str, Any], tries: int = 3) -> Any:
    """A read through `raw` spends the same quota as every other call: when it is spent, wait and ask again."""
    for _ in range(tries - 1):
        try:
            return await client.raw(method, args)
        except brokapi.RateLimited as error:
            await asyncio.sleep(error.retry_after.total_seconds())
    return await client.raw(method, args)

Чтение и команда

  • command=False (по умолчанию) - для чтения. Сетевой сбой и таймаут - Transport, вызов можно повторить.
  • command=True - для всего, что меняет счет. Нет ответа (таймаут, обрыв, ошибка сервера) - Unknown: брокер мог выполнить команду. Библиотека такой вызов не повторяет. Как и place, команда доходит до конца, даже если вы перестали ее ждать (отмена задачи, таймаут).

Если сомневаетесь, меняет ли метод счет, ставьте command=True.

async def raw_command(client: brokapi.Client, method: str, args: dict[str, Any]) -> Any:
    """Anything that changes the account goes with command=True: a lost answer is Unknown and it is never repeated."""
    try:
        return await client.raw(method, args, command=True)
    except brokapi.Unknown:
        raise  # ask the broker what became of it (a report, a list): do not call this again

Чего raw не делает

raw обходит гарантии общего интерфейса.

  • Он не знает про заявки клиента. Заявка, поставленная через raw, не ведется коннектором как своя: ни учета «в пути», ни шлюза reconcile, ни проверок (шаг цены, лот, цена у стоп-лимита). Заявки ставьте place и place_stop. Через raw вызывайте то, чего в общем интерфейсе нет.
  • Он не добавляет счет в ваш запрос: account_id кладите сами.
  • Потоки не отдаются. Метод, который открывает поток (server-streaming), дает Unsupported с capability == "raw_streaming". В Rust квота покрывает открытие, а поток читает ваш код. Python так передать не может. Потоки читайте общим интерфейсом (subscribe_*).
  • Неизвестный метод и аргументы, которые не подходят методу, дают Config. Ничего не отправлено, квота не потрачена.
  • У БКС и АЛОРа WebSocket-каналы через raw не открываются: это есть в Rust (connect_ws, у АЛОРа еще connect_cws), в Python нет.

Как проверить

Для этой команды нужен клон репозитория (git clone https://github.com/runinfra/brokapi, затем pip install brokapi pytest pytest-asyncio): тесты и примеры в пакет pip install brokapi не входят.

pytest crates/python/tests/test_raw.py

На имитации всех брокеров: чтение возвращает ответ словарем, неизвестный метод и лишнее поле отвергаются без отправки, потоковый метод отвергается, квота тратится общая с интерфейсом и с соседним клиентом на том же limiter, команда с отказом брокера не становится Unknown.

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

brokapi by RunInfra