How to Build a Portfolio Rebalancing Bot Using Python
intermediateAgent Building

How to Build a Portfolio Rebalancing Bot Using Python

April 24, 2026 · 11 min read
Key Takeaways
  • Threshold-based rebalancing outperforms calendar-based rebalancing in high-volatility crypto markets because it responds to actual drift, not arbitrary time intervals.
  • Always account for trading fees and slippage before executing a rebalance — small portfolios can lose more to transaction costs than they gain from rebalancing.
  • Backtesting your rebalancing logic on historical data is non-negotiable before running any bot with real funds.
  • Use paper trading mode to validate your bot's live behavior for at least two weeks before switching to production.
  • API key security and rate limit handling are the two most common failure points in beginner Python trading bots.

If you've ever held a crypto portfolio for more than a few months, you've watched your carefully planned allocations turn into something you barely recognize. Bitcoin runs 40%, your altcoin bag gets crushed, and suddenly what was a balanced portfolio looks more like a concentrated BTC bet. Portfolio rebalancing fixes this — but doing it manually is tedious, emotionally fraught, and easy to procrastinate on.

This guide walks you through how to build a portfolio rebalancing bot using Python. We'll cover strategy design, connecting to exchange APIs, writing the core rebalancing logic, handling fees, and backtesting the whole thing before you risk real capital. You don't need to be a software engineer, but you should be comfortable with basic Python and have used a crypto exchange before.


What Automated Portfolio Rebalancing Actually Does

Rebalancing sounds simple: sell what grew too much, buy what shrank. But the mechanics matter enormously.

Think of it like a chef keeping a mise en place. You've decided your kitchen should be 50% proteins, 30% vegetables, 20% starches. After a busy service, you're running low on vegetables and drowning in leftover protein. Rebalancing is restocking — getting back to your intended ratios before the next service. Do it too rarely and your portfolio drifts far from your risk profile. Do it too often and you're paying transaction costs constantly.

The two dominant strategies for automated portfolio rebalancing crypto setups are:

StrategyTriggerBest ForWeakness
Calendar-basedEvery X daysSimplicity, predictabilityIgnores actual drift
Threshold-basedWhen drift exceeds Y%Responsiveness, cost efficiencyMore complex to implement
HybridCalendar + thresholdBest of bothSlightly more code

Most experienced quants prefer threshold-based or hybrid approaches. Calendar rebalancing on a weekly schedule might force a rebalance when nothing meaningful has drifted — you'd be paying fees for nothing.


Prerequisites and Setup

Before writing a single line of rebalancing logic, get your environment right.

What you need:

  • Python 3.10+
  • A Binance, Coinbase Advanced, or Kraken account with API access enabled
  • Basic familiarity with pip and virtual environments
  • An understanding of slippage and maker vs taker fees — these will eat your returns if ignored

Install the core libraries:

pip install ccxt pandas numpy python-dotenv schedule

CCXT is the workhorse here — it provides a unified interface to 100+ exchanges, so you write the code once and can switch exchanges without rewriting your logic. schedule handles the periodic execution. python-dotenv keeps your API keys out of your source code.

Set up your .env file:

EXCHANGE_ID=binance
API_KEY=your_api_key_here
API_SECRET=your_api_secret_here

Critical warning: Never hardcode API keys in your Python scripts. Never commit your .env file to GitHub. Use .gitignore religiously. A leaked key with withdrawal permissions means total fund loss — there's no recourse.


Step 1: Define Your Target Allocations

Start with a clear allocation config. This is the single source of truth your bot will always try to return to.

# config.py

TARGET_ALLOCATIONS = {
    "BTC/USDT": 0.40,   # 40%
    "ETH/USDT": 0.30,   # 30%
    "SOL/USDT": 0.20,   # 20%
    "USDT":     0.10,   # 10% cash buffer
}

REBALANCE_THRESHOLD = 0.05   # Trigger rebalance if any asset drifts >5%
MIN_TRADE_USD = 10.0          # Don't execute trades smaller than $10
FEE_RATE = 0.001              # 0.1% taker fee (adjust per exchange)

