← Notas
Propriedade por estratégia: sua cozinha, suas chaves
architecture·security·ownership

Propriedade por estratégia: sua cozinha, suas chaves

Largamos o modelo de trading-user global. Cada estratégia agora lê chaves de exchange do cofre do dono.

· Mikhail Savchenko · Atualizado

Modelo antigo: um único dict settings["exchanges"] no banco, segurando chaves de Binance + OKX, usado por todo mundo. Quem estivesse logado como admin podia colocar suas chaves ali, e a engine de trading usaria para toda estratégia - long sleeves, intraday levels, paper, live.

Isso quebra assim que você tem dois operadores. Quebra pior quando você pensa em embarcar um terceiro. Quebra do pior jeito quando você pensa em resposta a incidente de equipe: rotacionar chaves significa parar todas as estratégias.

O modelo novo

Duas colunas e uma tabela nova:

  • PortfolioMeta.owner_user_id - toda estratégia long-sleeve pertence a exatamente um usuário.
  • TradingAccount (no banco de trading) - toda estratégia da classe trading pertence a exatamente um usuário, tem venue + credential label.
  • UserCredential - blob criptografado com Fernet, um por (usuário, provedor, label).

Quando a engine quer operar paper_levels_bob, pergunta para _strategy_creds:

def _strategy_creds(strategy_id, provider):
    # 1. Trading account?
    ta = TradingAccount.find(strategy_id)
    if ta and ta.owner_user_id:
        return load_credential(ta.owner_user_id, ta.venue_provider, ta.credential_label)
    # 2. Portfolio strategy?
    pm = PortfolioMeta.find(strategy_id)
    if pm and pm.owner_user_id:
        return load_credential(pm.owner_user_id, provider, "default")
    # 3. No owner → no creds. Engine logs warning and skips.
    return {}

Sem fallback. Uma estratégia sem dono não opera nada. A engine loga WARNING: no credentials for strategy X e pula o tick. Quietamente. Sem ordens, sem posições, sem surpresa.

O que isso destrava

Onboarding agora é um formulário admin de quatro passos: criar usuário → atribuir estratégia → usuário adiciona chaves ao cofre dele → admin credita o cash. Cada passo é reversível.

Rotação de chaves é por usuário. O Bob revoga a chave Binance dele na exchange; vai em /account, deleta a cred, gera uma nova, salva. O próximo tick da estratégia pega as chaves novas. Sem reinício, sem intervenção do admin, sem afetar nenhuma outra estratégia.

Resposta a incidente quando um operador é comprometido: revoga o token (MCP para de funcionar), rotaciona as chaves de exchange (apaga do cofre), e as estratégias dele entram em modo degradado. O raio de impacto é um usuário.

Isolamento de saldo de trading

O helper compute_user_balance(user_id) faz join nos dois bancos e devolve a árvore daquele usuário. Não existe view global de “AUM do fundo” por padrão - o site de marketing exibe (Total AUM = soma das estratégias públicas) porque a gente mantém uma flag is_public=True especificamente para esse caso de uso. Os dashboards internos de operação mostram só o seu pedaço.

Por que sem fallback

Consideramos um fallback temporário para settings["exchanges"] por compatibilidade. Tiramos antes de subir. Motivos:

  1. Um fallback cria atrativo perigoso - operadores colocam chaves “default” “só para teste em paper”, esquecem que existem, e depois uma estratégia real sem dono pega elas.
  2. O comportamento da engine tem que ser chato. “Estratégia sem dono não faz nada” é chato. “Estratégia sem dono cai num fallback de chaves globais” exige saber do fallback para prever o que vai acontecer.
  3. A migração foram algumas linhas de SQL. Estratégias antigas receberam is_public=True para visibilidade pública read-only, sem dono, sem chaves, sem operar. Novos admins atribuem dono explicitamente.

O que o site de marketing vê

O endpoint público /api/public/strategies/{id} tira owner_user_id, control internals, sinais e o motivo do kill-switch antes de responder. O que você vê em inite.fund/strategies é exatamente isso: patrimônio + drawdown + sleeves + últimas 50 operações + últimos 365 pontos de equity. Sem identificadores, sem estado interno, sem segredos.

As páginas Astro fazem fetch SSR desse endpoint com cache de borda de 30s. Se você der reload duas vezes seguidas, vê os mesmos números; 30 segundos depois atualiza. Esse é o nosso “tempo real” - perto o suficiente para uma página de track record, longe o suficiente da origem para absorver tráfego.

O que custou

O refactor mexeu em 12 arquivos no repositório do produto, 2 tabelas novas, uma migração de startup única, ~600 linhas de código e ~70 testes novos. Os testes cobrem a resolução de propriedade, o cofre criptografado, fluxos de capital com idempotência e as chamadas MCP com RBAC.

Antes: 1004 testes passando. Depois: 1180. Zero regressão.

O site de marketing (este site) é construído no mesmo modelo. A faixa de métricas no topo? É compute_user_balance(NULL) agregado pelas estratégias públicas, buscado server-side, renderizado em HTML. Nenhum dado privado cruza a fronteira.

Notas relacionadas
Todas as notas →