Реализация Grid Bot для Bybit на Python: Гайд Quant-разработчика

Реализация Grid Bot для Bybit на Python: Гайд Quant-разработчика Python & API
Пошаговое руководство по созданию асинхронного сеточного торгового робота (Grid Bot) для биржи Bybit V5 на Python с использованием aiohttp.
Суть: Пошаговое руководство по созданию асинхронного сеточного торгового робота (Grid Bot) для биржи Bybit (API v5) на Python. Код реализует автоматическое выставление лимитных ордеров на покупку и продажу вокруг текущей рыночной цены с динамическим отслеживанием их исполнения и перевыставлением сетки.

Исходный код

Сеточный маркет-мейкинг (Grid Trading) — одна из наиболее устойчивых алгоритмических стратегий, которая извлекает прибыль из колебаний цены в боковом тренде (флэте). В этой статье мы разберем профессиональную асинхронную реализацию Grid-бота для линейных деривативов Bybit V5 с использованием библиотеки aiohttp для максимальной производительности и минимального latency.

Для оптимизации работы с ордерами в высокочастотных сценариях крайне важно уметь быстро очищать стакан. Рекомендуем ознакомиться с нашей статьей про Пакетная отмена ордеров (Batch Cancel) на Bybit V5 через asyncio.gather. Если вы планируете масштабировать систему на несколько аккаунтов или обходить лимиты биржи по rate-limits, вам поможет Настройка ротации API-ключей Binance для HFT на Python. Также не забывайте, что при торговле бессрочными контрактами критически важно учитывать издержки на финансирование — подробнее об этом читайте в материале Парсинг истории фандинга OKX на Python Asyncio: Гайд.

import asyncio
import time
import hmac
import hashlib
import json
import aiohttp
import logging

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

class BybitGridBot:
    def __init__(self, api_key: str, api_secret: str, symbol: str, grid_step_pct: float, grid_levels: int, qty_per_level: float, is_testnet: bool = True):
        self.api_key = api_key
        self.api_secret = api_secret
        self.symbol = symbol
        self.grid_step_pct = grid_step_pct
        self.grid_levels = grid_levels
        self.qty_per_level = qty_per_level
        self.base_url = "https://api-testnet.bybit.com" if is_testnet else "https://api.bybit.com"
        self.session = None
        self.active_orders = {}

    async def init_session(self):
        self.session = aiohttp.ClientSession()

    async def close_session(self):
        if self.session:
            await self.session.close()

    def _generate_signature(self, timestamp: str, recv_window: str, payload: str) -> str:
        param_str = timestamp + self.api_key + recv_window + payload
        return hmac.new(self.api_secret.encode('utf-8'), param_str.encode('utf-8'), hashlib.sha256).hexdigest()

    async def _request(self, method: str, path: str, data: dict = None) -> dict:
        url = f"{self.base_url}{path}"
        timestamp = str(int(time.time() * 1000))
        recv_window = "5000"
        payload = json.dumps(data) if data else ""
        
        signature = self._generate_signature(timestamp, recv_window, payload)
        
        headers = {
            "X-BAPI-API-KEY": self.api_key,
            "X-BAPI-SIGN": signature,
            "X-BAPI-SIGN-TYPE": "2",
            "X-BAPI-TIMESTAMP": timestamp,
            "X-BAPI-RECV-WINDOW": recv_window,
            "Content-Type": "application/json"
        }
        
        async with self.session.request(method, url, headers=headers, data=payload) as response:
            result = await response.json()
            if result.get("retCode") != 0:
                logging.error(f"API Error: {result.get('retMsg')} (Code: {result.get('retCode')})")
            return result

    async def get_ticker_price(self) -> float:
        path = f"/v5/market/tickers?category=linear&symbol={self.symbol}"
        res = await self._request("GET", path)
        return float(res["result"]["list"][0]["lastPrice"])

    async def cancel_all_orders(self):
        logging.info("Cancelling all active orders...")
        path = "/v5/order/cancel-all"
        payload = {"category": "linear", "symbol": self.symbol}
        await self._request("POST", path, payload)

    async def place_order(self, side: str, price: float, qty: float) -> str:
        path = "/v5/order/create"
        payload = {
            "category": "linear",
            "symbol": self.symbol,
            "side": side,
            "orderType": "Limit",
            "qty": str(qty),
            "price": f"{price:.4f}",
            "timeInForce": "GTC"
        }
        res = await self._request("POST", path, payload)
        if res.get("retCode") == 0:
            order_id = res["result"]["orderId"]
            logging.info(f"Placed {side} order at {price} | ID: {order_id}")
            return order_id
        return None

    async def setup_grid(self):
        await self.cancel_all_orders()
        self.active_orders.clear()
        
        current_price = await self.get_ticker_price()
        logging.info(f"Current price of {self.symbol}: {current_price}")
        
        for i in range(1, self.grid_levels + 1):
            price = current_price * (1 - self.grid_step_pct * i)
            order_id = await self.place_order("Buy", price, self.qty_per_level)
            if order_id:
                self.active_orders[order_id] = {"side": "Buy", "price": price, "level": i}
        
        for i in range(1, self.grid_levels + 1):
            price = current_price * (1 + self.grid_step_pct * i)
            order_id = await self.place_order("Sell", price, self.qty_per_level)
            if order_id:
                self.active_orders[order_id] = {"side": "Sell", "price": price, "level": i}

    async def monitor_grid(self):
        logging.info("Starting grid monitoring loop...")
        while True:
            try:
                await asyncio.sleep(5)
                path = f"/v5/order/realtime?category=linear&symbol={self.symbol}"
                res = await self._request("GET", path)
                
                if res.get("retCode") != 0:
                    continue
                    
                server_orders = {o["orderId"]: o["orderStatus"] for o in res["result"]["list"]}
                
                filled_orders = []
                for order_id, local_info in list(self.active_orders.items()):
                    status = server_orders.get(order_id)
                    
                    if status == "Filled" or (status is None and order_id not in server_orders):
                        logging.info(f"Order {order_id} ({local_info['side']} at {local_info['price']}) FILLED!")
                        filled_orders.append(order_id)
                        
                for order_id in filled_orders:
                    old_info = self.active_orders.pop(order_id)
                    
                    new_side = "Sell" if old_info["side"] == "Buy" else "Buy"
                    multiplier = (1 + self.grid_step_pct) if new_side == "Sell" else (1 - self.grid_step_pct)
                    new_price = old_info["price"] * multiplier
                    
                    new_order_id = await self.place_order(new_side, new_price, self.qty_per_level)
                    if new_order_id:
                        self.active_orders[new_order_id] = {
                            "side": new_side,
                            "price": new_price,
                            "level": old_info["level"]
                        }
                        
            except Exception as e:
                logging.error(f"Error in monitor loop: {e}")
                await asyncio.sleep(10)