The REBALANCE_THRESHOLD of 5% is a reasonable starting point. Go below 2% and you'll be trading constantly. Go above 10% and your portfolio can drift significantly before the bot acts. I've seen traders use 3% thresholds on volatile altcoin portfolios and spend more on fees than they ever recovered in rebalancing gains — don't underestimate execution risk.


Step 2: Connect to the Exchange and Fetch Portfolio State

# exchange.py

import ccxt
import os
from dotenv import load_dotenv

load_dotenv()

def get_exchange():
    exchange_class = getattr(ccxt, os.getenv("EXCHANGE_ID"))
    return exchange_class({
        "apiKey": os.getenv("API_KEY"),
        "secret": os.getenv("API_SECRET"),
        "enableRateLimit": True,
    })

def get_portfolio_value(exchange, target_pairs):
    """Returns current holdings in USD and total portfolio value."""
    balance = exchange.fetch_balance()
    prices = {}
    holdings = {}

    for pair in target_pairs:
        if pair == "USDT":
            prices["USDT"] = 1.0
            holdings["USDT"] = balance["free"].get("USDT", 0)
            continue
        
        ticker = exchange.fetch_ticker(pair)
        asset = pair.split("/")[0]
        price = ticker["last"]
        prices[pair] = price
        holdings[pair] = balance["free"].get(asset, 0) * price

    total_value = sum(holdings.values())
    return holdings, total_value, prices

enableRateLimit: True is not optional. Exchanges enforce rate limits, and a bot that hammers the API will get IP-banned. CCXT handles the throttling automatically when this flag is set.


Step 3: Calculate Drift and Determine What Needs Rebalancing

This is the core logic. You compare current weights against target weights and identify which assets are over- or underweight.

# rebalancer.py

from config import TARGET_ALLOCATIONS, REBALANCE_THRESHOLD, MIN_TRADE_USD

def calculate_drift(holdings, total_value):
    """Returns current weights and drift from targets."""
    current_weights = {asset: value / total_value for asset, value in holdings.items()}
    drift = {}

    for asset, target_weight in TARGET_ALLOCATIONS.items():
        current = current_weights.get(asset, 0)
        drift[asset] = current - target_weight

    return current_weights, drift

def needs_rebalance(drift):
    """Returns True if any asset has drifted beyond the threshold."""
    return any(abs(d) > REBALANCE_THRESHOLD for d in drift.values())

def calculate_trades(drift, total_value):
    """Returns a dict of {asset: usd_amount} — positive means buy, negative means sell."""
    trades = {}
    for asset, d in drift.items():
        usd_delta = -d * total_value   # negative drift = underweight = need to buy
        if abs(usd_delta) >= MIN_TRADE_USD:
            trades[asset] = usd_delta
    return trades

The sign convention matters. If BTC's current weight is 0.45 and target is 0.40, drift is +0.05 — it's overweight. usd_delta becomes negative, meaning sell. Underweight assets get positive deltas, meaning buy.


Step 4: Execute Trades With Fee Awareness

Most Python rebalancing bot tutorials skip fee handling. That's a mistake. On a $10,000 portfolio rebalancing monthly with a 0.1% taker fee across four assets, you're spending roughly $120/year in fees alone — before considering slippage on less liquid pairs.

# executor.py

import ccxt
from config import FEE_RATE

def execute_trades(exchange, trades, prices):
    """Execute sell orders first, then buys, to ensure USDT liquidity."""
    results = []

    # Sort: sells first (negative USD delta), then buys
    sorted_trades = sorted(trades.items(), key=lambda x: x[1])

    for asset, usd_amount in sorted_trades:
        if asset == "USDT":
            continue   # Can't "trade" USDT directly

        pair = asset  # e.g. "BTC/USDT"
        price = prices[pair]
        # Apply fee adjustment to buy amounts
        adjusted_amount = abs(usd_amount) * (1 - FEE_RATE)
        qty = adjusted_amount / price

        try:
            if usd_amount < 0:   # Sell
                order = exchange.create_market_sell_order(pair, qty)
                results.append({"pair": pair, "side": "sell", "qty": qty, "order": order})
            else:                # Buy
                order = exchange.create_market_buy_order(pair, qty)
                results.append({"pair": pair, "side": "buy", "qty": qty, "order": order})
        except ccxt.BaseError as e:
            print(f"Order failed for {pair}: {e}")

    return results

