Aufbau eines Social-Technology-Startups, Teil II: Programmierung eines MQL5-REST-Clients

laplacianlab | 13 Juni, 2016

Einleitung

Im vorherigen Teil dieses Beitrags haben wir die Architektur eines sogenannten sozialen Entscheidungsunterstützungssystems (Social Decision Support System, SDSS) vorgestellt. Auf der einen Seite besteht dieses System aus einem MetaTrader-5-Terminal, das die automatischen Entscheidungen eines Expert Advisors an die Serverseite sendet. Auf der anderen Seite befindet sich eine auf dem Slim-PHP-Framework erstellte Twitter-Anwendung, die diese Handelssignale empfängt, sie in einer MySQL-Datenbank speichert und sie schließlich an die Öffentlichkeit twittert. Das Hauptziel des SDSS ist es, menschliche Handlungen, die auf Robotersignalen ausgeführt werden, aufzuzeichnen und dementsprechend menschliche Entscheidungen zu treffen. Das ist möglich, weil Robotersignale auf diese Weise einem sehr großen Zielpublikum von Experten zugänglich gemacht werden können.

In diesem zweiten Teil werden wir die Client-Seite des SDSS mit der Programmiersprache MQL5 entwickeln. Wir besprechen einige Alternativen und identifizieren die Vor- und Nachteile von jeder davon. Anschließend setzen wir alle Puzzlestücke zusammen und formen somit die PHP REST API, die Handelssignale von Expert Advisors empfängt. Dazu müssen wir einige Aspekte der Programmierung der Client-Seite berücksichtigen.

Sie können Ihre MQL5-Handelssignale jetzt twittern!

Sie können Ihre MQL5-Handelssignale jetzt twittern!


1. Die Client-Seite des SDSS

1.1. Twittern von Handelssignalen im Ereignis-Handler OnTimer

Ich habe zu Veranschaulichungszwecken beschlossen, vorzuführen, wie Handelssignale aus dem Event-Handler OnTimer verarbeitet werden. Nachdem Sie gesehen haben, wie dieses einfache Beispiel funktioniert, wird es einfach sein, dieses Grundverhalten auf einen herkömmlichen Expert Advisor zu übertragen.

dummy_ontimer.mq5:

#property copyright     "Author: laplacianlab, CC Attribution-Noncommercial-No Derivate 3.0"
#property link          "https://www.mql5.com/de/users/laplacianlab"
#property version       "1.00"
#property description   "Simple REST client built on the OnTimer event for learning purposes."

int OnInit()
  {          
   EventSetTimer(10);    
   return(0);  
  }
  
void OnDeinit(const int reason)
  {  
  }
  
void OnTimer()
  {
//--- REST client's HTTP vars
   string uri="http://api.laplacianlab.com/signal/add";
   char post[];
   char result[];
   string headers;
   int res;
   string signal = "id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&";
   StringToCharArray(signal,post);
//--- reset last error
   ResetLastError();
//--- post data to REST API
   res=WebRequest("POST",uri,NULL,NULL,50,post,ArraySize(post),result,headers);
//--- check errors
   if(res==-1)
     {
      Print("Error code =",GetLastError());
      //--- maybe the URL is not added, show message to add it
      MessageBox("Add address '"+uri+"' in Expert Advisors tab of the Options window","Error",MB_ICONINFORMATION);
     }
   else
     {
      //--- successful
      Print("REST client's POST: ",signal);
      Print("Server response: ",CharArrayToString(result,0,-1));
     }         
  }

Wie Sie sehen können, ist der zentrale Bestandteil dieser Client-Anwendung die neue MQL5-Funktion WebRequest.

Eine alternative Lösung wäre die Programmierung einer benutzerdefinierten MQL5-Komponente für die HTTP-Kommunikation, doch es ist sicherer, diese Aufgabe über diese neue Funktion der Sprache an MetaQuotes zu delegieren.

Das oben aufgeführte MQL5-Programm gibt Folgendes aus:

