Pular para o conteúdo

Desafios de entrevista em Go · parte 2 de 11

Nubank - Capital Gains

avançadoPublicada em · 7 min de leitura

Tamanho da fonte:
Largura da coluna:

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:

  1. Domínio do domínio financeiro: Tratamento correto de arredondamentos, preço médio ponderado e deduções de prejuízos acumulados.
  2. 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.
  3. 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

  1. Compras não pagam imposto: Toda operação de compra ("buy") gera imposto zero ({"tax": 0.00}).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Arredondamento e formato decimal:
    • Valores monetários devem ser impressos com exatamente duas casas decimais (ex.: 0.00, 1250.50).

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

  1. 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 como 0.20000000000000007. Em Go, para este teste de CLI, é comum utilizar float64 com arredondamento para duas casas decimais no momento da serialização (fmt.Sprintf("%.2f")) ou tipos inteiros baseados em centavos (cents).
  2. 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.
  3. Misturar estado entre simulações: Cada linha recebida no stdin deve inicializar uma nova instância de Portfolio. O vazamento de estado entre linhas é motivo de reprovação automática.
  4. 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.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?