Building a Portfolio Rebalancing Script in Python: From Drift to Trades
A practical walkthrough of writing a Python rebalancing script: measuring allocation drift, generating a self-funding trade list, and using 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-06-22
Tiingo vs Polygon.io: Market Data APIs for Indie Quant Projects in 2026
A practical comparison of Tiingo and Polygon.io for solo quant builders in 2026 — pricing, rate limits, data coverage, and which one fits a weekend backtester.
2026-06-22
Position Sizing and Risk per Trade: The Math Retail Investors Skip in 2026
How to size a trade from risk instead of conviction: the fixed-fractional formula, fractional Kelly, R-multiples, and the drawdown math that decides whether a losing streak is survivable.
2026-06-22
Dollar-Cost Averaging vs Lump Sum: What the Math Really Says
A measured look at why lump-sum investing usually beats dollar-cost averaging on expected return, when DCA still makes sense, and how to decide for your own cash.
2026-06-22
What the Sharpe Ratio Actually Tells You (and Where It Misleads)
The Sharpe ratio measures excess return per unit of volatility. Here is exactly what the number captures, the four assumptions that break it, and when to trust it.
2026-06-10
Building a Market-Data Pipeline: Caching, Rate Limits, and Gaps
Reliable backtests need reliable data, and pulling it live from an API on every run is slow, fragile, and costly. Here's how to build a local market-data pipeline that caches, respects rate limits, and handles gaps.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.