DEX · Asterdex

Automatic Portfolio Rebalancing with the AsterDEX API

This advanced tutorial shows how to build a Python script that automatically rebalances your crypto portfolio on the AsterDEX exchange.

What Is Rebalancing and Why Automate It?

Portfolio rebalancing is the process of restoring the original percentage allocation of assets. For example, you decided your portfolio should consist of 50% BTC and 50% ETH. Due to market fluctuations, a month later the ratio may drift to 60% BTC and 40% ETH. Rebalancing brings it back to 50/50 by selling the excess BTC and buying the missing ETH.

Automating this process via the API helps maintain discipline, saves time and removes the emotional factor from decision-making.

Prerequisites:

  • Knowledge from our Python bot tutorial, especially the function for sending signed requests.
  • Python installed along with the `requests` library.

Step 1: Defining the Target Portfolio and Setup

First of all, let's define our ideal portfolio structure. We will also gather all the necessary functions from the previous tutorial in one place, including the function for sending signed requests (read more about it in our HMAC authentication guide).

import requests
import hmac
import hashlib
import time

# --- SETTINGS ---
API_KEY = "YOUR_API_KEY"
SECRET_KEY = "YOUR_SECRET_KEY"
BASE_URL = "https://fapi.asterdex.com"

# Our target portfolio allocation (must sum up to 1.0)
TARGET_ALLOCATION = {
    'BTC': 0.5,  # 50%
    'ETH': 0.3,  # 30%
    'USDT': 0.2  # 20% (stablecoin for stability)
}

# --- HELPER FUNCTIONS (from the previous tutorial) ---
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('.') # Formatting the quantity
    }
    if order_type.upper() == "LIMIT":
        params["timeInForce"] = "GTC"
        params["price"] = price

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

Step 2: Fetching Balances and Prices

Let's create functions for fetching asset balances and their current market prices. We need the prices to estimate the total portfolio value.

def get_account_balance():
    """Fetches balances and returns a dict such as {'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():
    """Fetches prices and returns a dict such as {'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

Step 3: Calculating Current and Target Allocations

This is the core of our script. Here we compute the total portfolio value in USDT and compare the current allocation with the target one.

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

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

    # Keep only the assets present in our target allocation
    portfolio = {asset: amount for asset, amount in balances.items() if asset in TARGET_ALLOCATION}

    # Calculating the total portfolio value in USDT
    total_portfolio_value_usdt = 0
    for asset, amount in portfolio.items():
        if asset == 'USDT':
            total_portfolio_value_usdt += amount
        else:
            # Look up the asset price relative to 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}")

    # Calculating current and target allocations in USDT
    current_alloc_values = {}
    target_alloc_values = {}
    for asset, target_pct in TARGET_ALLOCATION.items():
        # Target value
        target_value = total_portfolio_value_usdt * target_pct
        target_alloc_values[asset] = target_value

        # Current 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
    }

Step 4: Generating and Executing Rebalancing Orders

Now that we know the difference between the current and target state, we can generate orders. The logic is simple: sell overweight assets and use the resulting USDT to buy underweight ones.

WARNING: This script will execute real trades. Start with very small amounts or use the test endpoint as shown in the `place_order` function.

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

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

    # --- Step 1: Selling overweight assets (except USDT) ---
    print("
--- Продажа избыточных активов ---")
    for asset, current_value in current_values.items():
        if asset == 'USDT':
            continue

        target_value = target_values[asset]
        if current_value > target_value:
            # Calculate how much needs to be sold
            value_to_sell = current_value - target_value
            symbol = f"{asset}USDT"
            if symbol in prices:
                amount_to_sell = value_to_sell / prices[symbol]
                # A minimum order size check should be added here
                print(f"Планируем продать {amount_to_sell:.6f} {asset}")
                place_order(symbol, "SELL", amount_to_sell)

    # --- Step 2: Buying underweight assets (except USDT) ---
    # (We need to wait until the sell orders fill and the USDT balance updates)
    print("
Ожидание исполнения ордеров на продажу (в реальном боте здесь нужна проверка статуса)...")
    time.sleep(10) # Simple wait

    # Refreshing the USDT balance
    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 # Reducing the available balance
            else:
                print(f"Недостаточно USDT для покупки {asset}")

# Running the rebalancing
rebalance_portfolio()

Conclusion and Important Improvements

This script is a powerful foundation for automatic rebalancing. For real-world use, however, it needs significant hardening.

Key improvements:

  • Minimum order size validation: Every trading pair has a `MIN_NOTIONAL` filter (minimum order value). Before placing an order, make sure `quantity * price > MIN_NOTIONAL`.
  • Fee handling: Trading fees reduce the final balance. They must be factored into your calculations.
  • Smart order waiting: Instead of `time.sleep()`, poll the status of submitted orders via the API in a loop, proceeding to purchases only after they are fully filled.
  • Logging: Write every action the script takes to a file in detail for later analysis and debugging.
---