Construa um bot de trading em tempo real em Delphi com sgcWebSockets + Binance

· Recursos

O que vamos construir

Ao final deste tutorial, você terá uma aplicação Delphi VCL funcional que faz streaming de dados ao vivo de trades e order book da Binance, roda uma estratégia simples de breakout, envia ordens reais via REST API e impõe controles de risco (tamanho máximo de posição, limite diário de perda, kill switch). A mesma encanação funciona para qualquer exchange suportada pelo sgcWebSockets — Coinbase, Kraken, OKX, Bybit, Bitfinex — mudando só o credencial e o mapeamento de símbolos.

Essa é a mesma arquitetura usada por nosso sample de referência sgcTrader. Se você quer um ponto de partida maior com UI completa, gráficos e roteamento multi-exchange, pegue aquele. O passo a passo abaixo mostra o que acontece por baixo em cerca de 300 linhas de código.

Dois pré-requisitos antes de começar. Primeiro, gere uma chave de API na Binance (Account > API Management). Para desenvolvimento, gere uma chave que tenha apenas “Enable Reading” e “Enable Spot Trading”, e coloque seu IP na whitelist. Nunca coloque uma chave com saque liberado no código. Segundo, faça tudo primeiro no testnet da Binance (testnet.binance.vision). Os endpoints, formatos de mensagem e algoritmo de assinatura são idênticos aos de produção, mas os fundos são fake. Já perdemos dinheiro real para “tenho certeza que minha estratégia está certa” exatamente o número de vezes que não testamos no testnet antes.

Arquitetura em um diagrama

Três threads, dois componentes, um guarda de risco:

Componente Papel Thread
TsgcWSAPI_Binance Stream WebSocket: trades, depth, klines, user data I/O worker
TsgcHTTP_API_Binance REST: envio de ordem, cancelamento, snapshot de conta Trader worker
Fila de estratégia Desacoplamento: eventos de mercado → decisões → ordens Strategy worker
Gate de risco Bloqueia / reduz / autoriza cada ordem Inline no trader

Passo 1: streaming de dados de mercado

Arraste um TsgcWSAPI_Binance no form. Ele já fala o protocolo combined stream da Binance — você só assina os canais que quer.

uses
  sgcWSAPI_Binance;

procedure TForm1.FormCreate(Sender: TObject);
begin
  oBinance := TsgcWSAPI_Binance.Create(Self);
  oBinance.WatchDog.Enabled  := True;
  oBinance.WatchDog.Interval := 5;
  oBinance.HeartBeat.Enabled := True;

  oBinance.OnBinanceMessage := DoStream;
  oBinance.OnDisconnect     := DoDisconnect;

  // Subscribe to 1-minute klines and aggregated trades for BTCUSDT
  oBinance.Streams.Add('btcusdt@kline_1m');
  oBinance.Streams.Add('btcusdt@aggTrade');

  oBinance.Active := True;
end;

procedure TForm1.DoStream(Sender: TObject; const aStream, aData: string);
begin
  // Fire-and-forget: push to the strategy queue
  oStrategyQueue.Push(TMarketEvent.Create(aStream, aData));
end;

Essa é toda a camada de ingestão de dados de mercado. Reconexão em quedas, heartbeat para detectar links mortos e um push não bloqueante para a fila de estratégia.

Uma coisa que o componente faz e que você teria que escrever de outra forma: a URL de combined stream da Binance é /stream?streams=name1/name2/name3 e, se você quer adicionar ou remover streams sem derrubar a conexão, precisa enviar uma mensagem JSON-RPC subscribe/unsubscribe pelo mesmo socket. O TsgcWSAPI_Binance expõe métodos SubscribeStream e UnsubscribeStream que tratam o handshake JSON-RPC para você. Útil quando o usuário escolhe um novo ticker na UI — sem reconexão, sem mensagens perdidas.

Além disso: a Binance impõe limites por IP e por stream. Para depth completo em cada par USDT, você bate no limite de taxa de mensagens rapidamente. Assine só o que de fato precisa, e prefira streams agregados (!miniTicker@arr) a streams por símbolo quando precisar de uma visão ampla do mercado.

Passo 2: uma estratégia mínima

A estratégia roda em sua própria thread. Mantém uma janela deslizante das últimas N fechamentos do stream de klines de 1 minuto e fica comprada quando o preço rompe a máxima de 20 períodos. Pura ilustração — por favor, não coloque isso na frente de dinheiro real.

procedure TStrategyThread.Execute;
var
  oEvent : TMarketEvent;
  oJSON  : TsgcJSONObject;
  vClose : Double;
  vHigh  : Double;
