在本文的 之前部分, 我们提出了一种所谓的社交决策支持系统的体系结构。一方面,该系统包括一个 MetaTrader 5 客户端,用来发送 EA 的自动决策至服务器端。在通信的另一端,有一个建立在瘦 PHP 框架的 Twitter(推特)应用程序,用于接收交易信号,并将它们存储到一个 MySQL 数据库,最后发布至有关人士。该 SDSS 的主要目的是记录可由人工执行的自动交易信号,并由人工做出相应的决策。这是可能的,因为自动交易信号可以通过这种方式展示给大量的专业观众。
在此第二部分我们打算采用 MQL5 编程语言开发 SDSS 的客户端。我们也会讨论一些替代方法,辨别它们的优点以及缺点。最后, 我们将把拼图的所有碎片集合到一起, 并完成塑造 PHP REST API 用于接收来自 EA 的交易信号。要做到这一点,我们必须研究某些涉及客户端编程的方面。
现在您可以将您的 MQL5 交易信号发布了!
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 权限来发布他的代表
顺便说一句,布林带的名字出现在这个例子中,因为这是在本文的第一部分我们存储在 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. 基于瘦 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 应用程序之间的通信安全,所以请不要在实际环境中运行此应用程序,因为它只是作为本文中的介绍。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程