Sell before you buy. Always. If you buy first and your sells fail, you've either exceeded your balance or created an unintended leveraged position. The sell-first ordering is a simple guardrail that most beginner bots miss.


Step 5: Add the Scheduler and Main Loop

# main.py

import schedule
import time
from exchange import get_exchange, get_portfolio_value
from rebalancer import calculate_drift, needs_rebalance, calculate_trades
from executor import execute_trades
from config import TARGET_ALLOCATIONS

def run_rebalance():
    print("Running portfolio check...")
    exchange = get_exchange()
    
    holdings, total_value, prices = get_portfolio_value(exchange, TARGET_ALLOCATIONS.keys())
    current_weights, drift = calculate_drift(holdings, total_value)
    
    print(f"Portfolio value: ${total_value:.2f}")
    for asset, d in drift.items():
        print(f"  {asset}: drift={d:.2%}")

    if needs_rebalance(drift):
        print("Drift threshold exceeded — executing rebalance...")
        trades = calculate_trades(drift, total_value)
        results = execute_trades(exchange, trades, prices)
        print(f"Completed {len(results)} trades.")
    else:
        print("Portfolio within tolerance. No action needed.")

# Run every 4 hours
schedule.every(4).hours.do(run_rebalance)

if __name__ == "__main__":
    run_rebalance()   # Run immediately on start
    while True:
        schedule.run_pending()
        time.sleep(60)

Four-hour intervals work well as a starting cadence for most crypto portfolios. You catch meaningful moves without over-trading. Adjust based on your assets' volatility and your fee tolerance.


Step 6: Backtesting Your Rebalancing Strategy

Don't skip this. Running a bot live without backtesting is the equivalent of driving a car you've never started before onto a highway at night.

A proper backtesting strategy for a rebalancer tests your threshold settings, fee drag, and drift frequency against historical price data. Here's a simplified backtester using historical OHLCV data from CCXT:

# backtest.py

import ccxt
import pandas as pd

def fetch_historical_prices(exchange, pairs, timeframe='1d', limit=365):
    """Fetch one year of daily closes for each pair."""
    price_data = {}
    for pair in pairs:
        ohlcv = exchange.fetch_ohlcv(pair, timeframe=timeframe, limit=limit)
        df = pd.DataFrame(ohlcv, columns=['timestamp','open','high','low','close','volume'])
        price_data[pair] = df.set_index('timestamp')['close']
    return pd.DataFrame(price_data)

def backtest_rebalancer(prices_df, target_alloc, threshold, fee_rate, initial_capital=10000):
    portfolio_value = initial_capital
    holdings = {asset: (weight * initial_capital) / prices_df[asset].iloc[0]
                for asset, weight in target_alloc.items()}
    
    trade_log = []
    
    for date, prices in prices_df.iterrows():
        values = {asset: qty * prices[asset] for asset, qty in holdings.items()}
        total = sum(values.values())
        weights = {asset: v / total for asset, v in values.items()}
        
        max_drift = max(abs(weights[a] - target_alloc[a]) for a in target_alloc)
        
        if max_drift > threshold:
            fee_cost = total * fee_rate * len(target_alloc)
            total -= fee_cost
            # Reset to target weights
            holdings = {asset: (weight * total) / prices[asset]
                       for asset, weight in target_alloc.items()}
            trade_log.append({"date": date, "total_after_fees": total})
    
    final_values = {asset: qty * prices_df[asset].iloc[-1] for asset, qty in holdings.items()}
    return sum(final_values.values()), trade_log

Run this against multiple threshold values — try 2%, 5%, 8%, 10% — and compare final portfolio values and trade counts. You're looking for the threshold that maximizes returns after fees. You might also want to cross-reference your maximum drawdown across different threshold settings, since aggressive rebalancing during a crash can accelerate losses.

For more rigorous validation, pair this with walk-forward analysis to avoid overfitting your threshold to a specific historical period.


Step 7: Paper Trading Before Going Live

