Bybit: подключение к API
Эта страница о техническом подключении к публичному API стороннего сервиса. Она не является рекламой, предложением услуг или рекомендацией совершать сделки с цифровой валютой. Законность таких операций зависит от вашей юрисдикции. Для резидентов РФ действует 282-ФЗ «О цифровых валютах и цифровых правах», с 1 июля 2027 года сделки с цифровой валютой допускаются только через российских организаторов обращения. Проверьте требования закона до использования.
Крейты: brokapi-bybit-protocol (клиент HTTP и WebSocket: подпись запросов, методы API V5 и оба канала WebSocket) и brokapi-bybit (коннекторы).
Bybit - зарубежный сервис, не российский брокер: у него нет торговых сессий и лотов, и книга отмечает отличия там, где они меняют код.
Что поддержано. Контракты с расчетом в USDT (категория linear API V5). Спота пока нет, как и инверсных контрактов и опционов:
категория входит в идентификатор инструмента, поэтому спот добавится рядом, не ломая существующий код.
Вход
| Что | Как |
|---|---|
| Ключ | пара из ключа и секрета API: BYBIT_API_KEY и BYBIT_API_SECRET. Каждый запрос подписывается секретом (HMAC-SHA256), секрет по сети не уходит. Время для подписи берется у биржи: при старте коннектор читает ее часы и запоминает сдвиг, окно приема запроса 5000 мс (recv_window_ms) |
| Права ключа | для бота нужны чтение и торговля по деривативам (Unified Trading) и чтение кошелька. Вывод средств и переводы не включайте: бот им не пользуется. Привязка ключа к IP-адресу сервера - хорошая защита, на демо-счете ее можно не ставить |
| Счет | выбора счета нет: ключ принадлежит одному счету (или субсчету). Нужен единый торговый счет (Unified Trading Account) |
| Контуры | основной (api.bybit.com, по умолчанию), демо-счет (api-demo.bybit.com, виртуальные деньги, круглосуточно) и тестовая сеть (api-testnet.bybit.com). Контур выбирает BYBIT_ENV (mainnet, demo, testnet), в коде BybitConfig::with_env(BybitEnv::Demo), в Python env="demo", в Gateway sandbox = true значит демо-счет |
| Ключ демо-счета | у него своя пара переменных, BYBIT_DEMO_API_KEY и BYBIT_DEMO_API_SECRET: ключ демо-счета на основном адресе не действует, поэтому обе пары могут лежать в одном .env, а контур выбирает BYBIT_ENV |
| Лимиты | brokapi::bybit::limits::default_limits(): встроенный файл limits.toml крейта. Подробно ниже |
Где взять ключ
Ключ выпускает человек, чей это счет (правило 0). Шаги по документации Bybit:
Демо-счет (виртуальные деньги, с него начинайте):
- Войти на сайт Bybit и включить режим «Demo Trading».
- В этом режиме открыть страницу API и создать ключ с правами на чтение и торговлю по деривативам (Unified Trading) и чтение кошелька.
- Положить ключ и секрет в
BYBIT_DEMO_API_KEYиBYBIT_DEMO_API_SECRET. Демо-счет пустой: бот пополняет его методомPOST /v5/account/demo-apply-money(черезraw, не чаще раза в минуту, до 100 000 USDT за запрос).
Основной контур: ключ выпускают для отдельного субсчета, без права вывода и с привязкой к IP-адресу. Ключ основного счета программе не давайте.
Первый запуск
План от безопасного к боевому. Каждый шаг начинается, когда прошел предыдущий.
- Имитация. Код сначала проходит на
brokapi.testing.mock_bybit()(тестовый инструментMOCKUSDT): быстрый старт на Python. - Демо-счет. Тот же код с
env="demo": деньги виртуальные, поэтому проверка идет вживую и без риска, но с настоящей подписью, настоящими потоками и настоящими отказами биржи. - Основной контур - только с минимальной заявкой далеко от рынка и сразу с отменой, на субсчете.
Программа делает на любом контуре одно и то же: читает контракт и его правила, сверяется со счетом, ставит самую малую заявку вдвое ниже лучшей покупки, видит ее в потоке, снимает, ставит стоп-заявку и тоже снимает. Ничего не может исполниться: обе цены далеко от рынка.
async def order_event(client: brokapi.Client, cid: uuid.UUID) -> brokapi.OrderSnapshot:
"""The first event of the stream that is a state of the order with this cid."""
async for event in client.events:
if isinstance(event.payload, brokapi.OrderSnapshot) and event.payload.cid == cid:
return event.payload
raise RuntimeError("the stream ended")
async def first_day(client: brokapi.Client, symbol: str) -> None:
"""Reads the contract, puts the smallest order at half of the best bid, sees it in the stream, takes it off, and
puts a stop order that is also taken off. Nothing here can fill: both prices are far from the market."""
# 1. The contract and its rules: the quantity step, the smallest order and the price step. They come from the exchange.
perp = await client.instrument(symbol) # "BTCUSDT" becomes "BTCUSDT@linear": the category is part of the id
params = await client.trading_params(perp)
book = await client.order_book(perp, depth=1)
bid = book.bids[0].price
print(f"{perp}: bid {bid}, ask {book.asks[0].price}, smallest order {params.min_qty}, quantity step {params.lot_size}")
# 2. Subscribe to the 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
print(f"reconcile: {len(report.foreign)} foreign order(s) on the account")
# 3. The order: half of the best bid, rounded down to the price step. The exchange refuses a price too far from the
# market with a code of its own, then try a nearer one with a NEW cid.
price = brokapi.round_to_step(bid / 2, 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=perp, side=brokapi.Side.BUY, qty=params.min_qty, price=price)
print(f"placed: order {ack.broker_id} at {price}")
seen = await asyncio.wait_for(order_event(client, cid), timeout=10) # waits for the event, not for a pause
print(f"in the stream: {seen.state.kind.value}")
# 4. Take it off and ask the exchange 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())}")
# 5. A stop order is a conditional order of Bybit. It waits apart from the ordinary orders: `active_stop_orders`.
stop_cid = brokapi.new_cid()
await client.place_stop(
cid=stop_cid,
instrument=perp,
side=brokapi.Side.SELL,
kind=brokapi.StopOrderKind.STOP_LOSS,
qty=params.min_qty,
stop_price=brokapi.round_to_step(bid / 2, params.price_step, brokapi.RoundMode.DOWN),
)
print(f"stop orders waiting: {len(await client.active_stop_orders())}, ordinary orders: {len(await client.active_orders())}")
await client.cancel_stop(stop_cid)
print(f"stop orders after cancel: {len(await client.active_stop_orders())}")
Запуск на имитации (сеть и деньги не нужны) и на демо-счете:
async def on_the_imitation() -> None:
async with await mock_bybit() as mock:
client = await mock.connect()
await first_day(client, "MOCKUSDT")
await client.close()
async def on_the_demo_account() -> None:
# BYBIT_DEMO_API_KEY and BYBIT_DEMO_API_SECRET from the environment: a key of the demo account is not valid on the
# main host, so the contour is chosen by the name, not by the address.
client = await brokapi.connect("bybit", env="demo")
try:
await first_day(client, "BTCUSDT")
finally:
await client.close()
На имитации программа печатает MOCKUSDT@linear: bid 100.00, ask 100.01, ..., in the stream: Acked, after cancel: Cancelled, orders alive: 0 и stop orders after cancel: 0.
Если биржа отвергла цену как слишком далекую от рынка (окно допустимых цен у контрактов разное), подвиньте цену ближе и повторите с новым cid.
Идентификатор инструмента
NativeId::SymbolMic, ключ библиотеки BTCUSDT@linear: тикер контракта и категория Bybit (у Bybit нет кода рынка MIC, его место занимает категория). Построить идентификатор
без сети: brokapi::bybit::util::instrument_id("BTCUSDT"). В Python await client.instrument("BTCUSDT") или "BTCUSDT@linear". Идентификатор другого брокера или другой категории - Config до сети.
Неизвестный тикер биржа отвергает кодом 10001 (symbol invalid), коннектор отдает это как Rejected { code: "not_found" }.
У контрактов Bybit два вида: бессрочный (InstrumentKind::Perpetual, без срока, с ставкой финансирования) и срочный фьючерс (InstrumentKind::Future, с датой исполнения, например BTC-26DEC26).
Поиск по виду Other дает Unsupported, остальные виды вне категории linear - пустой список.
Единицы и стакан
| Что | Как |
|---|---|
| Числа | цены, количества и суммы читаются из JSON в Decimal по тексту ответа |
| Количество | в библиотеке единицы базовой монеты: 0.001 для BTC, целые для мелких монет. Шаг количества (qtyStep) и самое малое количество (minOrderQty) биржа называет у каждого контракта, коннектор читает их один раз и отказывает до отправки: invalid_qty (не кратно шагу или меньше минимума), price_off_step (цена не кратна шагу tickSize), missing_price (лимитная заявка без цены). TradingParams: lot_size это шаг количества, min_qty это самое малое количество |
| Минимальная стоимость заявки | у биржи есть и минимум по сумме (minNotionalValue, обычно 5 USDT) и предел количества (maxOrderQty). Коннектор их не проверяет: ответит биржа, отказ придет как Rejected { code: "bybit_<код>" }. Самую малую заявку считайте сами: количество на цену не меньше минимальной стоимости, с запасом |
| Стакан | до 1000 уровней, коннектор подписывается на глубину 1, 50, 200 или 1000 (ближайшую не меньше запрошенной, лишнее отрезает). Биржа присылает снимок и изменения, коннектор держит свой стакан и после каждого изменения отдает целый (incremental_order_book = false). Изменения идут подряд по номеру u: пропуск номера сбрасывает стакан, бот получает Resync { SequenceGap }, коннектор просит свежий снимок |
| Котировки | лучшая покупка и продажа из стакана глубины 1 (снимки до 100 раз в секунду) |
| Свечи | интервалы 1, 5, 15, 30, 60, 240 минут, день, неделя, месяц. 10 минут нет: Unsupported. Один запрос дает до 1000 свечей (candles_max_span), период длиннее - Rejected { range_too_long }, длинную историю читает candle_pages или load_candles |
| Лента сделок | last_trades и поток публичных сделок |
| Статус торгов | Trading это Open, PreLaunch, PendingOpen, Delivering и Closed это Closed. Расписания у сервиса нет: schedule отвечает Unsupported |
Как устроены команды
Команды идут по HTTP, по одному запросу, без повтора: заявка, отмена, стоп-заявка. Канал команд по WebSocket (/v5/trade) коннектор не использует, на демо-счете его нет.
- Отказ известен сразу:
Rejectedс кодомbybit_<код биржи>и текстом. - В
OrderAckприходит номер заявки биржи (orderId). - Нет ответа, обрыв, таймаут, 5xx и коды 10000 и 10016 это
Unknown: повторов нет, выход из неизвестности -order_stateиreconcile(Круг команд). - Отмена успешна, когда биржа приняла запрос. Подтверждение - событие статуса
Cancelledили чтениеorder_state. - Рыночная заявка у Bybit исполняется сразу или отменяется (IOC), лимитная держится до отмены (GTC). Других сроков и условий (post-only, FOK) в общем интерфейсе нет, они доступны через
raw().
Как cid уходит к бирже
В поле orderLinkId заявки, как UUID с дефисами (36 знаков из 36 допустимых). Обратимо: orderLinkId возвращается в списке заявок, в потоке и в сделках, так cid находится в ответах.
Повтор orderLinkId биржа отвергает (код 110072), это видно как Rejected { code: "bybit_110072" }. Как долго она помнит закрытую заявку, в документации не названо, поэтому
place_idempotency = LabelOnly, а повтор того же cid библиотека сама не отправляет. Заявка без нашего orderLinkId (из приложения, чужая программа) получает подставной cid
(стабильный: тот же номер дает тот же cid), по нему ее можно снять.
Порядок как у остальных брокеров: шлюз команд, проверка правил контракта, квота, очередь по инструменту, шлюз еще раз и только потом cid помечается использованным. После RateLimited тот же cid годен.
Общее правило: Круг команд.
Изменение заявки
Общего replace у Bybit нет (supports_replace = false, ответ Unsupported). Биржа меняет заявку на месте: номер и orderLinkId остаются, а общий replace обещает новую заявку под новым cid
и отмену старой. Выдавать одно за другое нельзя: после перезапуска бота сверка искала бы заявку по новому cid и сказала бы «закрыта». Поменять цену или количество можно двумя способами:
снять заявку и поставить новую под новым cid (так надежнее) или вызвать POST /v5/order/amend через raw() и помнить, что cid остается прежним.
Свои сделки, портфель
| Что | Как |
|---|---|
| Поток исполнений | поток execution: каждая сделка с cid заявки и полем execFee (как есть, знак сохраняется). Записи, что не сделки заявки (например, выплата финансирования), пропускаются |
| История | исполнения читаются окнами по 7 дней (ограничение биржи), страницами по курсору, от старого к новому. Биржа хранит 2 года. Больше 40 страниц в окне - Rejected { range_too_long } |
| Портфель | деньги по монетам: blocked это удерживаемое (в заявках и под позиции), available это капитал за вычетом blocked, не меньше нуля. Это наше чтение полей кошелька, биржа дает свободную сумму только по счету в долларах. Позиции: количество со знаком (продажа - минус), средняя цена, цена маркировки |
| Поток портфеля | потоки position и wallet присылают только изменения, без снимка на старте, поэтому subscribe_portfolio сначала отдает текущее состояние (чтение по HTTP), потом изменения. Поток позиции шлет сообщение на каждую заявку, даже без изменений |
| Список активных заявок | рабочие заявки категории. Ожидающие стоп-заявки в него не входят, они в active_stop_orders |
| Режим позиции | только односторонний. На счете с хеджированием биржа откажет в заявке, отказ виден как Rejected |
| Плечо и маржа | библиотека плечо не меняет. Установить его можно через raw() (POST /v5/position/set-leverage) до первой заявки |
Стоп-заявки
Стоп-заявка Bybit - обычная заявка с ценой срабатывания (triggerPrice). StopLoss и TakeProfit при срабатывании ставят рыночную заявку, StopLimit - лимитную по limit_price
(без нее Rejected { missing_price }). Направление срабатывания (рост или падение цены) коннектор выводит из вида и стороны.
| Что | Как |
|---|---|
| Срок | только «до отмены» (StopExpiry::GoodTillCancel): срок у условной заявки Bybit не задается. Остальное - Rejected { code: "unsupported_expiry" } |
| Список | ожидающая стоп-заявка видна в active_stop_orders. После срабатывания она приходит обычной заявкой в потоке заявок под тем же cid |
| Поток | потока стоп-заявок нет (subscribe_stop_orders отвечает Unsupported), список читает active_stop_orders |
| Закрытие позиции | reduceOnly и closeOnTrigger коннектор не ставит: стоп без позиции может открыть встречную. Размер стопа задавайте равным позиции |
| TP и SL позиции | POST /v5/position/trading-stop коннектор не создает (им нужна позиция, cid они не несут), но видит их в списке с придуманным cid |
Подробно: Стоп-заявки, для Python Стоп-заявки, Python.
Чего Bybit не поддерживает или делает иначе
| Что | Как |
|---|---|
| Спот, инверсные контракты, опционы | нет. Категория linear только |
Замена заявки общим replace | Unsupported, см. выше |
| Свечи по 10 минут | Unsupported("candles_m10") |
| Расписание торгов | Unsupported("schedule"): торги круглосуточные |
| Справочник по ISIN | Unsupported("find_by_isin") |
| Поток стоп-заявок | Unsupported |
Пакетные методы (create-batch, amend-batch, cancel-batch), trading-stop, set-leverage, switch-mode, поток tickers, команды по WebSocket | через raw() или совсем нет. Таблица ниже |
| Обрыв сокета подписок | виден боту: Event::Reconnected на подписки заявок и исполнений и закрытый шлюз до reconcile, Resync на стаканы (Круг событий). Публичный сокет пингуется каждые 20 секунд, молчание дольше 50 секунд - переподключение с восстановлением подписок |
| Отмененная заявка без исполнений | биржа держит ее в истории 24 часа, дальше она не находится: reconcile видит ее как closed_offline, как у остальных брокеров «не видел сегодня» |
Что закрыто общим интерфейсом, что через raw()
| Метод API | Общий интерфейс |
|---|---|
GET /v5/market/time, instruments-info, orderbook, recent-trade, kline | часы, справочник и правила, стакан, лента, свечи |
POST /v5/order/create, cancel | place, place_stop, cancel, cancel_stop |
GET /v5/order/realtime, history | order_state, active_orders, active_stop_orders, reconcile |
GET /v5/execution/list | executions |
GET /v5/position/list, account/wallet-balance | portfolio, positions, balances |
POST /v5/order/amend, cancel-all, create-batch, amend-batch, cancel-batch | raw() (cancel-all есть и командой brokapi cancel-all) |
GET /v5/market/tickers, POST /v5/position/trading-stop, set-leverage, switch-mode, POST /v5/account/demo-apply-money | raw() |
Публичные потоки orderbook, publicTrade, kline и приватные order, execution, position, wallet | стакан, сделки, свечи, котировки, заявки, исполнения, портфель |
Через raw() годится любой метод по глаголу и пути: "GET /v5/market/time", "POST /v5/position/set-leverage". Параметры GET идут в query, тело POST в body. Пути /v5/market/... читаются без подписи,
остальные подписываются ключом. Каждый вызов тратит квоту и идет через общий счетчик лимитов.
Ошибки
| Что пришло от биржи | Что вернет библиотека |
|---|---|
| HTTP 401 и коды 10003, 10004, 10005, 10007, 10010, 33004 (ключ или подпись неверны, нет прав, IP не в списке, ключ другого контура, ключ истек) | Config. При старте это ошибка запуска, поэтому неверный ключ виден сразу |
| HTTP 429 и код 10006 (слишком частые запросы) | RateLimited |
| HTTP 403 | в чтении Transport с пояснением: превышен лимит запросов с адреса (бан на 10 минут) или регион запрещен. В команде Rejected { code: "http_403" }. Другие 4xx команды - Rejected { code: "http_<статус>" } |
| Код 10002 (время запроса вне окна приема) | Rejected. Часы сервера ушли далеко от биржи: сдвиг при старте с этим справляется, дальнейший уход часов нет |
| Код 110001 (нет такой заявки) | Rejected { code: "not_found" } |
| Код 10001 (неизвестный символ) | Rejected { code: "not_found" } |
| Остальные коды команды | Rejected { code: "bybit_<код>" } с текстом биржи |
| Нет ответа, 5xx, коды 10000 и 10016 | Unknown для команды: не повторять, выяснить через order_state и reconcile |
Общая таблица ошибок: Ошибки.
Лимиты
crates/bybit/limits.toml: по документации Bybit 10 запросов в секунду на создание, изменение, отмену и «снять все» (по пользователю и методу), 50 в секунду на чтения, 600 запросов за 5 секунд на адрес
(сверх этого HTTP 403 и бан адреса на 10 минут). Библиотека держит по ведру на каждый вид команды, чтобы поток заявок не занял бюджет отмен, и общее ведро адреса (100 в секунду, всплеск 100):
оно держит адрес в пределах 600 за 5 секунд, даже если рядом работает другая программа. Заголовки X-Bapi-Limit-Status, которые биржа присылает с каждым ответом, библиотека не читает, счет ведется у нее самой.
Лимит общий на логин (ключ), поэтому все коннекторы одного ключа берут один BrokerLimiter. Демо-счет по документации считает так же, вживую это не сверяли.
Что проверено вживую
Проверено на настоящем сервисе, не на имитации:
- Демо-счет, сценарий службы и гейтвея, 12 шагов из 12, с первого раза. Подпись принята, приватные потоки подписываются, пополнение демо-счета работает. Гейтвей читает котировки и стакан, бот видит счет и ставит лимитную заявку на 30% ниже рынка
(принята), получает событие,
order_stateи отмену. Лимитная и стоп-заявка оставлены стоять, процесс гейтвея убит, служба подняла его, журнал помнит заявку, бот переподключился, сверка подтвердила обе заявки, обе сняты. - Основной контур, отдельный субсчет, минимальные заявки. Лимитная заявка далеко от рынка принята, событие пришло в потоке заявок, отмена подтверждена. Рыночная заявка и обратная ей исполнены, исполнения пришли с
cidи полемexecFee, позиций и заявок на счете не осталось. - Публичные данные: справочник, стакан, лента, свечи и публичные потоки на основном контуре и на демо.
Не проверено вживую, учитывайте в боте:
| Что | Что делать |
|---|---|
| Срабатывание стоп-заявки. Стоп ставился и снимался (на демо-счете), но цена до него не доходила | Первый настоящий стоп проверьте на минимальной позиции и дождитесь срабатывания, прежде чем на него полагаться |
| Стоп-заявки на основном контуре | Те же шаги, что для демо-счета, но на минимальной позиции |
| Обрывы потоков без убийства гейтвея: переподключение приватного потока по истечении срока его жизни, потеря событий заявки за время обрыва | После Reconnected делайте reconcile |
| Лимиты: бан по адресу (HTTP 403), совпадение счета лимитов демо-счета и основного | Не превышайте 10 команд в секунду, при раннем RateLimited уменьшите значения в limits.toml |
| Режим позиции с хеджированием, смена плеча и режима маржи, портфельная маржа | Держите односторонний режим, плечо ставьте до первой заявки через raw() |
Формула available и blocked для нескольких монет в залоге | Для сумм, от которых зависит бот, читайте кошелек через raw() |
| Курсор страниц при более чем 50 заявках, история исполнений длиннее одного окна | Читайте историю короткими периодами |
Как долго биржа помнит закрытый orderLinkId | Не повторяйте cid никогда |
Поток Execution и поле execFee на заявках, поставленных не этой программой | Сверяйтесь по reconcile |
Прогон на демо-счете повторяется: tools/bybit-demo-run (служба, гейтвей, бот, убитый гейтвей и сверка, ключ только демо-счета, без ключей и сети сценарий проверяет себя на имитации: --self-test). Допущения, на которые опирается код, собраны в Допущениях по документации брокеров.
Что учесть
- Контур выбирается именем, а не адресом:
BYBIT_ENV=demoберет паруBYBIT_DEMO_API_KEYиBYBIT_DEMO_API_SECRET. ЗабытаяBYBIT_ENVоставит основной контур и основной ключ. - Количество в базовой монете и с шагом контракта (
lot_size). Заявка меньшеmin_qtyили дешевле минимальной стоимости (около 5 USDT) уйдет к бирже и вернетсяRejected. - Замены нет: меняйте заявку отменой и новой заявкой под новым
cid. - Стоп-заявки не входят в
active_orders: проверяйте оба списка при сверке и в скрипте аварийной остановки (Аварийная остановка бота). - Рыночная заявка исполняется как IOC: если ликвидности не хватает, остаток отменяется, проверяйте
filledвorder_state. - Один ключ - один
BrokerLimiter: все коннекторы и боты одного ключа берут общий счетчик.
Листать можно клавишами Влево и Вправо.