Trading bot: de stand van zaken

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

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!

Een introductie: mijn eigen crypto trading bot

Door NaliXL op dinsdag 6 maart 2018 23:37 - Reacties (7)
Categorie: Chitchat, Views: 3.327

Al een aantal maanden ben ik bezig om in mijn avonduurtjes een crypto trading bot te schrijven. Nu schrijf ik crypto trading, maar mocht dit projectje gaan functioneren zoals ik dat wil, dan wil ik het ook breder gaan inzetten, op exchanges die ook andere assets aanbieden dan cryptovaluta.

Dat is allemaal begonnen toen ik begin vorig jaar een account maakte op bitstamp.net en er voor de grap eens ¤100 inzette. Toch eens meeliften op de hype, kijken hoe dat gaat. En gaan deed het. Je kon niet veel fout doen. Inzetten op bitcoin. Zakte 'ie een keer wat in en stapte je op tijd uit, dan had je geluk. Stapte je niet op tijd uit, no problemo. De koers is toch zeker binnen een paar weken weer op het oude niveau. Na enige tijd nog eens ¤500 bij gestort. Het ging lekker.

Toen de BTC/USD koers de ¤ 10000 ging aantikken, hield ik mijn geld (inzet plus een verdienste van ongeveer 280 euro, had meer kunnen zijn maar ik heb zeker geen klagen) even een poosje op de euro. Niet onverstandig, bleek achteraf. De koersen na die dagen kunnen we immers allemaal opzoeken. Ik merkte echter ook toen er weer wat meer rust in de markt kwam, mijn kansen om er daadwerkelijk mee te verdienen aanmerkelijk kleiner geworden waren, en ik een groot risico zou lopen bij de grote groep gebruikers te gaan horen die alleen maar geld verliest op cryptotrading. Tijd dus om alles eens goed op een rijtje te zetten.

Kijkend naar de markt besloot ik dat ik wellicht veel kennis zou kunnen opdoen via anderhande wegen en dan via trial-and-error uitvinden wat voor mij werkt, maar dat zou een proces zijn dat van mij vereist dat ik steeds met mijn neus bovenop de koersen zou zitten (wat eigenlijk niet mogelijk is i.v.m. mijn werk en gezin). Bovendien denk ik dat er heel veel kansen zitten in het spreiden van geld over meerdere valuta (ik hield het eerst vooral bij BTC/EUR voor mijn eigen overzicht en tijd) en de kleinere waardeschommelingen. Tijd om mijn 1337 PHP scripting skillz :X eens uit de kast te halen dus.

"Moet je daar nou echt een blog over maken, prutser!?" hoor ik jullie zuchten. Het antwoord is ja. Ten eerste is dat omdat ik denk dat het bijhouden van dit blog een goede manier voor mijzelf zal zijn om mijn gedachten te ordenen en mijzelf doelen te stellen. Ten tweede omdat onderwerpen als cryptotrading en daarvoor bestemde bots erg blijken te leven onder tweakers (sterker, ondergetekende was hieraan nooit begonnen zonder tweakers.net). Ten derde, omdat ik nog wel wat vragen heb die ik eens in de groep zou willen gooien. Ben benieuwd naar jullie reacties.

Inmiddels ben ik al een maandje of twee verder en heb ik al een grondige basis gelegd voor één en ander, waarover ik meer uit de doeken hoop te doen in de volgende post.

Eerst maar eens een algemene vraag: wat zijn jullie gedachten over een open-source trading bot? Wat is het effect als je een bot die goed werkt open source maakt? Zou dit de effectiviteit van deze bot teniet doen of zou het juist iedereen ten goede komen? Waarom?