Автоматическая ребалансировка портфеля с API AsterDEX

Этот продвинутый туториал покажет, как создать Python-скрипт для автоматической ребалансировки вашего криптовалютного портфеля на бирже AsterDEX.

Что такое ребалансировка и зачем ее автоматизировать?

Ребалансировка портфеля — это процесс восстановления исходного процентного соотношения активов. Например, вы решили, что ваш портфель должен состоять из 50% BTC и 50% ETH. Из-за колебаний рынка через месяц соотношение может стать 60% BTC и 40% ETH. Ребалансировка вернет его к 50/50 путем продажи избытка BTC и покупки недостающего ETH.

Автоматизация этого процесса с помощью API помогает поддерживать дисциплину, экономит время и устраняет эмоциональный фактор из принятия решений.

Необходимые условия:

Шаг 1: Определение целевого портфеля и настройка

В первую очередь, определим нашу идеальную структуру портфеля. Мы также соберем в одном месте все необходимые функции из предыдущего туториала, включая функцию для отправки подписанных запросов (подробнее о ней читайте в гайде по HMAC-аутентификации).

import requests
import hmac
import hashlib
import time

# --- НАСТРОЙКИ ---
API_KEY = "YOUR_API_KEY"
SECRET_KEY = "YOUR_SECRET_KEY"
BASE_URL = "https://fapi.asterdex.com"

# Наша целевая структура портфеля (в сумме должно быть 1.0)
TARGET_ALLOCATION = {
    'BTC': 0.5,  # 50%
    'ETH': 0.3,  # 30%
    'USDT': 0.2  # 20% (стейблкоин для стабильности)
}

# --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ (из прошлого туториала) ---
def send_signed_request(method, path, params=None):
    if params is None:
        params = {}
    timestamp = int(time.time() * 1000)
    params['timestamp'] = timestamp
    query_string = '&'.join([f"{key}={params[key]}" for key in sorted(params)])
    signature = hmac.new(SECRET_KEY.encode('utf-8'), query_string.encode('utf-8'), hashlib.sha256).hexdigest()
    params['signature'] = signature
    url = BASE_URL + path
    headers = {'X-MBX-APIKEY': API_KEY}
    
    try:
        if method.upper() == 'POST':
            response = requests.post(url, headers=headers, params=params)
        elif method.upper() == 'GET':
            response = requests.get(url, headers=headers, params=params)
        else:
            return None
        return response.json()
    except Exception as e:
        print(f"Ошибка при отправке запроса: {e}")
        return None

def place_order(symbol, side, quantity, price=None, order_type="MARKET"):
    path = "/fapi/v1/order"
    params = {
        "symbol": symbol,
        "side": side.upper(),
        "type": order_type.upper(),
        "quantity": f"{quantity:.8f}".rstrip('0').rstrip('.') # Форматирование количества
    }
    if order_type.upper() == "LIMIT":
        params["timeInForce"] = "GTC"
        params["price"] = price

    print(f"Размещение ордера: {side} {quantity} {symbol} по цене {price if price else 'рыночной'}")
    # Для теста используйте '/fapi/v1/order/test'
    # response = send_signed_request('POST', path + '/test', params)
    response = send_signed_request('POST', path, params)
    print("Ответ биржи:", response)
    return response

Шаг 2: Получение текущего баланса и цен

Создадим функции для получения баланса активов и их текущих рыночных цен. Цены нам нужны, чтобы оценить общую стоимость портфеля.

def get_account_balance():
    """Получает баланс и возвращает словарь вида {'BTC': 1.5, 'USDT': 3000}"""
    path = "/fapi/v2/balance"
    balances_raw = send_signed_request('GET', path)
    if not balances_raw:
        return {}

    balances = {item['asset']: float(item['balance']) for item in balances_raw}
    return balances

def get_market_prices():
    """Получает цены и возвращает словарь вида {'BTCUSDT': 60000.0}"""
    path = "/fapi/v1/ticker/price"
    prices_raw = send_signed_request('GET', path)
    if not prices_raw:
        return {}

    prices = {item['symbol']: float(item['price']) for item in prices_raw}
    return prices

Шаг 3: Расчет текущего и целевого распределения

Это ядро нашего скрипта. Здесь мы вычисляем общую стоимость портфеля в USDT и сравниваем текущее распределение с целевым.

