VOOZH about

URL: https://dev.to/diegocard117/rebalanceando-uma-carteira-sem-vender-nada-o-algoritmo-do-aporte-4549

⇱ Rebalanceando uma carteira sem vender nada: o algoritmo do aporte - DEV Community


Como dividir um aporte mensal entre vários ativos pra aproximar a carteira das metas — sem precisar vender nada nem pagar imposto.

Todo investidor de longo prazo bate na mesma parede: você define uma alocação-alvo (digamos 40% ações, 25% renda fixa, 20% FIIs, 15% internacional), o mercado se mexe, e três meses depois a carteira tá torta. O conselho clássico é rebalancear vendendo o que subiu e comprando o que caiu.

O problema: vender gera imposto, custo de corretagem e, no Brasil, dor de cabeça com DARF. Existe um jeito muito mais barato de rebalancear se você aporta todo mês — direcionar o dinheiro novo pras categorias que estão atrasadas. Sem vender uma cota sequer.

Esse post mostra o algoritmo que usei pra isso. É mais sutil do que parece.

A tentação ingênua (que está errada)

A primeira ideia de todo mundo é: "tenho R$ 1.000 pra aportar, divido pelas metas". 40% vão pra ações, 25% pra renda fixa, e assim por diante.

# ERRADO
for categoria in categorias:
 orcamento[categoria] = aporte * categoria.meta_pct / 100

Isso não rebalanceia nada — só perpetua a alocação atual. Se suas ações já estão em 50% (acima dos 40% de meta), jogar mais 40% do aporte nelas piora o desvio.

O que você quer é o contrário: dar mais dinheiro pra quem está mais atrás da meta.

O conceito central: o gap

Para cada categoria, calcule quanto ela deveria valer depois do aporte, e quanto falta pra chegar lá:

novo_total = total_atual + aporte

def gap(categoria):
 valor_alvo = novo_total * categoria.meta_pct / 100
 return max(0, valor_alvo - categoria.valor_atual)

O max(0, ...) é importante: categorias que já passaram da meta têm gap zero — não recebem nada do aporte (e vão naturalmente "voltar" à meta conforme o resto cresce em volta delas).

Agora distribua o orçamento proporcionalmente aos gaps, não às metas:

total_gap = sum(gap(c) for c in categorias)

for categoria in categorias:
 orcamento[categoria] = aporte * gap(categoria) / total_gap

Pronto — o dinheiro flui sozinho pras categorias subalocadas. Quanto mais atrás uma categoria está, maior a fatia que ela recebe. Quando todas estão na meta, os gaps se igualam e o aporte cai proporcionalmente como no caso ingênuo (que aí, sim, é o comportamento certo).

Caso de borda: se todas as categorias já estão na ou acima da meta, total_gap == 0 e você cairia numa divisão por zero. Nesse caso, faça fallback pra distribuição por meta — não há o que corrigir, só manter a proporção.

O problema chato: ações são indivisíveis

Até aqui é aritmética limpa. A realidade quebra ela: você não compra "R$ 213,47 de PETR4". Compra um número inteiro de ações.

def sugestao_compra(ativo, orcamento):
 quantidade = int(orcamento / ativo.preco) # arredonda pra baixo
 if quantidade <= 0:
 return None
 custo = quantidade * ativo.preco
 return {'ticker': ativo.ticker, 'quantidade': quantidade, 'custo': custo}

Esse int() joga fora as sobras de cada ativo. Some isso por uma carteira de 15 ativos e você facilmente deixa R$ 150 do aporte parados. Inaceitável — o usuário quer ver o dinheiro alocado.

Gastando o troco: a passada gulosa

A solução é uma segunda passada que pega o que sobrou e gasta de forma gulosa, sempre na categoria com maior déficit restante:

def gastar_restante(restante, ativos, simulado):
 while restante > 0:
 melhor = None
 maior_deficit = 0

 for ativo in ativos:
 if ativo.preco > restante: # não cabe nem 1 unidade
 continue
 deficit = ativo.categoria.valor_alvo - simulado[ativo.categoria]
 if deficit > maior_deficit:
 maior_deficit = deficit
 melhor = ativo

 if melhor is None: # nada mais cabe no troco
 break

 restante -= melhor.preco # compra +1 unidade
 simulado[melhor.categoria] += melhor.preco
 registrar_compra(melhor, quantidade=1)

 return restante

Cada volta do loop compra uma unidade do ativo cuja categoria está mais atrás, atualiza o déficit simulado, e repete. Para quando o troco não compra nem a ação mais barata. Isso espreme o aporte até o último real possível, sem nunca furar a lógica de meta.

(Renda fixa é mais fácil: como é divisível, dá pra alocar centavos exatos — sem o problema do inteiro.)

Vender só quando você quiser

Às vezes o desvio é grande demais pra corrigir só com aporte. Aí o usuário opta por rebalancear vendendo. O cálculo do excesso é o espelho do gap:

def sugestoes_venda(categoria):
 excesso = categoria.valor_atual - categoria.valor_alvo
 if excesso <= 0:
 return []
 # vende de cada ativo proporcional ao peso dele na categoria
 vendas = []
 for ativo in categoria.ativos:
 fatia = excesso * (ativo.valor_atual / categoria.valor_atual)
 qtd = int(fatia / ativo.preco)
 if qtd > 0:
 vendas.append({'ticker': ativo.ticker, 'quantidade': qtd})
 return vendas

E como isso é Brasil, dá pra adicionar um flag evitar_vendas_ir=True que pula ETFs e FIIs (tickers terminados em 11, sempre tributados) das sugestões de venda — você rebalanceia mexendo só no que é isento.

Por que isso importa

O resultado é que o investidor abre o app, digita "vou aportar R$ 1.000", e recebe uma lista do tipo:

Comprar 12 BOVA11 × R$ 38,50 = R$ 462,00
Comprar 8 MXRF11 × R$ 10,30 = R$ 82,40
Aportar CDB Nubank = R$ 455,60
Sobra: R$ 0,00

Tudo alocado, carteira mais perto das metas, e nenhum imposto pago. É a diferença entre "rebalanceamento" como tarefa trimestral chata e como algo que acontece sozinho a cada aporte.


Construí isso no Balance, um app de código que ajuda investidores brasileiros a manter a carteira na meta calculando exatamente essas sugestões a cada aporte. O serviço completo lida ainda com cripto (frações de 8 casas), múltiplos mercados e cálculo de IR — mas o coração é o algoritmo acima.