Скрипт автоматического ребалансирования криптовалютного портфеля на Python через CCXT

Скрипт автоматического ребалансирования криптовалютного портфеля на Python через CCXT ИНВЕСТИЦИИ, ВКЛАДЫ и СБЕРЕЖЕНИЯ
Пошаговое руководство от Senior Quant Developer по созданию скрипта автоматической ребалансировки криптопортфеля по заданным весам с использованием CCXT.
Суть: Готовый Python-скрипт на базе библиотеки ccxt, который автоматически приводит доли активов в портфеле к заданным целевым весам. Скрипт учитывает минимальные лимиты биржи, округляет объемы до нужной точности и сначала исполняет ордера на продажу, чтобы гарантировать наличие свободной маржи перед покупкой.

Исходный код

Для реализации надежного ребалансировщика на спотовом рынке нам необходимо решить несколько инфраструктурных задач: получить точные балансы, рассчитать текущую стоимость портфеля в базовой валюте (например, USDT), сопоставить текущие веса с целевыми и отправить скорректированные рыночные ордера с учетом шага объема (lot size) и минимальной стоимости ордера (min cost).

Если вы планируете масштабировать систему до высокочастотного отслеживания цен, стандартные REST-запросы могут упереться в лимиты биржи (rate limits). В таких случаях рекомендуется использовать веб-сокеты, о чем подробно рассказано в статье Мультиплексирование WebSocket Binance Futures на Python.

import ccxt
import logging

# Настройка логирования для мониторинга процесса
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger()

def rebalance_portfolio(api_key, api_secret, target_weights, quote_asset='USDT', threshold=0.02):
    # Инициализируем подключение к бирже (в данном примере Binance)
    exchange = ccxt.binance({
        'apiKey': api_key,
        'secret': api_secret,
        'enableRateLimit': True,
        'options': {'defaultType': 'spot'}
    })
    
    logger.info("Загрузка спецификаций рынков...")
    exchange.load_markets()
    
    logger.info("Получение текущих балансов...")
    balance = exchange.fetch_balance()
    
    # Собираем актуальные цены для всех целевых активов к quote_asset
    prices = {}
    for asset in target_weights.keys():
        if asset == quote_asset:
            prices[asset] = 1.0
            continue
        
        symbol = f"{asset}/{quote_asset}"
        if symbol not in exchange.markets:
            logger.error(f"Рыночная пара {symbol} не найдена на бирже!")
            return
            
        ticker = exchange.fetch_ticker(symbol)
        prices[asset] = float(ticker['last'])
    
    # Расчет текущей стоимости портфеля в quote_asset
    total_value = 0.0
    current_values = {}
    for asset in target_weights.keys():
        qty = float(balance.get(asset, {}).get('free', 0.0))
        val = qty * prices[asset]
        current_values[asset] = val
        total_value += val
    
    logger.info(f"Общая стоимость портфеля: {total_value:.2f} {quote_asset}")
    
    orders_to_execute = []
    
    for asset, target_weight in target_weights.items():
        if asset == quote_asset:
            continue
            
        symbol = f"{asset}/{quote_asset}"
        market = exchange.market(symbol)
        
        current_weight = current_values[asset] / total_value if total_value > 0 else 0.0
        weight_diff = target_weight - current_weight
        
        # Проверяем, превышает ли отклонение заданный порог (threshold)
        if abs(weight_diff) < threshold:
            logger.info(f"Ассет {asset} сбалансирован. Текущий вес: {current_weight:.4f}, Целевой: {target_weight:.4f}. Пропуск.")
            continue
            
        target_value = total_value * target_weight
        diff_value = target_value - current_values[asset]
        
        price = prices[asset]
        amount = abs(diff_value) / price
        
        # Округление объема в соответствии с правилами биржи (lot size)
        amount_rounded = exchange.amount_to_precision(symbol, amount)
        amount_fixed = float(amount_rounded)
        
        # Проверка минимальной стоимости ордера (min cost)
        min_cost = market['limits']['cost']['min'] or 10.0
        if (amount_fixed * price) < min_cost:
            logger.info(f"Сделка по {symbol} отменена: объем {amount_fixed * price:.2f} {quote_asset} ниже лимита {min_cost}")
            continue
            
        side = 'buy' if diff_value > 0 else 'sell'
        orders_to_execute.append({
            'symbol': symbol,
            'side': side,
            'amount': amount_fixed,
            'diff_value': diff_value
        })
    
    # Разделяем ордера на продажу и покупку
    sell_orders = [o for o in orders_to_execute if o['side'] == 'sell']
    buy_orders = [o for o in orders_to_execute if o['side'] == 'buy']
    
    # Сначала исполняем продажи, чтобы высвободить quote_asset (USDT)
    for order in sell_orders:
        logger.info(f"Исполнение ордера: SELL {order['amount']} {order['symbol']}")
        try:
            res = exchange.create_market_sell_order(order['symbol'], order['amount'])
            logger.info(f"Ордер на продажу исполнен успешно. ID: {res['id']}")
        except Exception as e:
            logger.error(f"Ошибка при продаже {order['symbol']}: {e}")
            
    # Затем исполняем покупки
    for order in buy_orders:
        logger.info(f"Исполнение ордера: BUY {order['amount']} {order['symbol']}")
        try:
            res = exchange.create_market_buy_order(order['symbol'], order['amount'])
            logger.info(f"Ордер на покупку исполнен успешно. ID: {res['id']}")
        except Exception as e:
            logger.error(f"Ошибка при покупке {order['symbol']}: {e}")
            
    logger.info("Ребалансировка успешно завершена!")

