Пинг-понг стратегия на Bybit V5: Post-Only ордера на Python

Пинг-понг стратегия на Bybit V5: Post-Only ордера на Python Python & API
Пошаговое руководство по созданию маркет-мейкинг робота (Ping-Pong) на Bybit API V5 с использованием Post-Only ордеров для экономии на комиссиях.
Суть: Реализация отказоустойчивого алгоритма сетки ордеров (Ping-Pong) на Bybit API V5 с использованием флага PostOnly для гарантированного получения maker-тарифов и минимизации торговых издержек.

Исходный код

Пинг-понг (Ping-Pong) — это классическая маркет-мейкинговая стратегия, основанная на выставлении лимитных ордеров по обе стороны спреда или на фиксированных уровнях поддержки и сопротивления. Главная задача квант-разработчика при реализации такой стратегии — минимизировать транзакционные издержки (комиссии биржи). На бирже Bybit V5 для этого используется тип выполнения PostOnly.

Опция PostOnly гарантирует, что ваш ордер будет добавлен в стакан как мейкер (Maker) и не исполнится мгновенно по рынку как тейкер (Taker). Если цена изменилась и ордер должен исполниться сразу, биржа автоматически отменит его, защищая вас от уплаты повышенной комиссии тейкера.

При проектировании высокочастотных систем, таких как наш пинг-понг бот, критически важно понимать структуру транзакций и работу с мемпулом на L1/L2 уровнях. Рекомендуем ознакомиться со статьями о том, как устроено декодирование транзакций Uniswap V3 Mint/Burn из мемпула на Python и как реализовать мониторинг мемпула Ethereum на Python: Скрипт Whale Alerts через Web3.py.

Ниже представлен готовый класс на Python для запуска Ping-Pong стратегии на линейных деривативах (USDT-M) Bybit V5 с использованием официальной библиотеки pybit.

import time
import logging
from pybit.unified_trading import HTTP

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class BybitPingPong:
    def __init__(self, api_key: str, api_secret: str, symbol: str, buy_price: float, sell_price: float, qty: float):
        self.session = HTTP(
            testnet=True,
            api_key=api_key,
            api_secret=api_secret
        )
        self.symbol = symbol
        self.buy_price = str(buy_price)
        self.sell_price = str(sell_price)
        self.qty = str(qty)
        self.active_order_id = None
        self.state = 'PLACING_PING'  # PLACING_PING, WAITING_PING, PLACING_PONG, WAITING_PONG

    def place_post_only_order(self, side: str, price: str) -> str:
        try:
            response = self.session.place_order(
                category='linear',
                symbol=self.symbol,
                side=side,
                orderType='Limit',
                qty=self.qty,
                price=price,
                timeInForce='PostOnly',
                orderLinkId=f'pingpong_{int(time.time())}'
            )
            if response.get('retCode') == 0:
                order_id = response['result']['orderId']
                logging.info(f'Успешно размещен Post-Only ордер {side} на {price}. ID: {order_id}')
                return order_id
            else:
                logging.error(f'Ошибка размещения ордера: {response.get("retMsg")}')
        except Exception as e:
            logging.error(f'Исключение при размещении ордера: {e}')
        return None

    def check_order_status(self, order_id: str) -> str:
        try:
            response = self.session.get_open_orders(
                category='linear',
                symbol=self.symbol,
                orderId=order_id
            )
            if response.get('retCode') == 0 and response['result']['list']:
                order = response['result']['list'][0]
                return order['orderStatus']
            else:
                history = self.session.get_order_history(
                    category='linear',
                    symbol=self.symbol,
                    orderId=order_id
                )
                if history.get('retCode') == 0 and history['result']['list']:
                    order = history['result']['list'][0]
                    return order['orderStatus']
        except Exception as e:
            logging.error(f'Ошибка проверки статуса ордера {order_id}: {e}')
        return 'UNKNOWN'

    def run(self):
        logging.info('Запуск Ping-Pong стратегии...')
        while True:
            if self.state == 'PLACING_PING':
                logging.info('Размещение Ping (Buy) ордера...')
                order_id = self.place_post_only_order('Buy', self.buy_price)
                if order_id:
                    self.active_order_id = order_id
                    self.state = 'WAITING_PING'
                else:
                    logging.info('Повтор размещения через 5 секунд...')
                    time.sleep(5)

            elif self.state == 'WAITING_PING':
                status = self.check_order_status(self.active_order_id)
                logging.info(f'Статус Ping ордера {self.active_order_id}: {status}')
                if status == 'Filled':
                    logging.info('Ping ордер исполнен! Переходим к размещению Pong (Sell)...')
                    self.state = 'PLACING_PONG'
                elif status in ['Cancelled', 'Rejected']:
                    logging.warning('Ping ордер был отменен (возможно, отклонен Post-Only). Пересоздаем...')
                    self.state = 'PLACING_PING'
                time.sleep(3)

            elif self.state == 'PLACING_PONG':
                logging.info('Размещение Pong (Sell) ордера...')
                order_id = self.place_post_only_order('Sell', self.sell_price)
                if order_id:
                    self.active_order_id = order_id
                    self.state = 'WAITING_PONG'
                else:
                    logging.info('Повтор размещения через 5 секунд...')
                    time.sleep(5)

            elif self.state == 'WAITING_PONG':
                status = self.check_order_status(self.active_order_id)
                logging.info(f'Статус Pong ордера {self.active_order_id}: {status}')
                if status == 'Filled':
                    logging.info('Pong ордер исполнен! Цикл завершен. Начинаем заново...')
                    self.state = 'PLACING_PING'
                elif status in ['Cancelled', 'Rejected']:
                    logging.warning('Pong ордер был отменен (возможно, отклонен Post-Only). Пересоздаем...')
                    self.state = 'PLACING_PONG'
                time.sleep(3)

Разбор параметров

  • category: Определяет тип торгового инструмента. Для бессрочных фьючерсов на Bybit V5 используется значение linear.
  • timeInForce='PostOnly': Инструктирует биржу исполнять ордер исключительно как Maker. Если ордер пытается исполниться немедленно по рыночной цене, он автоматически отменяется биржей без взимания комиссий.
  • orderLinkId: Уникальный идентификатор ордера, генерируемый на стороне клиента. Помогает избежать дублирования транзакций при сетевых сбоях.
  • orderStatus: Текущий статус ордера. Мы отслеживаем переход в статус Filled для переключения состояний конечного автомата бота.

Как запустить

Для запуска скрипта вам понадобятся установленная библиотека pybit и API-ключи от тестовой или реальной сети Bybit. Установите зависимость с помощью команды pip install pybit.

Замените значения api_key, api_secret, а также целевой инструмент symbol и ценовые уровни на ваши собственные. Запустите скрипт в терминале. Бот начнет циклическое выставление ордеров: сначала покупка (Ping), затем продажа (Pong).

Для обеспечения безопасности API-ключей на децентрализованных площадках полезно изучить подпись запросов Ed25519 для Jupiter DEX на Python: Гайд.

Оцените статью
FinFluct