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:
| Strategy | Trigger | Best For | Weakness |
|---|---|---|---|
| Calendar-based | Every X days | Simplicity, predictability | Ignores actual drift |
| Threshold-based | When drift exceeds Y% | Responsiveness, cost efficiency | More complex to implement |
| Hybrid | Calendar + threshold | Best of both | Slightly 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
pipand 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
.envfile to GitHub. Use.gitignorereligiously. 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 - [ ]
.envin.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
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.
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.
Sell before you buy. Always sequence your trade execution sells-first to avoid balance shortfalls and unintended leverage.
Backtest, then paper trade, then go live. Skipping either validation step is how traders discover expensive bugs with real money.
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.
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
- CCXT Documentation — the definitive reference for exchange connectivity
- Binance Testnet — free sandbox for testing live trading logic
- DeFiLlama — useful for tracking on-chain portfolio exposure if you expand beyond CEX
- How to Build a Simple Mean Reversion Trading Bot — a complementary guide if you want to combine rebalancing with signal-based entry logic
