Modelando um Portfólio de Criptomoedas - Posições, P&L em Tempo Real e Price Alerts!
O usuário comprou 0.3 BTC a 50 mil em janeiro e 0.2 BTC a 70 mil em março. Quanto ele está lucrando? Depende. Se você modelou como saldo ("o usuário tem 0.5 BTC"), não tem como responder porque perdeu a informação de preço de cada compra. Se modelou como posições separadas, a resposta é imediata: a primeira compra está com lucro de X%, a segunda com prejuízo de Y%. Essa decisão de modelagem no dia zero determina se o sistema vai funcionar ou virar uma bola de gambiarras.
Guia de tópicos:
- O Modelo Mental: Posições, Não Saldos
- Modelando uma Posição de Investimento
- Múltiplas Posições na Mesma Moeda: Por Que Não Agregar
- Profit Takes: Retiradas Parciais de Lucro
- Calculando P&L em Tempo Real
- Price Alerts: Monitorar e Disparar Uma Única Vez
- O Portfólio Completo: Agregando Tudo
- Considerações Finais
- Links Indicativos
O Modelo Mental: Posições, Não Saldos
O primeiro erro que muita gente comete ao modelar um portfólio de criptomoedas é pensar em saldos. "O usuário tem 0.5 BTC". Isso é uma simplificação perigosa porque esconde informação crucial: a que preço ele comprou? Quando comprou? Quanto investiu? Se ele comprou 0.3 BTC a 50 mil e 0.2 BTC a 70 mil, o preço médio é diferente, o lucro/prejuízo de cada compra é diferente, e a decisão de vender uma ou outra também é diferente.
A abordagem correta é modelar por posições. Uma posição é um registro individual de compra: "o usuário comprou X quantidade da moeda Y ao preço Z na data W". Cada compra é uma posição separada. O usuário pode ter 5 posições em Bitcoin, cada uma com preço e data diferentes. Isso permite calcular o P&L de cada compra individualmente, fechar posições específicas (vender só a que está no lucro), e ter um histórico completo de todas as operações.
No projeto que uso como referência, o modelo de dados ficou assim: um usuário tem N posições, cada posição pertence a uma criptomoeda, e cada posição pode ter N retiradas de lucro (profit takes). A criptomoeda é um registro único no sistema que define quais moedas estão sendo monitoradas. Essa separação permite que o sistema saiba exatamente quanto cada posição individual rendeu ou perdeu.
Modelando uma Posição de Investimento
Uma posição precisa ter no mínimo: o usuário dono, a criptomoeda, a quantidade comprada, o preço de compra, a data de compra e um status (ativa ou fechada). Mas na prática, você vai querer mais campos derivados para facilitar consultas e cálculos.
O campo invested_amount (valor investido) é calculado automaticamente: quantidade multiplicada pelo preço de compra. Parece redundante, mas ter esse campo pré-calculado evita fazer multiplicação em toda query de listagem e facilita agregações como "total investido pelo usuário".
O campo last_profit_price guarda o preço da última retirada de lucro. Isso é útil para o usuário saber a que preço ele vendeu pela última vez e tomar decisões sobre a próxima venda.
O campo status é crucial e só tem dois valores: "active" ou "closed". Uma posição ativa significa que o usuário ainda detém aquela quantidade de crypto. Uma posição fechada significa que ele vendeu tudo. Não existe "parcialmente fechada" porque retiradas parciais são modeladas como profit takes, não como fechamento de posição.
A regra de negócio para criar uma posição é: a quantidade precisa ser maior que zero, o preço precisa ser maior que zero, a criptomoeda precisa existir no sistema, e o valor investido é calculado automaticamente. O status sempre começa como "active". Simples, mas essas validações evitam dados inconsistentes no banco.
Múltiplas Posições na Mesma Moeda: Por Que Não Agregar
Uma decisão de modelagem que pode parecer estranha no começo é permitir múltiplas posições na mesma moeda para o mesmo usuário. Por que não ter uma única posição "BTC" e ir somando as compras?
Porque agregar perde informação. Se o usuário comprou 0.1 BTC a 30 mil em janeiro e 0.1 BTC a 70 mil em março, ele tem duas posições com realidades completamente diferentes. A primeira está com lucro enorme, a segunda pode estar no prejuízo. Se você agregar em uma única posição de 0.2 BTC com preço médio de 50 mil, o usuário perde a visibilidade de que uma compra foi excelente e outra foi ruim.
Além disso, na hora de vender, o usuário pode querer fechar só a posição que está no lucro e manter a que está no prejuízo (esperando recuperar). Com posições separadas, isso é trivial: fecha a posição 1, mantém a posição 2. Com posição agregada, você precisaria de lógica complexa de FIFO/LIFO para decidir qual "parte" está sendo vendida.
No projeto, a regra é clara: cada compra é uma posição. O portfólio do usuário é a soma de todas as posições ativas. Se ele quer ver "quanto tenho de BTC no total", é uma query que soma as quantidades das posições ativas daquela moeda. Mas o detalhe de cada compra individual está preservado.
Profit Takes: Retiradas Parciais de Lucro
Aqui entra um conceito que complica um pouco a modelagem mas é essencial para um portfólio real: o profit take. É quando o usuário vende uma parte do lucro de uma posição sem fechar ela completamente.
Exemplo: o usuário comprou 1 BTC a 50 mil (investiu 50 mil). O preço subiu para 80 mil (posição vale 80 mil, lucro de 30 mil). Ele decide retirar 10 mil de lucro. Ele não está fechando a posição, ele ainda tem o BTC. Ele está realizando parte do lucro.
O profit take registra: de qual posição veio, quanto foi retirado em valor, a que preço a crypto estava no momento da retirada, e quanto ficou restante na posição. Isso permite rastrear o histórico completo de realizações de lucro de cada posição.
As regras de negócio para criar um profit take são: a posição precisa existir, precisa pertencer ao usuário que está fazendo a operação (ownership), precisa estar ativa (não pode tirar lucro de posição fechada), e o valor retirado não pode exceder o valor atual da posição. Essa última validação é importante: se a posição vale 30 mil no momento, o usuário não pode retirar 50 mil.
Depois de criar o profit take, o sistema atualiza o last_profit_price da posição com o preço no momento da retirada. Isso serve como referência para o usuário saber "a última vez que realizei lucro, o preço estava em X".
Uma posição pode ter N profit takes ao longo do tempo. O usuário pode ir retirando lucro aos poucos conforme o preço sobe. O histórico completo fica registrado.
Calculando P&L em Tempo Real
Agora vem a parte mais interessante: como calcular o lucro ou prejuízo de cada posição em tempo real, considerando que o preço muda a cada segundo?
O cálculo base é simples: valor atual menos valor investido. O valor atual é a quantidade multiplicada pelo preço atual da crypto. O valor investido é o que foi registrado na criação da posição. A diferença é o P&L absoluto. Dividindo pelo investido e multiplicando por 100, você tem o P&L percentual.
No projeto, o preço atual vem do InfluxDB (banco de séries temporais que recebe dados da Binance via WebSocket). O cálculo do portfólio funciona assim: busca todas as posições ativas do usuário, busca o preço mais recente de cada criptomoeda no InfluxDB, e para cada posição calcula o valor atual, o P&L absoluto e o P&L percentual.
O desafio aqui é a performance. Se o usuário tem 20 posições em 8 moedas diferentes, você precisa buscar o preço atual de 8 moedas. Se fizer uma query por moeda, são 8 queries ao InfluxDB. A otimização é buscar todos os preços de uma vez (uma única query que retorna o último preço de todas as moedas monitoradas) e montar um mapa em memória de symbol para preço. Depois é só iterar nas posições e fazer lookup no mapa. Uma query ao banco em vez de 8.
Outro ponto importante: o P&L não considera os profit takes já realizados. Se o usuário investiu 50 mil, a posição vale 80 mil (P&L de +30 mil), mas ele já retirou 10 mil de lucro, o P&L mostrado ainda é +30 mil porque ele ainda tem a posição inteira. Os profit takes são mostrados separadamente como "lucro já realizado". Isso evita confusão entre "quanto estou ganhando agora" e "quanto já coloquei no bolso".
Para o portfólio completo, o sistema agrega: total investido (soma dos invested_amount de todas as posições ativas), número de posições ativas, número de posições fechadas, e a lista detalhada com P&L de cada uma. O frontend pode então mostrar cards de resumo e uma tabela detalhada.
Price Alerts: Monitorar e Disparar Uma Única Vez
Price alerts são notificações que o usuário configura: "me avise quando o BTC passar de 70 mil" ou "me avise quando o ETH cair abaixo de 3 mil". Parece simples, mas tem armadilhas na implementação.
O modelo de um alert tem: o usuário dono, a criptomoeda, o preço alvo, a direção ("above" ou "below"), se está ativo, e quando foi disparado. A direção é crucial: "above" significa "me avise quando o preço subir acima do alvo", "below" significa "me avise quando cair abaixo".
A primeira armadilha é o disparo repetido. Se o preço do BTC está oscilando entre 69.900 e 70.100, e o alerta é "acima de 70 mil", ele vai disparar, depois o preço cai, depois sobe de novo e dispara de novo. A solução é simples: quando o alerta dispara, ele é desativado automaticamente. O campo active vai para false e o triggered_at registra quando aconteceu. Se o usuário quiser ser notificado de novo, cria um novo alerta.
A segunda armadilha é a frequência de verificação. Você não pode verificar alertas a cada trade que chega (seria milhares de verificações por segundo). A estratégia é verificar periodicamente (a cada 10 segundos, por exemplo) usando o mapa de preços atuais que já existe em memória. Busca todos os alertas ativos, itera, compara o preço atual com o alvo, e dispara os que atingiram a condição.
A terceira armadilha é a concorrência. Se dois ticks de verificação rodam quase simultaneamente e ambos detectam que o alerta deve disparar, você pode ter disparo duplicado. A solução é usar uma operação atômica no banco: "UPDATE alerts SET active = false WHERE id = X AND active = true". Se o primeiro tick já desativou, o segundo não encontra o registro e não faz nada.
No projeto, a verificação de alertas recebe um mapa de preços (symbol para preço atual), busca todos os alertas ativos no banco, e para cada um verifica: se a direção é "above" e o preço atual é maior ou igual ao alvo, dispara. Se a direção é "below" e o preço atual é menor ou igual ao alvo, dispara. Disparar significa desativar o alerta e retornar na lista de alertas disparados (para que o sistema possa notificar o usuário via push, email, ou o que for).
O Portfólio Completo: Agregando Tudo
Juntando todas as peças, o endpoint de portfólio retorna uma visão completa do estado financeiro do usuário em criptomoedas. Ele agrega: o total investido em todas as posições ativas, quantas posições estão ativas e quantas foram fechadas, e para cada posição individual mostra o preço atual, o valor atual, o P&L absoluto e percentual, e a lista de profit takes já realizados.
Essa agregação cruza dados de múltiplas fontes: posições e profit takes vêm do banco relacional (PostgreSQL), preços atuais vêm do banco de séries temporais (InfluxDB), e o mapeamento de ID de crypto para symbol vem do cadastro de criptomoedas (PostgreSQL). São 4 consultas a 2 bancos diferentes, tudo orquestrado em um único use case que não sabe nada sobre a infraestrutura por baixo.
O resultado é um JSON rico que o frontend pode usar para montar dashboards com cards de resumo (total investido, lucro total, melhor posição, pior posição), tabelas detalhadas com cada posição e seu P&L, e gráficos de evolução usando o histórico de preços.
Considerações Finais
No enfrentamento da complexidade de modelar um sistema financeiro, mesmo que seja "só crypto", as decisões de modelagem no início determinam se o sistema vai escalar ou virar uma bola de neve de gambiarras. Modelar por posições em vez de saldos, separar profit takes como entidade própria, e calcular P&L em tempo real com dados de streaming são decisões que parecem mais complexas no começo mas simplificam enormemente a evolução do sistema.
O que eu quero que você leve deste artigo é que domínio financeiro exige pensar em estados (ativa/fechada), em ownership (o usuário só mexe no que é dele), em idempotência (alerta dispara uma vez só), e em composição de dados (preço atual + preço de compra = P&L). Se você acertar essas 4 coisas na modelagem, o resto é implementação mecânica.
E lembre-se: não agregue dados cedo demais. Mantenha o grão fino (cada compra individual, cada retirada individual) e agregue na hora de apresentar. É muito mais fácil agregar dados granulares do que desagregar dados que já foram somados.
Links indicativos:
- Portfolio Management Concepts: https://www.investopedia.com/terms/p/portfolio.asp
- Profit Taking Strategy: https://www.investopedia.com/terms/p/profittaking.asp
- Time-series databases for financial data: https://www.influxdata.com/time-series-database/
- Repositório do Trivium: https://github.com/carloseduardodb/trivium_backend