Trading bot: de stand van zaken

Door NaliXL op zaterdag 10 maart 2018 23:53 - Reacties (3)
Categorie: Dev log, Views: 2.047

Ik ga me maar eens aan wat design/implementatie wagen. Een mooi stroomschema maken. Geeft mijzelf ook meteen aanleiding om nog eens na te denken over een paar puntjes.

Schematisch overzicht van mijn trading bot
http://jaapvanwingerden.nl/blog-trader-layout.jpg

Om het allemaal wat beter uit te leggen, te zien zijn de volgende onderdelen:
  • Broker: de broker waarmee gecommuniceerd wordt. Dit kunnen er 1 of meerdere zijn.
  • Broker specifieke data logger: Deze module zorgt ervoor dat (indien mogelijk live) trades en orders worden opgeslagen in de SQL database. Aangezien dit onderdeel als eerste alle trading gegevens binnen krijgt, zit ik er sterk aan te denken om het bij deze module ook triggers in te stellen die andere modules activeert
  • Gestandaardiseerde broker interface: Een abstractielaag boven de broker's eigen API. Dit dient om meer dan één broker te kunnen ondersteunen en ondersteunt tenminste basisacties, zoals het plaatsen van een order.
  • Gestandaardiseerde SQL database: Hierin worden orders en trades opgeslagen. Tevens kunnen de diverse analyse modules er gegevens opslaan.
  • Marktanalyse module: Van deze modules kan er een flink aantal aanwezig zijn, zeker gezien iedere module voor ieder trading pair gestart moet worden. Iedere module imlementeert een specifiek algoritme wat een inzicht in de markt kan geven. Een voorbeeld daarvan kan zijn een module die een verwacht piek of dalpunt in de koers aangeeft doormiddel van fibbonaci reeksen.
  • Efficiëntiescoremaker voor marktanalyse modules: Deze deelt aan iedere marktanalyse module een score toe op basis van gebleken accuraatheid.
  • Trade beslissingen module: Deze modules kunnen ook weer veelvuldig voorkomen en moeten tenminste gestart zijn voor ieder trading pair waar (een deel van) de inleg aanwezig is in één of beide munteenheden in dat pair. Ze nemen de beslissingen voor het al dan niet plaatsen van orders, ieder gebaseerd op zijn eigen beslissings-algoritme.
  • Efficiëntiescore maker voor trade-beslissingen modules: Deze is verantwoordelijk voor het toekennen van een efficiëntiescore aan de verschillende trade beslissingen modules. Op basis van deze efficiëntiescore wordt een bedrag toegekend aan de trade beslissingen modules waarover deze mogen beslissen.
Tot op de dag van vandaag ben ik vooral aan het werk geweest aan de broker specifieke datalogger module. Dat blijkt namelijk al gauw tot uitdagingen te leiden. Bijvoorbeeld: op dit moment werk ik lauter met bitstamp.net. Om netjes up-to-date te blijven, maak ik gebruik van de websocket api en daarvoor bleek deze pusher client heel geschikt.
Helaas bleek al snel dat de grote stroom aan orders en trades die in de database geplaatst moesten worden, vaak per order/trade teveel verwerkingstijd ging vragen (data parsen en database query), waardoor er orders gemist werden.

In eerste instantie wilde ik dat oplossen door voor iedere parse + query een thread te starten. Maar voor wie ooit weleens bezig is geweest met PHP + threads weet waarschijnlijk dat dit soms makkelijker gezegd dan gedaan is. Omdat ik zelf een niet threadsafe PHP stack draai, gebruikte ik een oplossing die ik al eens in een vorig experiment van mijzelf had ontwikkeld: het emuleren van threads door een nieuw PHP proces te starten.

Voor wie het nog eens kan gebruiken: hieronder volgt een door mij geschreven alternatief voor PHP's thread class in non-thread-safe PHP, die ik bij deze public domain (MIT license) maak:

PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
<?php
    /*
        The MIT License (MIT)

        Copyright (c) 2018 Jaap van Wingerden

        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
    */

    /*
    * This class is meant to be a replacement for the Thread class provided
    * by PHP's PThread extension that does not depend on PThreads to be installed,
    * but instead emulates threads by starting code in a new PHP process.
    */
    if (!class_exists('Thread')){
        abstract class Thread implements Serializable {
            private $creatorPid;
            private $childPid;
            private $childName;
            private $started = false;
            private $myClassDefinition;
            
            /*
            * Determine if this is the parent or a running child thread.
            */
            private function isThread(){
                return (getmypid() == $this->getPidByProcessName($this->childName));
            }
            
            /*
            * Takes a process name and returns a PID (ps has to be installed in order for this to work).
            */
            private function getPidByProcessName($processName){
                exec(sprintf("ps aux | grep '%s' | grep -v grep | awk '{ print $2 }' | head -1", $processName), $out);
                if (is_array($out))
                    if (array_key_exists(0, $out))
                        if (is_numeric($out[0]))
                            return $out[0];
                return false;
            }
            
            /*
            * Implementation of Serializable interface.
            * Makes sure that child objects are also serialized when teleporting.
            */
            public function serialize(){ 
                $result = array();
                foreach(get_object_vars($this) as $key => $value){
                    $mirror = new ReflectionClass($this);
                    if (is_object($value)){
                        $mirror = new \ReflectionClass($value);
                    }
                    $result[$key] = array(
                        'data' => is_object($value) ? serialize($value) : $value,
                        'serialized' => is_object($value),
                        'include' => $mirror->getFileName(),
                    );
                }
                $result = serialize($result);
                return $result;
            }
            
            /*
            * Implementation of Serializable interface.
            * Unserialize child objects too.
            */
            public function unserialize($serialized){ 
                $data = unserialize($serialized);
                foreach ($data as $key => $value){
                    if ($value['serialized']){
                        include_once($value['include']);
                    }
                    $this->$key = $value['serialized'] ? unserialize($value['data']) : $value['data'];
                }
            }
            
            /*
            * Constructor. Save the (parent) PID.
            */
            function __construct(){
                // Set the creator ID to the current PID upon instantiation.
                // The new thread will be this class instance serialized into a new process.
                $this->creatorPid = getmypid();
            }
            
            /*
            * Since detach() has been removed as of pThreads v3, this class
            * only implements it as a bogus function.
            */
            public function detach(){
                
            }
            
            /*
            * The creator ID will be the PID of the parent process for now.
            */
            public function getCreatorId(){
                return $this->creatorPid;
            }
            
            /*
            * According to PHP documentation:
            * "Return a reference to the currently executing Thread"
            *
            * But since this implementation runs it's thread in a separate process,
            * that's not possible, so return false.
            */
            public function getCurrentThread(){
                return false;
            }
            
            /*
            * According to PHP documentation:
            * "Will return the identity of the currently executing Thread"
            *
            * In this case, the (unique) process name will have to do as "identity".
            */
            public function getCurrentThreadId(){
                return $this->childName;
            }
            
            /*
            * Hmm, what shall we do with this one?
            */
            public function getThreadId(){
                $result = $this->getCurrentThreadId();
                return $result;
            }
            
            /*
            * BOGUS IMPLEMENTATION.
            * According to PHP docs:
            * 
            * Warning: This method has been removed in pthreads v3.
            */
            public static function globally(){
                return false;
            }
            
            /*
            * Determines if the child thread has ended.
            */
            public function isJoined(){
                if (!$this->isStarted()){
                    return false;
                }
                if (is_numeric($this->childPid)){ // If started is true, and a PID id was stored, return the state of that PID.
                    if (is_numeric($this->getPidByProcessName($this->childName)))
                        return false;
                    return true;
                }
                // If started is true, but no PID id is set, then this instance is probably the thread, so return false (we're running!).
                return false;
            }
            
            /*
            * Did the child thread ever start?
            */
            public function isStarted(){
                return $this->started;
            }
            
            /*
            * This will keep the parent process busy until the child process exits.
            */
            public function join(){
                if ($this->isThread()) return;
                while(!$this->isJoined()){
                    usleep(250000);
                }
            }
            
            /*
            * Die, evil child thread!
            */
            public function kill(){
                if (!$this->isStarted())
                    return false;
                if ($this->isJoined())
                    return false;
                if (is_numeric($this->childPid)){
                    $result = posix_kill($this->childPid, SIGKILL);
                    return $result;
                }
                // Kill the current thread!
                die();
            }
            
            /*
            * materialize() is called right after this class has been teleported to the child process
            * and is responsible for initializing some values and executing the run() function
            * which should be implemented in the inheriting class.
            */
            public function materialize(){
                $this->childPid = (int)$this->getPidByProcessName($this->childName);
                $this->run();
            }
            
            /*
            * Start the child process.
            */
            public function start(){
                $this->started = true; // So isStarted() will know.
                $this->childName = microtime(). "phpthread"; // Create a name for the child process.
                $mirror = new ReflectionClass($this); // This is needed to get some info about the current class instance.
                
                /*
                * This process of starting a thread works by piping some bootstrapping code to the
                * PHP executable. The bootstrapping code makes sure to include the nessecary files, unserializes
                * this class and calls the materialize() function.
                */
                $teleportcode = escapeshellarg(sprintf('<?php set_include_path("%s"); include_once("%s"); cli_set_process_title("%s"); $scr = unserialize(base64_decode("%s")); $scr->materialize();', addslashes(pathinfo($mirror->getFileName(), PATHINFO_DIRNAME)), addslashes($mirror->getFileName()), $this->childName, base64_encode(serialize($this))));
                $teleportcommand = '/bin/echo '. $teleportcode . ' | '.  PHP_BINARY . ' >> logthreads.txt &';
                exec($teleportcommand);
                sleep(5); // Some delay to allow the child process to initialize and set it's proces name.
                $this->childPid = $this->getPidByProcessName($this->childName);
            }
        }
    }



