Wat we bouwen
Aan het eind van deze tutorial heb je een werkende Delphi-VCL-applicatie die live trade- en orderbook-gegevens van Binance streamt, een eenvoudige breakout-strategie draait, echte orders plaatst via de REST-API, en risicocontroles afdwingt (max positiegrootte, dagelijkse verlieslimiet, kill switch). Dezelfde bedrading werkt voor elke exchange die door sgcWebSockets wordt ondersteund — Coinbase, Kraken, OKX, Bybit, Bitfinex — waarbij alleen de credentials en symbol mapping veranderen.
Dit is dezelfde architectuur die wordt gebruikt door onze referentie-sgcTrader-sample. Als je een groter startpunt wilt met volledige UI, charting en multi-exchange-routing, pak die. De doorloop hieronder laat zien wat er onder de motorkap gebeurt in ongeveer 300 regels code.
Twee voorwaarden voor we beginnen. Eerst, haal een Binance API-sleutel op (Account > API Management). Voor development, genereer een sleutel die alleen “Enable Reading” en “Enable Spot Trading” heeft, en whitelist je IP. Stop nooit een withdrawal-enabled sleutel in code. Ten tweede, doe alles eerst op Binance’s testnet (testnet.binance.vision). De endpoints, berichtformaten en signature-algoritme zijn identiek aan productie, maar de funds zijn nep. We hebben echt geld verloren aan “ik weet zeker dat mijn strategie correct is” precies zo vaak als we niet eerst op de testnet hebben getest.
Architectuur in een diagram
Drie threads, twee componenten, een risico-poortwachter:
| Component | Rol | Thread |
TsgcWSAPI_Binance |
WebSocket-stream: trades, depth, klines, user data | I/O-worker |
TsgcHTTP_API_Binance |
REST: orderplaatsing, cancel, account-snapshot | Trader-worker |
| Strategie-wachtrij | Ontkoppeling: marktevents → beslissingen → orders | Strategie-worker |
| Risico-poort | Blokkeer / verklein / sta elke order toe | Inline in trader |
Stap 1: Stream marktgegevens
Plaats een TsgcWSAPI_Binance op het formulier. Het spreekt al het Binance combined stream-protocol — je abonneert je gewoon op de kanalen die je wilt.
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;
Dat is de hele marktgegevens-ingestie-laag. Reconnect bij drops, heartbeat om dode links te detecteren, en een non-blocking push naar de strategie-wachtrij.
Een ding dat de component doet dat je anders zelf zou moeten schrijven: de Binance combined stream URL is /stream?streams=name1/name2/name3, en als je streams wilt toevoegen of verwijderen zonder de verbinding te verbreken, moet je een JSON-RPC subscribe/unsubscribe-bericht over dezelfde socket sturen. TsgcWSAPI_Binance stelt SubscribeStream- en UnsubscribeStream-methodes bloot die de JSON-RPC-handshake voor je afhandelen. Nuttig wanneer de gebruiker een nieuwe ticker in de UI kiest — geen reconnect, geen verloren berichten.
Ook: Binance legt per-IP- en per-stream-limieten op. Voor full depth op elk USDT-paar zul je snel de message-rate-limiet raken. Abonneer je alleen op wat je echt nodig hebt, en geef de voorkeur aan aggregated streams (!miniTicker@arr) boven per-symbol-streams wanneer je een breed marktbeeld nodig hebt.
Stap 2: Een minimale strategie
De strategie draait op zijn eigen thread. Hij behoudt een rolling window van de laatste N closes uit de 1-minuut kline-stream en gaat long wanneer de prijs door een 20-periode-high breekt. Pure illustratie — zet dit alsjeblieft niet voor echt geld.
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;
Stap 3: Risico-poort
Laat een strategie nooit direct met de exchange praten. Trechter elke intentie door een poort die je limieten kent.
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;
Stap 4: Plaats de order via REST
De order-worker haalt gevalideerde intenties op, ondertekent het request en post naar 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;
Stap 4b: REST-authenticatie en -ondertekening
De TsgcHTTP_API_Binance-component ondertekent requests voor je met het API-secret. Achter de schermen bouwt hij de canonical query string, berekent een HMAC-SHA256 met je secret, en voegt deze toe als de signature-parameter. Je levert de key en secret eenmaal bij opstart.
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));
Als je tegen de testnet wilt draaien, zet BinanceOptions.Testnet := True — de component schakelt zowel de REST-base-URL als de WebSocket-host automatisch. Bouw en test tegen de testnet, klik een enkele flag om, deploy naar productie. De Binance API-documentatie is verder identiek tussen de twee omgevingen.
Stap 5: User data-stream
Dezelfde WebSocket-component abonneert zich ook op je private user data-stream — account-updates, order-events, positiewijzigingen. Zo verzoen je fills die buiten je bot om zijn gebeurd (handmatige cancel vanuit de web-UI, liquidatie, 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;
Een opmerking over backtesting
Niets hierboven beantwoordt “is deze strategie daadwerkelijk winstgevend?” Daarvoor is backtesting — dezelfde strategie afspelen tegen historische data om de forward-prestaties te schatten. De architectuur hierboven maakt dit bijna gratis: de strategie-thread maakt niet uit of marktevents van een live WebSocket of een CSV-reader komen. Bouw een synthetische event-bron die klines van disk leest en in dezelfde wachtrij voert, en je strategiecode draait ongewijzigd tegen jaren historische data.
Twee valkuilen om te vermijden. Look-ahead bias: laat de strategie niet gluren naar data-punten die op de timestamp van verwerking niet beschikbaar zouden zijn geweest. En survivorship bias: train en test op het universum van symbolen dat op dat moment bestond, niet op de gecureerde lijst van “succesvolle” symbolen die het tot vandaag hebben gehaald. Beide hebben meer strategieen in productie gedood dan elke codingbug samen.
Operationele checklist
| Zorg | Waar af te handelen |
| Reconnect bij wifi-drop | WatchDog.Enabled := True |
| Dead-link-detectie | HeartBeat.Enabled := True |
| Tijdsynchronisatie (Binance weigert scheef-signed signatures) | NTP op het OS, plus een dagelijkse call naar het server-time-endpoint |
| Order-idempotentie | Gebruik newClientOrderId op elke order |
| Rate limits | Volg headers; back off wanneer je binnen 90% van limiet bent |
| Kill switch | Enkele boolean, omgeklapt vanuit UI of een watchdog-proces |
| Audit log | Elke intent, elke fill, elke reject, append-only |
Voorbij Binance
Wissel TsgcWSAPI_Binance uit voor TsgcWSAPI_Coinbase, TsgcWSAPI_Kraken, of een van de andere 20+ exchange-componenten. De strategie, risico-poort en order-worker veranderen niet — alleen de credential-setup en symbol-naamgeving. Voor een productie-grade multi-exchange trader met charts, positiebeheer en order-routing-UI out-of-the-box, kijk naar de sgcTrader-sample.
Echte multi-exchange-systemen voegen nog een laag toe bovenop wat je hierboven zag: een symbol-normaliser. Binance noemt het BTCUSDT, Coinbase noemt het BTC-USD, Kraken noemt het XBT/USD. Bouw een intern symbol-model met een canonical naam en per-exchange-aliassen, en vertaal aan de API-grens. Vijf minuten werk vooraf, oneindige bespaarde bugs downstream.
Het andere ding om toe te voegen voor multi-exchange-operaties is een clock-skew-check bij opstart. Binance, Coinbase en de rest weigeren ondertekende requests met een timestamp van meer dan 1000 ms verschil met hun server. NTP houdt je meestal ruim binnen die marge, maar een verkeerd geconfigureerde VPS kan in een uur seconden afdrijven. Bevraag het server-time-endpoint bij opstart, log de offset, weiger te traden als het >500 ms is.
Waarom Delphi hiervoor?
“Waarom niet in Python schrijven?” is de voor de hand liggende vraag. Drie antwoorden uit productie. Eerst, de JIT-opwarming en de GIL maken CPython een slechte fit voor low-latency event loops — dezelfde strategie die 0,8 ms mediane latentie in Delphi haalt, neemt 6 ms in CPython zonder serieuze inspanning. Ten tweede is de deployment-verhaal eenvoudiger: een signed exe vs een virtualenv met honderd wheels, waarvan de helft een C-compiler bij installatie vereisen. Ten derde, het bestaande back-office is in Delphi. Die klassen hergebruiken — account ledger, P&L-calculator, journal, audit log — in de nieuwe bot in plaats van ze in een andere taal te herimplementeren, elimineert een hele categorie reconciliation-bugs.
Voor pure research en notebook-style backtests wint Python makkelijk — het ecosysteem van pandas, statsmodels, vectorbt en consorten is ongeevenaard. De splitsing die voor de meeste shops werkt: research in Python, productie in Delphi. Exporteer de strategielogica als een kleine state machine, port hem eenmaal, draai hem op een battle-tested Delphi-runtime. De twee helften hoeven geen taal te delen om resultaten te delen.
Wat hierna lezen
Als je dit 24/7 op een VPS wilt draaien, lees dan vervolgens Performance Tuning. Om de meest voorkomende valkuilen te vermijden, zie 10 Veelgemaakte fouten. En als je sgcWebSockets nog niet hebt geinstalleerd, brengt de Aan de slag-hub je in vijf minuten live.
Disclaimer: de strategie in dit bericht is voor educatieve doeleinden. Het handelen in cryptocurrency brengt aanzienlijk risico met zich mee. Implementeer geen ongeteste code met echt kapitaal.