Genetisk börshandel Att hitta mönster i börsmarknaden med genetiska algoritmer Alexander Löfstrand HT 2017
1. Inledning 1.1. Bakgrund Börsmarknaden är full av möjligheter att tjäna pengar, men även på att förlora lika mycket. Det är ett område som många jobbar inom, men samtidigt kan det vara lika bra odds som att spela på roulette. Men tänk att du har ett övertag och vet att när klockan är 16:45 och John står vid roulette bordet finns det en mycket större chans att det blir rött. Det finns självklart inte något annat än tur när det kommer till roulette men att hitta övertag börsmarknaden har snarare blivit något av en vetenskap. Genom att identifiera olika mönster i marknaden kan man i vissa fall förutspå om en aktie kommer att gå upp i pris eller sjunka. Man kan åtminstone få bättre odds än om man singlar en slant. Människor sitter och gör analyser på dagarna och försöker lista ut vilka aktier man bör investera för att göra en vinst. Människan är dock inte särskilt bra på att göra mängder av uträkningar och många vänder sig därför till datorn för att hitta trender och mönster i den oändliga mängd data som strömmar genom börsmarknaden. 1.2. Börsmarknad För att få överhand på börsmarknaden spenderar människor mycket tid på att analysera den data som finns om prisutveckling för olika aktier för att försöka hitta mönster i prisändringarna. Likaså spenderar man mycket tid med att läsa nyheter om allt som möjligen kan påverka kursen. Men när alla deltar i denna jakt på övertag bottnar det i att man försöker lista ut hur andra reagerar på olika mönster. Mönster ändras konstant och kan vara olika för varje enskild aktie och att modellera ett sådant kaotiskt system anser jag vara en skrämmande tanke. Denna rapport beskriver ett försök till att visa hur man med genetiska algoritmer snabbt och utan en stor mängd data kan identifiera köpmönster och skapa handelsregler för handel på börsmarknaden. Jag har skapat ett enkelt program i Python med hjälp av ett bibliotek för genetiska algoritmer. Eftersom syftet är att visa att genetiska algoritmer har möjligheten att identifiera köpmönster har jag skapat min egen börs med påhittade aktier och värden som algoritmen utgår ifrån. En användbar implementering av genetiska algoritmer för att förutspå börsmarknaden skulle kräva ett mycket mer avancerat program med användning av riktig data, och om det var lätt att genomföra skulle marknaden bli oerhört utnyttjad. Trots detta används genetiska algoritmer av börshandlare för att identifiera trender och mönster, men det förväntas 1 inte ge en absolut sanning eller optimal regel. 1 Straßburg, Janko. González-Martel, Christian. Alexandrov, Vassil. Parallel genetic algorithms for stock market trading rules. Procedia Computer Science. 2012.
2. Teori 2.1. Genetiska Algoritmer Genom naturligt urval, mutationer, arv och gener har vi människor utvecklats och även om vi inte fullt ut förstår denna process kan man argumentera för att den varit någorlunda framgångsrik. Genetiska algoritmer har uppkommit i ett försök att efterlikna och utnyttja denna process för att lösa problem. Det finns ett antal steg som som måste finnas och som utgör grunden för genetiska 2 algoritmer: 1. Skapa en initial population där varje individer har en uppsättning gener. 2. Evaluera (Fitness) hur bra dessa gener visat sig vara för uppgiften. 3. Skapa en ny population. För att skapa en ny population krävs ytterligare ett antal steg, vilka är nödvändiga för att det ska ske någon evolution: 1. Selektionen av två föräldrar, två uppsättningar gener, från populationen där man även tar hänsyn till evalueringen. 2. Crossover, skapa två nya avkommor eller uppsättningar gener från föräldrarna. De nya avkommorna bör ha en del av sin genuppsättning från bäggedera föräldrar, annars blir det en exakt kopia. 3. Mutation, precis som i naturen finns en chans för att mutation att ske och för genuppsättningen att förändras. Efter detta steg är slutfört har man genererat den nya populationen. Hela denna process itereras sedan flera gånger och resultatet presenteras sedan som den individ med mest fördelaktig genuppsättning enligt evalueringen. 3. Implementering 3.1. Allmänt Jag implementerade ett flertal olika algoritmer i mitt försök att skapa en bra modell för att hitta mönster i en börsmarknad. De första modellerna var relativt simpla och bestod av en ganska liten gen och arbetade med en hårdkodad börs med relativt få värden. Detta gav dock inte önskat resultat. Algoritmen kunde endast identifiera med vilka värden på olika indikatorer som var optimalt att handla efter, men kunde inte identifiera de som inte spelade någon roll. 2 Sivanandam, S. N. Deepa, S. N. Introduction to genetic algorithms. Introduction to Genetic Algorithms. 2010. s. 30/31.
Den slutgiltiga versionen modifierades så att den hade fler indikatorer och en börsmarknad som genererades baserat på ett antal regler. Strukturen på generna tillät även agenterna att identifiera vilka indikatorer som är irrelevanta för handel. 3 Programmet är skrivet i Python med hjälp av biblioteket pyeasyga. Biblioteket tillhandahåller en klass som kan modifieras och användas som grund till sin genetiska algoritm. Jag har implementerat de funktioner som anropas när algoritmen körs, samt en algoritm som skapar en börs utifrån en uppsättning regler. Nedan följer en genomgång av implementeringen av den slutgiltiga versionen. 3.2. Genetiska Struktur Den genetiska strukturen såg ut på följande sätt med tre handlingar och nio indikatorer i varje. Notera att jag inte skrivit ut alla nio värden och att exemplet endast innehåller 2 indikatorer på varje handling. { Buy : { Indicator : Value, Indicator :Value}, Sell: { Indicator : Value, Indicator :Value}, Hold : { Indicator : Value, Indicator : Value}, } Av de nio indikatorerna var det fem som inte hade någon direkt påverkan på börsutvecklingen. Varje värde kunde vara antingen en integer eller boolean beroende på vilken indikator det var. En indikator var Increase som kunde ha värdet True eller False vilket representerar att börsvärdet ökat eller minskat sedan dagen innan. Exempel på värde med integer är den fullständigt irrelevanta indikatorn NicholasCageMovies som har ett slumpat integer värde mellan 1 och 20. Samtliga indikatorer kan även ha värdet None vilket utesluter den indikatorn som regel vid börshandeln. 3.3. Fitness 3.3.1. Börsmarknaden För att beräkna hur bra varje genuppsättning var behövdes en börs att testa den på. Därför skapade jag en algoritm som genererade en börsutvecklingen på 20 dagar. De första värdena i börsen var hårdkodade och utvecklingen räknades ut av en algoritm som följde en uppsättning regler men var ändå lite slumpad. Detta gjordes med hjälp av funktionerna, stock_market_algorithm(days), där days var antalet dagar som börsen skulle ha data för, och med calc_new_value(day, data) där day var den senaste dagen av börsdata som genererats och data var ett dictionary med all börsdata. Funktionen 3 http://pyeasyga.readthedocs.io/en/latest/index.html
stock_market_algorithm anropar calc_new_value x antal gånger (x= days), och efter varje anrop uppdaterar den börsdatan med de nya värden som calc_new_value returnerar utifrån en uppsättning regler. Reglerna som calc_new_value använder vid bestämmande av börspriser var följande: Om indikator MovingAverage > Price Priest går ner Om indikator P/E < (Price / 10) Priset går ner Om indikator P/S < (Price / 60) Priset går ner Annars: Priset går upp 3.3.2. Fitness-funktionen Agentens fitness-värde beräknades med funktionen fitness(individual) som jämförde priset den dagen agenten köpte/sålde aktien med värdet dagen efter. Valde agenten att varken köpa eller sälja gick den varken plus eller minus. Exempel: Priset dag 1 är 100 kr och priset dag 2 är 120 kr, om agenten köper dag 1 går den plus 20kr, säljer den går den minus 20kr, gör den ingenting så går den + 0 kronor. Det är en ganska simpel abstrahering av börshandel som även är väldigt kortsiktig. Det är snarare så att agenten försöker förutspå prisutvecklingen än att den försöker handla, då den inte heller kan bestämma hur mycket den vill investera. Agenten väljer att handla på börsen givet att den genuppsättning som finns matchar den på börsen. Det vill säga om en agent har värdena Increase: True på handlingen Buy så kommer den endast att köpa om värdet gått upp senaste dagen, samma sak gäller för samtliga indikatorer och handlingar. Detta kräver att en handelsregel måste korrelera med aktiedatan på alla nio indikatorer för att den ska bestämma sig för att handla, den kan dock strunta i några indikatorer som tidigare nämnt om de har värdet None. Förhoppningen var att den agent som lyckades handla bäst, få mest avkastning ( profit ), på börsen genom att handla med sina regler skulle ha en genuppsättning som matchade de regler som börsalgoritmen baserades på. Det vill säga skulle den optimala lösningen vara en som helt ignorerade de fem indikatorerna som inte hade någon påverkan på värdet och hade korrekta värden på de övriga indikatorerna. 3.4. Selektion Min implementation använder sig av en Tournament Selection för att välja vilka agenter som ska paras och föra vidare sina gener till nästa generation. Denna selektionsmetod går ut på att ett antal (n) agenter slumpmässigt väljs ut ur populationen och där den med bäst fitness-värde, dvs. den som klarade sig bäst på börsen vinner, och därmed väljs ut för crossover.
Vilka agenter som väljs i min implementation sker genom funktionen selection(population), där populationen är samtliga agenter i denna generation. Antalet agenter i turneringen är (population_size // 10) och om populationen är för liten är det minst två individer i turneringen. Om storleken på turneringen, n, är lika stor som populationen får man problemet att det alltid blir den bästa som väljs, är det istället n = 1 blir det snarare helt slumpmässigt. Om (n = population_size) så skulle alltid den bästa agenten väljas ut för crossover och paras med sig själv och genpoolen skulle vara väldigt enfaldig. Om man istället väljer värdet (n = 1) så är det lika med att välja en slumpad individ och därmed spelar inte fitness-värdet någon roll. Denna selektionsmetod möjliggör risken att den bästa individen inte väljs ut för turneringen och att den genuppsättningen därför försvinner. För att hindra detta används Elitism vilket innebär att den bästa individen i varje generation förs vidare utan att ändra genuppsättningen. Detta tillåter partiella lösningar att föras vidare och kan undvika att sådana måste återhittas i framtida iterationer. Exempelvis kanske den bästa genen väljs ut för crossover, men barnen får sedan en missgynnande genuppsättning och väljs inte i nästa selektion. Detta medför att den genuppsättningen försvinner helt från genpoolen endast för att möjligen återfinnas senare igen. Men med elitism så försvinner inte denna genuppsättning utan tas med i nästa generation oförändrad. 3.5. Crossover Den första generationen agenter genereras med slumpmässiga värden med funktionen create_individual(data), men framtida generationer ärver gener från tidigare generationer. De agenter som valts ut i selektionen blir föräldrar till den nya generationen agenter genom som skapas genom crossover. För varje två föräldrar som väljs ut skapas två stycken nya agenter, deras barn. Ett av dessa barn har i min implementation, crossover(parent_1, parent_2), fått ärva fyra indikatorvärden i varje handling av den första förälderns genuppsättning, och fem indikatorvärden från den andra föräldern vilka alla är slumpmässigt valda. Det andra av barnen får fem indikatorvärden av den andra föräldern och fyra från den första. För att förtydliga så ärver barnet fem indikatorvärden på varje handling (Buy, Sell, Hold) för totalt 15st från ena föräldern och resten från andra föräldern. 3.6. Mutation Det finns en 8% chans för att en given agent muteras innan den hamnar i den nya generationen. Mutationen sker genom funktionen mutation(individual) på det vis att en handling (Buy, Sell, Hold) slumpmässigt väljs ut och värdet på en slumpmässigt vald indikator förändras sedan. Notera att här kan värdet bli None.
3.7. Resultat Resultatet var förvånansvärt bra dock inte optimalt. Ingen agent hade funnit det optimala handelsmönstret men hade ändå kommit rätt nära. De flesta agenter som klarat sig bra, dvs. hade en hög profit, hade identifierat värden på indikatorer som stämde överrens med de regler som börsmarknaden baserades på. Agenterna hade dock i de flesta fall kvar värden på nonsens indikatorerna som inte blivit korrekt identifierade med None, vilka kunde hindra agenternas handlingar. Programmet kördes flera gånger med ett antal olika inställningar, den körning som gav bäst resultat inkluderade 50 individer och 100 generationer, en crossover chans på 80% och en mutationschans på 90%. Programmet använde sig som sagt av Tournament Selection och Elitism. Resultatet blev genomsnittligen 48.7375kr i vinst efter algoritmen körts åtta gånger. Andra inställningar gav inte lika bra resultat och sänktes chansen för mutation eller antalet individer blev resultatet lidande. Notera att samtliga program kördes gentemot samma genererade börsmarknad. Samma inställningar som i det bästa resultatet fast med en mutationschans på 8% ger istället 35.82kr vinst i genomsnitt efter algoritmen körts åtta gånger. Byter man dessutom antalet individer till 10 istället för 50, sjunker den genomsnittliga vinsten till 31.29kr. 4. Diskussion 4.1. Problematik 4 De problem som jag stötte på i denna modell var så kallad premature convergence. Det som skedde var att agenterna liknade varandra väldigt mycket vilket är symptomatiskt för att algoritmen konvergerat i ett svar, men det tydes ske väldigt snabbt och vid noggrannare granskning verkade det som att de fastnade i lokala optimum. Det verkar även som att en ökad mängd individer och en större chans för mutation gör så att man kan undkomma detta lokala optimum bättre med en mer mångfaldig genpool. Algoritmen hinner inte konvergera lika snabbt i en lösning och hinner utforska fler genuppsättningar innan den konvergerar i ett svar. Den låga chansen för mutation gör att samtliga genuppsättningar snabbt blir för likartade. Men det främsta problemet som jag fann det var incest vid selektionen. Många gånger valdes samma förälder två gånger och parades med sig själv vilket innebar en likadan individ. Detta motverkades med mutation vilket gjorde att varje individ blev annorlunda trots att de hade två likadana föräldrar. Av denna anledning lyckades den 4 How to avoid getting stuck on local optimum, for genetic algorithm. (2017, September 19). https://cs.stackexchange.com/questions/54828/how-to-avoid-getting-stuck-on-local-optimum-for-genet ic-algorithms
höga chansen för mutation motverka konvergensen. Likaså lyckades en större initial population sakta ner konvergensen, dock inte lika bra som mutationen gjorde. En problematik med den höga mutationen är att den genetiska algoritmen blir väldigt slumpmässig i sin utveckling. Man förlitar sig mycket på mutation och crossover spelar allt mindre roll. Jag vill dock inte påstå att crossover blir obsolet, den för vidare de muterade generna och ser till att partiella lösningar förs vidare och att dessa kan muteras för vidare förbättring. Men en hög chans för mutation gör det svårare eller omöjligt att konvergera på en optimal lösning. En annan aspekt av detta är dock att min mutation funktion inte förändrar genuppsättningen något avsevärt, endast 1 gen av 27 förändras, dvs en indikator på en handling. Därför krävs det möjligen att en större mutation sker för att kunna minska chansen för mutation och ändå undvika premature convergence. Man kan säga att crossover försök konvergera och att mutation försöker motverka denna konvergens för att utforska fler möjligheter. Båda vilka är viktiga, har man en hög chans för mutation blir resultatet väldigt slumpmässigt och konvergerar inte på ett globalt optimum, har man en låg chans fastnar man istället i lokala optimum. 5 Trots det har jag en extremt hög, 90%, chans för mutation för att undvika att fastna i ett lokalt optimum väldigt tidigt i min algoritm. Vilket tyder på att min selektion och crossover gör att algoritmen konvergerar i lokalt optimum väldigt snabbt med incest. Jag observerade att det väldigt snabbt blev identiska genuppsättningar i populationen efter endast ett fåtal generationer. Om det var så att inga förbättringar gjordes av crossover funktionen på ett antal generationer, dvs. ingen fick ett bättre fitnessvärde, skulle det innebära att samma genuppsättning förts vidare ett antal gånger, och antagligen med hjälp av incest förblivit likadant. På så sätt kan genpool blivit enfaldig relativt snabbt. För att motverka detta skulle man behövt skriva om algoritmen för att tillåta fler sämre individer att överleva längre, men även kanske skriva om fitness algoritmen för att agenterna ska förbättras lättare. I nuläget sker förbättringen i stora hopp när genuppsättningen finner ett specifikt mönster. Om en av de vitala generna MovingAverage, som verkade spela störst roll för en agents framgång, fick de önskade värdet i en genuppsättning fick agenten en stor ökning i sin profit. Medan många förändringar inte påverkade resultaten alls. Dessa hopp i förbättringarna gör att man letar efter en väldigt specifikt uppsättning som inte stämmer överens med verkligheten. Dessutom finns det bara ett rätt, istället för flera olika mönster som agenten skulle kunna hitta i riktigt data. 5 Why is the mutation rate in genetic algorithms very small. (2012). https://www.researchgate.net/post/why_is_the_mutation_rate_in_genetic_algorithms_very_small
Ett annat problem som hittades var att agenten är starkt biased till att köpa aktier. Logiken i de bästa agenterna är att om de inte köper så säljer de. Alltså de har en regel för när de ska köpa, men ingen regel alls för att sälja. Generna för Sell består främst av None eller enbart None, vilket tyder på att agenten helt enkelt listat ut att om priset inte går upp så går det ner. 4.2. Förbättringsförslag För att förbättra min implementation skulle jag behöva skapa ett mer realistiskt dataset med fler indikatorer i genuppsättningen. Eftersom fitness-värdet hoppar väldigt mycket och att många förändringar i genuppsättningen inte gör någon skillnad kan det ta många generationer för en agent att bli bättre. På denna tid hinner algoritmen konvergera på den partiella lösningen och då finns ingen mångfald i genpoolen längre. Om agenterna förbättrades mer stadigt vid indikatorförändringar och fitness-värdet ökade mer regelbundet snarare än sporadiskt, skulle algoritmen kunna hantera premature convergence mycket bättre. Ett bättre dataset skulle även tillåta fler lösningar/rätta mönster och därmed inte tillåta lösningar av agenten som att, om min köpregel inte stämmer så sälj, att vara effektiva. En stadigare förbättring av agenternas prestation skulle även kunna nås genom att använda en single-point crossover, där jag tar en av indikator i varje handling från en förälder och resten från den andra. Detta skulle ge en långsammare evolution och förhoppningsvis inte råka hoppa över någon lösning. Jag skulle dessutom implementera någon form av incest-prevention och olika strategier som crowding för att behålla den genetiska mångfalden som initial population har. Crowding innebär att endast en liten del av populationen väljs ut för crossover varje generation och därför ändras inte hela populationen på en gång. Detta ger då en större mångfald i genpoolen vid varje generation, då en population kan innehålla individer från flera generationer tillbaka. Ett annat sätt att behålla en mångfaldig genpool och en idé för att undvika lokala optimum skulle vara att vid vissa intervall skapa en ny individ med en ny uppsättning gener som blir förälder tillsammans med en annan individ i populationen.
4.3. Sammanfattning Sammanfattningsvis skulle jag vilja påstå att en implementation av genetiska algoritmer för att hitta mönster i börsmarknaden är mycket möjligt, men inte utan några problem. Min implementation lyckades, i de flesta fall, hitta de mönster som jag placerat i den genererade börsmarknaden, men lyckades med detta främst genom slumpen i form av en hög chans för mutation. Detta tyder på att min algoritm inte kommer lyckas att finna mönster i börsmarknaden med ett större dataset och med fler indikatorer utan förbättringar i algoritmen. Ett problem med uppgiftens natur är att jag letar efter ett specifikt mönster i handeln som kan vara bra för att göra transaktioner av en viss typ. Detta mönster kan dock vara av sådan natur att det är svårt att konvergera mot utan bara kan finnas av slumpen, exempelvis kanske den inte funkar alls om alla förutom en indikator har rätt värde, men plötsligt är fullständigt optimal om den har rätt värde. Detta gör det svårt att konvergera på en lösning genom crossover och varför mutationens slumpmässiga natur lyckas bra med att undvika att fastna i en partiell lösning. Hur denna algoritm skulle fungera om man implementerade den med ett dataset från den riktiga börsmarknaden vet jag inte. Men möjligen skulle det finnas fler partiella lösningar som gör att man lättare undviker premature convergence och kanske fler fördelaktiga mönster. Men algoritmen bör implementera olika tekniker för att behålla en mångfaldig genpool så att man inte konvergerar på ett svar för snabbt och att agenterna utforskar en större problemrymd. En förbättring av algoritmen kanske skulle tillåta varje agent att handla på börsmarknaden med en uppsättning pengar och om den går bankrupt ta bort den från genpoolen helt och håller och skapar en ny individ istället. Även om man inte hittar en optimal lösning skulle en implementation av genetiska algoritmer kanske kunna hitta en lösning snabbt och som är, tillräckligt bra.
5. Referenser Straßburg, Janko. González-Martel, Christian. Alexandrov, Vassil. Parallel genetic algorithms for stock market trading rules. Procedia Computer Science. 2012. Sivanandam, S. N. Deepa, S. N. Introduction to genetic algorithms. Introduction to Genetic Algorithms. 2010. Why is the mutation rate in genetic algorithms very small. (2012). https://www.researchgate.net/post/why_is_the_mutation_rate_in_genetic_algorithms_ very_small How to avoid getting stuck on local optimum, for genetic algorithm. (2017, September 19). https://cs.stackexchange.com/questions/54828/how-to-avoid-getting-stuck-on-local-o ptimum-for-genetic-algorithms http://pyeasyga.readthedocs.io/en/latest/index.html