Każda giełda publikuje limity zapytań i każda giełda je egzekwuje. Binance odpowiada na wywołanie REST przekraczające limit kodem HTTP 429 i nagłówkiem Retry-After, który należy uszanować, oraz zamyka połączenie WebSocket, które wysyła do niej więcej niż 5 komunikatów na sekundę. W odstępie kilku dni wpłynęły dwa zgłoszenia od klientów, każde dotyczące jednej z tych połówek, a razem ujawniły trzy problemy warte porządnego naprawienia.
Wszystkie trzy są już naprawione. Jeden z nich okazał się dotyczyć znacznie więcej niż tylko Binance.
Nieudane żądanie gubiło swoje nagłówki
Kiedy żądanie wykonane przez jednego z gotowych klientów API kończyło się błędnym statusem HTTP, komponent zgłaszał wyjątek niosący kod statusu, tekst statusu i treść odpowiedzi. Nagłówków odpowiedzi już nie było. Nie były ukryte, po prostu przepadały. Obiekt HTTP był niszczony, gdy wyjątek wciąż wędrował w górę stosu, a nagłówki znikały razem z nim.
To sprawia, że nie da się zbudować na komponencie zgodnego z zasadami Binance mechanizmu odczekiwania. Retry-After mówi dokładnie, jak długo czekać, a zignorowanie go prowadzi od 429 do HTTP 418 i tymczasowej blokady adresu IP. Liczniki limitów zwracane przez Binance, takie jak X-MBX-USED-WEIGHT-1M, były równie niedostępne, więc aplikacja nie mogła też sama zwolnić tempa, zanim uderzyła w ścianę.
Zgłaszanym wyjątkiem jest teraz EsgcHTTPAPIProtocolException i niesie ze sobą nagłówki odpowiedzi. Dziedziczy po wyjątku zgłaszanym wcześniej, więc istniejące procedury obsługi nadal go przechwytują i nic nie trzeba zmieniać, chyba że chcesz skorzystać z nowych informacji.
try
vResponse := oBinance.GetAggregateTrades('BTCUSDT');
except
on E: EsgcHTTPAPIProtocolException do
begin
if E.ErrorCode = 429 then
Sleep(E.RetryAfterMs);
vWeight := E.GetHeader('X-MBX-USED-WEIGHT-1M');
// E.ResponseHeaders holds the complete list
end;
end;
RetryAfterMs zwraca opóźnienie w milisekundach i rozumie obie postacie, jakie może przyjąć ten nagłówek, zwykłą liczbę sekund oraz datę HTTP. Zwraca -1, gdy nagłówka nie ma. Poprawka obejmuje wszystkie osiem metod żądań, nie tylko Get, a parametr ResponseHeaders, który Post i Query już wcześniej przyjmowały, jest teraz wypełniany, gdy żądanie się nie powiedzie, czyli dokładnie wtedy, gdy jest potrzebny.
Warto przy tej okazji wiedzieć, że od wersji 2026.7.0 klient HTTP potrafi też odczekać za Ciebie.
oBinance.RetryOptions.Enabled := True;
oBinance.RetryOptions.MaxRetries := 3;
// HonorRetryAfter is already True by default
Jedna ramka zamiast dwunastu
Protokół WebSocket Binance przyjmuje listę strumieni w pojedynczej ramce SUBSCRIBE. Komponent z tego nie korzystał. Każda funkcja pomocnicza Subscribe* zapisywała natychmiast jedną ramkę niosącą dokładnie jeden strumień, więc zwykła lista obserwowanych złożona z sześciu symboli na dwóch kanałach wysyłała dwanaście ramek w ciągu kilku milisekund. Binance zamykał połączenie, dokładnie tak, jak opisuje dokumentacja.
Są teraz metody grupujące, które wysyłają całą listę jako jedną ramkę:
oBinance.SubscribeStreams(['btcusdt@aggTrade', 'btcusdt@depth',
'ethusdt@aggTrade', 'ethusdt@depth']);
Ramka pozostaje daleko poniżej limitu 8 KB na żądanie nawet przy setkach strumieni, więc w praktyce lista obserwowanych to zawsze jedna ramka. UnsubscribeStreams działa lustrzanie.
Część, której nikt nie mógł obejść
To właśnie ten problem miał największe znaczenie i żadne ze zgłoszeń nie uchwyciło go w pełni.
Każde API giełdowe przechowuje kanały, które zasubskrybowałeś, aby móc je przywrócić po ponownym połączeniu. To odtwarzanie wysyłało jedną ramkę na strumień, jedna za drugą, zupełnie bez kontroli tempa, na połączenie liczące sobie kilka milisekund. Lista obserwowanych o dowolnym realnym rozmiarze powodowała natychmiastowe zamknięcie świeżego połączenia. WatchDog łączył się wtedy ponownie, odtwarzał tę samą serię i znów zostawał rozłączony. Pojedyncze chwilowe rozłączenie zamieniało się w niekończącą się pętlę ponownych połączeń.
Ponieważ ta seria jest generowana wewnątrz komponentu, aplikacja nie mogła jej uniknąć. Pisanie własnych ramek zamiast korzystania z funkcji pomocniczych nic nie dawało, bo odtwarzania się nie wywołuje.
Audyt reszty biblioteki pokazał, że ta sama pętla odtwarzania została skopiowana do 17 interfejsów API giełd: Binance, Bitfinex, Bitget, Bitmex, Bitstamp, Bybit, Cex, CexPlus, Coinbase, CryptoCom, Deribit, GateIO, Huobi, Kraken, Kucoin, MEXC i OKX. Każda z nich łamała limit własnej giełdy.
Klienci API WebSocket mają teraz opcję Throttle:
oBinance.Throttle.Enabled := True; // paces your own Subscribe calls
oBinance.Throttle.MaxMessages := 4; // default, leaves room for PING/PONG
oBinance.Throttle.IntervalMs := 1000; // default
Ustawienia domyślne wymagają wyjaśnienia, bo są celowo niesymetryczne. Throttle.Enabled ma wartość False, więc czas wywołań wykonywanych przez Twoją aplikację nie zmienia się, dopóki sam o to nie poprosisz. Ale Throttle.PaceResubscribe ma wartość True, więc odtwarzanie po ponownym połączeniu jest rozłożone w czasie od razu, bez żadnej konfiguracji.
Uzasadnienie jest proste. Kontrolujesz własną pętlę subskrypcji i możesz ją pogrupować, więc kontrola tempa w tym miejscu to Twoja decyzja. Nie kontrolujesz odtwarzania, więc jest ono rozkładane w czasie za Ciebie. Pętla ponownych połączeń jest naprawiona w każdej istniejącej aplikacji, całkowicie bez zmian w kodzie. W przypadku Binance odtwarzanie jest dodatkowo wysyłane jako ramki łączone, więc lista dwunastu strumieni odtwarza się teraz jako pojedyncza ramka zamiast dwunastu. Ustaw PaceResubscribe na False, jeśli chcesz wrócić do poprzedniego zachowania.
Jeden szczegół, jeśli korzystasz z kilku giełd: domyślny budżet 4 komunikatów na sekundę pasuje do Binance, którego udokumentowany limit wynosi 5. OKX dokumentuje 3, więc klient OKX domyślnie używa 3. Sprawdź limit swojej giełdy, zanim zwiększysz MaxMessages.
Aktualizacja
Bezproblemowa. Nowy wyjątek dziedziczy po tym zgłaszanym wcześniej, metody grupujące są dodatkami, a Throttle jest domyślnie wyłączony dla wszystkiego poza odtwarzaniem, które psuło ponowne połączenia. Pobierz najnowszą wersję ze strony pobierania sgcWebSockets.
Oba te zgłoszenia pochodzą od klientów, którzy poświęcili czas na przeczytanie kodu źródłowego, odtworzenie problemu i precyzyjne go opisanie. Właśnie tak został znaleziony trzeci i najgorszy błąd. Jeśli coś w komponentach zachowuje się w sposób, którego nie potrafisz wyjaśnić, daj nam znać.
Pytania, uwagi albo pomoc w migracji? Skontaktuj się z nami, odpowiedź otrzymasz od osób, które napisały ten kod.
