DEV Community

Diego
Diego

Posted on

Rebalanceando a carteira só com o próximo aporte (sem vender)

Parte de uma série sobre construir o Balance, um app de rebalanceamento de carteira
para investidores BR/US/Cripto, como dev solo. O código aqui é simplificado em relação ao real.

Todo investidor que faz aportes recorrentes esbarra na mesma parede:

"Tenho R$ 1.000 pra investir esse mês. O que eu compro pra minha carteira ficar perto da meta?"

A resposta de manual — "venda o que está acima, compre o que está abaixo" — tem custo: IR, corretagem e a disciplina emocional de vender o que está subindo. Existe um caminho mais gentil, que quase ninguém formaliza: rebalancear usando só o novo aporte.

É esse o algoritmo no coração do Balance. Vamos construir.

A ideia em um exemplo

Digamos que sua meta seja:

Categoria Meta
Ações 40%
Renda fixa 25%
FIIs 20%
Internacional 15%

Esse mês, depois de algumas oscilações de preço, você está em 46% de ações e 16% de FIIs. Ações acima, FIIs abaixo.

Em vez de vender ações (e gerar um fato gerador de IR), você direciona o aporte do mês inteiro para as categorias abaixo da meta. A carteira converge para o alvo sem uma única venda.

O algoritmo

A coisa toda é: descobrir o quanto cada categoria está longe de onde deveria estar depois do aporte, e distribuir o aporte proporcionalmente a esses gaps.

from collections import defaultdict
from decimal import Decimal

def calculate(self, deposit_amount: Decimal) -> dict:
    deposit = Decimal(deposit_amount).quantize(Decimal('0.01'))

    investments = self._category_investments()   # {category_id: [(kind, item), ...]}
    current_total = self.portfolio.total_value
    new_total = current_total + deposit

    # 1. Valor atual em cada categoria
    cat_value = defaultdict(lambda: Decimal('0'))
    for cat_id, rows in investments.items():
        for kind, item in rows:
            cat_value[cat_id] += item.current_value

    # 2. Gap = o quanto cada categoria está ABAIXO da meta pós-aporte
    gaps = {}
    for cat_id, rows in investments.items():
        category = rows[0][1].category
        target_value = new_total * category.target_percentage / Decimal('100')
        gaps[cat_id] = max(Decimal('0'), target_value - cat_value[cat_id])

    total_gap = sum(gaps.values())

    # 3. Divide o aporte entre as categorias, proporcional a cada gap
    cat_budget = {
        cat_id: (deposit * gaps[cat_id] / total_gap).quantize(Decimal('0.01'))
        for cat_id in investments
    }
    ...
Enter fullscreen mode Exit fullscreen mode

Três passos:

  1. Valor por categoria — soma o que você tem hoje em cada uma.
  2. Gap por categoriamax(0, meta − atual). Categorias já na meta ou acima têm gap zero; não recebem nada.
  3. Orçamento por categoria — distribui o aporte proporcionalmente aos gaps.

O max(Decimal('0'), ...) é o pulo do gato: categorias acima da meta simplesmente não competem pelo aporte. O dinheiro flui para onde falta.

Caso de borda: se todas as categorias já estão na meta ou acima (total_gap == 0), não há déficit a preencher. Caímos num fallback que divide o aporte por peso de meta, mantendo a alocação atual em vez de dividir por zero.

Do orçamento para as ordens

Orçamento por categoria ainda não é uma ordem — você não compra 3,7 ações. Então o orçamento de cada categoria é dividido entre os ativos e vira quantidade inteira:

def _buy_suggestion(self, kind, item, budget: Decimal):
    if budget <= 0:
        return None

    if self.portfolio.market == 'CRYPTO':
        quantity = (budget / item.current_price).quantize(Decimal('0.00000001'))
    else:
        quantity = int(budget / item.current_price)   # só ações inteiras

    if quantity <= 0:
        return None
    cost = (quantity * item.current_price).quantize(Decimal('0.01'))
    return self._asset_suggestion_row(item, quantity, cost)
Enter fullscreen mode Exit fullscreen mode

Repare no branch por mercado: ações compram unidades inteiras (int()), cripto compra frações de até 8 casas decimais. Esse único if é o que permite o mesmo motor servir uma carteira de ações e uma de Bitcoin. (Tem um artigo só sobre essa decisão mais pra frente na série.)

O problema do troco

Arredondar para ações inteiras deixa dinheiro na mesa. Compre int(500 / 140) = 3 ações a R$140 e você gastou R$420 — sobram R$80 do aporte sem alocação.

Por isso há uma segunda passada que gasta o restante, de forma gananciosa, na categoria que ainda tem o maior déficit:

while remaining > 0:
    # pega a categoria mais abaixo da meta que ainda dá pra comprar
    best_item = max(
        affordable_items,
        key=lambda it: target(it) - simulated_value[it.category_id],
        default=None,
    )
    if best_item is None:
        break
    # compra mais uma ação (ou, pra cripto, gasta todo o resto como fração)
    ...
Enter fullscreen mode Exit fullscreen mode

Para ações isso compra uma ação extra por vez até não sobrar nada comprável. Para cripto, joga o restante no ativo mais abaixo da meta como fração. De qualquer jeito, o aporte é totalmente deployado em vez de deixar caixa parado.

Opcional: rebalancear com vendas (driblando o IR)

Às vezes o aporte não basta — uma categoria está tão acima que nenhum aporte realista resolve. Para esses casos, o serviço pode sugerir vendas também, com uma sacada específica da lei brasileira:

if avoid_ir_sells and _is_always_taxed(ticker):
    continue   # nunca sugere vender um ativo sempre tributado (ETF/FII)
Enter fullscreen mode Exit fullscreen mode

No Brasil, vendas de ações até R$20k/mês são isentas, mas ETFs e FIIs (tickers terminados em 11) são sempre tributados. A flag avoid_ir_sells manda o motor rebalancear vendendo só o que é isento, deixando os ativos tributáveis intocados. O mesmo cálculo, agora consciente do IR.

Por que isso virou o produto

A matemática aqui não é sofisticada — é distribuição proporcional com alguns guardas. Mas embrulhada numa UI que busca cotação ao vivo, sabe suas metas e respeita as regras de IR, ela responde uma pergunta que investidores reais fazem todo mês e hoje resolvem com planilha e chute.

É a lição que eu fico reaprendendo construindo o Balance: a parte valiosa raramente é o algoritmo. É remover o atrito em volta dele.


O Balance é uma ferramenta de rebalanceamento para investidores BR/US/Cripto — diz o que comprar com o próximo aporte pra ficar na meta. Se você faz aportes recorrentes e quer testar, o link está no perfil. Dúvidas sobre a abordagem? Comenta aí.

Top comments (0)