Build a Portfolio Rebalancing Bot with Python and Alpaca API

Last month I noticed my portfolio had drifted 12% off target allocation. Tech was at 45% instead of 30%, bonds had dropped to 8%. I’d been meaning to rebalance for weeks but kept putting it off. So I spent a Saturday afternoon writing a Python script that does it automatically — and it’s been running every Monday morning since.

Here’s exactly how I built it, what went wrong, and why I ended up preferring Alpaca’s API over the alternatives I tried.

Why Automate Rebalancing?

Manual rebalancing has two problems: you forget to do it, and when you do remember, emotions get in the way. “NVDA is up 40% — maybe I should let it ride?” That’s not a strategy, that’s gambling with extra steps.

A rebalancing bot doesn’t care about feelings. It sells what’s overweight, buys what’s underweight, and moves on. Studies from Vanguard show that disciplined rebalancing adds roughly 0.35% annually in risk-adjusted returns. Not huge, but it compounds.

The Setup: Alpaca + Python in 50 Lines

I picked Alpaca because it offers commission-free trading with a proper REST API. No screen scraping, no Selenium hacks. You get a paper trading environment that mirrors production exactly — same endpoints, same response formats.

First, install the SDK:

pip install alpaca-trade-api pandas

Here’s the core logic. It’s shorter than you’d expect:

import alpaca_trade_api as tradeapi
import pandas as pd

# Target allocation (adjust these to your strategy)
TARGET = {
    'SPY': 0.40,   # S&P 500
    'QQQ': 0.20,   # Nasdaq
    'TLT': 0.15,   # Long-term bonds
    'GLD': 0.10,   # Gold
    'VWO': 0.10,   # Emerging markets
    'BIL': 0.05,   # Short-term treasury (cash-like)
}

api = tradeapi.REST(
    key_id='your-key',
    secret_key='your-secret',
    base_url='https://paper-api.alpaca.markets'  # paper first!
)

def get_current_allocation():
    account = api.get_account()
    portfolio_value = float(account.portfolio_value)
    positions = {p.symbol: float(p.market_value) 
                 for p in api.list_positions()}
    return {sym: positions.get(sym, 0) / portfolio_value 
            for sym in TARGET}

def rebalance():
    account = api.get_account()
    portfolio_value = float(account.portfolio_value)
    current = get_current_allocation()
    
    for symbol, target_pct in TARGET.items():
        current_pct = current.get(symbol, 0)
        drift = target_pct - current_pct
        
        # Only trade if drift exceeds 2% threshold
        if abs(drift) < 0.02:
            continue
            
        dollar_amount = abs(drift) * portfolio_value
        side = 'buy' if drift > 0 else 'sell'
        
        api.submit_order(
            symbol=symbol,
            notional=round(dollar_amount, 2),
            side=side,
            type='market',
            time_in_force='day'
        )
        print(f"{side.upper()} ${dollar_amount:.2f} of {symbol} "
              f"(drift: {drift:+.1%})")

The 2% drift threshold is important. Without it, you’d be making tiny trades every run, racking up tax events for no real benefit. I tested thresholds from 1% to 5% — 2% hit the sweet spot between staying close to target and minimizing unnecessary trades.

The Gotcha That Cost Me an Hour

Alpaca’s notional parameter (dollar-based orders) only works for stocks, not ETFs on the old API version. I kept getting 422 Unprocessable Entity errors when trying to buy fractional TLT shares. The fix: make sure you’re using API v2 and that fractional shares are enabled on your account. It’s a checkbox in the dashboard that’s off by default.

Another thing: market orders submitted before 9:30 AM ET queue until open. That’s fine for rebalancing — you’re not trying to time anything. But if you’re running this as a cron job at 6 AM Pacific like I do, don’t panic when orders show as “pending” for a few hours.

Scheduling: Cron vs. Cloud Functions

I run mine as a weekly cron job on my homelab server:

# Every Monday at 6:00 AM Pacific (13:00 UTC)
0 13 * * 1 /usr/bin/python3 /home/scripts/rebalance.py >> /var/log/rebalance.log 2>&1

