Påskynda backtesting för freqtrade kryptohandelsbot.
Freqtrade Är en kryptohandelsbot i python. Jag brukade leka med en gaffel ett tag, fixa några buggar och något beteende som jag inte gillade. Att lista buggarna här skulle vara tråkigt så jag ska istället prata om saker som jag tycker är intressanta.
FQT är inte händelsebaserat, det är loopbaserat. Vad betyder det här? Vi kan prata om utföraren (live operation) eller backtestern. I det här fallet är de båda loopbaserade.
Slingor är generellt sett enklare och lättare att resonera med, jämfört med händelser. Händelser OTOH kan modellera den verkliga världen bättre. Slingor är också lättare att parallellisera jämfört med händelser och presterar generellt snabbare.
Slingbaserad backtesting kan köras sida vid sida med den live-exekutor. Detta innebär att bakåtvärdering blir en del av din handelsstrategi. Kör ett händelsebaserat backtesting-system leva verkar som en röra för mig. Det beror på att händelser kräver en uppfattning om tid, men under livehandel har du bara nuet, så vad skulle du simulera ändå och hur?
...återgår till FQT, i live-läge, bearbetar den order i en loop. Var X:e sekund frågar den börsen efter uppdateringar. Den håller koll på:
parlistan som innehåller alla de sista priserna på alla tillgångar (för börser som stöder det)
färska OHLCV-data
status för öppna beställningar
Loopbaserad liveexekvering har ett uppenbart problem. Den är synkron. Om du bearbetar till många par kommer exekveringen att vara mindre lyhörd och upprepas mer sällan, vilket släpar efter utvecklingen av prisåtgärder. Så trots att jag föredrar loopar för backtesting så föredrar jag event för live-exekvering.
Nackdelen? Om du använder händelser för det ena och loopar för det andra är det lättare för modeller eller parametrar för strategin att skilja sig åt och inte matcha verkligheten.
Mitt försvarsargument mot detta är att att ständigt köra självutvärdering (backtesting) och liveexekvering undviker detta problem eftersom de kommer så småningom justera.
FQT har många konfigurationsparametrar och många gånger skulle skillnaden mellan boten och strategin bli gråaktig eftersom strategin inte kan styra hur orderna exekveras av boten. Boten hanterar "roi" som är ett rutnät av take-profit prisgränser och "släpande stoploss" på samma sätt men på nedsidan. Strategin ger bara parametrarna och boten gör resten. Jag var inte ett fan av detta.
Återuppringningar för köp- och säljsignaler. Avslut skulle ske baserat på signaler som returneras av återuppringningar som skulle beräkna signalen på en dataram. Problemet med detta är att signalen är statisk; du kan bara dynamiskt beräkna en 1-rads dataram och mata den till boten, men det skulle inte vara backtestbart. Det här är bra om du inte värderar backtesting så mycket, för var och en.
Det jag modifierade i FQT för att försöka få det att bete sig efter min smak.
Fler parlistfilter . Genom att minska antalet par att bearbeta på varje iteration, skulle du minska belastningen till priset av viss reaktivitet. Du skulle fortfarande behandla alla tillgångar för vilka det fanns öppna affärer, och ett begränsat antal nya. Så jag byggde
A blanda filter att bara välja par på måfå.
Ett round robin-filter för att iterera på par lite i taget.
En statisk lista att ladda från lagring
Dataformat: Jag lade till stöd för hdf, parkett- och zarr . Av dessa kom jag på mig själv med att använda zarr, tack vare dess inbyggda snabba de/komprimering av data. Enklare än parkett och mer ergonomiskt.
Det finns ett gäng andra justeringar, som utvärdering av parallella signaler som jag så småningom tog bort när jag fokuserade mer på att bygga snabba signaler, och justeringar för plottning och konfigurationsalternativ, men de är inte anmärkningsvärda.
Jag provade många versioner av backtesting för att påskynda det. Det var allt, inte alls, värt det. Beräkningslogik mellan köp och säljoperation med hjälp av numpy arrays. Det är som att spela tetris med din hjärna. Jag menar där blocken är gjorda av din grå substans, och du försöker sätta ihop dem. Här listar vi dem
Chunked backtest . I den här skulle vi försöka kollapsa affärer med endast numpy arrays. Jonglering mellan det korrekta flödet av utförande av köp/sälj, signaler, stoplosses, trailing stoplosses, roi grid beräkning, tidsvägda roi beräkningar. Alla med numpy arrays, det var en hel röra.
VBT baserad Denna version försökte utnyttja vectorbtnumbabaserat pythonbibliotek för att köra backtestet. Överraskande nog var den inte mycket snabbare än den numpy versionen. Det berodde på att det måste finnas många typkonverteringar till VBT-kompatibla typer, så vilken hastighet som än uppnåddes från utförandet gick förlorad där.
Slingor över avstånden Detta var det första försöket att använda numba själv. Eftersom numba naturligt kan anropa numpy-funktioner, anpassade jag mitt numpy-baserade backtest-ramverk för att fungera under numba. Vinsten var mycket liten. Kostnaden för att lägga allt i numpy arrays var flaskhalsen. Vinsterna som uppnåddes efteråt var minimala eftersom numpy-utförandet redan är snabbt efter numpy-konvertering.
Slingor över ljus Detta var och med rätta den sista. Det var implementeringen som presterade bäst. Det beror på att det var "enkel" iteration över ljus, allt utfört inom numba jittede funktioner. Konceptet var enkelt, men att implementera all avkastning och stoploss-logik när man hanterade numba-buggar var inte trevligt. Det fungerade ganska bra till slut, men det var efter ett berg av numba-inkompatibiliteter som jag var tvungen att bestiga.
Vi har också lagt till några ytterligare funktioner till backtestet, som spridning och likviditet beräkning, och den tidigare nämnda tidsvägda avkastningen.
Varför ville jag ha en snabb backtester? Eftersom jag vill köra många av dem så att jag kan hitta den bästa av de bästa parameterkonfigurationerna...naturligtvis, bortse från alla begrepp som överanpassning, överparametrisering, bristande fokus, etc... Låt oss lista upp vad jag jobbade med:
Skicka jobb till andra arbetare (processer) så fort som möjligt. Den första förbättringen var att ändra sättet att skicka jobb. Från att ha skickat N jobb och väntat på att alla ska bli klara, ändrade jag processen för att mata in nya jobb så snart de tidigare var klara. Det fanns dock ett problem med detta tillvägagångssätt. Optimering är många gånger sekventiell. Du kan inte veta vilka punkter i parameterutrymmet som ska observeras utan att ha beräknat de föregående. Detta gäller åtminstone för bayesiansk optimering. Det betyder att om vi provar nya tester kontinuerligt skulle vi inte förbättra sökningen så mycket mellan optimering, men sökningen skulle vara mer robust.
Vi skulle kunna använda en annan optimerare som inte är baserad på bayesiansk optimering, som den här . Eller kör N oberoende optimerare, så att varje process/optimerare skulle fråga efter nya jobb med sin egen historik, eller valfritt historiken som delas med alla andra (även om detta skulle få alla optimerare att konvergera efter en tid, vilket kunde ha varit trevligt egendom eller inte, beroende på fallet).
Att dela observationer mellan alla processer var en total katastrof. Betning och avbetning av stora listor av flöten går mycket långsamt. Allt eftersom sökningen fortskrider ökar antalet observationer och hela processen blir långsammare och långsammare till en punkt där serialisering tar längre tid än observationsutvärderingen. Det var långsamt både med en hanterarprocess, vanliga filer eller mem-mappade. Det bästa prisvärd tillvägagångssätt visade sig därför vara att köra separata optimerare och aldrig dela observationer mellan dem, hålla allt i minnet.
Hålla reda på optimeringsstatus. Föreställ dig att köra en optimering i timmar, sedan kraschar något och du förlorar allt. Det ville vi naturligtvis undvika, så vi lade till logik spara med jämna mellanrum . Detta var huvuddrivrutinen för att lägga till zarr-lagringsstöd, vilket sparade tillstånd med zarr förbättrade saker avsevärt.
Visualisering av utförd optimering. FQT hade redan bra visualisering för att köra optimering. Jag förbättrade det. Jag var tvungen att skapa en objekt att representera varje observation. Då skulle jag kunna köra ytterligare efterbehandling som normalisering och filtrering
För Korsvalidering vi måste dela upp data och tillämpa backtesting över olika intervall. Tack vare våra ansträngningar för spårning av optimeringstillstånd kunde vi använda utdata från korsvalidering som frö till en annan mer förfinad optimeringsprocess...för att överanpassa...naturligtvis.
Ytterligare optimeringslogik. För att uppnå max churn vi var också tvungna att ta hänsyn till några problem:
Ibland kan en parameterkonfiguration helt enkelt misslyckas, och optimeraren kan fastna, så vi var tvungna att hålla koll på misslyckade observationer och så småningom starta om en eller alla optimerare.
Eftersom vi körde flera optimerare var vi tvungna att hålla reda på vilken optimerare som producerade vilken observation.
Tack vare vår snabba backtester och vår parallella optimerare körde vi massor av observationer, FQT skulle skriva ut varje observation när den slutfördes. Vi var tvungna att gruppera observationerna innan vi skrev ut dem, så att vi skulle skriva ut en"tableful" av dem istället för bara en observation åt gången, eftersom IO-operationer är dyra.
Utforskning/exploateringbalans. Vissa körningar kunde fastna i minima, eller konvergera till långsamt, så vi lade till möjligheten att ändra sökutrymmet under flygning . Detta skulle hjälpa till att normalisera sökningstakten.
En bättre framstegsindikator. Att köra optimerare på flera processer krävde synkronisering för att uppdatera förloppsindikatorn från flera arbetare. Och det fanns ett bibliotek bara för den där , vilket gjorde att vi också kunde lägga till mer information till optimeringens körläge.
Eftersom vi körde flera optimerare lade vi också till stöd för att ge olika förlustfunktioner till olika optimerare, vilket skulle göra det möjligt för oss att se vilken förlustfunktion som fungerade bäst.
För att köra optimeringarna implementerar vi en cli-skript för att köra optimeringen med olika konfigurationsparametrar, återuppta från ett tidigare tillstånd, eller bara visa och filtrera sparade försök.
Vi körde oberoende optimerare med olika förlustfunktioner, varför inte köra olika optimerare på olika förlustfunktioner ? Överväldigad. Så vi implementerade en abstraktion för optimerare så att vi (med lite VVS) på ett magiskt sätt kan byta optimeringsmetoden. Detta krävde också modifiering av optimeringen gränssnitt så att "hyperopt" kunde förstå optimeraren. Sedan genomförde vi om scikit-optimera som ett exempel på vår optimeringsabstraktion. Samtidigt som man kopplar in yxa, emukit och sherpa.
Vi startade FQT backtesting till 11 . Men vi använde det aldrig riktigt i produktion :).
Lite efter numbifieringav backtestaren lades ytterligare callbacks till strategin som bröt åtskillnaden mellan backtesting och strategiutvärdering, vilket innebar att för att hålla saken snabb måste du också skriva din strategi i numba! Men jag blev trött på den skakiga blandningen av python/numpy/numba gotchas och eftersom jag inte gillade liveexekveringen av FQT så släppte jag det hela ändå, för grönare (eller ska jag säga) rosa! ) betesmarker.
Hur som helst...optimering är crack.