← Notas
O saldo de trading não-linear
architecture·trading·capital-tree

O saldo de trading não-linear

Por que o seu NAV de trading pode ser maior do que sua alocação - e por que mantemos os dois em bancos diferentes.

· Mikhail Savchenko · Atualizado

O saldo de um usuário no inite.fund é uma árvore:

cash (idle)
 ├─ long allocations[]    →  PortfolioMeta(allocation, equity)         [linear]
 └─ trading allocations[] →  TradingAccount + AccountSnapshot          [non-linear]
                              cash + nav + margin_used
                              + realized_pnl + unrealized_pnl
total = cash + Σ long.equity + Σ trading.nav

Duas pilhas, um saldo. Alocações long são lineares: o equity que você vê é o mark-to-market da estratégia em cima do cash que você colocou. Trading é não-linear: o NAV diverge da alocação por causa do PnL, da margem (você pode usar o mesmo dólar duas vezes - short de perp hedgeando long de spot), das oscilações não-realizadas. Por design.

Por que dois bancos

Tentador encaixar o estado de trading no mesmo arquivo SQLite do portfolio. A gente não fez. Trading carrega seu próprio schema (tm_orders, tm_fills, tm_positions, tm_account_snapshots, tm_capital_flows) num trading_module.db separado, com seu próprio SQLAlchemy Base.

Duas razões:

  1. A camada de trading existia antes como ferramenta de pesquisa independente. Tinha seu próprio banco, sua própria session factory, suas próprias migrações. Dobrar isso em web/ teria significado reescrever a serialização do trade history sem upside real. O custo de “dois bancos numa app só” é pequeno: um segundo sessionmaker e uma API cuidadosa para capital flows.
  2. Raio de impacto. Um bug na engine de trading que corrompe estado fica em trading_module.db. O lado do portfolio - que segura cash parado, alocações long, audit log, contas de usuário - fica intacto. A gente pode dropar o banco de trading e reconstruir a partir do log de ordens; não pode dropar o banco do portfolio sem perder a trilha do dinheiro.

Primitivas de capital flow

O contrato entre os dois bancos é um conjunto pequeno de helpers atômicos em web/balance.py:

  • get_user_cash(user_id) - soma das linhas de UserCashLedger.
  • move_to_long(user, strategy, amount) - acrescenta uma linha assinada no ledger (cash negativo) e incrementa PortfolioMeta.allocation numa única transação.
  • move_to_trading(user, strategy_id, amount) - mesma coisa, mas também escreve uma CapitalFlowRow no lado de trading. Transação em dois bancos, idempotente em audit_ref.
  • compute_user_balance(user) - faz join nos dois lados e devolve a árvore.

Cada helper aceita um audit_ref opcional. Passa o mesmo ref duas vezes → na segunda é no-op. Hiccup de rede, retry quatro vezes → o ledger tem uma entrada só.

O que “não-linear” significa na prática

O Bob tem $100k em cash. Aloca $30k para paper_levels_bob. A engine de trading abre um short de perp ETH com 3x notional → margin_used = $9k, notional da posição = $27k. O preço anda contra, não-realizado = -$2k.

O saldo do Bob:

cash             = $70,000
long             = $0  (sem alocações long ainda)
trading[paper_levels_bob]:
  initial_alloc  = $30,000
  cash           = $21,000   (travado: $9k de margem)
  unrealized_pnl = -$2,000
  nav            = $28,000   (cash + não-realizado, fora a margem)
total            = $98,000   (= 70 + 0 + 28)

Repare: nav ≠ initial_allocation. Repare também: total ≠ Σ allocations. A matemática:

total = cash + Σ long.equity + Σ trading.nav

Se você quer uma foto “honesta”, marca o trading pelo NAV (o que você teria se a posição fechasse agora), não pela alocação. É o que fazemos.

Por que isso importa para a tool MCP

get_my_balance retorna a árvore inteira. Quando um usuário pergunta ao Claude “qual meu saldo”, recebe de volta exatamente esse dict. O trabalho da skill é dizer direito:

  • Comece pelo total.
  • Cash mencionado em separado.
  • Alocações long: equity + retorno %.
  • Contas de trading: NAV + alerta de margem se margin_used / nav > 0.5.

Um usuário que visse só um número (“$98,000”) perderia o fato de que $30k do capital dele está em risco em trading - e que $9k desse total estão travados em margem. Matemática linear esconde isso. A árvore mostra.

Lições

  1. Separe responsabilidades pelo raio de impacto, não pela contagem de tabelas. Dois bancos é tranquilo se tiverem um contrato fino e atômico.
  2. Idempotência-por-padrão deixa retries seguros. Toda API de capital flow aceita audit_ref; um re-run com o mesmo ref é no-op. A camada MCP pode ser ingênua sobre retries; a camada de auditoria não pode.
  3. Um “saldo” com margem dentro é uma árvore, não um número. Mostrar um número é mentir. Mostrar a árvore é a única UI honesta.

O schema completo está em trading/state/models.py se você quiser ler.

Notas relacionadas
Todas as notas →