Costruisci un bot di trading real-time in Delphi con sgcWebSockets + Binance

· Funzionalità

Cosa stiamo costruendo

Alla fine di questo tutorial avrai un'applicazione Delphi VCL funzionante che fa lo streaming di dati live di trade e order book da Binance, esegue una semplice strategia di breakout, piazza ordini reali via REST API e impone controlli di rischio (dimensione massima della posizione, limite di perdita giornaliero, kill switch). La stessa infrastruttura funziona per qualsiasi exchange supportato da sgcWebSockets — Coinbase, Kraken, OKX, Bybit, Bitfinex — cambiando solo le credenziali e la mappatura dei simboli.

Questa è la stessa architettura usata dal nostro sample di riferimento sgcTrader. Se vuoi un punto di partenza più grande con UI completa, charting e routing multi-exchange, prendi quello. La guida sotto mostra cosa sta accadendo sotto il cofano in circa 300 righe di codice.

Due prerequisiti prima di iniziare. Primo, prendi una API key di Binance (Account > API Management). Per lo sviluppo, genera una chiave che abbia solo "Enable Reading" ed "Enable Spot Trading", e metti in whitelist il tuo IP. Non mettere mai una chiave con prelievo abilitato nel codice. Secondo, fai tutto sul testnet di Binance prima (testnet.binance.vision). Gli endpoint, i formati dei messaggi e l'algoritmo di firma sono identici alla produzione, ma i fondi sono finti. Abbiamo perso soldi veri per "sono sicuro che la mia strategia sia corretta" esattamente quante volte non abbiamo testato prima sul testnet.

Architettura in un diagramma

Tre thread, due componenti, un gatekeeper del rischio:

Componente Ruolo Thread
TsgcWSAPI_Binance Stream WebSocket: trade, depth, kline, user data Worker di I/O
TsgcHTTP_API_Binance REST: piazzamento ordini, cancellazione, snapshot dell'account Worker trader
Coda della strategia Disaccoppiamento: eventi di mercato → decisioni → ordini Worker strategia
Risk gate Blocca / riduce / consente ogni ordine Inline nel trader

Passo 1: stream dei market data

Metti un TsgcWSAPI_Binance sulla form. Parla già il protocollo combined stream di Binance — ti basta sottoscriverti ai canali che vuoi.

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;

Questo è l'intero layer di ingestione dei market data. Riconnessione sui drop, heartbeat per rilevare link morti e un push non bloccante alla coda della strategia.

Una cosa che il componente fa che altrimenti dovresti scrivere da te: l'URL del combined stream Binance è /stream?streams=name1/name2/name3, e se vuoi aggiungere o rimuovere stream senza far cadere la connessione devi inviare un messaggio JSON-RPC di subscribe/unsubscribe sullo stesso socket. TsgcWSAPI_Binance espone i metodi SubscribeStream e UnsubscribeStream che gestiscono per te l'handshake JSON-RPC. Utile quando l'utente sceglie un nuovo ticker nella UI — nessuna riconnessione, nessun messaggio perso.

Inoltre: Binance impone limiti per IP e per stream. Per la depth completa su ogni coppia USDT raggiungerai velocemente il limite di message-rate. Sottoscrivi solo a ciò di cui hai effettivamente bisogno, e preferisci gli stream aggregati (!miniTicker@arr) agli stream per simbolo quando hai bisogno di una vista ampia del mercato.

Passo 2: una strategia minimale

La strategia gira sul proprio thread. Mantiene una finestra scorrevole degli ultimi N close dallo stream kline 1 minuto e va long quando il prezzo rompe sopra un massimo di 20 periodi. Pura illustrazione — per favore non metterla davanti a soldi reali.

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: risk gate

Non lasciare mai che una strategia parli direttamente con l'exchange. Convoglia ogni intent attraverso un gate che conosce i tuoi limiti.

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: piazzare l'ordine via REST

Il worker degli ordini estrae gli intent validati, firma la richiesta e fa il 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;

Passo 4b: autenticazione e firma REST

Il componente TsgcHTTP_API_Binance firma le richieste per te usando l'API secret. Dietro le quinte costruisce la query string canonica, calcola un HMAC-SHA256 con il tuo secret e lo aggiunge come parametro signature. Tu fornisci chiave e secret una volta all'avvio.

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 vuoi girare contro il testnet, imposta BinanceOptions.Testnet := True — il componente cambia automaticamente sia l'URL base REST sia l'host WebSocket. Costruisci e testa contro il testnet, cambia un singolo flag, deploya in produzione. La documentazione dell'API Binance è altrimenti identica tra i due ambienti.

Passo 5: stream dei dati utente