De code die ik gebruikte in mijn datalogger is niet exact hetzelfde, maar het idee was hetzelfde.

Let wel, deze implementatie is verre van compleet en werkt waarschijnlijk niet (volledig) hetzelfde als PHP's eigen thread class, maar het biedt wat basisfunctionaliteit.

Goed, terug naar de logger. Ook deze implementatie bleek echter problemen op te leveren. Ooit in MySQL weleens de melding gehad "Deadlock found when trying to get lock"? Ik nu wel dus. De oplossing blijkt eenvoudig genoeg: nogmaals proberen.

Echter, wanneer dit probleem zich voordoet op een druk gebruikte database die vanuit meerdere processen benaderd wordt, dan krijg je dat een heleboel queries een wedstrijdje gaan uitvoeren wat uiteindelijk zoveel vertraging oplevert dat het niet meer werkbaar is.

De queries moesten dus één voor één uitgevoerd worden, zonder daarbij de logger te blokkeren. Wat kan daaraan gedaan worden?

Een oplossing werd gevonden in de vorm van gearman. Een server waarbij z.g.n. "workers" zich kunnen registreren en hun beschikbare functies bekend kunnen maken. Vervolgens kunnen clients een job versturen naar de gearman server met de vraag om het te laten verwerken door een van de workers die functie x aanbieden. Zo'n gearman worker kan bijvoorbeeld ook een MySQL database zijn.

Dat ik gearman niet eerder had gezien! Het gaf een heel ander beeld van hoe ik mijn bot wilde implementeren. Want wat als je iedere functionaliteit in een eigen proces zou starten, aan elkaar geknoopt door die gearman server? Dat zou, onder andere bij de analyse-modules, heel wat efficientie schelen, aangezien ze dan in parallel kunnen werken aan een voorspelling in plaats van op elkaar te wachten.

Het implementeren van zo'n multi-process/thread oplossing was wel weer een volgende stap: het starten en draaiend houden van alle threads is een kunst op zich, zeker gezien de code die ik daarvoor gebruikte. Dat moest beter kunnen, en dat kon het ook.

Ik koos ervoor om aan de gang te gaan met docker containers. Hoewel mijn kennis daarvan slechts uit wat theorie bestond, was de documentatie helder genoeg om vlot ermee aan de slag te kunnen, en het biedt een comfortabele oplossing om processen te starten en in de lucht te houden. Voor wie het nog eens zou kunnen gebruiken: hier is een dockerfile voor een container waarin PHP 7.1, mysqli en gearman beschikbaar zijn:

code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM php:7.1-cli

# PHP Extension: mysqli
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli

# PHP Extension: Gearman
# Download Gearman PECL extension for Gearman supporting PHP 7
RUN apt-get update\
    && apt-get -y --allow-unauthenticated install libgearman-dev git
RUN cd /tmp \  
    && git clone https://github.com/wcgallego/pecl-gearman.git \
    && cd pecl-gearman \
    && git checkout gearman-2.0.3 \
    && phpize \
    && ./configure \
    && make \
    && make install \
    && apt-get remove -y --purge git \
    && apt-get -y autoremove
