Быстрый старт на Python
Цель страницы: за один заход дойти от пустого окружения до первой заявки и не рисковать деньгами по дороге. Ступеней три: сначала имитация брокера в вашем процессе (без токена и без денег), потом песочница Т-Банка, и только потом боевой счет.
Боевой токен выдает человек, и первую боевую заявку ставят только с его слова. ИИ-агент токен с правом торговли сам не создает и флаги записи (
BCS_IS_WRITEи подобные) сам не включает: правило 0. На настоящем счете БКС, Т-Банка, Финама и АЛОРа библиотеку пока не запускали (Bybit проверен вживую), подробности: Перед боевым счетом.
Шаг 1. Получить библиотеку
Нужен Python 3.10 или новее. Пакет brokapi ставится в виртуальное окружение:
python -m venv .venv && . .venv/bin/activate
pip install brokapi
Самая свежая версия из репозитория - вместо второй строки (нужен Rust 1.88 или новее, расширение собирается при установке, это несколько минут. Подробности и частые ошибки - на странице Установка):
pip install "git+https://github.com/runinfra/brokapi#subdirectory=crates/python"
Проверка:
python -c "import brokapi; print(brokapi.__version__)"
Должна напечататься версия 0.1.0a5. Не напечатала: таблица «Если что-то пошло не так» на странице
Установка.
Шаг 2. Первая программа: без токена и без денег
Обычный скрипт. Имитация (brokapi.testing.mock_tbank) - это настоящий сервер на 127.0.0.1, который говорит на протоколе Т-Банка, а
клиент к нему тот же brokapi.Client, что вернул бы brokapi.connect("tbank", ...). Токен, счет и сеть не нужны.
Программа начинается с импортов:
import asyncio
import sys
from decimal import Decimal
import brokapi
from brokapi.testing import mock_tbank
Сама работа - функция first_order. Она берет клиента любого брокера, поэтому дальше, на песочнице и на боевом счете, она не меняется:
async def first_order(client: brokapi.Client) -> None:
"""Reads the market, puts one order far below it and takes it off at once."""
# 1. What to trade and by what rules: the step of the price and the lot.
sber = await client.instrument("SBER")
params = await client.trading_params(sber)
# 2. The quote: the best bid and the best ask are the top of the order book.
book = await client.order_book(sber, depth=1)
best_bid = book.bids[0].price
print(f"SBER: bid {best_bid}, ask {book.asks[0].price}, lot {params.lot_size}, price step {params.price_step}")
# 3. Subscribe to the states of your orders, and only then reconcile: no event is lost between the two.
await client.subscribe_order_state()
report = await client.reconcile([]) # a new bot has an empty ledger. Until now `place` is NotReconciled
print(f"reconcile: {len(report.foreign)} foreign order(s) on the account")
# 4. An order far from the market: 90% of the best bid, rounded down to the price step, one lot.
price = brokapi.round_to_step(best_bid * Decimal("0.9"), params.price_step, brokapi.RoundMode.DOWN)
cid = brokapi.new_cid() # a real bot writes the cid to its journal here, before `place`
ack = await client.place(cid=cid, instrument=sber, side=brokapi.Side.BUY, qty=params.lot_size, price=price)
print(f"placed: broker order id {ack.broker_id}, price {price}")
# 5. Take it off and ask the broker what became of it. The answer of `order_state` is the proof.
await client.cancel(cid)
state = await client.order_state(cid)
print(f"after cancel: {state.state.kind.value}, orders alive: {len(await client.active_orders())}")
Запуск на имитации: блок async with поднимает сервер и останавливает его в конце, mock.connect() возвращает клиента.
async def on_the_imitation() -> None:
# The block starts the imitation and stops it at the end. `mock.connect()` is `brokapi.connect(...)` to it.
async with await mock_tbank() as mock:
client = await mock.connect()
await first_order(client)
# The price of the imitation stands still: it has no market behind it.
book = await client.order_book(await client.instrument("SBER"), depth=1)
print(f"the price stands still: bid {book.bids[0].price}")
Готовая программа целиком - в разделе «Вся программа» ниже (к этим кускам добавлены импорты и запуск через asyncio.run). Сохраните ее как
quickstart_tbank.py и запустите python quickstart_tbank.py. Должно напечататься:
SBER: bid 273.9, ask 274, lot 10, price step 0.01
reconcile: 0 foreign order(s) on the account
placed: broker order id EX1, price 246.51
after cancel: Cancelled, orders alive: 0
the price stands still: bid 273.9
Что здесь произошло:
- Котировка - верх стакана. Отдельного метода «котировка» у клиента нет:
order_book(sber, depth=1)дает лучшую цену покупки и продажи. - Подписка на статусы своих заявок идет до
reconcile, иначе событие между снимком сверки и подпиской пропало бы.reconcile([])открыл шлюз торговых команд: до негоplaceвернул быNotReconciled. Список пустой, потому что бот новый. - Цена заявки - 90% от лучшей покупки, округленная вниз до шага точной арифметикой (
round_to_step). Количество - один лот, в штуках (лотSBERв имитации 10). cidбот придумывает сам. В настоящем боте в этот момент строка уходит в журнал на диск: «отправляю».placeвернулOrderAck: «брокер принял». Что стало с заявкой, говоритorder_state, и послеcancelэтоCancelled.
Цена в имитации стоит на месте. За стаканом нет рынка: сколько раз ни спроси, будет 273.9 и 274, а заявка сама не исполнится. Исполнить ее
просят у имитации: mock.fill(cid) (Тесты против имитаций). Поэтому на имитации проверяют порядок вызовов и разбор
исходов, а стратегию по движению цены на ней проверить нельзя. Имитация написана по документации брокера и не доказывает, что настоящий брокер
ответит так же.
Такие же имитации есть у БКС, Финама и АЛОРа (mock_bcs(), mock_finam(), mock_alor()), first_order работает на каждой. У Bybit имитация mock_bybit() с контрактом MOCKUSDT и свой первый запуск, с демо-счетом вместо песочницы: Bybit.
Шаг 3. Песочница Т-Банка
Следующая ступень - настоящий сервер брокера, но с игровыми деньгами. Нужны токен песочницы и счет песочницы. Токен выдает человек в кабинете
Т-Банка, как это сделать - в разделе «Вход» страницы Т-Банк. Счет песочницы сам не появляется: его открывает метод
OpenSandboxAccount и пополняет SandboxPayIn (по документации Т-Банка). Без счета connect ответит Config: у токена нет открытого счета.
export TBANK_TOKEN=... # токен песочницы
python quickstart_tbank.py sandbox
В коде меняется одна строка: клиента делает connect, а не имитация. sandbox=True выбирает адрес песочницы (без него адрес боевой, и токен песочницы
не подойдет), то же делает переменная TBANK_SANDBOX=true. Токен берется из окружения или из файла .env рядом с программой.
async def on_the_sandbox() -> None:
# TBANK_TOKEN is the token of the sandbox. sandbox=True is the address of the sandbox (the real one is the default).
client = await brokapi.connect("tbank", sandbox=True)
try:
await first_order(client)
finally:
await client.close()
Печатается то же самое, но с живыми ценами песочницы. Заявка покупает, поэтому на счете песочницы должны быть деньги: пополните его на сумму
больше цены лота. Если счетов у токена несколько, укажите нужный: TBANK_ACCOUNT_ID или account_id= в connect.
Шаг 4. Боевой счет
Сюда идут, когда имитация и песочница прошли, а человек, чей это счет, сам выдал токен и сказал «можно». Первая заявка та же: лимитная, минимальное количество, цена далеко от рынка, сразу отмена.
| Брокер | Подключение | Переменные |
|---|---|---|
| БКС | await brokapi.connect("bcs") | BCS_REFRESH_TOKEN (обязательно), BCS_IS_WRITE (true - токен на запись). Песочницы нет |
| Т-Банк | await brokapi.connect("tbank") | TBANK_TOKEN, TBANK_ACCOUNT_ID (если у токена несколько счетов), TBANK_ENDPOINT, TBANK_APP_NAME. Без TBANK_SANDBOX адрес боевой |
| Финам | await brokapi.connect("finam") | FINAM_SECRET, FINAM_ACCOUNT_ID (обязательно), FINAM_API_URL. Песочницы нет |
| АЛОР | await brokapi.connect("alor") | ALOR_REFRESH_TOKEN (обязательно), ALOR_PORTFOLIO (обязательно, если токен называет несколько портфелей), ALOR_EXCHANGE (MOEX или SPBX), ALOR_TEST (true - тестовый контур) |
| Bybit | await brokapi.connect("bybit") | BYBIT_API_KEY и BYBIT_API_SECRET (обязательно), BYBIT_ENV (mainnet по умолчанию, demo - демо-счет с виртуальными деньгами, у него свои BYBIT_DEMO_API_KEY и BYBIT_DEMO_API_SECRET) |
Где взять токен и номер счета у каждого брокера: раздел «Вход» на страницах БКС, Т-Банк, Финам, АЛОР, Bybit. Первый день на Финаме, где песочницы нет: Финам.
await client.instrument("SBER") у Т-Банка делает один запрос к справочнику и берет uid инструмента, у БКС, Финама, АЛОРа и Bybit идентификатор строится
по тексту, второй аргумент задает площадку (TQBR, MISX, у Bybit категорию linear). Особенности брокеров: БКС, Т-Банк,
Финам, АЛОР, Bybit.
Пример для БКС
У БКС нет песочницы, счет настоящий. Заявка ставится только по токену на запись, ее цена заведомо ниже рынка, количество минимальное, и она
снимается сразу. Токен без записи (BCS_IS_WRITE не true) дойдет только до чтения.
Настройки лежат в переменных окружения. Удобно файлом .env рядом с программой, пакет читает его сам. Токены не печатайте в логи и не кладите
в коммиты. Те же настройки можно передать аргументами: brokapi.connect("bcs", refresh_token="...", write=True). Что не передано, берется из окружения.
connect поднимает два коннектора, рыночных данных и счета, и возвращает клиента. Если токен не подошел, это Config или Transport прямо здесь,
а не на первом запросе.
# BCS_REFRESH_TOKEN and BCS_IS_WRITE come from the environment (a .env file works)
client = await brokapi.connect("bcs")
print("connected: bcs-md and bcs-ud started")
Должно напечататься connected: bcs-md and bcs-ud started. Не напечатало: Config называет, какой настройки нет, Transport говорит, что БКС не
отвечает или отверг токен.
Чтение: три запроса, которые ничего не меняют. Инструмент SBER на основной площадке БКС (TQBR) получает метод instrument, он собирает
идентификатор по тикеру.
sber = await client.instrument("SBER") # the ticker on the board TQBR
# 1. Portfolio: money and positions of the account.
portfolio = await client.portfolio()
print(f"portfolio: {len(portfolio.balances)} balance(s), {len(portfolio.positions)} position(s)")
for balance in portfolio.balances:
print(f" money {balance.currency}: {balance.available} free, {balance.blocked} blocked")
# 2. Order book: best bid and best ask.
book = await client.order_book(sber, depth=5)
best_bid = book.bids[0].price if book.bids else None
best_ask = book.asks[0].price if book.asks else None
print(f"order book SBER: best bid {best_bid}, best ask {best_ask}")
# 3. Candles: the last hour of five-minute bars.
now = datetime.now(timezone.utc)
bars = await client.candles(sber, brokapi.Timeframe.M5, now - timedelta(hours=1), now)
print(f"candles M5: {len(bars)} bar(s), last close {bars[-1].close if bars else None}")
Должно напечататься число балансов и позиций, лучшая цена покупки и продажи и число свечей. Цены и суммы здесь Decimal: цена печатается точной
десятичной записью, как ее отдал брокер, без хвостов вроде 0.30000000000000004.
Дальше программа пишет в счет. Если токен не на запись, она остановится здесь.
print("WARNING: BCS has no sandbox. The account is real, the order below goes to the exchange.")
Заявка далеко от рынка и отмена. Порядок важен, причины каждого шага разобраны в Круге жизни и Круге команд:
- Правила инструмента: шаг цены и минимальное количество (
trading_params). Метод заодно сообщает шаг цены коннектору, и цена не на шаге будет отвергнута сразу, до отправки. - Цена: 90% от лучшей покупки, округленная вниз до шага точной арифметикой (
round_to_step). - Подписка на статусы своих заявок, и только потом
reconcile: событие между снимком сверки и подпиской иначе потерялось бы. cidпридумывает бот. В настоящем боте здесь строка в журнал: «отправляю».place, события статусов,cancelи проверкаorder_state.
# The price: far below the best bid, a multiple of the price step. The quantity: the minimum.
params = await client.trading_params(sber)
reference = best_bid or (bars[-1].close if bars else None)
if reference is None:
raise SystemExit("no price to start from: the book and the candles are empty")
# The rounding is exact: the price is a whole number of steps, no 0.9999999 noise.
price = brokapi.round_to_step(reference * FAR_FROM_MARKET, params.price_step, brokapi.RoundMode.DOWN)
print(f"order: buy {params.min_qty} at {price} (best bid {reference}, price step {params.price_step})")
# The gate is closed until the first reconcile. The ledger of this new bot is empty. Subscribe first, so no
# event between the snapshot of the reconcile and the subscription is lost.
await client.subscribe_order_state()
report = await client.reconcile([])
print(f"reconcile: {len(report.foreign)} foreign order(s) on the account")
# The cid is made by the bot and belongs in its journal before the order is sent.
cid = brokapi.new_cid()
# Here the bot writes `cid` to its journal as "sending".
print(f"cid {cid}")
ack = await client.place(
cid=cid,
instrument=sber,
side=brokapi.Side.BUY,
qty=params.min_qty,
price=price,
)
print(f"accepted: broker order id {ack.broker_id}")
await wait_for_order(client, cid, brokapi.OrderStateKind.ACKED)
# Cancel at once and look at the final state.
await client.cancel(cid)
await wait_for_order(client, cid, brokapi.OrderStateKind.CANCELLED)
last = await client.order_state(cid)
print(f"order_state: {last.state.kind.value}")
Должно напечататься: accepted: broker order id ..., события Acked, затем Cancelled, в конце order_state: Cancelled.
Событие это уведомление, а доказательство - ответ order_state, поэтому программа не падает, если события не пришли.
Если place вернул Rejected, у ошибки есть code: price_off_step (цена не на шаге), missing_price (у лимитной заявки нет цены),
отказ брокера с его текстом. Если Unknown, заявка могла встать: не повторяйте, спросите order_state(cid)
(Если что-то пошло не так, общая логика одна для всех языков).
Остановка:
await client.close()
print("stopped")
Заявки на бирже при остановке не снимаются. Если cancel не прошел, заявка останется. Как остановить бота так, чтобы на счете не осталось
лишнего: Аварийная остановка бота.
Вся программа
Текст программы crates/python/examples/quickstart_tbank.py (имитация и песочница)
"""Quick start: the first run without a token and without money, then the sandbox of T-Bank.
This program is the code of the page "Быстрый старт" of the Python section of the book. The book includes the marked
parts below, so the text and the code that is tested cannot drift apart.
Without a token (the imitation of T-Bank in your own process; no account, no network)::
python quickstart_tbank.py
On the sandbox of T-Bank (a token of the sandbox and an account of the sandbox are needed)::
TBANK_TOKEN=... python quickstart_tbank.py sandbox
The imitation (`brokapi.testing.mock_tbank`) is a server on 127.0.0.1 that speaks the protocol of T-Bank. The client is
the same `brokapi.Client` that `brokapi.connect("tbank", ...)` returns, so `first_order` below is the code of a real
run: it works with a client of any broker.
"""
import asyncio
import sys
from decimal import Decimal
import brokapi
from brokapi.testing import mock_tbank
# ANCHOR: first_order
async def first_order(client: brokapi.Client) -> None:
"""Reads the market, puts one order far below it and takes it off at once."""
# 1. What to trade and by what rules: the step of the price and the lot.
sber = await client.instrument("SBER")
params = await client.trading_params(sber)
# 2. The quote: the best bid and the best ask are the top of the order book.
book = await client.order_book(sber, depth=1)
best_bid = book.bids[0].price
print(f"SBER: bid {best_bid}, ask {book.asks[0].price}, lot {params.lot_size}, price step {params.price_step}")
# 3. Subscribe to the states of your orders, and only then reconcile: no event is lost between the two.
await client.subscribe_order_state()
report = await client.reconcile([]) # a new bot has an empty ledger. Until now `place` is NotReconciled
print(f"reconcile: {len(report.foreign)} foreign order(s) on the account")
# 4. An order far from the market: 90% of the best bid, rounded down to the price step, one lot.
price = brokapi.round_to_step(best_bid * Decimal("0.9"), params.price_step, brokapi.RoundMode.DOWN)
cid = brokapi.new_cid() # a real bot writes the cid to its journal here, before `place`
ack = await client.place(cid=cid, instrument=sber, side=brokapi.Side.BUY, qty=params.lot_size, price=price)
print(f"placed: broker order id {ack.broker_id}, price {price}")
# 5. Take it off and ask the broker what became of it. The answer of `order_state` is the proof.
await client.cancel(cid)
state = await client.order_state(cid)
print(f"after cancel: {state.state.kind.value}, orders alive: {len(await client.active_orders())}")
# ANCHOR_END: first_order
# ANCHOR: mock
async def on_the_imitation() -> None:
# The block starts the imitation and stops it at the end. `mock.connect()` is `brokapi.connect(...)` to it.
async with await mock_tbank() as mock:
client = await mock.connect()
await first_order(client)
# The price of the imitation stands still: it has no market behind it.
book = await client.order_book(await client.instrument("SBER"), depth=1)
print(f"the price stands still: bid {book.bids[0].price}")
# ANCHOR_END: mock
# ANCHOR: sandbox
async def on_the_sandbox() -> None:
# TBANK_TOKEN is the token of the sandbox. sandbox=True is the address of the sandbox (the real one is the default).
client = await brokapi.connect("tbank", sandbox=True)
try:
await first_order(client)
finally:
await client.close()
# ANCHOR_END: sandbox
if __name__ == "__main__":
asyncio.run(on_the_sandbox() if sys.argv[1:] == ["sandbox"] else on_the_imitation())
Текст программы crates/python/examples/quickstart_bcs.py (боевой счет БКС)
"""Quick start on BCS: read first, then one order far from the market, cancelled at once.
This program is the code of the page "Быстрый старт" of the Python section of the book. The book includes the marked
parts below, so the text and the code that is tested cannot drift apart.
Run it with a real account (BCS has no sandbox, the account is real)::
BCS_REFRESH_TOKEN=... BCS_IS_WRITE=true python quickstart_bcs.py
The tests run `run()` against the imitation of BCS (`brokapi.testing`): no network, no account, no money.
"""
import asyncio
import os
from datetime import datetime, timedelta, timezone
from decimal import Decimal
import brokapi
# The price of the test order is this share of the best bid. Far enough that it does not fill in a normal session,
# near enough that the exchange does not refuse it as being outside the allowed corridor.
FAR_FROM_MARKET = Decimal("0.9")
async def wait_for_order(client: brokapi.Client, cid, kind: brokapi.OrderStateKind) -> None:
"""Reads the events until the order `cid` reaches `kind`, and prints every state it goes through.
An event is a notice. The answer of `order_state` is the proof, so a missing event is not an error here."""
async def read() -> None:
async for envelope in client.events:
order = envelope.payload
if isinstance(order, brokapi.OrderSnapshot) and order.cid == cid:
print(f"event: order {cid} is {order.state.kind.value}")
if order.state.kind is kind:
return
try:
await asyncio.wait_for(read(), 5)
except asyncio.TimeoutError:
print(f"no event {kind.value} within 5 s, the state is checked with order_state below")
async def run(client: brokapi.Client, *, can_trade: bool) -> None:
"""The whole quick start on a connected client."""
# ANCHOR: read
sber = await client.instrument("SBER") # the ticker on the board TQBR
# 1. Portfolio: money and positions of the account.
portfolio = await client.portfolio()
print(f"portfolio: {len(portfolio.balances)} balance(s), {len(portfolio.positions)} position(s)")
for balance in portfolio.balances:
print(f" money {balance.currency}: {balance.available} free, {balance.blocked} blocked")
# 2. Order book: best bid and best ask.
book = await client.order_book(sber, depth=5)
best_bid = book.bids[0].price if book.bids else None
best_ask = book.asks[0].price if book.asks else None
print(f"order book SBER: best bid {best_bid}, best ask {best_ask}")
# 3. Candles: the last hour of five-minute bars.
now = datetime.now(timezone.utc)
bars = await client.candles(sber, brokapi.Timeframe.M5, now - timedelta(hours=1), now)
print(f"candles M5: {len(bars)} bar(s), last close {bars[-1].close if bars else None}")
# ANCHOR_END: read
if not can_trade:
print("the token is read-only (BCS_IS_WRITE is not true): the order step is skipped")
return
# ANCHOR: warning
print("WARNING: BCS has no sandbox. The account is real, the order below goes to the exchange.")
# ANCHOR_END: warning
# ANCHOR: order
# The price: far below the best bid, a multiple of the price step. The quantity: the minimum.
params = await client.trading_params(sber)
reference = best_bid or (bars[-1].close if bars else None)
if reference is None:
raise SystemExit("no price to start from: the book and the candles are empty")
# The rounding is exact: the price is a whole number of steps, no 0.9999999 noise.
price = brokapi.round_to_step(reference * FAR_FROM_MARKET, params.price_step, brokapi.RoundMode.DOWN)
print(f"order: buy {params.min_qty} at {price} (best bid {reference}, price step {params.price_step})")
# The gate is closed until the first reconcile. The ledger of this new bot is empty. Subscribe first, so no
# event between the snapshot of the reconcile and the subscription is lost.
await client.subscribe_order_state()
report = await client.reconcile([])
print(f"reconcile: {len(report.foreign)} foreign order(s) on the account")
# The cid is made by the bot and belongs in its journal before the order is sent.
cid = brokapi.new_cid()
# Here the bot writes `cid` to its journal as "sending".
print(f"cid {cid}")
ack = await client.place(
cid=cid,
instrument=sber,
side=brokapi.Side.BUY,
qty=params.min_qty,
price=price,
)
print(f"accepted: broker order id {ack.broker_id}")
await wait_for_order(client, cid, brokapi.OrderStateKind.ACKED)
# Cancel at once and look at the final state.
await client.cancel(cid)
await wait_for_order(client, cid, brokapi.OrderStateKind.CANCELLED)
last = await client.order_state(cid)
print(f"order_state: {last.state.kind.value}")
# ANCHOR_END: order
async def main() -> None:
# ANCHOR: connect
# BCS_REFRESH_TOKEN and BCS_IS_WRITE come from the environment (a .env file works)
client = await brokapi.connect("bcs")
print("connected: bcs-md and bcs-ud started")
# ANCHOR_END: connect
try:
await run(client, can_trade=os.environ.get("BCS_IS_WRITE") == "true")
finally:
# ANCHOR: stop
await client.close()
print("stopped")
# ANCHOR_END: stop
if __name__ == "__main__":
asyncio.run(main())
Что произошло по шагам на БКС
connectподнял оба коннектора. У БКС, Финама, АЛОРа и Bybit он сразу проходит авторизацию, поэтому неверный токен виден на этом шаге.- Чтение ничего не меняет и не требует сверки.
subscribe_order_stateдал подписку на события своих заявок и вернулся, когда поток открыт. Она сделана до сверки нарочно.reconcile([])открыл шлюз команд. До негоplaceвернул быNotReconciled.placeвернулOrderAck: «брокер принял». Статусы приходят событиями вclient.events, порядок между ответом и событием не гарантирован.cancelснял заявку,closeостановил коннекторы.
Дальше: торговый бот на событиях, стоп-заявки, аварийная остановка. Если вы
пришли с tinkoff-investments: таблица соответствий.
Листать можно клавишами Влево и Вправо.