惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
Google DeepMind News
Google DeepMind News
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
I
InfoQ
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
量子位
博客园 - 叶小钗
月光博客
月光博客
IT之家
IT之家
G
Google Developers Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Rebalancing a portfolio without selling anything: the con...
Diego · 2026-06-19 · via DEV Community

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 == 0 and 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.