Building a Portfolio Rebalancing Script in Python
Measure allocation drift, generate a self-funding trade list, and use threshold bands to avoid over-trading.
A target allocation is a decision you make once and then quietly betray. You pick 60% US stocks, 30% international, 10% bonds, and the market spends the next year pulling those numbers apart. Bonds rally, equities stall, and the portfolio you actually hold stops matching the one you designed. Rebalancing is the act of dragging it back. The mechanics are simple enough that you don’t need a brokerage feature for it — about forty lines of Python turns a pile of holdings into an exact trade list.
This walks through that script in three pieces: measuring how far each position has drifted, converting that drift into share-level buy and sell orders, and adding the one rule that stops you from trading every time a price ticks.
Measuring drift before you trade
Drift is the gap between what you hold and what you meant to hold, expressed in percentage points. Before you can correct it, you have to compute it from the only inputs you reliably have: share counts, current prices, and your target weights.
holdings = {
"VTI": {"shares": 42, "price": 268.40, "target": 0.60},
"VXUS": {"shares": 73, "price": 61.20, "target": 0.30},
"BND": {"shares": 88, "price": 72.10, "target": 0.10},
}
values = {t: h["shares"] * h["price"] for t, h in holdings.items()}
total = sum(values.values())
for ticker, h in holdings.items():
weight = values[ticker] / total
drift = weight - h["target"]
print(f"{ticker}: {weight:5.1%} (target {h['target']:.0%}, drift {drift:+.1%})")
Run against this portfolio and the output is unambiguous:
VTI: 51.0% (target 60%, drift -9.0%)
VXUS: 20.2% (target 30%, drift -9.8%)
BND: 28.7% (target 10%, drift +18.7%)
The total is about $22,085. Bonds were supposed to be a tenth of it and have grown to nearly a third — a textbook case of the defensive sleeve swelling while equities lagged. Eyeballing account balances would never have surfaced an 18-point overweight that precisely. The single source of truth here is values, computed once; everything downstream divides into it.
Note what the script does not do: it doesn’t fetch live prices. Hardcoding the price field keeps the logic testable and deterministic. When you’re ready to automate, swap that field for a quote API call, but build and verify the math against fixed numbers first.
Turning drift into a trade list
Drift tells you the problem; it doesn’t tell you how many shares to move. For that, you compare each position’s current dollar value against its target dollar value — the target weight times the portfolio total — and divide the difference by the price.
THRESHOLD = 0.05 # only touch a sleeve once it drifts 5 points
trades = []
for ticker, h in holdings.items():
current_value = values[ticker]
target_value = h["target"] * total
drift = current_value / total - h["target"]
if abs(drift) < THRESHOLD:
continue
delta_value = target_value - current_value
shares = delta_value / h["price"]
trades.append((ticker, shares, delta_value))
for ticker, shares, delta in sorted(trades, key=lambda t: t[1]):
action = "BUY " if shares > 0 else "SELL"
print(f"{action} {abs(shares):6.1f} {ticker} ({delta:+,.0f})")
The result is a complete set of orders:
SELL 57.4 BND (-4,136)
BUY 7.4 VTI (+1,978)
BUY 35.3 VXUS (+2,158)
The sells and buys net to roughly zero on purpose. You sell the $4,136 of overweight bonds and use exactly that cash to top up the two underweight equity sleeves. No new money enters; the portfolio rearranges itself back to 60/30/10. That self-funding property is the whole appeal of rebalancing as a trade-generation problem — delta_value summed across all positions is always zero, because the target weights sum to one and they’re all multiplied by the same total.
The rules that keep you from over-trading
The THRESHOLD constant is doing quiet, important work. Without it, the script would generate a trade for any deviation at all — including a 0.3-point wobble that costs you spreads and tax for no meaningful benefit. A tolerance band says: ignore noise, act only on drift that has become structural.
Five percentage points is a common absolute band, but it treats a 10% target and a 60% target identically, which isn’t quite right — a 5-point move is half of a 10% sleeve and a twelfth of a 60% one. A widely cited refinement is the “5/25” rule: rebalance a position when it drifts more than 5 absolute points or more than 25% of its own target weight, whichever is smaller. For your 10% bond sleeve, 25% of target is 2.5 points, so that’s the trigger; for the 60% equity sleeve, the 5-point absolute band binds first.
def should_trade(current_weight, target):
absolute = abs(current_weight - target)
relative = abs(current_weight - target) / target
return absolute > 0.05 or relative > 0.25
Swap should_trade(...) in for the flat THRESHOLD check and small allocations get the tighter leash they need while large ones aren’t whipsawed by every move. The other discipline worth adding is frequency: don’t run this daily. Calendar-based rebalancing (quarterly or annually) combined with a band check tends to keep turnover low, because most checks return an empty trade list and cost you nothing.
Cursor
An AI-native code editor that's well suited to iterating on small financial scripts like this one — generating test cases for the drift math, refactoring the threshold logic, and wiring in a price API without leaving the file.
Free tier; Pro at $20/mo
Affiliate link · We earn a commission at no cost to you.
The progression matters more than any single number. Get the drift calculation correct against fixed prices, confirm the trades net to zero, then layer thresholds on top. A rebalancing script that you’ve verified by hand is worth more than a more elaborate one you have to trust blindly — these are real orders against real money.
FAQ
How often should the script actually rebalance?
Does rebalancing improve returns?
What about fractional shares and leftover cash?
Related reading
2026-08-12
Building a Dividend Reinvestment Tracker in Python
A data model and sync loop for DRIP lots, cost basis, and yield on cost from a brokerage API, plus the corporate actions that break naive trackers.
2026-06-22
Tiingo vs Polygon.io: Market Data APIs in 2026
Pricing, rate limits, and data coverage compared for solo quant builders, plus which one fits a weekend backtester.
2026-08-12
What IV Rank Tells You Before an Earnings Report
IV rank compresses a year of implied volatility into one number. How it is calculated, and the three checks that keep it from misleading you.
2026-08-12
Reading a 10-K: The Five Sections That Change a Thesis
Most of a 10-K is boilerplate carried over from last year. SEC endpoints let you diff filings instead of reading them front to back.
2026-06-22
Position Sizing: The Math Retail Investors Skip in 2026
Size each trade from risk, not conviction: the fixed-fractional formula, fractional Kelly, R-multiples, and drawdown survival math.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.