Let op: Tweakers stopt per 2023 met Tweakblogs. In
dit artikel
leggen we uit waarom we hiervoor hebben gekozen.
Hier wordt over implementatie gepraat
Trading bot: de stand van zaken
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

Om het allemaal wat beter uit te leggen, te zien zijn de volgende onderdelen:
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:
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:
Anyway, daar was ik zo ongeveer gebleven. De volgende keer meer!
Schematisch overzicht van mijn trading bot

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.
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!