if __name__ == '__main__':
    # Укажите ваши API ключи (рекомендуется использовать переменные окружения)
    API_KEY = 'YOUR_API_KEY'
    API_SECRET = 'YOUR_API_SECRET'
    
    # Сумма весов должна строго равняться 1.0 (включая долю кэша в USDT)
    TARGET_WEIGHTS = {
        'BTC': 0.40,
        'ETH': 0.30,
        'SOL': 0.20,
        'USDT': 0.10
    }
    
    rebalance_portfolio(API_KEY, API_SECRET, TARGET_WEIGHTS, threshold=0.02)

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

  • target_weights: Словарь, где ключами выступают тикеры активов (например, BTC, ETH), а значениями — их целевые доли в портфеле. Сумма всех весов, включая базовую валюту (кэш), должна быть равна 1.0.
  • quote_asset: Базовый актив (обычно стейблкоин вроде USDT или USDC), относительно которого рассчитывается общая стоимость портфеля и через который совершаются все промежуточные сделки.
  • threshold: Порог отклонения (например, 0.02 означает 2%). Если текущий вес актива отличается от целевого менее чем на эту величину, сделка по нему не совершается. Это позволяет экономить на торговых комиссиях при незначительных колебаниях рынка.
  • exchange.amount_to_precision: Встроенный метод библиотеки ccxt, который автоматически округляет расчетный объем сделки до допустимого биржей количества знаков после запятой для конкретного торгового инструмента.

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

Для запуска скрипта вам понадобится установленный интерпретатор Python версии 3.8 или выше, а также библиотека ccxt. Установите ее с помощью пакетного менеджера:

pip install ccxt

После этого замените заглушки YOUR_API_KEY и YOUR_API_SECRET на реальные ключи от вашего аккаунта биржи (убедитесь, что у ключей включены права на спотовую торговлю, но отключен вывод средств в целях безопасности).

Если вы решите перенести данный алгоритм на децентрализованные биржи (DEX) или работать с пулами ликвидности напрямую, вам неизбежно придется столкнуться с парсингом ончейн-данных и обходом блокировок нод. В этом случае изучите руководство Как обойти блокировки по IP при парсинге стакана Raydium на Python.

Для крупных институциональных инфраструктур, где ребалансировка происходит на основе сложных математических моделей в реальном времени, стандартного скрипта по расписанию будет недостаточно. Потребуется построение событийно-ориентированной архитектуры. О том, как связать поток рыночных данных с торговым модулем, читайте в нашей статье Интеграция Kafka для обработки потоковых тиковых данных на Python.

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