Любой метод брокера, 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 замыкания нет, поэтому метод называют строкой, а аргументы передают словарем. Ответ приходит словарем.
Как называть метод и что передавать
| Брокер | method | args |
|---|---|---|
| Т-Банк | имя метода из описания 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).
Что происходит при вызове
- Берется квота. Имя метода ищется в файле лимитов брокера. Нет такого имени - тратится общее ведро (маршрут
"*"). Бесплатно не уходит ни один вызов. Квоты нет - вызов не отправляется, и этоRateLimitedсretry_after. Счетчик общий с клиентом и с клиентами на том жеlimiter=:rawиportfolio()тратят одно ведро, если у них один маршрут. - Идет вызов с токеном клиента и его таймаутом.
- Ошибка брокера переводится в те же исключения, что везде (Ошибки).
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.
Листать можно клавишами Влево и Вправо.