Desafios de entrevista em Go · parte 2 de 11
Nubank - Capital Gains
avançadoPublicada em · 7 min de leitura
Dados da empresa e da vaga
- Empresa: Nubank (Nu Holdings Ltd.)
- Sede: São Paulo, Brasil (com polos em Buenos Aires, Cidade do México, Berlim e contratação remota global)
- Perfil da vaga: Software Engineer (Backend / Systems) — Níveis Pleno, Sênior e Especialista
- Modelo de trabalho: Híbrido flexível ou remoto internacional
- Faixa salarial estimada da vaga:
- Pleno (Brasil / LATAM): R$ 18.000 a R$ 26.000/mês (CLT ou equivalente) / $45.000 a $65.000 USD/ano
- Sênior / Staff: R$ 28.000 a R$ 42.000+/mês / $80.000 a $130.000+ USD/ano + RSU (ações na NYSE: NU)
- Stack principal do time: Go, Clojure, Kafka, Docker, Kubernetes, AWS, Datomic
Contexto do desafio técnico
O desafio Capital Gains (Ganho de Capital) é um dos testes práticos mais famosos e aplicados do Nubank em etapas de take-home test ou sessões de pair programming.
O objetivo é construir uma aplicação de linha de comando (CLI) autônoma em Go que leia uma sequência contínua de operações financeiras de compra e venda de ações via entrada padrão (stdin), processe o cálculo de impostos sobre ganho de capital de acordo com a legislação tributária brasileira e emita o resultado em JSON na saída padrão (stdout).
O avaliador do Nubank analisa três pontos capitais:
- Domínio do domínio financeiro: Tratamento correto de arredondamentos, preço médio ponderado e deduções de prejuízos acumulados.
- Arquitetura limpa e idiomática: Código legível, sem dependências externas desnecessárias, com separação nítida entre a camada de I/O (
stdin/stdout) e as regras de negócio puras. - Cobertura de testes e resiliência: Testes de tabela cobrindo todos os cenários limite (edge cases) e ausência total de pânicos com entradas malformadas.
Regras de negócio e cálculo tributário
1. Formato de entrada (stdin)
O programa lê linhas da entrada padrão. Cada linha contém um array JSON com uma simulação independente de operações de mercado. Uma linha não compartilha estado com a linha anterior nem com a posterior.
Cada operação possui os seguintes campos:
operation: string, podendo ser"buy"(compra) ou"sell"(venda).unit-cost: número decimal com duas casas decimais representando o custo unitário da ação.quantity: número inteiro representando o volume de ações negociadas.
Exemplo de linha de entrada:
[{"operation":"buy", "unit-cost":10.00, "quantity": 100}, {"operation":"sell", "unit-cost":15.00, "quantity": 50}, {"operation":"sell", "unit-cost":15.00, "quantity": 50}]
2. Formato de saída (stdout)
Para cada linha recebida, o programa deve imprimir no stdout um array JSON correspondente, onde cada elemento indica o imposto (tax) a ser pago para cada operação:
[{"tax": 0.00}, {"tax": 0.00}, {"tax": 0.00}]
3. Regras fiscais e financeiras
- Compras não pagam imposto: Toda operação de compra (
"buy") gera imposto zero ({"tax": 0.00}). - Atualização do Preço Médio Ponderado: Ao realizar uma compra de ações, o preço médio ponderado do estoque atual deve ser recalculado pela fórmula: $$\text{novo_preço_médio} = \frac{(\text{quantidade_atual} \times \text{preço_médio_atual}) + (\text{quantidade_comprada} \times \text{valor_da_compra})}{\text{quantidade_atual} + \text{quantidade_comprada}}$$ Observação: Operações de venda não alteram o preço médio ponderado unitário das ações restantes.
- Limite de isenção de R$ 20.000,00:
- Em operações de venda (
"sell"), se o valor total da operação ($\text{unit-cost} \times \text{quantity}$) for menor ou igual a R$ 20.000,00, a operação é isenta de imposto ({"tax": 0.00}), mesmo que tenha ocorrido lucro. - O lucro de uma venda isenta não abate prejuízos anteriores, mas se a venda for com prejuízo, esse prejuízo deve ser registrado para abater lucros tributáveis futuros.
- Em operações de venda (
- Alíquota de 20% sobre o lucro:
- Se o valor total da operação de venda for superior a R$ 20.000,00 e houver lucro real (venda por valor superior ao preço médio ponderado), cobra-se 20% de imposto sobre o lucro líquido após dedução de eventuais prejuízos acumulados.
- Compensação de prejuízos acumulados:
- Se uma venda for realizada por um valor abaixo do preço médio ponderado, ocorre um prejuízo.
- Esse prejuízo não prescreve e deve ser acumulado em um saldo devedor.
- Em vendas futuras com lucro (mesmo em operações acima de R$ 20.000,00), o imposto de 20% incidirá apenas sobre o lucro que exceder o saldo de prejuízo acumulado. O prejuízo usado é deduzido do saldo devedor.
- Arredondamento e formato decimal:
- Valores monetários devem ser impressos com exatamente duas casas decimais (ex.:
0.00,1250.50).
- Valores monetários devem ser impressos com exatamente duas casas decimais (ex.:
Casos de teste oficiais da avaliação
Cenário 1: Isenção simples por valor total abaixo de R$ 20.000,00
Entrada:
[{"operation":"buy", "unit-cost":10.00, "quantity": 100}, {"operation":"sell", "unit-cost":15.00, "quantity": 50}, {"operation":"sell", "unit-cost":15.00, "quantity": 50}]
Saída esperada:
[{"tax": 0.00}, {"tax": 0.00}, {"tax": 0.00}]
Explicação: Nenhuma das duas vendas superou o valor total de R$ 20.000,00 ($15 \times 50 = 750$). Logo, ambas são isentas.
Cenário 2: Venda acima de R$ 20.000,00 com lucro tributável
Entrada:
[{"operation":"buy", "unit-cost":10.00, "quantity": 10000}, {"operation":"sell", "unit-cost":20.00, "quantity": 5000}, {"operation":"sell", "unit-cost":5.00, "quantity": 5000}]
Saída esperada:
[{"tax": 0.00}, {"tax": 10000.00}, {"tax": 0.00}]
Explicação:
- Compra: 10.000 ações a R$ 10,00 (Preço Médio = 10,00). Imposto = 0.
- 1ª Venda: 5.000 ações a R$ 20,00 = R$ 100.000,00 (acima de 20k). Lucro de R$ 10,00 por ação ($5.000 \times 10 = \text{R$} 50.000,00$). Imposto de 20% sobre 50.000 = R$ 10.000,00.
- 2ª Venda: 5.000 ações a R$ 5,00 = R$ 25.000,00 (acima de 20k), mas com prejuízo de R$ 5,00 por ação. Imposto = 0, prejuízo acumulado = R$ 25.000,00.
Cenário 3: Abatimento de prejuízo acumulado
Entrada:
[{"operation":"buy", "unit-cost":10.00, "quantity": 10000}, {"operation":"sell", "unit-cost":5.00, "quantity": 5000}, {"operation":"sell", "unit-cost":20.00, "quantity": 3000}]
Saída esperada:
[{"tax": 0.00}, {"tax": 0.00}, {"tax": 1000.00}]
Explicação:
- A primeira venda gerou um prejuízo de R$ 25.000,00 ($5.000 \times 5$).
- A segunda venda movimentou R$ 60.000,00 ($3.000 \times 20$), com lucro bruto de R$ 30.000,00 ($3.000 \times (20 - 10)$).
- O lucro líquido tributável é $30.000 - 25.000 = \text{R$} 5.000,00$.
- Imposto de 20% sobre 5.000 = R$ 1.000,00. O saldo de prejuízo acumulado é zerado.
Arquitetura idiomática recomendada em Go
cmd/
capital-gains/
main.go # Leitura do stdin via bufio.Scanner e saída via stdout
internal/
domain/
operation.go # Structs Operation e TaxResult
portfolio.go # Estado e regras de negócio puras (Preço Médio, Lucro, Prejuízo)
tax.go # Regras fiscais e cálculo do imposto
Exemplo de implementação central do cálculo em Go:
package domain
type OperationType string
const (
Buy OperationType = "buy"
Sell OperationType = "sell"
)
type Operation struct {
Type OperationType `json:"operation"`
UnitCost float64 `json:"unit-cost"`
Quantity int `json:"quantity"`
}
type TaxResult struct {
Tax float64 `json:"tax"`
}
type Portfolio struct {
totalQuantity int
weightedAvgCost float64
accumulatedLoss float64
}
func NewPortfolio() *Portfolio {
return &Portfolio{}
}
func (p *Portfolio) Process(op Operation) TaxResult {
if op.Type == Buy {
totalCost := (float64(p.totalQuantity) * p.weightedAvgCost) + (float64(op.Quantity) * op.UnitCost)
p.totalQuantity += op.Quantity
p.weightedAvgCost = totalCost / float64(p.totalQuantity)
return TaxResult{Tax: 0.00}
}
// Operação de Venda
totalValue := float64(op.Quantity) * op.UnitCost
profitOrLoss := float64(op.Quantity) * (op.UnitCost - p.weightedAvgCost)
p.totalQuantity -= op.Quantity
if profitOrLoss < 0 {
p.accumulatedLoss += -profitOrLoss
return TaxResult{Tax: 0.00}
}
// Venda com lucro ou empate
if totalValue <= 20000.00 {
return TaxResult{Tax: 0.00}
}
if profitOrLoss > 0 {
if p.accumulatedLoss > 0 {
if profitOrLoss >= p.accumulatedLoss {
taxableProfit := profitOrLoss - p.accumulatedLoss
p.accumulatedLoss = 0
return TaxResult{Tax: taxableProfit * 0.20}
} else {
p.accumulatedLoss -= profitOrLoss
return TaxResult{Tax: 0.00}
}
}
return TaxResult{Tax: profitOrLoss * 0.20}
}
return TaxResult{Tax: 0.00}
}
Critérios de avaliação e armadilhas comuns
- Problemas de precisão com ponto flutuante: Em aplicações financeiras, a representação binária de números float (
float64) pode gerar imprecisões como0.20000000000000007. Em Go, para este teste de CLI, é comum utilizarfloat64com arredondamento para duas casas decimais no momento da serialização (fmt.Sprintf("%.2f")) ou tipos inteiros baseados em centavos (cents). - Ignorar prejuízo em vendas abaixo de R$ 20.000,00: Uma das maiores pegadinhas do Nubank. Vendas abaixo de 20k são isentas de imposto sobre o lucro, mas se derem prejuízo, esse prejuízo deve ser registrado para abater lucros futuros.
- Misturar estado entre simulações: Cada linha recebida no
stdindeve inicializar uma nova instância dePortfolio. O vazamento de estado entre linhas é motivo de reprovação automática. - Complexidade excessiva: O Nubank desaprova bibliotecas externas pesadas ou a criação de interfaces com dezenas de métodos para um problema linear de processamento de fluxo.