Construire un bot de trading temps réel en Delphi avec sgcWebSockets + Binance

· Fonctionnalités

Ce que nous construisons

À la fin de ce tutoriel vous aurez une application VCL Delphi fonctionnelle qui streame en direct les données de trade et de carnet d'ordres depuis Binance, exécute une stratégie de breakout simple, passe de vrais ordres via l'API REST, et applique des contrôles de risque (taille de position max, limite de perte quotidienne, kill switch). La même plomberie fonctionne pour tout exchange supporté par sgcWebSockets — Coinbase, Kraken, OKX, Bybit, Bitfinex — avec seulement le mapping des identifiants et des symboles qui change.

C'est la même architecture utilisée par notre exemple de référence sgcTrader. Si vous voulez un plus gros point de départ avec UI complète, graphiques et routage multi-exchanges, prenez-le. La marche à suivre ci-dessous montre ce qui se passe sous le capot en environ 300 lignes de code.

Deux prérequis avant de commencer. Premièrement, obtenez une clé API Binance (Compte > Gestion API). Pour le développement, générez une clé qui a uniquement « Activer la lecture » et « Activer le trading spot », et mettez votre IP en liste blanche. Ne mettez jamais une clé avec retrait activé dans le code. Deuxièmement, faites tout sur le testnet de Binance d'abord (testnet.binance.vision). Les endpoints, formats de message et algorithme de signature sont identiques à la production, mais les fonds sont fictifs. Nous avons perdu de l'argent réel à « je suis sûr que ma stratégie est correcte » exactement le nombre de fois où nous n'avons pas testé sur le testnet d'abord.

Architecture en un schéma

Trois threads, deux composants, un gardien des risques :

Composant Rôle Thread
TsgcWSAPI_Binance Flux WebSocket : trades, depth, klines, user data Worker E/S
TsgcHTTP_API_Binance REST : passation d'ordre, annulation, snapshot du compte Worker trader
File de stratégie Découplage : événements de marché → décisions → ordres Worker stratégie
Gate de risque Bloquer / réduire / autoriser chaque ordre Inline dans le trader

Étape 1 : streamer les données de marché

Déposez un TsgcWSAPI_Binance sur la fiche. Il parle déjà le protocole combined stream Binance — il vous suffit de vous abonner aux canaux que vous voulez.

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;

C'est toute la couche d'ingestion des données de marché. Reconnexion sur coupure, heartbeat pour détecter les liens morts, et un push non-bloquant vers la file de stratégie.

Une chose que le composant fait que vous devriez sinon écrire vous-même : l'URL du combined stream Binance est /stream?streams=name1/name2/name3, et si vous voulez ajouter ou retirer des streams sans couper la connexion vous devez envoyer un message subscribe/unsubscribe JSON-RPC sur la même socket. TsgcWSAPI_Binance expose les méthodes SubscribeStream et UnsubscribeStream qui gèrent le handshake JSON-RPC pour vous. Utile quand l'utilisateur choisit un nouveau ticker dans l'UI — pas de reconnexion, pas de messages perdus.

Aussi : Binance impose des limites par IP et par stream. Pour la profondeur complète sur chaque paire USDT vous atteindrez rapidement la limite de débit de messages. Abonnez-vous uniquement à ce dont vous avez réellement besoin, et préférez les streams agrégés (!miniTicker@arr) aux streams par symbole quand vous avez besoin d'une vue de marché large.

Étape 2 : une stratégie minimale

La stratégie tourne sur son propre thread. Elle maintient une fenêtre glissante des N derniers closes du stream kline 1 minute et passe long quand le prix casse au-dessus d'un plus haut sur 20 périodes. Pure illustration — ne mettez pas ceci devant de l'argent réel s'il vous plaît.

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;

Étape 3 : gate de risque

Ne laissez jamais une stratégie parler directement à l'exchange. Faites passer chaque intention par un gate qui connaît vos 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;

Étape 4 : passer l'ordre via REST

Le worker d'ordres tire les intentions validées, signe la requête et poste à 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;

Étape 4b : authentification et signature REST

Le composant TsgcHTTP_API_Binance signe les requêtes pour vous en utilisant le secret API. En coulisses, il construit la chaîne de requête canonique, calcule un HMAC-SHA256 avec votre secret, et l'ajoute comme paramètre signature. Vous fournissez la clé et le secret une fois au démarrage.

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 vous voulez tourner contre le testnet, définissez BinanceOptions.Testnet := True — le composant bascule à la fois l'URL de base REST et l'hôte WebSocket automatiquement. Buildez et testez contre le testnet, basculez un seul flag, déployez en production. La documentation de l'API Binance est sinon identique entre les deux environnements.

Étape 5 : flux de données utilisateur

Le même composant WebSocket s'abonne aussi à votre flux de données utilisateur privé — mises à jour de compte, événements d'ordres, changements de position. C'est ainsi que vous réconciliez les fills qui ont eu lieu en dehors de votre bot (annulation manuelle depuis l'UI web, liquidation, 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;

