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.