begin
  while not Terminated do
  begin
    if not oStrategyQueue.Pop(oEvent, 100) then Continue;
    try
      oJSON := TsgcJSONObject.Parse(oEvent.Data);
      try
        if oEvent.Stream.EndsWith('@kline_1m') then
        begin
          vClose := oJSON.O['k'].F['c'];
          FCloses.Append(vClose);
          if FCloses.Count >= 21 then
          begin
            vHigh := FCloses.Max(20);                  // prior 20-bar high
            if (FPosition = 0) and (vClose > vHigh) then
              oOrderQueue.Push(TIntent.New(siBuy, 'BTCUSDT', 0.001))
            else if (FPosition > 0) and (vClose < FCloses.MA(20)) then
              oOrderQueue.Push(TIntent.New(siSell, 'BTCUSDT', FPosition));
          end;
        end;
      finally
        oJSON.Free;
      end;
    finally
      oEvent.Free;
    end;
  end;
end;

Passo 3: gate de risco

Nunca deixe uma estratégia falar diretamente com a exchange. Canalize cada intenção por um gate que conheça seus limites.

function TRiskGate.Validate(const aIntent: TIntent;
  out aReason: string): Boolean;
begin
  Result := False;

  if FKillSwitch then
    Exit(False);

  if Abs(FDailyPnL) > FConfig.MaxDailyLoss then
  begin
    aReason := 'Daily loss limit hit';
    Exit;
  end;

  if (aIntent.Side = siBuy)
     and (FPositionUSD + aIntent.Qty * FLastPrice > FConfig.MaxPositionUSD) then
  begin
    aReason := 'Max position size';
    Exit;
  end;

  Result := True;
end;

Passo 4: enviar a ordem via REST

O worker de ordens puxa intenções validadas, assina a requisição e faz POST para a Binance.

procedure TTraderThread.Execute;
var
  oIntent  : TIntent;
  vReason  : string;
  oResponse: TsgcBinanceClass_Response_NewOrder;
begin
  while not Terminated do
  begin
    if not oOrderQueue.Pop(oIntent, 100) then Continue;
    try
      if not FRisk.Validate(oIntent, vReason) then
      begin
        Log(Format('REJECT %s %s qty=%.6f reason=%s',
          [SideName(oIntent.Side), oIntent.Symbol, oIntent.Qty, vReason]));
        Continue;
      end;

      oResponse := oHttp.NewOrder(
        oIntent.Symbol,
        IfThen(oIntent.Side = siBuy, 'BUY', 'SELL'),
        'MARKET',
        oIntent.Qty,
        0  // price ignored for MARKET
      );
      try
        Log(Format('FILL  %s qty=%.6f price=%.2f id=%d',
          [oIntent.Symbol, oResponse.ExecutedQty,
           oResponse.AvgPrice, oResponse.OrderId]));
        FRisk.OnFill(oIntent.Side, oResponse.ExecutedQty, oResponse.AvgPrice);
      finally
        oResponse.Free;
      end;
    finally
      oIntent.Free;
    end;
  end;
end;

Passo 4b: autenticação e assinatura REST

O componente TsgcHTTP_API_Binance assina as requisições por você usando o API secret. Por baixo, ele monta a query string canônica, calcula um HMAC-SHA256 com seu secret e anexa como parâmetro signature. Você fornece chave e secret uma única vez na inicialização.

oHttp := TsgcHTTP_API_Binance.Create(Self);
oHttp.BinanceOptions.ApiKey    := FConfig.ApiKey;
oHttp.BinanceOptions.ApiSecret := FConfig.ApiSecret;
oHttp.BinanceOptions.RecvWindow := 5000;   // ms tolerance for signed requests
// Test connectivity and confirm your IP whitelist
ShowMessage('Server time: ' + IntToStr(oHttp.GetServerTime));

Se quiser rodar contra o testnet, defina BinanceOptions.Testnet := True — o componente troca tanto a base URL do REST quanto o host do WebSocket automaticamente. Construa e teste contra o testnet, vire uma única flag, suba para produção. A documentação da API da Binance é, no mais, idêntica entre os dois ambientes.

Passo 5: user data stream

O mesmo componente WebSocket também assina seu user data stream privado — atualizações de conta, eventos de ordem, mudanças de posição. É assim que você concilia fills que aconteceram fora do bot (cancelamento manual pela UI web, liquidação etc.).

oBinance.AuthOptions.ApiKey    := FConfig.ApiKey;
oBinance.AuthOptions.ApiSecret := FConfig.ApiSecret;
oBinance.Streams.Add('!userData');

procedure TForm1.DoStream(Sender: TObject; const aStream, aData: string);
var
  oJSON: TsgcJSONObject;