Une note sur le backtesting

Rien de ce qui précède ne répond à « cette stratégie est-elle réellement profitable ? » C'est pour cela que sert le backtesting — rejouer la même stratégie contre des données historiques pour estimer sa performance future. L'architecture ci-dessus rend cela presque gratuit : le thread de stratégie ne se soucie pas que les événements de marché viennent d'un WebSocket en direct ou d'un lecteur CSV. Construisez une source d'événements synthétique qui lit les klines depuis le disque et les alimente dans la même file, et votre code de stratégie tourne inchangé contre des années de données historiques.

Deux pièges à éviter. Le biais look-ahead : ne laissez pas la stratégie jeter un œil à un point de données qui n'aurait pas été disponible au timestamp qu'elle traite. Et le biais de survie : entraînez et testez sur l'univers des symboles qui existaient à l'époque, pas sur la liste sélectionnée des symboles « réussis » qui ont survécu jusqu'aujourd'hui. Les deux ont tué plus de stratégies en production que tous les bugs de code combinés.

Checklist opérationnelle

Préoccupation Où la gérer
Reconnexion sur coupure Wi-Fi WatchDog.Enabled := True
Détection de lien mort HeartBeat.Enabled := True
Sync horloge (Binance rejette les signatures décalées) NTP sur l'OS, plus un appel quotidien à l'endpoint server time
Idempotence d'ordre Utilisez newClientOrderId sur chaque ordre
Limites de débit Suivez les en-têtes ; reculez quand vous êtes à 90 % de la limite
Kill switch Booléen unique, basculé depuis l'UI ou un processus watchdog
Log d'audit Chaque intention, chaque fill, chaque reject, append-only

Au-delà de Binance

Remplacez TsgcWSAPI_Binance par TsgcWSAPI_Coinbase, TsgcWSAPI_Kraken, ou n'importe lequel des 20+ autres composants d'exchange. La stratégie, le gate de risque et le worker d'ordres ne changent pas — seuls la configuration des identifiants et le nommage des symboles. Pour un trader multi-exchanges de qualité production avec graphiques, gestion de position et UI de routage d'ordres d'office, regardez l'exemple sgcTrader.

Les vrais systèmes multi-exchanges ajoutent une couche de plus au-dessus de ce que vous avez vu ici : un normaliseur de symboles. Binance l'appelle BTCUSDT, Coinbase l'appelle BTC-USD, Kraken l'appelle XBT/USD. Construisez un modèle de symbole interne avec un nom canonique et des alias par exchange, et traduisez à la frontière de l'API. Cinq minutes de travail en amont, infinité de bugs économisés en aval.

L'autre chose à ajouter pour les opérations multi-exchanges est une vérification du décalage d'horloge au démarrage. Binance, Coinbase et les autres rejettent les requêtes signées avec un timestamp à plus de 1 000 ms du leur. NTP vous garde habituellement bien dans cette plage, mais un VPS mal configuré peut dériver de secondes en une heure. Interrogez l'endpoint server time au démarrage, journalisez l'offset, refusez de trader s'il est > 500 ms.

Pourquoi Delphi pour cela ?

« Pourquoi pas l'écrire en Python ? » est la question évidente. Trois réponses depuis la production. Premièrement, le warm-up JIT et le GIL font de CPython un mauvais choix pour les boucles d'événements à faible latence — la même stratégie qui atteint 0,8 ms de latence médiane en Delphi prend 6 ms en CPython sans effort sérieux. Deuxièmement, l'histoire du déploiement est plus simple : un exe signé vs un virtualenv avec cent wheels, dont la moitié nécessite un compilateur C à l'installation. Troisièmement, le back office existant est en Delphi. Réutiliser ces classes — livre de comptes, calculateur P&L, journal, log d'audit — dans le nouveau bot au lieu de les ré-implémenter dans un autre langage élimine toute une catégorie de bugs de réconciliation.

Pour la recherche pure et les backtests style notebook, Python gagne facilement — l'écosystème pandas, statsmodels, vectorbt et compagnie est inégalé. La séparation qui fonctionne pour la plupart des shops : recherche en Python, production en Delphi. Exportez la logique de stratégie en petite machine à états, portez-la une fois, exécutez-la sur un runtime Delphi éprouvé au combat. Les deux moitiés n'ont pas besoin de partager un langage pour partager des résultats.

Que lire ensuite

Si vous prévoyez de faire tourner ceci 24/7 sur un VPS, lisez ensuite Optimisation des performances. Pour éviter les pièges les plus courants, voir 10 erreurs courantes. Et si vous n'avez pas encore installé sgcWebSockets, le hub Premiers pas vous met en ligne en cinq minutes.

Avertissement : la stratégie de ce billet est à des fins éducatives. Le trading de cryptomonnaies implique un risque substantiel. Ne déployez pas du code non testé avec du capital réel.