Construye un bot de trading en tiempo real en Delphi con sgcWebSockets + Binance

· Funciones

Lo que vamos a construir

Al final de este tutorial tendrás una aplicación Delphi VCL funcionando que recibe en streaming datos en vivo de trades y order book de Binance, ejecuta una estrategia de breakout sencilla, coloca órdenes reales vía la API REST y aplica controles de riesgo (tamaño máximo de posición, límite diario de pérdidas, kill switch). La misma fontanería funciona para cualquier exchange soportado por sgcWebSockets — Coinbase, Kraken, OKX, Bybit, Bitfinex — cambiando sólo el mapeo de credenciales y símbolos.

Es la misma arquitectura que usa nuestro sample de referencia sgcTrader. Si quieres un punto de partida mayor con UI completa, charting y routing multi-exchange, cógelo. El recorrido de abajo muestra lo que ocurre por debajo en unas 300 líneas de código.

Dos prerrequisitos antes de empezar. Primero, consigue una API key de Binance (Account > API Management). Para desarrollo, genera una key que tenga sólo "Enable Reading" y "Enable Spot Trading" y mete tu IP en la whitelist. Nunca pongas en código una key con withdrawal activado. Segundo, hazlo todo primero en el testnet de Binance (testnet.binance.vision). Los endpoints, formatos de mensaje y algoritmo de firma son idénticos a producción, pero los fondos son falsos. Hemos perdido dinero real con "seguro que mi estrategia es correcta" exactamente el número de veces que no probamos primero en el testnet.

Arquitectura en un diagrama

Tres hilos, dos componentes, un gatekeeper de riesgo:

Componente Rol Hilo
TsgcWSAPI_Binance Stream WebSocket: trades, depth, klines, user data Worker de I/O
TsgcHTTP_API_Binance REST: colocación de órdenes, cancelar, snapshot de cuenta Worker trader
Cola de estrategia Desacoplamiento: eventos de mercado → decisiones → órdenes Worker de estrategia
Puerta de riesgo Bloquear / reducir / permitir cada orden En línea en el trader

Paso 1: stream de datos de mercado

Arrastra un TsgcWSAPI_Binance al form. Ya habla el protocolo combinado de streams de Binance — sólo te suscribes a los canales que quieras.

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;

Esa es toda la capa de ingesta de datos de mercado. Reconexión en caídas, heartbeat para detectar enlaces muertos y un push no bloqueante a la cola de estrategia.

Una cosa que el componente hace y que de otro modo tendrías que escribir tú: la URL del combined stream de Binance es /stream?streams=name1/name2/name3, y si quieres añadir o quitar streams sin tirar la conexión tienes que enviar un mensaje JSON-RPC subscribe/unsubscribe sobre el mismo socket. TsgcWSAPI_Binance expone métodos SubscribeStream y UnsubscribeStream que manejan por ti el handshake JSON-RPC. Útil cuando el usuario elige un nuevo ticker en la UI — sin reconexión, sin mensajes perdidos.

Además: Binance impone límites por-IP y por-stream. Para profundidad completa en cada par USDT alcanzarás el límite de tasa de mensajes rápidamente. Suscríbete sólo a lo que realmente necesites y prefiere streams agregados (!miniTicker@arr) sobre streams por símbolo cuando necesites una vista amplia de mercado.

Paso 2: una estrategia mínima

La estrategia corre en su propio hilo. Mantiene una ventana rodante de los últimos N cierres del stream de klines de 1 minuto y se pone larga cuando el precio rompe por encima del máximo de 20 períodos. Pura ilustración — por favor no pongas esto delante de dinero 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;

Paso 3: puerta de riesgo

Nunca dejes que una estrategia hable directamente con el exchange. Hace pasar cada intención por una puerta que conoce tus límites.

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;

Paso 4: coloca la orden vía REST

El worker de órdenes saca intenciones validadas, firma la petición y hace POST 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;

Paso 4b: autenticación REST y firma

El componente TsgcHTTP_API_Binance firma las peticiones por ti usando el secreto de la API. Entre bambalinas construye la query string canónica, calcula un HMAC-SHA256 con tu secreto y lo añade como parámetro signature. Aportas la key y el secreto una vez al arranque.

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));

Si quieres correr contra el testnet, fija BinanceOptions.Testnet := True — el componente cambia automáticamente tanto la URL base REST como el host WebSocket. Construye y prueba contra el testnet, cambia un único flag, despliega a producción. La documentación de la API de Binance es por lo demás idéntica entre los dos entornos.

Paso 5: stream de datos de usuario