If you don’t have a server running 24/7, AWS Lambda with EventBridge works too. The free tier covers it — this script runs in under 3 seconds and uses maybe 5MB of memory. But honestly, a $35 Raspberry Pi is simpler. No IAM roles, no deployment pipeline, no cold start delays.

For monitoring, I have it post results to a Telegram channel. If any order fails, I get a push notification. The Finnhub WebSocket alert system I built earlier handles the real-time price monitoring side.

Backtesting: Does This Actually Work?

I backtested this exact allocation with monthly rebalancing against a buy-and-hold SPY position from 2015-2025 using vectorbt:

import vectorbt as vbt

# Results over 10 years:
# Rebalanced portfolio: 11.2% CAGR, max drawdown -18.4%
# Buy-and-hold SPY:    13.1% CAGR, max drawdown -33.7%

SPY beat on raw returns (it was a great decade for US large caps), but the rebalanced portfolio had nearly half the max drawdown. In 2020, when SPY dropped 33%, my diversified mix only fell 18%. That’s the difference between sleeping fine and stress-refreshing your brokerage app at 3 AM.

If you want to dig deeper into the technical indicators behind timing decisions, I wrote about RSI, Ichimoku, and Stochastic indicators — useful if you want to add tactical overlays on top of the base rebalancing strategy.

Tax-Loss Harvesting Add-On

Once you have the rebalancing bot running, adding tax-loss harvesting is straightforward. The idea: when selling an overweight position at a loss, you book that loss for tax purposes and immediately buy a correlated (but not “substantially identical”) replacement.

# Tax-loss harvesting pairs
PAIRS = {
    'SPY': 'VOO',   # Both track S&P 500 (different providers)
    'QQQ': 'QQQM',  # Both track Nasdaq-100
    'VWO': 'IEMG',  # Both track emerging markets
}

def harvest_losses(symbol, current_price, cost_basis):
    if current_price < cost_basis * 0.95:  # 5%+ loss
        loss = (cost_basis - current_price) * shares
        # Sell losing position, buy the pair
        api.submit_order(symbol=symbol, qty=shares, side='sell')
        api.submit_order(symbol=PAIRS[symbol], qty=shares, side='buy')
        print(f"Harvested ${loss:.2f} loss on {symbol}")

Be careful with wash sale rules — you can’t buy back the same security within 30 days. The paired approach above avoids this while keeping your market exposure roughly the same.

Monitoring With a Proper Setup

Running trading automation without monitoring is asking for trouble. At minimum, you need:

  • Daily balance check — compare actual vs. expected portfolio value
  • Order failure alerts — any rejected order gets a push notification
  • Drift report — weekly email showing allocation vs. target
  • Kill switch — a way to disable the bot instantly if something goes wrong

I use a simple JSON log file and a Python script that reads it to generate a weekly summary. Nothing fancy, but it’s saved me twice — once when Alpaca had an API outage and orders were silently failing, and once when a stock split threw off my position calculations.

For the monitoring hardware side, a good multi-monitor setup helps when you’re watching positions. I use a dual monitor arm (affiliate link) to keep my terminal and brokerage dashboard side by side — worth it if you’re doing any kind of active development alongside automated trading.

What I’d Do Differently

If I started over, I’d skip the cron job and use Alpaca’s built-in webhook notifications to trigger rebalancing only when drift exceeds the threshold. Polling weekly works fine, but event-driven is cleaner.

I’d also add a volatility filter — during high-VIX periods (above 30), the bot should reduce position sizes or skip rebalancing entirely. Buying into a panic selloff sounds great in theory, but the bid-ask spreads on ETFs widen during volatility, and you’ll get worse fills.

The full script with logging, error handling, and Telegram notifications is about 200 lines. Not a weekend project — more like a focused afternoon. The hard part isn’t the code. It’s deciding on your target allocation and sticking with it when markets get weird.

For daily market analysis and trading signals, join Alpha Signal on Telegram — free market intelligence every morning.

📧 Get weekly insights on security, trading, and tech. No spam, unsubscribe anytime.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Also by us: StartCaaS — AI Company OS · Hype2You — AI Tech Trends