Разработка социального технологического стартапа, часть II: Программируем клиент MQL5 REST

laplacianlab | 1 сентября, 2014

Введение

В первой части статьи мы представили архитектуру так называемой Social Decision Support System (Социальной системы поддержки принятия решений) или SDSS. На одной стороне в этой системе терминал MetaTrader 5 отсылает на сервер решения советника, принятые автоматически. На другой стороне приложение Твиттер, созданное на Slim PHP фреймворке, получает эти сигналы, хранит их в базе данных MySQL и затем публикует их в аккаунте Твиттера. Главная цель SDSS - это регистрация действий человека по отношению к сигналам робота и принятие соответствующих решений. Это достижимо потому, что сигналы робота могут быть доступны очень широкой аудитории экспертов.

В этой части мы будем разрабатывать клиентскую сторону SDSS на языке MQL5. Мы обсудим возможные альтернативы, а также их достоинства и недостатки. В конце мы соберем вместе все отдельные части и в итоге получим приложение PHP REST API, получающее торговые сигналы эксперта. Для успешного завершения задачи мы должны принять во внимание некоторые аспекты программирования клиентской части.

Вы можете публиковать торговые сигналы 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()
  {
//--- 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 публиковать сигналы от его имени

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. Структура каталогов 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. Именно по этой причине представленное приложение в его нынешнем виде не пригодно для работы в реальных условиях.