begin
  if aStream = '!userData' then
  begin
    oJSON := TsgcJSONObject.Parse(aData);
    try
      if oJSON.S['e'] = 'executionReport' then
        FRisk.ReconcileExternalFill(oJSON);
    finally
      oJSON.Free;
    end;
  end;
end;

Uma nota sobre backtesting

Nada acima responde “essa estratégia é de fato lucrativa?” É para isso que serve o backtesting — reproduzir a mesma estratégia contra dados históricos para estimar seu desempenho futuro. A arquitetura acima torna isso quase de graça: a thread de estratégia não se importa se eventos de mercado vêm de um WebSocket ao vivo ou de um leitor de CSV. Construa uma fonte sintética de eventos que leia klines de disco e os jogue na mesma fila, e seu código de estratégia roda inalterado contra anos de dados históricos.

Duas armadilhas a evitar. Look-ahead bias: não deixe a estratégia espiar nenhum ponto de dado que não estaria disponível no timestamp que ela está processando. E survivorship bias: treine e teste no universo de símbolos que existiam à época, não na lista curada de símbolos “bem-sucedidos” que sobreviveram até hoje. Os dois mataram mais estratégias em produção do que todos os bugs de código somados.

Checklist operacional

Preocupação Onde tratar
Reconexão em queda de Wi-Fi WatchDog.Enabled := True
Detecção de link morto HeartBeat.Enabled := True
Sync de tempo (Binance rejeita assinaturas com clock desviado) NTP no SO, mais uma chamada diária ao endpoint de server time
Idempotência de ordem Use newClientOrderId em cada ordem
Rate limits Acompanhe headers; faça back-off ao chegar a 90% do limite
Kill switch Um único booleano, virado pela UI ou por um processo watchdog
Log de auditoria Cada intenção, cada fill, cada rejeição, append-only

Além da Binance

Troque TsgcWSAPI_Binance por TsgcWSAPI_Coinbase, TsgcWSAPI_Kraken ou qualquer dos 20+ componentes de exchange. A estratégia, o gate de risco e o worker de ordens não mudam — só o setup de credenciais e a nomenclatura de símbolos. Para um trader multi-exchange de nível de produção, com gráficos, gerenciamento de posição e UI de roteamento de ordens de fábrica, dê uma olhada no sample sgcTrader.

Sistemas multi-exchange reais adicionam mais uma camada acima do que você viu aqui: um normalizador de símbolos. A Binance chama de BTCUSDT, a Coinbase de BTC-USD, a Kraken de XBT/USD. Construa um modelo interno de símbolo com um nome canônico e aliases por exchange, e traduza na fronteira da API. Cinco minutos de trabalho upfront, infinitos bugs poupados depois.

A outra coisa a adicionar para operações multi-exchange é uma checagem de clock skew na inicialização. Binance, Coinbase e demais rejeitam requisições assinadas com timestamp mais de 1000 ms fora do server delas. NTP normalmente o mantém bem dentro disso, mas um VPS mal configurado pode derivar segundos em uma hora. Consulte o endpoint de server time na inicialização, logue o offset, recuse operar se for >500 ms.

Por que Delphi para isso?

“Por que não escrever em Python?” é a pergunta óbvia. Três respostas vindas de produção. Primeira, o aquecimento do JIT e a GIL tornam o CPython um mau encaixe para event loops de baixa latência — a mesma estratégia que atinge 0,8 ms de latência mediana em Delphi leva 6 ms em CPython sem esforço sério. Segunda, a história de deployment é mais simples: um único exe assinado vs uma virtualenv com uma centena de wheels, metade das quais exige um compilador C na instalação. Terceira, o back office existente é em Delphi. Reaproveitar essas classes — ledger de conta, calculadora de P&L, journal, log de auditoria — no novo bot, em vez de reimplementá-las em outra linguagem, elimina uma categoria inteira de bugs de conciliação.

Para pesquisa pura e backtests estilo notebook, Python ganha fácil — o ecossistema de pandas, statsmodels, vectorbt e companhia é inigualável. A divisão que funciona para a maioria das lojas: pesquisa em Python, produção em Delphi. Exporte a lógica da estratégia como uma pequena máquina de estados, porte uma vez, rode em runtime Delphi testado em batalha. As duas metades não precisam compartilhar linguagem para compartilhar resultados.

O que ler em seguida

Se você planeja rodar isso 24/7 em um VPS, leia em seguida Ajuste de Desempenho. Para evitar as armadilhas mais comuns, veja 10 erros comuns. E se você ainda não instalou o sgcWebSockets, o hub Primeiros Passos coloca você ao vivo em cinco minutos.

Aviso: a estratégia deste post tem fins educacionais. Operar criptomoeda envolve risco substancial. Não coloque código não testado com capital real.