使用 sgcWebSockets + Binance 在 Delphi 中构建实时交易机器人

· 功能

我们将构建什么

本教程结束时,您将拥有一个可工作的 Delphi VCL 应用程序,它从 Binance 流式传输实时交易和订单簿数据、运行简单的突破策略、通过 REST API 下真实订单,并执行风险控制(最大头寸大小、每日亏损限额、紧急停止开关)。相同的管道适用于 sgcWebSockets 支持的任何交易所——Coinbase、Kraken、OKX、Bybit、Bitfinex——只有凭据和符号映射会更改。

这与我们的参考 sgcTrader 示例使用的架构相同。如果您想要一个带完整 UI、图表和多交易所路由的更大起点,请获取它。下面的演练展示了大约 300 行代码下幕后发生的事情。

开始之前的两个先决条件。首先,获取 Binance API 密钥(账户 > API 管理)。对于开发,生成仅具有"启用读取"和"启用现货交易"的密钥,并将您的 IP 列入白名单。永远不要将启用提取的密钥放入代码中。其次,先在 Binance 的测试网(testnet.binance.vision)上做所有事情。端点、消息格式和签名算法与生产相同,但资金是假的。我们将真钱输给了"我确信我的策略是正确的",正好是我们首先没有在测试网上测试的次数。

一图架构

三个线程,两个组件,一个风险守门员:

组件 角色 线程
TsgcWSAPI_Binance WebSocket 流:交易、深度、k 线、用户数据 I/O 工作器
TsgcHTTP_API_Binance REST:下单、取消、账户快照 交易者工作器
策略队列 解耦:市场事件 → 决策 → 订单 策略工作器
风险门 阻止/缩减/允许每个订单 交易者中内联

步骤 1:流式传输市场数据

TsgcWSAPI_Binance 放到窗体上。它已经讲 Binance 组合流协议——您只需订阅您想要的通道。

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;

这就是整个市场数据摄取层。在断开时重连,心跳检测死链接,对策略队列进行非阻塞推送。

该组件做了一件您否则必须自己写的事情:Binance 组合流 URL 是 /stream?streams=name1/name2/name3,如果您想在不断开连接的情况下添加或删除流,您必须在同一套接字上发送 JSON-RPC 订阅/取消订阅消息。TsgcWSAPI_Binance 公开 SubscribeStreamUnsubscribeStream 方法,为您处理 JSON-RPC 握手。当用户在 UI 中选择新行情时很有用——无需重连,无丢失消息。

另外:Binance 施加每 IP 和每流限制。要获得每个 USDT 对的完整深度,您将很快达到消息速率限制。仅订阅您实际需要的,并在需要广泛市场视图时优先使用聚合流(!miniTicker@arr)而不是每符号流。

步骤 2:最小策略

策略在其自己的线程上运行。它维护 1 分钟 k 线流的最后 N 个收盘价的滚动窗口,当价格突破 20 周期高点时做多。纯说明——请不要将其放在真钱前面。

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;

步骤 3:风险门

永远不要让策略直接与交易所通信。通过一个知道您的限制的门漏斗每个意图。

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;

步骤 4:通过 REST 下单

订单工作器拉取经过验证的意图、签署请求并发送到 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;

步骤 4b:REST 身份验证和签名

TsgcHTTP_API_Binance 组件使用 API 密钥为您签署请求。在幕后,它构建规范查询字符串、使用您的密钥计算 HMAC-SHA256,并将其作为 signature 参数附加。您在启动时提供密钥和秘密一次。

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

如果您想针对测试网运行,请设置 BinanceOptions.Testnet := True——该组件自动切换 REST 基础 URL 和 WebSocket 主机。针对测试网构建和测试,翻转单个标志,部署到生产。Binance API 文档在这两个环境之间是相同的。

步骤 5:用户数据流

同一个 WebSocket 组件还订阅您的私有用户数据流——账户更新、订单事件、头寸更改。这是您协调在机器人外发生的成交方式(从 Web UI 手动取消、强平等)。

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;