El mismo componente WebSocket también se suscribe a tu stream privado de datos de usuario — actualizaciones de cuenta, eventos de órdenes, cambios de posición. Es así como concilias fills que ocurrieron fuera de tu bot (cancelación manual desde la web UI, liquidación, 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;

Una nota sobre backtesting

Nada de lo de arriba responde a "¿es esta estrategia realmente rentable?". Para eso está el backtesting — reproducir la misma estrategia contra datos históricos para estimar su rendimiento futuro. La arquitectura de arriba lo hace casi gratis: al hilo de estrategia no le importa si los eventos de mercado vienen de un WebSocket en vivo o de un lector CSV. Construye una fuente sintética de eventos que lea klines de disco y los alimente a la misma cola, y tu código de estrategia corre sin cambios contra años de datos históricos.

Dos pitfalls que evitar. Look-ahead bias: no dejes que la estrategia espíe ningún punto de dato que no habría estado disponible en el timestamp que está procesando. Y survivorship bias: entrena y testea con el universo de símbolos que existían en su momento, no con la lista curada de símbolos "exitosos" que sobrevivieron hasta hoy. Ambos han matado más estrategias en producción que todos los bugs de código juntos.

Checklist operativo

Preocupación Dónde manejarla
Reconexión en caída de Wi-Fi WatchDog.Enabled := True
Detección de enlace muerto HeartBeat.Enabled := True
Sincronía de tiempo (Binance rechaza firmas desfasadas) NTP en el SO, más una llamada diaria al endpoint de server time
Idempotencia de órdenes Usa newClientOrderId en cada orden
Límites de tasa Trackea cabeceras; haz back off cuando estés al 90% del límite
Kill switch Un único booleano, conmutado desde UI o un proceso watchdog
Log de auditoría Cada intención, cada fill, cada rechazo, append-only

Más allá de Binance

Cambia TsgcWSAPI_Binance por TsgcWSAPI_Coinbase, TsgcWSAPI_Kraken o cualquier otro de los 20+ componentes de exchange. La estrategia, puerta de riesgo y worker de órdenes no cambian — sólo cambian la configuración de credenciales y la nomenclatura de símbolos. Para un trader multi-exchange de nivel producción con charts, gestión de posición y UI de routing de órdenes de fábrica, mira el sample sgcTrader.

Los sistemas reales multi-exchange añaden una capa más por encima de lo que viste aquí: un normalizador de símbolos. Binance lo llama BTCUSDT, Coinbase lo llama BTC-USD, Kraken lo llama XBT/USD. Construye un modelo interno de símbolos con un nombre canónico y alias por exchange, y traduce en el límite de la API. Cinco minutos de trabajo por adelantado, infinitos bugs ahorrados aguas abajo.

La otra cosa a añadir para operaciones multi-exchange es una comprobación de clock-skew al arrancar. Binance, Coinbase y los demás rechazan peticiones firmadas con un timestamp con más de 1000 ms de desfase respecto a su servidor. NTP normalmente te mantiene bien dentro de eso, pero un VPS mal configurado puede derivar segundos en una hora. Consulta el endpoint de server time al arrancar, loguea el offset, niégate a tradear si está >500 ms.

¿Por qué Delphi para esto?

"¿Por qué no escribirlo en Python?" es la pregunta obvia. Tres respuestas desde producción. Primero, el warm-up del JIT y el GIL hacen que CPython encaje mal para event loops de baja latencia — la misma estrategia que alcanza 0,8 ms de latencia mediana en Delphi tarda 6 ms en CPython sin esfuerzo serio. Segundo, la historia de despliegue es más simple: un exe firmado vs un virtualenv con un centenar de wheels, la mitad de las cuales requieren un compilador C en tiempo de instalación. Tercero, el back office existente está en Delphi. Reutilizar esas clases — ledger de cuenta, calculadora P&L, journal, log de auditoría — en el nuevo bot en lugar de reimplementarlas en otro lenguaje elimina toda una categoría de bugs de conciliación.

Para investigación pura y backtests estilo notebook, Python gana fácilmente — el ecosistema de pandas, statsmodels, vectorbt y amigos es inigualable. El reparto que funciona para la mayoría de tiendas: investigación en Python, producción en Delphi. Exporta la lógica de la estrategia como una pequeña máquina de estados, pórtala una vez, ejecútala sobre un runtime Delphi probado en batalla. Las dos mitades no tienen que compartir lenguaje para compartir resultados.

Qué leer a continuación

Si planeas correr esto 24/7 en un VPS, lee a continuación Ajuste de rendimiento. Para evitar los pitfalls más comunes, mira 10 errores comunes. Y si todavía no has instalado sgcWebSockets, el hub de Primeros Pasos te pone en marcha en cinco minutos.

Aviso: la estrategia de este post es con fines educativos. Operar con criptomonedas implica un riesgo sustancial. No despliegues código sin probar con capital real.