OR      0       15:43:45.363    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
KK      0       15:43:45.365    RESTClient (EURUSD,H1)  Server response: {"id_ea":"1","symbol":"AUDUSD","operation":"BUY","value":"0.9281","id":77}
PD      0       15:43:54.579    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
CE      0       15:43:54.579    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
ME      0       15:44:04.172    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
JD      0       15:44:04.172    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
NE      0       15:44:14.129    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
ID      0       15:44:14.129    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
NR      0       15:44:24.175    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
IG      0       15:44:24.175    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
MR      0       15:44:34.162    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
JG      0       15:44:34.162    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
PR      0       15:44:44.179    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
CG      0       15:44:44.179    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}
HS      0       15:44:54.787    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
KJ      0       15:44:54.787    RESTClient (EURUSD,H1)  Server response: {"id_ea":"1","symbol":"AUDUSD","operation":"BUY","value":"0.9281","id":78}
DE      0       15:45:04.163    RESTClient (EURUSD,H1)  REST client's POST: id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&
OD      0       15:45:04.163    RESTClient (EURUSD,H1)  Server response: {"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}

Bitte beachten Sie, dass der Server mit dieser Nachricht antwortet:

{"status": "ok", "message": {"text": "Please wait until the time window has elapsed."}}

Das liegt daran, dass in der API-Methode signal/add ein kleiner Sicherheitsmechanismus integriert ist, der das SDSS vor hyperaktiven Scalper-Robotern schützt:

/**
 * REST method.
 * Adds and tweets a new trading signal.
 */
$app->post('/signal/add', function() {
    $tweeterer = new Tweeterer();
    // This condition is a simple mechanism to prevent hyperactive scalpers
    if ($tweeterer->canTweet($tweeterer->getLastSignal(1)->created_at, '1 minute'))
    {
        $signal = (object)($_POST);
        $signal->id = $tweeterer->addSignal(1, $signal);
        $tokens = $tweeterer->getTokens(1);
        $connection = new TwitterOAuth(
            API_KEY, 
            API_SECRET, 
            $tokens->access_token, 
            $tokens->access_token_secret);
        $connection->host = "https://api.twitter.com/1.1/";
        $ea = new EA();
        $message = "{$ea->get($signal->id_ea)->name} on $signal->symbol. $signal->operation at $signal->value";
        $connection->post('statuses/update', array('status' => $message));           
        echo '{"status": "ok", "message": {"text": "Signal processed."}}';
    }   
});

Der simple oben aufgeführte Mechanismus kommt innerhalb der Web-Anwendung zum Tragen, gleich nachdem der Webserver die eingehende HTTP-Anfrage auf Schädlichkeit geprüft hat (z. B., dass das eingehende Signal kein Denial-of-Service-Angriff ist).

Der Webserver kann solchen Angriffen vorbeugen. Zum Beispiel kann Apache sie durch die Module evasive und security verhindern.

Das ist eine typische Apache-Konfiguration mod_evasive, bei der der Serveradministrator die HTTP-Anfragen kontrollieren kann, die die Anwendung pro Sekunde usw. annehmen kann.

<IfModule mod_evasive20.c>
DOSHashTableSize    3097
DOSPageCount        2
DOSSiteCount        50
DOSPageInterval     1
DOSSiteInterval     1
DOSBlockingPeriod   60
DOSEmailNotify someone@somewhere.com
</IfModule>

Also ist das Ziel der PHP-Methode canTweet, wie wir schon sagten, hyperaktive Scalper zu blocken, die vom SDSS nicht als HTTP-Angriffe betrachtet werden. Die Methode canTweet ist in der Klasse Twetterer (die später besprochen wird) implementiert:

/**
 * Checks if it's been long enough so that the tweeterer can tweet again
 * @param string $timeLastTweet e.g. 2014-07-05 15:26:49
 * @param string $timeWindow A time window, e.g. 1 hour
 * @return boolean
 */
public function canTweet($timeLastTweet=null, $timeWindow=null)
{
    if(!isset($timeLastTweet)) return true;
    $diff = time() - strtotime($timeLastTweet);
    switch($timeWindow)
    {
        case '1 minute';                
            $diff <= 60 ? $canTweet = false : $canTweet = true;                
            break;                
        case '1 hour';                
            $diff <= 3600 ? $canTweet = false : $canTweet = true;                
            break;                
        case '1 day':                                
            $diff <= 86400 ? $canTweet = false : $canTweet = true;                
            break;
        default:                
            $canTweet = false;                
            break;                
    } 
    if($canTweet)
    {
        return true;
    }
    else 
    {
        throw new Exception('Please wait until the time window has elapsed.');
    }
}

Sehen wir uns nun einige Kopfzeilenfelder von HTTP-Anfragen an, die WebRequest automatisch für uns zusammenstellt:

Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*

Die POST-Anfrage von WebRequest nimmt an, dass Programmierer Daten in HTML-Form senden wollen. Dennoch möchten wir in diesem Szenario, dass der Server die folgenden Kopfzeilen der HTTP-Anfrage sendet:

Content-Type: application/json
Accept: application/json

Da es kein "Allheilmittel" gibt, müssen wir in unserer Entscheidung konsistent bleiben und gründlich studieren, wie gut WebRequest unsere Anforderungen erfüllt, um die Vor- und Nachteile zu entdecken.

Aus technischer Sicht wäre es besser, echte HTTP-REST-Dialoge einzurichten, doch wir haben gesagt, dass es eine sicherere Lösung ist, HTTP-Dialoge an MetaQuotes zu delegieren, auch wenn WebRequest() ursprünglich für Webseiten vorgesehen zu sein schien, nicht für Webdienste. Aus genau diesem Grund kodieren wir letztendlich die URL des Handelssignals des Clients. Die API erhält in der URL kodierte Signale und konvertiert sie in das stdClass-Format von PHP.

Eine Alternative zur Verwendung der Funktion WebRequest() ist es, eine benutzerdefinierte MQL5-Komponente zu schreiben, die unter Verwendung der Bibliothek wininet.dll auf betriebssystemnaher Ebene arbeitet. Die Beiträge Datenaustausch zwischen Terminals via Internet mit Hilfe von WinInet.dll und Die Verwendung von WinInet in MQL5 Teil 2: POST-Anfragen und -Dateien erklären die Grundlagen dieses Ansatzes. Allerdings haben die Erfahrungen von MQL5-Entwicklern und der MQL5-Community gezeigt, dass diese Lösung nicht so einfach ist, wie sie auf den ersten Blick scheint. Ein Nachteil von ihr ist, dass Aufrufe der WinINet-Funktionen beschädigt werden können, wenn MetaTrader aktualisiert wird.

1.2. Twittern der Handelssignale eines EAs

Wenden wir das oben Erklärte nun an. Ich habe den folgenden Dummy-Roboter erstellt, um das Problem der Kontrolle von Scalping und Denial-of-Service-Angriffen zu illustrieren.

Dummy.mq5:

//+------------------------------------------------------------------+
//|                                                        Dummy.mq5 |
//|                               Copyright © 2014, Jordi Bassagañas |
//+------------------------------------------------------------------+
#property copyright     "Author: laplacianlab, CC Attribution-Noncommercial-No Derivate 3.0"
#property link          "https://www.mql5.com/de/users/laplacianlab"
#property version       "1.00"
#property description   "Dummy REST client (for learning purposes)."
//+------------------------------------------------------------------+
//| Trade class                                                      |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| Declaration of variables                                         |
//+------------------------------------------------------------------+
CPositionInfo PositionInfo;
CTrade trade;
MqlTick tick;
int stopLoss = 20;
int takeProfit = 20;
double size = 0.1;
//+------------------------------------------------------------------+
//| Tweet trading signal                                             |
//+------------------------------------------------------------------+   
void Tweet(string uri, string signal)
  {
   char post[];
   char result[];
   string headers;
   int res;
   StringToCharArray(signal,post);
//--- reset last error
   ResetLastError();
//--- post data to REST API
   res=WebRequest("POST",uri,NULL,NULL,50,post,ArraySize(post),result,headers);
//--- check errors
   if(res==-1)
     {
      //--- error
      Print("Error code =",GetLastError());
     }
   else
     {
      //--- successful
      Print("REST client's POST: ",signal);
      Print("Server response: ",CharArrayToString(result,0,-1));
     }         
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {                
   return(0);  
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+  
void OnDeinit(const int reason)
  {  
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+  
void OnTick()
  {
//--- update tick
   SymbolInfoTick(_Symbol, tick);
//--- calculate Take Profit and Stop Loss levels
   double tp;
   double sl;   
   sl = tick.ask + stopLoss * _Point;
   tp = tick.bid - takeProfit * _Point;
//--- open position
   trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,size,tick.bid,sl,tp);
//--- trade URL-encoded signal "id_ea=1&symbol=AUDUSD&operation=BUY&value=0.9281&";
   string signal = "id_ea=1&symbol=" + _Symbol + "&operation=SELL&value=" + (string)tick.bid + "&";
   Tweet("http://api.laplacianlab.com/signal/add",signal);
}

Der Code könnte nicht einfacher sein. Dieser Expert Advisor platziert nur eine einzelne kurze Position bei jedem Tick. Aus diesem Grund ist es sehr wahrscheinlich, dass der Roboter viele Positionen in kurzer Zeit öffnet, insbesondere, wenn Sie ihn zu einem Zeitpunkt ausführen, zu dem ein hohes Maß an Volatilität herrscht. Doch es gibt keinen Grund zur Sorge. Die Serverseite kontrolliert das Twitter-Intervall, indem sie den Webserver so konfiguriert, dass er DoS-Angriffe verhindert, und indem sie, wie es bereits erklärt wurde, ein bestimmtes Zeitfenster in der PHP-Anwendung definiert.

Da das nun geklärt ist, können Sie die Tweet-Funktion dieses EAs in Ihrem bevorzugten Expert Advisor verwenden.

1.3. Wie sehen Benutzer ihre auf Twitter veröffentlichten Handelssignale?

Im nachfolgenden Beispiel gibt @laplacianlab dem SDSS die Erlaubnis, die Signale des Dummy-EAs aus dem vorherigen Abschnitt zu twittern:

Abbildung 1. @laplacianlab gibt dem SDSS die Erlaubnis, in seinem Namen zu twittern

Abbildung 1. @laplacianlab hat dem SDSS die Erlaubnis gegeben, in seinem Namen zu twittern

Im Übrigen erscheint der Begriff Bollinger Bands in diesem Beispiel, weil dieser Name in der MySQL-Datenbank aus dem ersten Teil dieses Beitrags gespeichert wurde. id_ea=1 wurde mit "Bollinger Bands" assoziiert, doch wir hätten den Namen auch in etwas wie "Dummy" ändern können, damit er zu dieser Erklärung passt. Auf jeden Fall ist dieser Aspekt zweitrangig, ich entschuldige mich allerdings für die kleine Unannehmlichkeit.

Die MySQL-Datenbank sieht letztendlich so aus:

# MySQL database creation...

CREATE DATABASE IF NOT EXISTS laplacianlab_com_sdss;

use laplacianlab_com_sdss;

CREATE TABLE IF NOT EXISTS twitterers (
    id mediumint UNSIGNED NOT NULL AUTO_INCREMENT, 
    twitter_id VARCHAR(255),
    access_token TEXT,
    access_token_secret TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    PRIMARY KEY (id)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS eas (
    id mediumint UNSIGNED NOT NULL AUTO_INCREMENT, 
    name VARCHAR(32),
    description TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),    
    PRIMARY KEY (id)
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS signals (
    id int UNSIGNED NOT NULL AUTO_INCREMENT,
    id_ea mediumint UNSIGNED NOT NULL,
    id_twitterer mediumint UNSIGNED NOT NULL,
    symbol VARCHAR(10) NOT NULL,
    operation VARCHAR(6) NOT NULL,
    value DECIMAL(9,5) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    PRIMARY KEY (id),
    FOREIGN KEY (id_ea) REFERENCES eas(id),
    FOREIGN KEY (id_twitterer) REFERENCES twitterers(id)
) ENGINE=InnoDB;

# Dump some sample data...

# As explained in Part I, there's one single twitterer

INSERT INTO eas(name, description) VALUES
('Bollinger Bands', '<p>Robot based on Bollinger Bands. Works with H4 charts.</p>'),
('Two EMA', '<p>Robot based on the crossing of two MA. Works with H4 charts.</p>');
<

2. Die Serverseite des SDSS

Bevor wir mit der Ausgestaltung der Serverseite unsers Social Decision Support Systems fortfahren, rufen wir uns kurz ins Gedächtnis, dass wir im Moment die folgende Ordnerstruktur haben:

Abbildung 2. Ordnerstruktur der PHP-API auf Basis von Slim

Abbildung 2. Ordnerstruktur der PHP-API auf Basis von Slim

2.1. Code der PHP-API

In Übereinstimmung mit dem bereits Erörterten sollte die Datei index.php nun so aussehen:

<?php
/**
 * Laplacianlab's SDSS - A REST API for tweeting MQL5 trading signals
 *
 * @author      Jordi Bassagañas
 * @copyright   2014 Jordi Bassagañas
 * @link        https://www.mql5.com/de/users/laplacianlab
 */

/* Bootstrap logic */
require_once 'config/config.php';
set_include_path(get_include_path() . PATH_SEPARATOR . APPLICATION_PATH . '/vendor/');
set_include_path(get_include_path() . PATH_SEPARATOR . APPLICATION_PATH . '/model/');
require_once 'slim/slim/Slim/Slim.php';
require_once 'abraham/twitteroauth/twitteroauth/twitteroauth.php';
require_once 'Tweeterer.php';
require_once 'EA.php';
session_start();

/* Init Slim */
use \Slim\Slim;
Slim::registerAutoloader();
$app = new Slim(array('debug' => false));
$app->response->headers->set('Content-Type', 'application/json');

/**
 * Slim's exception handler
 */
$app->error(function(Exception $e) use ($app) {
    echo '{"status": "error", "message": {"text": "' . $e->getMessage() . '"}}';
});

/**
 * REST method.
 * Custom 404 error.
 */
$app->notFound(function () use ($app) {
    echo '{"status": "error 404", "message": {"text": "Not found."}}';
});

/**
 * REST method.
 * Home page.
 */
$app->get('/', function () {
    echo '{"status": "ok", "message": {"text": "Service available, please check API."}}';
});

/**
 * REST method.
 * Adds and tweets a new trading signal.
 */
$app->post('/signal/add', function() {
    $tweeterer = new Tweeterer();
    // This condition is a simple mechanism to prevent hyperactive scalpers
    if ($tweeterer->canTweet($tweeterer->getLastSignal(1)->created_at, '1 minute'))
    {
        $signal = (object)($_POST);
        $signal->id = $tweeterer->addSignal(1, $signal);
        $tokens = $tweeterer->getTokens(1);
        $connection = new TwitterOAuth(
            API_KEY, 
            API_SECRET, 
            $tokens->access_token, 
            $tokens->access_token_secret);
        $connection->host = "https://api.twitter.com/1.1/";
        $ea = new EA();
        $message = "{$ea->get($signal->id_ea)->name} on $signal->symbol. $signal->operation at $signal->value";
        $connection->post('statuses/update', array('status' => $message));           
        echo '{"status": "ok", "message": {"text": "Signal processed."}}';
    }   
});

/**
 * REST implementation with TwitterOAuth.
 * Gives permissions to Laplacianlab's SDSS to tweet on the user's behalf.
 * Please, visit https://github.com/abraham/twitteroauth
 */
$app->get('/tweet-signals', function() use ($app) {   
    if (empty($_SESSION['twitter']['access_token']) || empty($_SESSION['twitter']['access_token_secret']))
    {
        $connection = new TwitterOAuth(API_KEY, API_SECRET);
        $request_token = $connection->getRequestToken(OAUTH_CALLBACK);
        if ($request_token)
        {
            $_SESSION['twitter'] = array(
                'request_token' => $request_token['oauth_token'],
                'request_token_secret' => $request_token['oauth_token_secret']
            );
            switch ($connection->http_code) 
            {
                case 200:
                    $url = $connection->getAuthorizeURL($request_token['oauth_token']);                    
                    // redirect to Twitter
                    $app->redirect($url);
                    break;
                default:
                    throw new Exception('Connection with Twitter failed.');
                break;
            }
        }
        else 
        {
            throw new Exception('Error Receiving Request Token.');
        }
    } 
    else 
    {    
        echo '{"status": "ok", "message": {"text": "Laplacianlab\'s SDSS can '
        . 'now access your Twitter account on your behalf. Please, if you no '
        . 'longer want this, log in your Twitter account and revoke access."}}';
    }    
});

/**
 * REST implementation with TwitterOAuth.
 * This is the OAuth callback of the method above. 
 * Stores the access tokens into the database.
 * Please, visit https://github.com/abraham/twitteroauth
 */
$app->get('/twitter/oauth_callback', function() use ($app) {
    if(isset($_GET['oauth_token']))
    {
        $connection = new TwitterOAuth(
            API_KEY, 
            API_SECRET, 
            $_SESSION['twitter']['request_token'], 
            $_SESSION['twitter']['request_token_secret']);
        $access_token = $connection->getAccessToken($_REQUEST['oauth_verifier']);
        if($access_token)
        {       
            $connection = new TwitterOAuth(
                API_KEY, 
                API_SECRET, 
                $access_token['oauth_token'], 
                $access_token['oauth_token_secret']);
            // Set Twitter API version to 1.1.
            $connection->host = "https://api.twitter.com/1.1/";
            $params = array('include_entities' => 'false');
            $content = $connection->get('account/verify_credentials', $params);    
            if($content && isset($content->screen_name) && isset($content->name))
            {
                $tweeterer = new Tweeterer();                
                $data = (object)array(
                    'twitter_id' => $content->id, 
                    'access_token' => $access_token['oauth_token'],
                    'access_token_secret' => $access_token['oauth_token_secret']);                
                $tweeterer->exists($content->id) 
                        ? $tweeterer->update($data) 
                        : $tweeterer->create($data);
                echo '{"status": "ok", "message": {"text": "Laplacianlab\'s SDSS can '
                . 'now access your Twitter account on your behalf. Please, if you no '
                . 'longer want this, log in your Twitter account and revoke access."}}';        
                session_destroy();
            }
            else
            {
                throw new Exception('Login error.');                
            }
        }
    } 
    else
    {
        throw new Exception('Login error.');
    }
});

/**
 * Run Slim!
 */
$app->run();

2.2. MySQL OOP Wrappers

Jetzt müssen wir die PHP-Klassen Tweeterer.php und EA.php im Modellverzeichnis der Slim-Anwendung erstellen. Bitte beachten Sie, dass wir die MySQL-Tabellen in einfachen objektorientierten Klassen verpacken, anstatt eine tatsächliche Modellschicht zu entwickeln.

model\Tweeterer.php:

<?php
require_once 'DBConnection.php';
/**
 * Tweeterer's simple OOP wrapper
 *
 * @author      Jordi Bassagañas
 * @copyright   2014 Jordi Bassagañas
 * @link        https://www.mql5.com/de/users/laplacianlab
 */
class Tweeterer
{   
    /**
     * @var string MySQL table
     */
    protected $table = 'twitterers';
    /**
     * Gets the user's OAuth tokens
     * @param integer $id
     * @return stdClass OAuth tokens: access_token and access_token_secret
     */
    public function getTokens($id)
    {
        $sql = "SELECT access_token, access_token_secret FROM $this->table WHERE id=$id";
        return DBConnection::getInstance()->query($sql)->fetch_object();        
    }    
    /**
     * Checks if it's been long enough so that the tweeterer can tweet again
     * @param string $timeLastTweet e.g. 2014-07-05 15:26:49
     * @param string $timeWindow A time window, e.g. 1 hour
     * @return boolean
     */
    public function canTweet($timeLastTweet=null, $timeWindow=null)
    {
        if(!isset($timeLastTweet)) return true;
        $diff = time() - strtotime($timeLastTweet);
        switch($timeWindow)
        {
            case '1 minute';                
                $diff <= 60 ? $canTweet = false : $canTweet = true;                
                break;                
            case '1 hour';                
                $diff <= 3600 ? $canTweet = false : $canTweet = true;                
                break;                
            case '1 day':                                
                $diff <= 86400 ? $canTweet = false : $canTweet = true;                
                break;
            default:                
                $canTweet = false;                
                break;                
        } 
        if($canTweet)
        {
            return true;
        }
        else 
        {
            throw new Exception('Please wait until the time window has elapsed.');
        }
    }
    /**
     * Adds a new signal
     * @param type $id_twitterer
     * @param stdClass $data
     * @return integer The new row id
     */
    public function addSignal($id_twitterer, stdClass $data)
    {
        $sql = 'INSERT INTO signals(id_ea, id_twitterer, symbol, operation, value) VALUES (' 
            . $data->id_ea . ","
            . $id_twitterer . ",'"    
            . $data->symbol . "','"
            . $data->operation . "',"
            . $data->value . ')'; 
        DBConnection::getInstance()->query($sql);        
        return DBConnection::getInstance()->getHandler()->insert_id;  
    }
    /**
     * Checks whether the given twitterer exists
     * @param string $id
     * @return boolean
     */
    public function exists($id)
    {
        $sql = "SELECT * FROM $this->table WHERE twitter_id='$id'";        
        $result = DBConnection::getInstance()->query($sql);        
        return (boolean)$result->num_rows;
    }    
    /**
     * Creates a new twitterer
     * @param stdClass $data
     * @return integer The new row id
     */
    public function create(stdClass $data)
    {
        $sql = "INSERT INTO $this->table(twitter_id, access_token, access_token_secret) "
            . "VALUES ('"
            . $data->twitter_id . "','"
            . $data->access_token . "','"
            . $data->access_token_secret . "')";        
        DBConnection::getInstance()->query($sql);
        return DBConnection::getInstance()->getHandler()->insert_id;
    }    
    /**
     * Updates the twitterer's data
     * @param stdClass $data
     * @return Mysqli object
     */
    public function update(stdClass $data)
    {
        $sql = "UPDATE $this->table SET "
            . "access_token = '" . $data->access_token . "', "
            . "access_token_secret = '" . $data->access_token_secret . "' "
            . "WHERE twitter_id ='" . $data->twitter_id . "'";        
        return DBConnection::getInstance()->query($sql);
    }    
    /**
     * Gets the last trading signal sent by the twitterer
     * @param type $id The twitterer id
     * @return mixed The last trading signal
     */
    public function getLastSignal($id)
    {
        $sql = "SELECT * FROM signals WHERE id_twitterer=$id ORDER BY id DESC LIMIT 1";
        $result = DBConnection::getInstance()->query($sql);
        if($result->num_rows == 1)
        {
            return $result->fetch_object();
        }
        else
        {
            $signal = new stdClass;
            $signal->created_at = null;
            return $signal;
        }
    }
}

model\EA.php:

<?php
require_once 'DBConnection.php';
/**
 * EA's simple OOP wrapper
 *
 * @author      Jordi Bassagañas
 * @copyright   2014 Jordi Bassagañas
 * @link        https://www.mql5.com/de/users/laplacianlab
 */
class EA
{   
    /**
     * @var string MySQL table
     */
    protected $table = 'eas';
    /**
     * Gets an EA by id
     * @param integer $id
     * @return stdClass
     */
    public function get($id)
    {
        $sql = "SELECT * FROM $this->table WHERE id=$id";     
        return DBConnection::getInstance()->query($sql)->fetch_object();
    }
}

model\DBConnection.php:

<?php
/**
 * DBConnection class
 * 
 * @author      Jordi Bassagañas
 * @copyright   2014 Jordi Bassagañas
 * @link        https://www.mql5.com/de/users/laplacianlab
 */
class DBConnection 
{ 
    /**
     * @var DBConnection Singleton instance
     */
    private static $instance;
    /**
     * @var mysqli Database handler
     */
    private $mysqli;
    /**
     *  Opens a new connection to the MySQL server
     */
    private function __construct()
    { 
        mysqli_report(MYSQLI_REPORT_STRICT);
        try {
            $this->mysqli = new MySQLI(DB_SERVER, DB_USER, DB_PASSWORD, DB_NAME); 
        } catch (Exception $e) {        
            throw new Exception('Unable to connect to the database, please, try again later.');
        }
    } 
    /**
     * Gets the singleton instance
     * @return type
     */
    public static function getInstance()
    {
        if (!self::$instance instanceof self) self::$instance = new self; 
        return self::$instance;
    } 
    /**
     * Gets the database handler
     * @return mysqli
     */
    public function getHandler()
    { 
        return $this->mysqli; 
    } 
    /**
     * Runs the given query
     * @param string $sql
     * @return mixed
     */
    public function query($sql)
    {
        $result = $this->mysqli->query($sql);
        if ($result === false)
        {       
            throw new Exception('Unable to run query, please, try again later.');
        }
        else
        {
            return $result;
        }
    } 
}

Fazit

Wir haben die Client-Seite des im ersten Teil dieses Beitrags vorgestellten SDSS entwickelt und haben die Gestaltung der Serverseite gemäß dieser Entscheidung abgeschlossen. Wir haben endlich die eingebaute MQL5-Funktion WebRequest() genutzt. In Bezug auf die Vor- und Nachteile dieser Lösung haben wir gesehen, dass die Funktion WebRequest() ursprünglich nicht für die Arbeit mit Webdiensten vorgesehen war, sondern zum Versenden von GET- und POST-Anfragen an Webseiten. Doch gleichzeitig haben wir beschlossen, dieses neue Feature zu nutzen, weil es sicherer ist als die Neuentwicklung einer benutzerdefinierten Komponente.

Es wäre eleganter gewesen, echte REST-Dialoge zwischen dem MQL5-Client und dem PHP-Server einzurichten, doch es war viel einfacher, WebRequest() an unsere Bedürfnisse anzupassen. Somit erhält der Webdienst in der URL kodierte Daten und konvertiert sie in ein für PHP verarbeitbares Format.

Ich arbeite derzeit an diesem System. Zum jetzigen Zeitpunkt kann ich meine persönlichen Handelssignale twittern. Das System ist funktionsfähig und funktioniert für einen einzelnen Benutzer, doch es fehlen noch einige Teile, damit es auch in einer realen Produktivumgebung funktioniert. Zum Beispiel handelt es sich bei Slim um ein datenbankunabhängiges Framework, also sollte man sich um SQL-Einschleusungen kümmern. Auch haben wir nicht geklärt, wie die Kommunikation zwischen dem MetaTrader-5-Terminal und der PHP-Anwendung schützen können. Bitte führen Sie die Anwendung deshalb nicht in einer realen Umgebung aus, wie es in diesem Beitrag vorgestellt wurde.