关于回测的说明

上面的任何内容都没有回答"这个策略实际上是否有利可图?"那就是回测的目的——针对历史数据重放相同的策略以估计其前向性能。上面的架构使其几乎免费:策略线程不关心市场事件来自实时 WebSocket 还是 CSV 读取器。构建一个从磁盘读取 k 线并将其馈送到同一队列的合成事件源,您的策略代码就会针对多年的历史数据无修改地运行。

要避免的两个陷阱。前瞻偏差:不要让策略偷看任何在它正在处理的时间戳上不会可用的数据点。以及幸存者偏差:在当时存在的符号宇宙上训练和测试,而不是幸存到今天的"成功"符号的精选列表。这两者在生产中杀死的策略比所有编码错误加起来都多。

运营清单

关注点 处理位置
Wi-Fi 断开时重连 WatchDog.Enabled := True
死链接检测 HeartBeat.Enabled := True
时间同步(Binance 拒绝偏斜签名) OS 上的 NTP,加上每天对服务器时间端点的调用
订单幂等性 在每个订单上使用 newClientOrderId
速率限制 跟踪标头;在限制的 90% 以内时退避
紧急停止开关 单个布尔值,从 UI 或 watchdog 进程翻转
审计日志 每个意图、每个成交、每个拒绝,仅追加

超越 Binance

TsgcWSAPI_Binance 换成 TsgcWSAPI_CoinbaseTsgcWSAPI_Kraken 或其他 20 多个交易所组件中的任意一个。策略、风险门和订单工作器不会更改——只有凭据设置和符号命名。对于开箱即用具有图表、头寸管理和订单路由 UI 的生产级多交易所交易者,请查看 sgcTrader 示例

真实的多交易所系统在您所见之上添加一层:符号规范化器。Binance 称之为 BTCUSDT,Coinbase 称之为 BTC-USD,Kraken 称之为 XBT/USD。构建一个具有规范名称和每交易所别名的内部符号模型,并在 API 边界处翻译。前期五分钟的工作,下游无限节省的错误。

多交易所操作要添加的另一件事是启动时的时钟偏差检查。Binance、Coinbase 和其他都拒绝时间戳与其服务器相差超过 1000 ms 的已签名请求。NTP 通常将您保持在该范围内,但配置错误的 VPS 可以在一小时内漂移几秒。在启动时查询服务器时间端点,记录偏移,如果 >500 ms 拒绝交易。

为什么选 Delphi?

"为什么不用 Python 写?"是显而易见的问题。生产中的三个答案。首先,JIT 预热和 GIL 使 CPython 不适合低延迟事件循环——在 Delphi 中达到 0.8 ms 中位延迟的相同策略,在 CPython 中无需认真努力需要 6 ms。其次,部署故事更简单:一个签名 exe vs 一个带 100 个 wheels 的 virtualenv,其中一半需要在安装时使用 C 编译器。第三,现有的后台已经是 Delphi。在新机器人中重用那些类(账户分类账、P&L 计算器、日记、审计日志)而不是用另一种语言重新实现它们,消除了整个类别的协调错误。

对于纯研究和笔记本式回测,Python 轻松获胜——pandas、statsmodels、vectorbt 和朋友的生态系统是无与伦比的。适用于大多数公司的拆分:Python 中的研究,Delphi 中的生产。将策略逻辑导出为小状态机,移植一次,在经过战斗考验的 Delphi 运行时上运行。两半不必共享语言来共享结果。

接下来读什么

如果您计划在 VPS 上 24/7 运行此程序,请接下来阅读性能调优。为避免最常见的陷阱,请参阅10 个常见错误。如果您还没有安装 sgcWebSockets,开始使用中心会在五分钟内让您上线。

免责声明:本文中的策略仅用于教育目的。交易加密货币涉及重大风险。不要使用未经测试的代码部署真实资本。