RUN docker-php-ext-enable gearman



Anyway, daar was ik zo ongeveer gebleven. De volgende keer meer!

Volgende: Een introductie: mijn eigen crypto trading bot 06-03 Een introductie: mijn eigen crypto trading bot

Reacties


Door Tweakers user Evianon, zondag 11 maart 2018 00:19

Wat betreft je data-collectie, is er een reden dat je per se alle orders en trades in de database wilt loggen? Dat is heel veel data en daarmee lijkt het me ook lastig om (eenvoudig) te bepalen hoe het order book er op een bepaald tijdstip uit zag en ziet.

Je zou een in-memory representatie van het order book kunnen bijhouden, en elke seconde een snapshot opslaan in de database. Hetzelfde voor de trades, die je dan optelt. Je verliest dan de precieze data van de trades, maar je kunt het volume per seconde opslaan. Dat scheelt een heleboel queries, threads en complicaties, en maakt de data volgens mij ook beter toegankelijk voor je analyse. Tenzij je een soort HFT wilt gaan doen zal de data per seconde samenbundelen weinig uitmaken voor de prestaties van je bot.

Als iemand die héél veel vrije uurtjes in diverse trading bots heeft gestopt, kan ik je verzekeren dat je nog heel veel meer uitdagingen gaat tegenkomen :P.

Door Tweakers user NaliXL, zondag 11 maart 2018 15:50

@Evianon Bedankt voor je input. Het verzamelen van de data heeft vooral te maken met mijn nieuwsgierigheid (wat gebeurt daar nou achter die candlestick charts op de website, is daar wat uit te halen) en het willen maken van een soort emulatie mode die de efficiëntie scores vast op orde brengt zonder daadwerkelijk te traden.
Je opmerking betreffende het bepalen van het order boek begrijp ik niet helemaal, immers je weet de status van alle orders?!

En hoewel echte hft het niet zal worden, lijkt het mij wel degelijk interessant om te zien of er wat te halen is door razendsnel op orders te reageren.

Door Tweakers user Evianon, zondag 11 maart 2018 23:36

Als ik het goed begrijp zet je nu dus elke toevoeging en delete van orders in je database, zoals je die binnenkrijgt in je stream, klopt dat? Dus "Om 19:01:01 order op 9954 USD van 0.03 BTC toegevoegd", "Om 19:01:02 order op 9832 USD verwijderd" etc.

In dat geval zou je dus, om te bepalen hoe het orderbook er op tijd X uitziet (of wat de hoogste bid of ask op dat moment is, of iets anders), alle orders die voor tijd X zijn geopend, maar nog niet zijn verwijderd, uit je database moeten vissen. Dat lijkt me behoorlijk inefficient en traag als je dat in grote hoeveelheden wilt bepalen (zoals in backtests / emulaties), of als je gemiddelden en indicators wilt berekenen.

Ook heb je dan een probleem als je even de connectie verliest met de websocket (en dat gebeurt vaak bij crypto exchanges), omdat je dan een paar seconden of langer aan data mist en bijv. niet zeker weet of een order nog bestaat of in die periode verwijderd is.

Als je het orderbook volledig in memory bijhoudt, en elke seconde een snapshot (bijv. alleen de hoogste bid en laagste ask van dat moment, of wat meer data) opslaat in de database heb je dat niet. Dan houd je in je geheugen iets bij wat er zo uitziet: https://www.bitstamp.net/...live_diff_order_book.html. En iedere seconde zet je met 1 database query daarvan wat data in de database.

Maar misschien zit ik totaal mis met mijn interpretatie van hoe je het nu opslaat, en komt het op die manier ook helemaal goed.

Als voorbeeld, misschien heb je er wat aan, dit is hoe ik voor mijn trading bots de data opsla: https://imgur.com/a/Vt37e. Iedere - in dit geval - 2 seconden wordt een "snapshot" van het orderbook opgeslagen. Dat is 1 rij in de database, en bevat de hoogste bid en laagste ask op dat moment, en dan hoeveel BTC er 0,1% onder / boven het huidige midden van de markt wordt aangeboden / gevraagd, hoeveel op 0,25%, hoeveel op 0,5% en zo door tot 10% boven en onder het midden.

[Reactie gewijzigd op zondag 11 maart 2018 23:40]


Reageren is niet meer mogelijk