Разработка социального технологического стартапа, часть II: Программируем клиент MQL5 REST
laplacianlab | 1 сентября, 2014
Введение
В первой части статьи мы представили архитектуру так называемой Social Decision Support System (Социальной системы поддержки принятия решений) или SDSS. На одной стороне в этой системе терминал MetaTrader 5 отсылает на сервер решения советника, принятые автоматически. На другой стороне приложение Твиттер, созданное на Slim PHP фреймворке, получает эти сигналы, хранит их в базе данных MySQL и затем публикует их в аккаунте Твиттера. Главная цель SDSS - это регистрация действий человека по отношению к сигналам робота и принятие соответствующих решений. Это достижимо потому, что сигналы робота могут быть доступны очень широкой аудитории экспертов.
В этой части мы будем разрабатывать клиентскую сторону SDSS на языке MQL5. Мы обсудим возможные альтернативы, а также их достоинства и недостатки. В конце мы соберем вместе все отдельные части и в итоге получим приложение PHP REST API, получающее торговые сигналы эксперта. Для успешного завершения задачи мы должны принять во внимание некоторые аспекты программирования клиентской части.
Вы можете публиковать торговые сигналы 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() { //--- HTTP-переменные REST-клиента 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); //--- сбросим последнюю ошибку ResetLastError(); //--- отправим данные в REST API res=WebRequest("POST",uri,NULL,NULL,50,post,ArraySize(post),result,headers); //--- проверим ошибки if(res==-1) { Print("Код ошибки =",GetLastError()); //--- возможно, URL не добавлен, покажем сообщение, чтобы добавить MessageBox("Добавить адрес '"+uri+"' на вкладке Expert Advisors окна Options","Ошибка",MB_ICONINFORMATION); } else { //--- успешно Print("POST REST-клиента: ",signal); Print("Ответ сервера: ",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": "Пожалуйста, дождитесь истечения временного интервала."}} 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": "Пожалуйста, дождитесь истечения временного интервала."}} 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": "Пожалуйста, дождитесь истечения временного интервала."}} 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": "Пожалуйста, дождитесь истечения временного интервала."}} 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": "Пожалуйста, дождитесь истечения временного интервала."}} 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": "Пожалуйста, дождитесь истечения временного интервала."}} 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": "Пожалуйста, дождитесь истечения временного интервала."}}
Заметьте, что ответ сервера выглядит так:
{"status": "ok", "message": {"text": "Пожалуйста, дождитесь истечения временного интервала."}}
Это срабатывает небольшой механизм защиты, встроенный в API-метод signal/add для того, чтобы защитить SDSS от гиперактивных скальперных роботов:
/** * Метод REST. * Добавляет и публикует в Твиттере новый торговый сигнал. */ $app->post('/signal/add', function() { $tweeterer = new Tweeterer(); // Это условие - простой механизм, защищающий от гиперактивных скальперов 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."}}'; } });
Этот простой механизм срабатывает в веб-приложении, как только веб-сервер подтвердит, что входящий запрос HTTP не вредоносный (например, не DoS-атака).
Веб-сервер может предотвращать такие атаки. Скажем, Apache это делает путем совмещения модулей evasive и security.
Перед вами типичная конфигурация mod_evasive на Apache, где администратор сервера может контролировать, сколько HTTP запросов приложение может принять в секунду, и т.п.
<IfModule mod_evasive20.c> DOSHashTableSize 3097 DOSPageCount 2 DOSSiteCount 50 DOSPageInterval 1 DOSSiteInterval 1 DOSBlockingPeriod 60 DOSEmailNotify someone@somewhere.com </IfModule>
Итак, как уже было сказано, цель метода PHP canTweet - блокировать гиперактивных скальперов, которые не определяются SDDS как HTTP-атаки. Метод canTweet реализован в классе Twetterer и будет обсуждаться ниже:
/** * Проверяет, достаточно ли прошло времени для новой публикации пользователя в Твиттере * @param string $timeLastTweet e.g. 2014-07-05 15:26:49 * @param string $timeWindow Временной интервал, например 1 час * @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('Пожалуйста, дождитесь истечения временного интервала.'); } }
А сейчас давайте посмотрим несколько полей заголовков HTTP-запросов, которые WebRequest автоматически формирует для нас:
Content-Type: application/x-www-form-urlencoded Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
POST-запрос WebRequest предполагает, что программисты хотят отсылать данные в формате HTML, однако в этом случае мы хотим посылать на сервер следующие заголовки HTTP-запроса:
Content-Type: application/json Accept: application/json
Панацеи в этом случае нет. Мы должны быть последовательными в наших решениях и тщательно изучить, насколько WebRequest соответствует нашим требованиям, для того чтобы взвесить все "за" и "против".
С технической точки зрения было бы правильнее установить диалоги HTTP REST, но, как уже было сказано, более безопасным будет использовать решение MetaQuotes, несмотря на то, что WebRequest() изначально предназначался для веб-страниц, а не для веб-сервисов. Именно по этой причине мы в итоге закодируем URL торгового сигнала клиента. API будет получать закодированные в URL сигналы и преобразовывать их в формат PHP stdClass.
Альтернативой функции WebRequest() является написание специализированного компонента MQL5, который работает на уровне, близком к операционной системе, использующей библиотеку wininet.dll. Статьи Использование WinInet.dll для обмена данными между терминалами через Интернет и Использование WinInet в MQL5. Часть 2: POST-запросы и файлы. объясняют, на чем основывается этот подход. Однако опыт сообщества MQL5-разработчиков показывает, что это решение не такое простое, каким кажется на первый взгляд. Там присутствует недостаток, который заключается в том, что вызовы функций WinINet могут сломаться при обновлении MetaTrader.
1.2. Публикация в Твиттере торговых сигналов эксперта
Сейчас мы применим все то, о чем говорили выше. Я создал макет робота, чтобы наглядно продемонстрировать проблему контроля скальпинга и атак типа "отказ в обслуживании" (DoS).
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)." //+------------------------------------------------------------------+ //| Торговый класс | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Объявление переменных | //+------------------------------------------------------------------+ CPositionInfo PositionInfo; CTrade trade; MqlTick tick; int stopLoss = 20; int takeProfit = 20; double size = 0.1; //+------------------------------------------------------------------+ //| Новый торговый сигнал | //+------------------------------------------------------------------+ void Tweet(string uri, string signal) { char post[]; char result[]; string headers; int res; StringToCharArray(signal,post); //--- сбросим последнюю ошибку ResetLastError(); //--- отправим данные в REST API res=WebRequest("POST",uri,NULL,NULL,50,post,ArraySize(post),result,headers); //--- проверим ошибки if(res==-1) { //--- ошибка Print("Код ошибки =",GetLastError()); } else { //--- успешно Print("POST REST-клиента: ",signal); Print("Ответ сервера: ",CharArrayToString(result,0,-1)); } } //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- обновим тики SymbolInfoTick(_Symbol, tick); //--- рассчитаем уровни Take Profit и Stop Loss double tp; double sl; sl = tick.ask + stopLoss * _Point; tp = tick.bid - takeProfit * _Point; //--- откроем позицию trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,size,tick.bid,sl,tp); //--- торговый сигнал с закодированным URL "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); }
Этот код - проще некуда. Эксперт открывает одну короткую позицию на каждом тике. По этой причине, скорее всего, робот откроет много позиций в короткий промежуток времени, особенно, если вы запустите его в момент высокой волатильности. Однако, причин волноваться нет. Сторона сервера контролирует интервалы публикаций как путем конфигурации веб-сервера для защиты от DoS-атак, так и установкой определенного временного интервала в приложении PHP, как уже упоминалось.
Мы разобрались с этой частью, и теперь вы можете добавить вашему любимому эксперту функцию публикации торговых сигналов в Твиттере.
1.3. Как пользователи видят их опубликованные сигналы?
В следующем примере @laplacianlab дает разрешение SDSS публиковать в Твиттере сигналы макета робота, рассмотренного в предыдущем параграфе:
Figure 1. @laplacianlab дал разрешение SDSS публиковать сигналы от его имени
Кстати, линии Боллинджера упоминаются в этом примере потому, что это название мы сохранили в базе данных MySQL в первой части этой статьи. id_ea=1 соответствовало "линиям Боллинджера", но мы должны были бы изменить его на что-то вроде "Dummy" (макет), чтобы оно соответствовало нашему примеру. В любом случае, это имеет второстепенную важность, но я приношу свои извинения за неудобство.
База данных MySQL показана ниже:
# Создание базы данных MySQL... 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; # Выгрузим данные ... # Как уже говорилось в первой части, есть только одно twitter-приложение INSERT INTO eas(name, description) VALUES ('Bollinger Bands', '<p>Робот, основанный на линиях Боллинджера. Работает с графиками H4.</p>'), ('Two EMA', '<p>Робот, основанный на пересечении двух скользящих средних. Работает с графиками H4.</p>');
2. Серверная часть SDSS
Перед тем, как мы приступим к оформлению серверной части нашей Social Decision Support System, давайте вспомним, что у нас в наличии имеется следующая структура каталогов:
Рисунок 2. Структура каталогов PHP API, основанного на Slim
2.1. PHP API код
В соответствии с тем, что было сказано ранее, файл index.php сейчас должен выглядеть так:
/** * Laplacianlab's SDSS - A REST API для публикации торговых сигналов MQL5 в Твиттере * * @author Jordi Bassagañas * @copyright 2014 Jordi Bassagañas * @link https://www.mql5.com/en/users/laplacianlab */ /* Логика самозагрузки */ 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(); /* Инициализация Slim */ use \Slim\Slim; Slim::registerAutoloader(); $app = new Slim(array('debug' => false)); $app->response->headers->set('Content-Type', 'application/json'); /** * Обработчик ошибок Slim */ $app->error(function(Exception $e) use ($app) { echo '{"status": "error", "message": {"text": "' . $e->getMessage() . '"}}'; }); /** * Метод REST. * Ошибка 404. */ $app->notFound(function () use ($app) { echo '{"status": "error 404", "message": {"text": "Not found."}}'; }); /** * Метод REST. * Домашняя страница. */ $app->get('/', function () { echo '{"status": "ok", "message": {"text": "Service available, please check API."}}'; }); /** * Метод REST. * Добавляет и публикует в Твиттере новый торговый сигнал. */ $app->post('/signal/add', function() { $tweeterer = new Tweeterer(); // Это условие - простой механизм, защищающий от гиперактивных скальперов 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 посредством TwitterOAuth. * Дает разрешение Laplacianlab's SDSS публиковать сигналы в Твиттере от имени пользователя. * Пожалуйста, перейдите по ссылке 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('Соединение с Твиттером не установлено.'); break; } } else { throw new Exception('Ошибка получения токена запроса.'); } } else { echo '{"status": "ok", "message": {"text": "Laplacianlab\'s SDSS может ' . 'получить доступ к аккаунту в Твиттере от вашего имени. Если это больше не ' . 'требуется, авторизуйтесь на вашем аккаунте в Твиттере и запретите доступ."}}'; } }); /** * Реализация REST посредством TwitterOAuth. * Это обратный вызов OAuth метода, описанного выше. * Хранит токены доступа в базе данных. * Пожалуйста, перейдите по ссылке 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']); // Установим версию Twitter API на 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 может ' . 'получить доступ к аккаунту в Твиттере от вашего имени. Если это больше не ' . 'требуется, авторизуйтесь на вашем аккаунте в Твиттере и запретите доступ."}}'; session_destroy(); } else { throw new Exception('Ошибка входа.'); } } } else { throw new Exception('Ошибка входа.'); } }); /** * Запустим Slim! */ $app->run();
2.2. MySQL OOP Wrappers
Сейчас мы переходим к созданию PHP классов Tweeterer.php и EA.php в каталоге моделей Slim приложения. Отметим, что вместо того, чтобы разрабатывать прослойку самой модели, мы просто обернем таблицы MySQL в простые объектно-ориентированные классы.
model\Tweeterer.php:
'DBConnection.php'; /** * Простой 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'; /** * Получим токены OAuth пользователя * @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(); } /** * Проверяет, достаточно ли прошло времени для новой публикации пользователя в Твиттере * @param string $timeLastTweet e.g. 2014-07-05 15:26:49 * @param string $timeWindow Временной интервал, например 1 час * @return boolean */ public function canTweet($timeLastTweet=null, $timeWindow=null) { if(!isset($timeLastTweet)) return true; $diff = time() - strtotime($timeLastTweet); switch($timeWindow) { case '1 минута'; $diff <= 60 ? $canTweet = false : $canTweet = true; break; case '1 час'; $diff <= 3600 ? $canTweet = false : $canTweet = true; break; case '1 день': $diff <= 86400 ? $canTweet = false : $canTweet = true; break; default: $canTweet = false; break; } if($canTweet) { return true; } else { throw new Exception('Пожалуйста, дождитесь истечения временного интервала.'); } } /** * Добавим новый сигнал * @param type $id_twitterer * @param stdClass $data * @return integer Идентификатор новой строки */ 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; } /** * Проверяет, существует ли упомянутый пользователь Твиттера * @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; } /** * Создает нового пользователя Твиттера * @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; } /** * Обновляет данные пользователя Твиттера * @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); } /** * Получает последние торговые сигналы, отправленные пользователем Твиттера * @param type $id The twitterer id * @return mixed Последний торговый сигнал */ 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:
'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:
/** * 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; /** * Открывает новое соединение с сервером MySQL */ 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('Отсутствует соединение с базой данных, пожалуйста, повторите попытку.'); } } /** * Gets the singleton instance * @return type */ public static function getInstance() { if (!self::$instance instanceof self) self::$instance = new self; return self::$instance; } /** * Получает обработчик баз данных * @return mysqli */ public function getHandler() { return $this->mysqli; } /** * Выполняет данный запрос * @param string $sql * @return mixed */ public function query($sql) { $result = $this->mysqli->query($sql); if ($result === false) { throw new Exception('Запрос невыполним, пожалуйста, повторите попытку.'); } else { return $result; } } }
Заключение
Мы разработали клиентскую часть SDSS, представленную в первой части статьи, и закончили оформление серверной части. Мы использовали встроенную функцию MQL5 - WebRequest(). Что касается всех "за" и "против" этого решения, то мы увидели, что функция WebRequest() изначально предназначена не для работы с веб-сервисами, а для GET- и POST-запросов веб-страниц. В это же время, мы решили использовать ее, потому, что это безопаснее, чем разрабатывать пользовательский компонент с нуля.
REST диалоги между MQL5-клиентом и PHP-сервером выглядели бы красивее, однако для нашей конкретной задачи было проще использовать WebRequest(). Таким образом, веб-сервис получает данные, закодированные в URL, и преобразовывает их в подходящий для PHP формат.
Сейчас я как раз работаю над этой системой. В настоящий момент я могу публиковать в Твиттере мои торговые сигналы. Система функциональна и прекрасно работает для одного пользователя, но ее еще нельзя использовать для торговли, потому что отсутствуют некоторые существенные части. Например, Slim - это фреймфорк, который не зависит от типа базы данных, поэтому нужно позаботиться о защите от внедрения SQL-кода. К тому же, мы не рассмотрели вопрос обеспечения безопасного взаимодействия между терминалом MetaTrader 5 и приложением PHP. Именно по этой причине представленное приложение в его нынешнем виде не пригодно для работы в реальных условиях.