构建新兴的社交技术, 第二部分: 编制 MQL5 的 REST 客户端

laplacianlab | 11 九月, 2014

介绍

在本文的 之前部分, 我们提出了一种所谓的社交决策支持系统的体系结构。一方面,该系统包括一个 MetaTrader 5 客户端,用来发送 EA 的自动决策至服务器端。在通信的另一端,有一个建立在瘦 PHP 框架的 Twitter(推特)应用程序,用于接收交易信号,并将它们存储到一个 MySQL 数据库,最后发布至有关人士。该 SDSS 的主要目的是记录可由人工执行的自动交易信号,并由人工做出相应的决策。这是可能的,因为自动交易信号可以通过这种方式展示给大量的专业观众。

在此第二部分我们打算采用 MQL5 编程语言开发 SDSS 的客户端。我们也会讨论一些替代方法,辨别它们的优点以及缺点。最后, 我们将把拼图的所有碎片集合到一起, 并完成塑造 PHP REST API 用于接收来自 EA 的交易信号。要做到这一点,我们必须研究某些涉及客户端编程的方面。

现在您可以将您的 MQL5 交易信号发布了!

现在您可以将您的 MQL5 交易信号发布了!


1. 该 SDSS 的客户端

1.1. 在 OnTimer 事件中发布交易信号

我已经研究过如何显示来自 OnTimer 事件发送的交易信号的简单问题。在看过这个简单例子如何工作之后, 很容易推断其核心行为。

dummy_ontimer.mq5:

#property copyright     "Author: laplacianlab, CC Attribution-Noncommercial-No Derivate 3.0"
#property link          "https://www.mql5.com/en/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));
     }         
  }

如您所见, 这个客户端应用的中心部分是 MQL5 的新函数 WebRequest

编制一个自定义 MQL5 控件来处理 HTTP 通信是一个替代方案, 不过将这个任务委派给 MetaQuotes 的 新语言特性 将会更安全。

上述 MQL5 程序的输出如下:

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."}}

请注意, 服务器的响应消息:

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

这是因为在 API 方法中实现了一个小型安全机制,用以防止高频剥头皮自动交易访问 SDSS :

/**
 * 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."}}';
    }   
});

在 web 应用之中,以上简单机制会在 web 服务器检查入访的 HTTP 请求无恶意之后调用 (例如,入访信号非是拒绝服务攻击)。

Web 服务器负责防止此类攻击。例如, Apache 可以通过回避和安全模块组合防止它们。

一个典型的 Apache 模块 mod_evasive 配置,服务器管理员可以控制应用每秒钟接收 HTTP 请求的次数,等等。

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

所以,如我们所说,该 PHP canTweet 方法的目的是阻止高频剥头皮,即把它看作对于 SDSS 的 HTTP 攻击。该 canTweet 方法在 Twetterer 类中实现 (将会稍后讨论):

/**
 * 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.');
    }
}

现在让我们来看看 WebRequest 自动为我们构建的 HTTP 请求的头部字段:

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

WebRequest 的 POST 方法假设程序员打算发送 HTML 表单数据,但在这种情况下,我们希望向服务器发送如下 HTTP 请求头部字段:

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

没有什么灵丹妙药,我们必须考虑我们的决定,深入学习如何令 WebRequest 适应我们的需求,并发现利弊。

从技术角度来看,建立真正的 HTTP REST 会话更正确,但如我们所说,更安全的方案是通过 MetaQuotes 来委派 HTTP 会话,尽管 WebRequest() 的本意看起来像是为了访问网页,而非 web 服务。正是出于这个原因,我们最终将客户的交易信号进行 URL 编码。该 API 将接收以 URL 编码的信号,并将它们转换至 PHP 的 stdClass 格式。

一种替代 WebRequest() 函数的方案是使用 wininet.dll 库,编写工作于接近操作系统级别的自定义 MQL5 控件。文章 Using WinInet.dll for Data Exchange between Terminals via the Internet(使用 WinInet.dll 通过因特网在终端间进行数据交换)Using WinInet in MQL5(在 MQL5 中使用 WinInet)。第二部分: POST 请求和文件 解释了这种方法的基本原理。然而,MQL5 开发者和 MQL5 社区的经验表明,这种解决方案并不像第一眼看上去那么简单。它表现出的缺点就是调用 WinINet 函数也许会由于 MetaTrader 的升级而遭到破坏。

1.2. 发布一款 EA 的交易信号

现在让我们来推断我们最近的解释。我已经创建了如下虚拟自动交易,以便说明有关控制剥头皮和拒绝服务攻击问题。

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/en/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);
}

以上代码不能再简化了。这个 EA 在每次即时报价时放置单一的空头仓位。出于这种原因, 这个自动交易十分类似在短间隔时间内放置许多仓位, 尤其是在频繁振荡中您运行了它一段时间之后。对此无需担心。服务器端可以用两种方式控制发布间隔,一种是通过 web 服务器配置来防止 DoS 攻击, 还可以在 PHP 应用中定义确定的时间窗口, 如前解释的那样。

一切理清之后,您现在可以把发布功能添加进您喜爱的 EA 当中。

1.3. 如何让用户看到他们的交易信号?

在以下例子中, @laplacianlab 给与 SDSS 权限来发布之前章节贴出的 dummy EA 的信号:

图例 1. @laplacianlab  给与 SDSS 权限来发布他的代表

图例 1. @laplacianlab 已经给与 SDSS 权限来发布他的代表

顺便说一句,布林带的名字出现在这个例子中,因为这是在本文的第一部分我们存储在 MySQL 数据库中的数据之一。id_ea=1 被关联到 "布林带", 但是我们将它修改为 "Dummy" 以便与解释搭配。在任何情况下,这是一个次要方面,但是很抱歉这有一点不便。

该 MySQL 数据库最终如下:

# 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. 该 SDSS 的服务器端

在继续塑造我们的社交决策支持系统服务器端之前,让我们简要地记得,此时我们有以下目录结构:

图例 2. 基于瘦 PHP API 的目录结构

图例 2. 基于瘦 PHP API 的目录结构

2.1. PHP API 代码

根据我们的解释,该 index.php 文件将看起来像这样:

<?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/en/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 面向对象编程封装器

我们现在必须在瘦应用的模块目录中创建 PHP 类 Tweeterer.php 和 EA.php。请注意,我们只是在一个简单的面向对象的类中封装一个 MySQL 数据表,而不必开发一个实际的模块层。

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/en/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/en/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/en/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;
        }
    } 
}

结论

我们已经开发出了本文第一部分介绍的 SDSS 客户端,以及根据决定塑造完成了服务器端。我们最终使用了新的 MQL5 的本地化函数 WebRequest()。有关规范方案的利弊, 我们已经看到 WebRequest() 原本不是作为 web 服务使用的, 只是构建访问网页的 GET 和 POST 请求。不过,与此同时,我们决定使用这项新功能,因为它比从头开发一个自定义控件更安全。

原本更优雅的做法是在 MQL5 客户端和 PHP 服务器之间建立真实的 REST 会话,但 WebRequest() 更适宜我们的具体需求。所以, 该 web 服务接收 URL-编码数据, 并将之转换到 PHP 可管理的格式。

我当下正为此系统工作。现在,我可以发布自己的个人交易信号了。这些功能, 可以为单用户工作, 但缺少了一些能在真实生产环境中完成复杂工作的部分。例如,瘦框架与数据库无关,所以你应该关心 SQL 注入。我们也没有解释如何保证 MetaTrader 5 终端和 PHP 应用程序之间的通信安全,所以请不要在实际环境中运行此应用程序,因为它只是作为本文中的介绍。