Before you touch real funds, run your bot in paper trading mode for at least two weeks. Paper trading simulates order execution without real money — most exchanges support this via testnet environments or sandbox APIs.

Binance testnet: https://testnet.binance.vision/

To switch your bot to testnet, modify the exchange initialization:

exchange = ccxt.binance({
    "apiKey": os.getenv("TESTNET_API_KEY"),
    "secret": os.getenv("TESTNET_API_SECRET"),
    "urls": {
        "api": {
            "public": "https://testnet.binance.vision/api",
            "private": "https://testnet.binance.vision/api",
        }
    }
})

Watch for unexpected behaviors: orders not filling at expected prices, rate limit hits, or assets with insufficient liquidity. These issues are far more common than people expect, and you want to find them with fake money.


Common Pitfalls and How to Avoid Them

Myth: Rebalancing always improves returns Reality: In trending markets, rebalancing can hurt you. If BTC is in a sustained bull run, you'll keep selling BTC to buy underperforming assets. Rebalancing optimizes for your target risk profile, not maximum returns.

Myth: Tighter thresholds always beat looser ones Reality: Below ~3% thresholds on a portfolio with taker fees, transaction costs frequently exceed rebalancing benefits. Run the backtest numbers before committing to any threshold.

Myth: Bigger portfolios don't need minimum trade sizes Reality: Position sizing matters at every scale. Micro-trades waste fees and can create odd lot issues on some exchanges.


Advanced Extensions Worth Considering

Once your basic bot is running cleanly, here are extensions that meaningfully improve performance:

  • Volatility-adjusted thresholds — use realized volatility to dynamically widen thresholds during high-vol periods and tighten them when markets are calm
  • Tax-loss harvesting logic — track cost basis and prioritize selling positions with unrealized losses to offset gains
  • Multi-exchange support — CCXT makes this straightforward; holding assets across exchanges improves execution risk management
  • Sharpe-optimized allocations — instead of fixed targets, recalculate target weights monthly using trailing Sharpe ratio data
  • Notification hooks — integrate Telegram or Discord bots to receive alerts when rebalances fire or errors occur

If you want to understand how more complex agent architectures handle multi-signal decision trees, the comparison of rule-based vs reinforcement learning frameworks is worth reading — the same decision logic concepts apply directly to rebalancing trigger design.

Also consider how your bot performs across different market regimes. A threshold that works in sideways markets might underperform badly in trending ones — this analysis on agent-based trading systems in volatile vs stable markets covers the mechanics well.


Security Checklist Before Deployment

Before running this bot with real funds:

  • [ ] API keys stored in .env, not hardcoded
  • [ ] .env in .gitignore
  • [ ] API permissions set to trade-only (no withdrawals enabled)
  • [ ] IP whitelist enabled on your exchange API settings
  • [ ] Bot tested on testnet for minimum two weeks
  • [ ] Error handling in place — all order calls wrapped in try/except
  • [ ] Logging enabled — you need a paper trail of every trade
  • [ ] Circuit breaker logic — halt the bot if portfolio value drops more than X% in a single session

The multi-signature wallet pattern isn't directly applicable to an exchange-based bot, but the underlying principle — don't give any single system unlimited access — absolutely is. Trade-only API keys with IP restrictions are your first line of defense.


Key Takeaways

  1. Threshold-based rebalancing beats calendar-based in crypto because markets move on momentum, not schedules. A 5% drift threshold is a solid starting point for most portfolios.

  2. Fees will kill you if you ignore them. Always model fee drag in your backtest before choosing a threshold. Run the numbers at 0.05%, 0.1%, and 0.25% fee rates to understand your actual break-even rebalance frequency.

  3. Sell before you buy. Always sequence your trade execution sells-first to avoid balance shortfalls and unintended leverage.

  4. Backtest, then paper trade, then go live. Skipping either validation step is how traders discover expensive bugs with real money.

  5. Secure your API keys like they're private keys. A compromised trading API key with no withdrawal limit is almost as dangerous as a compromised wallet.

  6. Rebalancing manages risk, not returns. The goal is maintaining your intended exposure profile — not maximizing performance. If you want the bot to also chase alpha, you're building something more complex than a rebalancer.


Further Reading