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

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

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

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

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

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:

Демо-счет (виртуальные деньги, с него начинайте):

  1. Войти на сайт Bybit и включить режим «Demo Trading».
  2. В этом режиме открыть страницу API и создать ключ с правами на чтение и торговлю по деривативам (Unified Trading) и чтение кошелька.
  3. Положить ключ и секрет в BYBIT_DEMO_API_KEY и BYBIT_DEMO_API_SECRET. Демо-счет пустой: бот пополняет его методом POST /v5/account/demo-apply-money (через raw, не чаще раза в минуту, до 100 000 USDT за запрос).

Основной контур: ключ выпускают для отдельного субсчета, без права вывода и с привязкой к IP-адресу. Ключ основного счета программе не давайте.

Первый запуск

План от безопасного к боевому. Каждый шаг начинается, когда прошел предыдущий.

  1. Имитация. Код сначала проходит на brokapi.testing.mock_bybit() (тестовый инструмент MOCKUSDT): быстрый старт на Python.
  2. Демо-счет. Тот же код с env="demo": деньги виртуальные, поэтому проверка идет вживую и без риска, но с настоящей подписью, настоящими потоками и настоящими отказами биржи.
  3. Основной контур - только с минимальной заявкой далеко от рынка и сразу с отменой, на субсчете.

Программа делает на любом контуре одно и то же: читает контракт и его правила, сверяется со счетом, ставит самую малую заявку вдвое ниже лучшей покупки, видит ее в потоке, снимает, ставит стоп-заявку и тоже снимает. Ничего не может исполниться: обе цены далеко от рынка.

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 только
Замена заявки общим replaceUnsupported, см. выше
Свечи по 10 минутUnsupported("candles_m10")
Расписание торговUnsupported("schedule"): торги круглосуточные
Справочник по ISINUnsupported("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, cancelplace, place_stop, cancel, cancel_stop
GET /v5/order/realtime, historyorder_state, active_orders, active_stop_orders, reconcile
GET /v5/execution/listexecutions
GET /v5/position/list, account/wallet-balanceportfolio, positions, balances
POST /v5/order/amend, cancel-all, create-batch, amend-batch, cancel-batchraw() (cancel-all есть и командой brokapi cancel-all)
GET /v5/market/tickers, POST /v5/position/trading-stop, set-leverage, switch-mode, POST /v5/account/demo-apply-moneyraw()
Публичные потоки 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 и 10016Unknown для команды: не повторять, выяснить через 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). Допущения, на которые опирается код, собраны в Допущениях по документации брокеров.

Что учесть

  1. Контур выбирается именем, а не адресом: BYBIT_ENV=demo берет пару BYBIT_DEMO_API_KEY и BYBIT_DEMO_API_SECRET. Забытая BYBIT_ENV оставит основной контур и основной ключ.
  2. Количество в базовой монете и с шагом контракта (lot_size). Заявка меньше min_qty или дешевле минимальной стоимости (около 5 USDT) уйдет к бирже и вернется Rejected.
  3. Замены нет: меняйте заявку отменой и новой заявкой под новым cid.
  4. Стоп-заявки не входят в active_orders: проверяйте оба списка при сверке и в скрипте аварийной остановки (Аварийная остановка бота).
  5. Рыночная заявка исполняется как IOC: если ликвидности не хватает, остаток отменяется, проверяйте filled в order_state.
  6. Один ключ - один BrokerLimiter: все коннекторы и боты одного ключа берут общий счетчик.

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

brokapi by RunInfra