How to split a monthly contribution across several assets to pull a portfolio back toward its targets — without selling anything or paying taxes.
Every long-term investor hits the same wall: you set a target allocation (say 40% stocks, 25% fixed income, 20% REITs, 15% international), the market moves, and three months later your portfolio is lopsided. The classic advice is to rebalance by selling what went up and buying what went down.
The problem: selling triggers taxes, brokerage fees, and — here in Brazil — the headache of filing a DARF. There's a far cheaper way to rebalance if you contribute every month: steer the new money toward the categories that are lagging. Without selling a single share.
This post walks through the algorithm I used for that. It's subtler than it looks.
The naive temptation (which is wrong)
Everyone's first idea is: "I have $1,000 to invest, I'll split it by the targets." 40% goes to stocks, 25% to fixed income, and so on.
# WRONG
for category in categories:
budget[category] = contribution * category.target_pct / 100
This rebalances nothing — it just perpetuates the current allocation. If your stocks are already at 50% (above the 40% target), throwing another 40% of the contribution at them makes the drift worse.
What you actually want is the opposite: give more money to whoever is furthest behind the target.
The core concept: the gap
For each category, compute how much it should be worth after the contribution, and how far it is from getting there:
new_total = current_total + contribution
def gap(category):
target_value = new_total * category.target_pct / 100
return max(0, target_value - category.current_value)
The max(0, ...) matters: categories that have already passed the target have a gap of zero — they get nothing from the contribution (and they naturally drift back toward the target as everything else grows around them).
Now distribute the budget proportionally to the gaps, not to the targets:
total_gap = sum(gap(c) for c in categories)
for category in categories:
budget[category] = contribution * gap(category) / total_gap
That's it — the money flows on its own toward the underweight categories. The further behind a category is, the bigger the slice it gets. When everything is on target, the gaps equalize and the contribution splits proportionally just like the naive case (which, in that case, is the correct behavior).
Edge case: if every category is already at or above target,
total_gap == 0and you'd hit a division by zero. Fall back to distributing by target — there's nothing to correct, just keep the proportions.
The annoying problem: shares are indivisible
So far it's clean arithmetic. Reality breaks it: you don't buy "$213.47 of PETR4". You buy a whole number of shares.
def buy_suggestion(asset, budget):
quantity = int(budget / asset.price) # rounds down
if quantity <= 0:
return None
cost = quantity * asset.price
return {'ticker': asset.ticker, 'quantity': quantity, 'cost': cost}
That int() throws away the leftovers from each asset. Add it up across a 15-asset portfolio and you can easily leave $150 of the contribution sitting idle. Unacceptable — the user wants to see the money allocated.
Spending the change: the greedy second pass
The fix is a second pass that takes whatever is left and spends it greedily, always on the category with the largest remaining deficit:
def spend_remaining(remaining, assets, simulated):
while remaining > 0:
best = None
largest_deficit = 0
for asset in assets:
if asset.price > remaining: # can't afford even one unit
continue
deficit = asset.category.target_value - simulated[asset.category]
if deficit > largest_deficit:
largest_deficit = deficit
best = asset
if best is None: # nothing else fits the change
break
remaining -= best.price # buy +1 unit
simulated[best.category] += best.price
record_buy(best, quantity=1)
return remaining
Each loop iteration buys one unit of the asset whose category is furthest behind, updates the simulated deficit, and repeats. It stops when the leftover can't buy even the cheapest share. This squeezes the contribution down to the last possible dollar, without ever breaking the target logic.
(Fixed income is easier: since it's divisible, you can allocate exact cents — no whole-number problem.)
Selling, only when you choose to
Sometimes the drift is too large to fix with contributions alone. Then the user opts in to rebalance by selling. The excess calculation is the mirror image of the gap:
def sell_suggestions(category):
excess = category.current_value - category.target_value
if excess <= 0:
return []
# sell from each asset proportionally to its weight in the category
sells = []
for asset in category.assets:
slice = excess * (asset.current_value / category.current_value)
qty = int(slice / asset.price)
if qty > 0:
sells.append({'ticker': asset.ticker, 'quantity': qty})
return sells
And since this is Brazil, you can add an avoid_tax_sells=True flag that skips ETFs and REITs (tickers ending in 11, always taxed here) from the sell suggestions — so you rebalance touching only the tax-exempt holdings.
Why it matters
The result is that the investor opens the app, types "I'm going to invest $1,000," and gets a list like:
Buy 12 BOVA11 × $ 38.50 = $ 462.00
Buy 8 MXRF11 × $ 10.30 = $ 82.40
Add Nubank CD = $ 455.60
Leftover: $ 0.00
Everything allocated, the portfolio closer to its targets, and no taxes paid. That's the difference between "rebalancing" as a tedious quarterly chore and as something that happens on its own with every contribution.
I built this into Balance, an app that helps Brazilian investors keep their portfolio on target by computing exactly these suggestions on every contribution. The full service also handles crypto (8-decimal fractions), multiple markets, and tax reporting — but the heart of it is the algorithm above.
Top comments (0)