What We Are Building
By the end of this tutorial you will have a working Delphi VCL application that streams live trade and order book data from Binance, runs a simple breakout strategy, places real orders via the REST API, and enforces risk controls (max position size, daily loss limit, kill switch). The same plumbing works for any exchange supported by sgcWebSockets — Coinbase, Kraken, OKX, Bybit, Bitfinex — with only the credential and symbol mapping changing.
This is the same architecture used by our reference sgcTrader sample. If you want a bigger starting point with full UI, charting, and multi-exchange routing, grab that. The walk-through below shows what is happening under the hood in around 300 lines of code.
Two prerequisites before we start. First, get a Binance API key (Account > API Management). For development, generate a key that has only "Enable Reading" and "Enable Spot Trading", and whitelist your IP. Never put a withdrawal-enabled key in code. Second, do everything on Binance's testnet first (testnet.binance.vision). The endpoints, message formats, and signature algorithm are identical to production, but the funds are fake. We have lost real money to "I'm sure my strategy is correct" exactly the number of times we did not test on the testnet first.
Architecture in One Diagram
Three threads, two components, one risk gatekeeper:
| Component | Role | Thread |
TsgcWSAPI_Binance |
WebSocket stream: trades, depth, klines, user data | I/O worker |
TsgcHTTP_API_Binance |
REST: order placement, cancel, account snapshot | Trader worker |
| Strategy queue | Decoupling: market events → decisions → orders | Strategy worker |
| Risk gate | Block / shrink / allow each order | Inline in trader |
Step 1: Stream Market Data
Drop a TsgcWSAPI_Binance on the form. It already speaks the Binance combined stream protocol — you just subscribe to the channels you want.
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;
That is the entire market-data ingestion layer. Reconnect on drops, heartbeat to detect dead links, and a non-blocking push to the strategy queue.
One thing the component does that you would otherwise have to write yourself: the Binance combined stream URL is /stream?streams=name1/name2/name3, and if you want to add or remove streams without dropping the connection you have to send a JSON-RPC subscribe/unsubscribe message over the same socket. TsgcWSAPI_Binance exposes SubscribeStream and UnsubscribeStream methods that handle the JSON-RPC handshake for you. Useful when the user picks a new ticker in the UI — no reconnect, no lost messages.
Also: Binance imposes per-IP and per-stream limits. For full depth on every USDT pair you will hit the message-rate limit quickly. Subscribe only to what you actually need, and prefer aggregated streams (!miniTicker@arr) over per-symbol streams when you need a wide market view.
Step 2: A Minimal Strategy
The strategy runs on its own thread. It maintains a rolling window of the last N closes from the 1-minute kline stream and goes long when price breaks above a 20-period high. Pure illustration — please do not put this in front of real money.
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;
Step 3: Risk Gate
Never let a strategy talk directly to the exchange. Funnel every intent through a gate that knows your limits.
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;
Step 4: Place the Order via REST
The order worker pulls validated intents, signs the request, and posts to 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;
Step 4b: REST Authentication and Signing
The TsgcHTTP_API_Binance component signs requests for you using the API secret. Behind the scenes it builds the canonical query string, computes an HMAC-SHA256 with your secret, and appends it as the signature parameter. You provide the key and secret once at startup.
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));
If you want to run against the testnet, set BinanceOptions.Testnet := True — the component switches both the REST base URL and the WebSocket host automatically. Build and test against the testnet, flip a single flag, deploy to production. The Binance API documentation is otherwise identical between the two environments.
Step 5: User Data Stream
The same WebSocket component also subscribes to your private user data stream — account updates, order events, position changes. This is how you reconcile fills that happened outside your bot (manual cancel from the web UI, 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;
A Note on Backtesting
Nothing above answers "is this strategy actually profitable?" That is what backtesting is for — replaying the same strategy against historical data to estimate its forward performance. The architecture above makes this almost free: the strategy thread does not care whether market events come from a live WebSocket or a CSV reader. Build a synthetic event source that reads klines from disk and feeds them into the same queue, and your strategy code runs unchanged against years of historical data.
Two pitfalls to avoid. Look-ahead bias: do not let the strategy peek at any data point that would not have been available at the timestamp it is processing. And survivorship bias: train and test on the universe of symbols that existed at the time, not the curated list of "successful" symbols that survived to today. Both have killed more strategies in production than every coding bug combined.
Operational Checklist
| Concern | Where to handle it |
| Reconnect on Wi-Fi drop | WatchDog.Enabled := True |
| Dead-link detection | HeartBeat.Enabled := True |
| Time sync (Binance rejects skewed signatures) | NTP on the OS, plus a daily call to the server time endpoint |
| Order idempotency | Use newClientOrderId on every order |
| Rate limits | Track headers; back off when within 90% of limit |
| Kill switch | Single boolean, flipped from UI or a watchdog process |
| Audit log | Every intent, every fill, every reject, append-only |
Beyond Binance
Swap TsgcWSAPI_Binance for TsgcWSAPI_Coinbase, TsgcWSAPI_Kraken, or any of the other 20+ exchange components. The strategy, risk gate, and order worker do not change — only the credential setup and symbol naming. For a production-grade multi-exchange trader with charts, position management, and order routing UI out of the box, look at the sgcTrader sample.
Real multi-exchange systems add one more layer above what you saw here: a symbol normaliser. Binance calls it BTCUSDT, Coinbase calls it BTC-USD, Kraken calls it XBT/USD. Build an internal symbol model with a canonical name and per-exchange aliases, and translate at the API boundary. Five minutes of work upfront, infinite saved bugs downstream.
The other thing to add for multi-exchange operations is a clock-skew check on startup. Binance, Coinbase and the rest reject signed requests with a timestamp more than 1000 ms off from their server. NTP usually keeps you well within that, but a misconfigured VPS can drift seconds in an hour. Query the server time endpoint on startup, log the offset, refuse to trade if it is >500 ms.
Why Delphi for This?
"Why not write it in Python?" is the obvious question. Three answers from production. First, the JIT warm-up and the GIL make CPython a poor fit for low-latency event loops — the same strategy that hits 0.8 ms median latency in Delphi takes 6 ms in CPython without serious effort. Second, the deployment story is simpler: one signed exe vs a virtualenv with a hundred wheels, half of which require a C compiler at install time. Third, the existing back office is in Delphi. Reusing those classes — account ledger, P&L calculator, journal, audit log — in the new bot instead of re-implementing them in another language eliminates an entire category of reconciliation bugs.
For pure research and notebook-style backtests, Python wins easily — the ecosystem of pandas, statsmodels, vectorbt and friends is unmatched. The split that works for most shops: research in Python, production in Delphi. Export the strategy logic as a small state machine, port it once, run it on a battle-tested Delphi runtime. The two halves do not have to share a language to share results.
What to Read Next
If you plan to run this 24/7 on a VPS, read Performance Tuning next. To avoid the most common pitfalls, see 10 Common Mistakes. And if you have not installed sgcWebSockets yet, the Getting Started hub gets you live in five minutes.
Disclaimer: the strategy in this post is for educational purposes. Trading cryptocurrency involves substantial risk. Do not deploy untested code with real capital.