ProCat Solutions
Kripto botok a Web3 után: rate limit, kill switch, őszinte backtest
Tőzsdei REST és WebSocket API-k, megbízás-életciklus, rate limitek, kockázatkezelés, monitorozás és backtesting: mérnöki tapasztalatok kripto botok fejlesztéséből.
Ha egy megbízás leadásakor timeoutol a REST-hívás, a bot nem tudja, bekerült-e a megbízás a tőzsdére. Ha vakon újrapróbálja, duplikátumot hozhat létre. A smart contract és token presale projektek után ilyen kérdésekkel találkoztunk, amikor a tőzsdei automatizálás felé fordultunk. Előbb egy tisztázás: ez a cikk nem befektetési tanácsadás. Mi kereskedési és automatizáló botok infrastruktúráját építjük, kereskedési stratégiát az ügyfelek pénzére nem. A stratégia mindig a megbízóé. A mi dolgunk, hogy a rendszer pontosan, kiszámíthatóan és biztonságosan azt tegye, amit a stratégia előír.

Két csatorna, két igazság
Szinte minden centralizált tőzsde két csatornát ad. A REST API-n adunk le és vonunk vissza megbízást, ott kérdezzük le az egyenleget és az előzményeket. A WebSocketen érkeznek az árfolyamok, az order book frissítései és a saját megbízásaink állapotváltozásai. A két csatorna nincs szinkronban. Előfordul, hogy a WebSocketen már látszik a teljesülés, a REST viszont még “nyitott” állapotot mutat, és fordítva is.
A megoldásunk: az állapot elsődleges forrása a WebSocket, mögötte periodikus REST-egyeztetés (reconciliation) fut, és eltérésnél a tőzsde REST-válasza számít igazságnak. A WebSocket-kapcsolatot heartbeat figyeli. Újracsatlakozáskor teljes snapshotot kérünk, mert a kimaradt üzenetek pótlása tőzsdénként másképp működik, és ritkán megbízható.
A timeoutolt megbízás
Egy megbízás állapotgépe egyszerűnek látszik: létrehozva, nyitott, részben teljesült, teljesült, visszavont, elutasított. A nehézség az átmenetekben van, és a legkellemetlenebb a timeout. Ezért minden megbízás saját kliensoldali azonosítót kap, amelyet a tőzsdék többsége elfogad. Egy ismétlés így nem hoz létre duplikátumot, a tőzsde a már létező megbízást adja vissza.
Minden állapotváltozás append-only eseménynaplóba kerül PostgreSQL-ben, az aktuális állapot ebből származik. Vita esetén az eseménysorból visszakereshető, mi történt és mikor.
Rate limit
A másik korlát, amelybe minden bot beleszalad, a rate limit. A tőzsdék súlyozott limiteket alkalmaznak: egy order book lekérés többe kerül, mint egy ping. A túllépés rövid tiltással jár, és ez éppen akkor fáj, amikor gyorsan vissza kellene vonni egy megbízást. Ezért:
- kliensoldali token bucket számolja a fogyasztást, a tőzsde saját fejléceiből visszaolvasva;
- a kritikus műveleteknek (visszavonás, kill switch) tartalék keretet hagyunk, amelyet a lekérdezések nem használhatnak el;
- hibánál exponenciális backoff jitterrel, 429-es válasz után pedig a tőzsde által kért várakozást tartjuk be, a sajátunk helyett.

A stratégia alatti réteg
A kockázatkezelés nálunk független a stratégiától. Alatta fut, és a stratégia nem tudja megkerülni:
- pozíciólimitek eszközönként és összesítve; a limit fölött a rendszer nem ad le nyitó megbízást;
- maximális napi veszteség, amelynek elérése után a bot csak záró megbízásokat enged;
- kill switch: egyetlen parancs, amely minden nyitott megbízást visszavon és passzív módba teszi a botot; külön csatornán is elérhető, a bot saját webes felületétől függetlenül;
- paper trading mód, amelyben minden ugyanúgy fut, csak a megbízások egy szimulált végrehajtóhoz mennek.
A paper trading a tesztelés alapja, kényelmi funkciónak nem tekintjük. Minden kódváltozás először ott fut napokig, valós adatfolyamon.
A csendes leállás
Egy csendben leálló bot rosszabb annál, amelyik hangosan hibázik. A bot állapotát ezért heartbeat metrikák jelzik: az utolsó árfolyam-frissítés kora, a nyitott megbízások száma, a kapcsolat állapota. Riasztás megy, ha bármelyik kilép a várt tartományból, vagy ha az adatfolyam elakad, akkor is, ha hibaüzenet nem jött. A riasztás több csatornán megy ki, mert a bot maga nem tudhatja, melyik csatorna él éppen.
Az API-kulcsok soha nem kerülnek a repóba vagy a Docker image-be. Titkos tárolóból, indításkor, környezeti változóként érkeznek. A jogosultságuk minimális (kereskedés igen, kivétel nem), és IP-korlátozottak. A visszavonhatóság fontosabb a titkosításnál: egy kompromittált kulcsot percek alatt cserélni kell tudni.
Szép görbék, rossz feltevések
A backtesting csábító, mert szép görbéket rajzol, az eredmény viszont csak annyit ér, amennyit a feltevések. A tipikus hibák, amelyeket szoftveroldalon kezelni tudunk:
- look-ahead: a stratégia olyan adatot használ, amely az adott pillanatban még nem volt elérhető; a backtesting motor ezért csak időbélyeg szerint “már megérkezett” adatot ad ki;
- csúszás és díjak: a szimulált végrehajtó az order book alapján, díjakkal teljesít, a középárfolyamot nem használja;
- adathiány: a hiányzó gyertyákat nem interpoláljuk, megjelöljük őket, és a stratégia is látja a hiányt;
- nem determinisztikus futás: ugyanaz a bemenet mindig ugyanazt az eredményt adja, mert a véletlenszerűséget seedeljük, az időt pedig injektáljuk, a rendszerórából nem olvassuk.
Ez a réteg azt garantálja, hogy a backtest reprodukálható és őszinte. Azt nem, hogy a stratégia jó. A kettőt külön kell választani, és a fejlesztés során mi az elsőért vállalunk felelősséget.