def calculate_allocations():
    balances = get_account_balance()
    prices = get_market_prices()

    if not balances or not prices:
        print("Не удалось получить данные о балансе или ценах.")
        return None

    # Оставляем только те активы, которые есть в нашей целевой структуре
    portfolio = {asset: amount for asset, amount in balances.items() if asset in TARGET_ALLOCATION}
    
    # Расчет общей стоимости портфеля в USDT
    total_portfolio_value_usdt = 0
    for asset, amount in portfolio.items():
        if asset == 'USDT':
            total_portfolio_value_usdt += amount
        else:
            # Находим цену актива по отношению к USDT
            symbol = f"{asset}USDT"
            if symbol in prices:
                total_portfolio_value_usdt += amount * prices[symbol]
    
    if total_portfolio_value_usdt == 0:
        print("Общая стоимость портфеля равна нулю.")
        return None

    print(f"
Общая стоимость портфеля: ${total_portfolio_value_usdt:.2f}")

    # Расчет текущего и целевого распределения в USDT
    current_alloc_values = {}
    target_alloc_values = {}
    for asset, target_pct in TARGET_ALLOCATION.items():
        # Целевая стоимость
        target_value = total_portfolio_value_usdt * target_pct
        target_alloc_values[asset] = target_value

        # Текущая стоимость
        current_value = 0
        if asset == 'USDT':
            current_value = portfolio.get(asset, 0)
        else:
            symbol = f"{asset}USDT"
            if symbol in prices:
                current_value = portfolio.get(asset, 0) * prices[symbol]
        current_alloc_values[asset] = current_value

        print(f"Актив {asset}:")
        print(f"  Текущая стоимость: ${current_value:.2f} ({(current_value / total_portfolio_value_usdt) * 100:.2f}%)")
        print(f"  Целевая стоимость: ${target_value:.2f} ({target_pct * 100:.2f}%)")

    return {
        "current": current_alloc_values,
        "target": target_alloc_values,
        "prices": prices
    }

Шаг 4: Генерация и выполнение ордеров для ребалансировки

Теперь, зная разницу между текущим и целевым состоянием, мы можем сгенерировать ордера. Логика проста: продаем избыточные активы, а на вырученные USDT покупаем недостающие.

ВНИМАНИЕ: Этот скрипт будет исполнять реальные сделки. Начните с очень маленьких сумм или используйте тестовый эндпоинт, как показано в функции `place_order`.

def rebalance_portfolio():
    allocations = calculate_allocations()
    if not allocations:
        return

    current_values = allocations['current']
    target_values = allocations['target']
    prices = allocations['prices']

    # --- Шаг 1: Продаем избыточные активы (кроме USDT) ---
    print("
--- Продажа избыточных активов ---")
    for asset, current_value in current_values.items():
        if asset == 'USDT':
            continue
        
        target_value = target_values[asset]
        if current_value > target_value:
            # Считаем, сколько нужно продать
            value_to_sell = current_value - target_value
            symbol = f"{asset}USDT"
            if symbol in prices:
                amount_to_sell = value_to_sell / prices[symbol]
                # Здесь нужно добавить проверку на минимальный размер ордера
                print(f"Планируем продать {amount_to_sell:.6f} {asset}")
                place_order(symbol, "SELL", amount_to_sell)
    
    # --- Шаг 2: Покупаем недостающие активы (кроме USDT) ---
    # (Нужно подождать, пока ордера на продажу исполнятся и баланс USDT обновится)
    print("
Ожидание исполнения ордеров на продажу (в реальном боте здесь нужна проверка статуса)...")
    time.sleep(10) # Простое ожидание

    # Обновляем баланс USDT
    updated_balances = get_account_balance()
    usdt_balance = updated_balances.get('USDT', 0)
    print(f"Обновленный баланс USDT: {usdt_balance}")

    print("
--- Покупка недостающих активов ---")
    for asset, current_value in current_values.items():
         if asset == 'USDT':
            continue

         target_value = target_values[asset]
         if current_value < target_value:
            value_to_buy = target_value - current_value
            if usdt_balance >= value_to_buy:
                symbol = f"{asset}USDT"
                if symbol in prices:
                    amount_to_buy = value_to_buy / prices[symbol]
                    print(f"Планируем купить {amount_to_buy:.6f} {asset}")
                    place_order(symbol, "BUY", amount_to_buy)
                    usdt_balance -= value_to_buy # Уменьшаем доступный баланс
            else:
                print(f"Недостаточно USDT для покупки {asset}")

# Запуск ребалансировки
rebalance_portfolio()

Заключение и важные доработки

Этот скрипт является мощной основой для автоматической ребалансировки. Однако для реального использования его необходимо серьезно доработать.

Ключевые улучшения:

  • Проверка минимального размера ордера: Каждая торговая пара имеет фильтр `MIN_NOTIONAL` (минимальная стоимость ордера). Перед размещением ордера нужно убедиться, что `количество * цена > MIN_NOTIONAL`.
  • Обработка комиссий: Торговые комиссии уменьшают итоговый баланс. Их нужно учитывать в расчетах.
  • Умное ожидание ордеров: Вместо `time.sleep()` нужно циклически проверять статус отправленных ордеров через API, чтобы приступать к покупке только после их полного исполнения.
  • Логирование: Подробно записывайте все действия скрипта в файл для последующего анализа и отладки.