Att leverera tillförlitlig orderexekvering för krypto
Tillförlitlig orderexekvering för krypto byggs med tillståndsmaskiner, idempotensnycklar och obeveklig avstämning. Lärdomar från arbetet med MadaiOps.
By Vitor Lima
De flesta krypto-handelsprodukter säljer hastighet och ett diagram. Det svåra är inte att rita ut ljusstakarna. Det är att kunna svara, i varje millisekund, på en till synes enkel fråga: vad är det sanna tillståndet för min order just nu? När vi började bygga MadaiOps, vår interna app för order och handelsoperationer, organiserade den frågan allt. Hastighet är en funktion. Sanning är produkten. Tillförlitlig orderexekvering för krypto är det du har när appen kan besvara den frågan ärligt även medan handelsplatsen försämras, och en app som visar en avslutad affär som inte hände — eller döljer en som hände — förstör förtroendet på ett sätt som ingen latensvinst kan köpa tillbaka.
Det här inlägget handlar om den teknik som gör en order-app pålitlig: tillståndsmaskinen, idempotens, avstämning över WebSockets, och de fellägen som bara dyker upp under belastning. Vi har starka åsikter om allt detta, eftersom de tråkiga valen är de som håller dig solvent.
Modellera ordern som en explicit tillståndsmaskin
Det enskilt bästa beslut vi tog var att sluta behandla en order som en rad man kör UPDATE på och börja behandla den som en ändlig tillståndsmaskin med namngivna, uppräknade övergångar. En order lever i tillstånd som PENDING_NEW, WORKING, PARTIALLY_FILLED, FILLED, CANCEL_REQUESTED, CANCELED, REJECTED och EXPIRED. Varje övergång är en funktion av det aktuella tillståndet plus en inkommande händelse, och otillåtna övergångar avvisas högljutt i stället för att absorberas tyst.
Vinsten: tvetydighet blir en fråga vid valideringstillfället i stället för en incident klockan två på natten. En FILLED-order kan aldrig gå tillbaka till WORKING. En avslutshändelse som anländer för en CANCELED-order är inte "omöjlig" — det är en verklig kapplöpning, och tillståndsmaskinen tvingar dig att avgöra vad den betyder. Vanligtvis betyder den att avbrytningen förlorade kapplöpningen, att ordern faktiskt avslutades, och att ditt lokala CANCELED var fel. Modellera ordrar som muterbara rader och den händelsen skriver bara över något; du märker det aldrig förrän avstämningen — eller en användare — fångar det.
Slutsatsen
Räkna upp dina tillstånd innan du skriver ett enda API-anrop. Om du inte kan rita diagrammet på en whiteboard förstår du inte din domän ännu, och börsen kommer att lära dig det på det hårda sättet. Lagra övergångarna, inte bara det aktuella tillståndet. En händelselogg som bara läggs till och beskriver vad som hände slår en enda muterbar statuskolumn, eftersom loggen kan spelas upp igen och kolumnen är en gissning.
Idempotens är inte valfritt
Nätverk gör om försök. Din egen omförsökslogik gör om försök. En användare dubbeltrycker på "Köp". Vilket som helst av dessa kan skicka samma order två gånger, och på en börs är en dubblettorder riktiga pengar. Försvaret är en klientgenererad idempotensnyckel — de flesta börser kallar den clientOrderId eller newClientOrderId — kopplad till varje order i det ögonblick användaren bekräftar, innan begäran ens lämnar enheten eller servern.
Regeln vi upprätthåller: nyckeln genereras en gång, sparas lokalt innan nätverksanropet, och återanvänds ordagrant vid varje omförsök. Om det första försöket gick ut i tidsgräns utan svar bär omförsöket samma nyckel, och börsen skapar antingen ordern en gång eller returnerar den befintliga. Hur som helst konvergerar du mot en enda order. Generera nyckeln efter ett fel och du har byggt en maskin för dubblettordrar.
Detta gäller även avbrytningar och ändringar. En avbrytningsbegäran bör också vara idempotent: att avbryta en redan avbruten order är en no-op som returnerar framgång, inte ett fel som utlöser ytterligare en omförsöksloop. Vi behandlar "det önskade sluttillståndet är redan uppfyllt" som framgång överallt. Det låter trivialt. Det eliminerar en hel kategori av omförsöksstormar.
Slutsatsen
Spara idempotensnyckeln innan du agerar, inte efter att du lyckas. Glappet mellan "jag skickade den" och "jag vet att det fungerade" är precis där pengar försvinner, och nyckeln är det enda som stänger det.
Stäm av obevekligt: WebSocket är en ledtråd, inte sanningen
Börser pushar orderuppdateringar över WebSockets, och de är underbara när de fungerar: avslut under en sekund, live-mängder för delavslut, avbrytningsbekräftelser. Men en WebSocket är en ström du kan missa. Du kommer att bli frånkopplad. Meddelanden anländer i fel ordning, tappas under en återanslutning, eller kommer helt enkelt aldrig eftersom handelsplatsen är försämrad. Om ditt ordertillstånd bara är vad socketen senast berättade för dig, har din app tvärsäkert fel i det ögonblick socketen hackar.
Så vi kör två loopar. Den snabba loopen konsumerar WebSocketen och tillämpar händelser optimistiskt. Den långsamma loopen pollar REST-API:et — GET /order och endpoints för öppna ordrar — med ett intervall och efter varje återanslutning, och stämmer sedan av den ögonblicksbilden mot det lokala tillståndet. REST är sanningskällan; socketen är en lågfördröjd ledtråd. När de inte stämmer överens vinner REST och vi loggar avvikelsen, eftersom en avvikelse är en felrapport som systemet skrev åt dig.
Sekvensering spelar roll. De flesta börser stämplar uppdateringar med ett monotont sekvensnummer eller en updateTime. Vi kastar bort varje händelse som är äldre än den senast tillämpade för den ordern, vilket gör leverans i fel ordning ofarlig. Vid återanslutning är det korrekta att göra: prenumerera på nytt, hämta omedelbart en fullständig REST-ögonblicksbild för att fylla i glappet, sedan lita på liveströmmen igen. Hoppa över ögonblicksbilden och du missar avslutet som hände under de två sekunder du var offline.
Slutsatsen
Låt aldrig en realtidsström vara den enda skrivaren av tillstånd. Socketen gör dig snabb; avstämningsloopen gör dig korrekt. Du behöver båda, och när de står i konflikt slår korrekt snabb varje gång.
Delavslut och frånkopplingar: de två som avslöjar dig
Delavslut är där naiva appar tyst ljuger. En order på 1,0 BTC som avslutar 0,3, sedan 0,4, och sedan hänger sig är inte "öppen" och den är inte "klar". Den är PARTIALLY_FILLED med 0,7 exekverat och 0,3 återstående, och vart och ett av de talen måste spåras från de individuella avslutshändelserna, inte härledas från ett enda statusfält. Vi ackumulerar exekverad mängd och volymviktat genomsnittspris från de diskreta avsluten, så att positionen alltid kan rekonstrueras från grundprimitiv. En användare som ser "avslutad" behöver veta avslutad hur mycket, till vilket genomsnittspris. Något mindre är en halvsanning.
Frånkopplingar förvärrar detta. Tappa offline mitt i ett avslut och WebSocketen berättade aldrig om de 0,4, så ditt lokala tillstånd är föråldrat. Det är precis därför disciplinen med återanslutning-sedan-ögonblicksbild finns: REST-ögonblicksbilden returnerar den kumulativt exekverade mängden, och avstämningsloopen stänger glappet utan att användaren någonsin ser ett felaktigt tal. Fellägetsom vi konstruerar mot är inte "appen kraschade" — krascher är ärliga. Det är "appen fortsatte köra och visade något falskt".
Slutsatsen
Spåra den kumulativt exekverade mängden som det primära faktumet och härled allt annat från den. Lagra aldrig "återstående" som ett oberoende fält du muterar. Härled det (beställt − exekverat) så att det inte kan glida ur synk med verkligheten.
Sanningsenlig under tryck slår smart
Det finns en ihärdig frestelse att vara smart: förutspå avslut före bekräftelse, dölja "övergående" fel, jämna över en återanslutning så att gränssnittet aldrig flimrar. Vi har lärt oss att motstå det mesta av det. En handelsoperatör vill inte ha en jämn lögn; de vill veta, ärligt, att anslutningen just tappades och att tillståndet stäms av. Ett synligt tillstånd "återansluter, verifierar ordrar" är värt mer än ett sömlöst gränssnitt som visar sig visa gårdagens sanning.
Konkret lyfter vi fram osäkerhet i stället för att tapetsera över den. När det lokala tillståndet och börsen inte har stämts av nyligen säger appen det. När en order är CANCEL_REQUESTED men ännu inte bekräftad som avbruten visar den exakt det, inte ett förhastat CANCELED. Optimistiskt gränssnitt är bra för en att-göra-app. För pengar är optimism en skuld du betalar för i supportärenden och förlorat förtroende.
Slutsatsen
När du är osäker, visa tvivlet. Det mest lugnande en finansapp kan göra under stress är att vägra gissa.
Avslutande tanke
Inget av detta är glamoröst. Det finns ingen demo där "våra idempotensnycklar sparas före nätverksanropet" får applåder. Men tillförlitlighet är inte en funktion du lägger till senare. Det är den uppsättning beslut du fattar tidigt och vägrar kompromissa om, och sedan försvarar varje gång en genväg ser frestande ut. Bygg systemet som talar sanning när allt brinner, så tar de snabba, snygga delarna hand om sig själva.