Lo stesso componente WebSocket si sottoscrive anche al tuo stream privato di user data — aggiornamenti account, eventi ordini, cambi posizione. È così che riconcili i fill avvenuti fuori dal tuo bot (cancellazione manuale dalla UI web, liquidazione, ecc.).

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 sul backtesting

Niente di quanto sopra risponde a "questa strategia è effettivamente profittevole?" Per quello c'è il backtesting — rigiocare la stessa strategia contro dati storici per stimarne la performance forward. L'architettura sopra rende questo quasi gratis: al thread della strategia non importa se gli eventi di mercato vengono da un WebSocket live o da un reader CSV. Costruisci una sorgente di eventi sintetica che legge le kline da disco e le passa nella stessa coda, e il codice della tua strategia gira invariato contro anni di dati storici.

Due trappole da evitare. Look-ahead bias: non far sbirciare alla strategia alcun dato che non sarebbe stato disponibile al timestamp che sta processando. E survivorship bias: addestra e testa sull'universo di simboli che esistevano al tempo, non sulla lista curata di simboli "di successo" sopravvissuti fino a oggi. Entrambi hanno ucciso più strategie in produzione che ogni bug di codifica messi insieme.

Checklist operativa

Preoccupazione Dove gestirla
Riconnessione su drop Wi-Fi WatchDog.Enabled := True
Rilevamento dead-link HeartBeat.Enabled := True
Sync dell'orario (Binance rifiuta firme con clock sfasato) NTP sull'OS, più una chiamata giornaliera all'endpoint server time
Idempotenza degli ordini Usa newClientOrderId su ogni ordine
Rate limit Traccia gli header; fai backoff quando sei al 90% del limite
Kill switch Singolo booleano, attivato dalla UI o da un processo watchdog
Audit log Ogni intent, ogni fill, ogni reject, append-only

Oltre Binance

Sostituisci TsgcWSAPI_Binance con TsgcWSAPI_Coinbase, TsgcWSAPI_Kraken o uno qualsiasi degli altri 20+ componenti exchange. La strategia, il risk gate e il worker degli ordini non cambiano — solo il setup delle credenziali e la nomenclatura dei simboli. Per un trader multi-exchange di livello produzione con grafici, gestione della posizione e UI di routing degli ordini out-of-the-box, guarda il sample sgcTrader.

I sistemi multi-exchange reali aggiungono un layer in più sopra a quanto hai visto qui: un normalizzatore di simboli. Binance lo chiama BTCUSDT, Coinbase lo chiama BTC-USD, Kraken lo chiama XBT/USD. Costruisci un modello interno di simboli con un nome canonico e alias per exchange, e traduci al confine dell'API. Cinque minuti di lavoro all'inizio, infiniti bug evitati a valle.

L'altra cosa da aggiungere per le operazioni multi-exchange è un controllo di clock-skew all'avvio. Binance, Coinbase e gli altri rifiutano le richieste firmate con un timestamp a più di 1000 ms dal loro server. NTP di solito ti tiene ben dentro a quello, ma un VPS mal configurato può andare alla deriva di secondi in un'ora. Interroga l'endpoint server time all'avvio, logga l'offset, rifiuta di tradare se è >500 ms.

Perché Delphi per questo?

"Perché non scriverlo in Python?" è la domanda ovvia. Tre risposte dalla produzione. Primo, il warm-up JIT e il GIL rendono CPython poco adatto a event loop a bassa latenza — la stessa strategia che raggiunge una latenza mediana di 0,8 ms in Delphi richiede 6 ms in CPython senza sforzo serio. Secondo, la storia del deployment è più semplice: un exe firmato contro un virtualenv con cento wheel, metà dei quali richiede un compilatore C al momento dell'installazione. Terzo, il back office esistente è in Delphi. Riusare quelle classi — ledger dell'account, calcolatore P&L, journal, audit log — nel nuovo bot invece di reimplementarle in un altro linguaggio elimina un'intera categoria di bug di riconciliazione.

Per pura ricerca e backtest in stile notebook, Python vince facilmente — l'ecosistema di pandas, statsmodels, vectorbt e affini è ineguagliabile. La divisione che funziona per la maggior parte dei team: ricerca in Python, produzione in Delphi. Esporta la logica della strategia come una piccola macchina a stati, portala una volta, eseguila su un runtime Delphi battle-tested. Le due metà non devono condividere un linguaggio per condividere risultati.

Cosa leggere dopo

Se prevedi di farlo girare 24/7 su un VPS, leggi Tuning delle performance dopo. Per evitare le trappole più comuni, vedi 10 errori comuni. E se non hai ancora installato sgcWebSockets, l'hub Per Iniziare ti fa partire in cinque minuti.

Disclaimer: la strategia in questo post è a scopo didattico. Tradare criptovalute comporta rischi sostanziali. Non deployare codice non testato con capitale reale.