async def main():
    API_KEY = "YOUR_API_KEY"
    API_SECRET = "YOUR_API_SECRET"
    SYMBOL = "BTCUSDT"
    
    bot = BybitGridBot(
        api_key=API_KEY,
        api_secret=API_SECRET,
        symbol=SYMBOL,
        grid_step_pct=0.002,
        grid_levels=5,
        qty_per_level=0.001,
        is_testnet=True
    )
    
    await bot.init_session()
    try:
        await bot.setup_grid()
        await bot.monitor_grid()
    finally:
        await bot.close_session()

if __name__ == "__main__":
    asyncio.run(main())

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

  • api_key / api_secret: Ключи доступа к API Bybit V5 с правами на торговлю деривативами.
  • symbol: Торговый инструмент (например, BTCUSDT). Бот настроен на работу с линейными бессрочными контрактами (Linear Perpetual).
  • grid_step_pct: Шаг сетки в процентах, выраженный в виде десятичной дроби (например, 0.002 соответствует 0.2%). Определяет расстояние между уровнями сетки.
  • grid_levels: Количество уровней сетки в каждую сторону (вверх и вниз) от стартовой цены. При значении 5 бот выставит 5 лимитных ордеров на покупку и 5 на продажу.
  • qty_per_level: Объем базового актива в каждом ордере (например, 0.001 BTC).
  • is_testnet: Булевый флаг. Если равен True, бот подключается к тестовой сети Bybit, если False — к реальной бирже.

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

Для запуска бота выполните следующие шаги:

1. Установите библиотеку aiohttp для работы с асинхронными HTTP-запросами:

pip install aiohttp

2. Получите API-ключи в личном кабинете Bybit (рекомендуется использовать Testnet для безопасного тестирования).

3. Скопируйте исходный код в файл grid_bot.py и замените значения YOUR_API_KEY и YOUR_API_SECRET в функции main() на ваши реальные ключи.

4. Запустите скрипт из терминала:

python grid_bot.py

Бот автоматически отменит все активные ордера по выбранному инструменту, рассчитает уровни сетки относительно текущей рыночной цены, выставит лимитные ордера и перейдет в режим асинхронного мониторинга. При исполнении ордера на покупку бот автоматически выставит ордер на продажу на уровень выше, фиксируя прибыль.

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