Скачать MetaTrader 5

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

1 сентября 2014, 10:28
Jordi Bassaganas
0
1 591

Введение

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

Перевод с английского произведен MetaQuotes Software Corp.
Оригинальная статья: https://www.mql5.com/en/articles/1044

Прикрепленные файлы |
database.txt (1.32 KB)
dummy_ontimer.mq5 (1.36 KB)
Dummy.mq5 (3.27 KB)
index.txt (6.08 KB)
Tweeterer.txt (4.59 KB)
EA.txt (0.58 KB)
DBConnection.txt (1.54 KB)
Как мы развивали сервис торговых сигналов MetaTrader и социальный трейдинг в целом Как мы развивали сервис торговых сигналов MetaTrader и социальный трейдинг в целом

Мы активно совершенствуем сервис Сигналы, последовательно избавляемся от прежних недоработок и вносим изменения в существующие механизмы. MetaTrader Signals двухлетней давности и MetaTrader Signals на текущий момент - это словно два различных сервиса.

Разработка социального технологического стартапа, часть I: Публикуем сигналы MetaTrader 5 в Твиттере Разработка социального технологического стартапа, часть I: Публикуем сигналы MetaTrader 5 в Твиттере

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

Регрессионный анализ влияния макроэкономических данных на изменение цен валют Регрессионный анализ влияния макроэкономических данных на изменение цен валют

В статье рассмотрен вопрос применения множественного регрессионного анализа к данным макроэкономической статистики и анализ влияния этих данных на изменение курса валют на примере пары EURUSD. Применение такого анализа позволяет автоматизировать проведение фундаментального анализа, который становится доступным практически каждому, даже начинающим трейдерам.

Рецепты MQL5 - обработка события TradeTransaction Рецепты MQL5 - обработка события TradeTransaction

В статье описываются возможности языка MQL5 с точки зрения событийно-ориентированного программирования. Преимущество данного подхода состоит в том, что программа может получать информацию о поэтапном выполнении торговой операции. Приводится пример того, как с помощью обработчика события TradeTransaction можно получать и обрабатывать информацию о совершаемых торговых действиях. Думаю, что такой подход можно смело применять для целей копирования торговых сделок с терминала на терминал.