
MQL5-RPC - Удаленный вызов процедур из MQL5: доступ к Web-сервисам и анализ данных Automated Trading Championship 2011
Введение
В этой статье будет рассмотрена структура взаимодействия MQL5-RPC, разработанная мной в последние несколько недель. Мы разберем основы технологии XML-PRC, ее реализацию на MQL5 и два примера ее практического использования. Первый пример представляет собой использование удаленного вызова процедур web-сервиса внешнего сайта, второй пример - клиентская часть нашего собственного XML-RPC сервера, который будет использован для обработки данных сайта Automated Trading Championship 2011, их анализа и предоставления клиентским приложениям. Если вас интересует вопрос программной реализации и анализа различных статистических характеристик участников ATC 2011, эта статья для вас.
Основы XML-RPC
Начнем с основ технологии XML-RPC (XML Remote Procedure Call). XML-RPC представляет собой протокол на базе XML, используемый для кодирования и декодирования параметров, передаваемых для вызова внешних процедур. В качестве транспортного механизма обмена данными используется протокол HTTP. Под внешней процедурой/методом я подразумеваю другую компьютерную программу или web-сервис, предоставляющий возможность вызова удаленных процедур.
Процедура/метод, доступный для удаленного вызова, может быть вызван при помощи программы, написанной на любом языке с любой машины, подключенной к сети, и поддерживающей стек протокола XML-RPC и имеющей доступ к серверу. Это также означает, что XML-RPC может быть использован на той же машине для вызова метода из программ, написанных на другом языке программирования. Этот вопрос будет рассматриваться во второй части статьи.
Модель данных XML-RPC
В спецификации XML-RPC используются 6 основных типов данных: int, double, boolean, string, datetime, base64 и два составных типа данных: array и struct. Массив (array) может содержать элементы любых основных, а структура представляет собой пары имя-значение (name-value) в виде ассоциативных массивов или свойств объекта.
Базовые типы данных в XML-RPC | ||
---|---|---|
Тип | Значение | Пример |
int or i4 | 32-bit integers between - 2,147,483,648 and 2,147,483,647. | <int>11<int> <i4>12345<i4> |
double | 64-bit floating-point numbers | <double>30.02354</double> <double>-1.53525</double> |
Boolean | true (1) or false (0) | <boolean>1</boolean> <boolean>0</boolean> |
string | ASCII text, many implementations support Unicode | <string>Hello</string> <string>MQL5</string> |
dateTime.iso8601 | Dates in ISO8601 format: CCYYMMDDTHH:MM:SS | <dateTime.iso8601> 20111125T02:20:04 </dateTime.iso8601> <dateTime.iso8601> 20101104T17:27:30 </dateTime.iso8601> |
base64 | Binary information encoded as defined in RFC 2045 | <base64> TDVsbG8sIFdvdwxkIE== </base64> |
Таблица 1. Основные типы данных XML-RPC
Массив может содержать любые базовые типы данных, причем элементы не обязательно должны быть одного типа. Элемент массива должен быть вложен в элемент value. Он содержит один элемент данных и один или более элементов-значений элемента данных. В примере, приведенном ниже, приведен массив, содержащий 4 значения типа integer.
<value> <array> <data> <value><int>111</int></value> <value><int>222</int></value> <value><int>-3456</int></value> <value><int>666</int></value> </data> </array> </value>
Второй пример иллюстрирует массив, содержащий 5 значений типа string.
<value> <array> <data> <value><string>MQL5</string></value> <value><string>is </string></value> <value><string>a</string></value> <value><string>great</string></value> <value><string>language.</string></value> </data> </array> </value>
Подобным образом можно построить и другие массивы XML-RPC.
Структуры имеют элемент структур внутри значений, а разделы членов содержатся внутри элемента структуры. Каждый элемент содержит имя и хранимое значение. Поэтому при помощи структур можно легко передавать значения ассоциативного массива или членов объекта.
Например:
<value> <struct> <member> <name>AccountHolder</name> <value><string>John Doe</string></value> </member> <member> <name>Age</name> <value><int>77</int></value> </member> <member> <name>Equity</name> <value><double>1000000.0</double></value> </member> </struct> </value>
Ознакомившись с моделью данных XML-RPC, пойдем дальше и рассмотрим механизм запроса и получения структур. Это послужит основой реализации на MQL5 клиента XML-RPC.
Формат структур запроса XML-RPC
Запрос XML-RPC состоит из заголовка сообщения (header) и тела самого сообщения (message payload). В заголовке сообщения указывается методе протокола HTTP (POST), относительный путь сервиса XML-RPC, версия протокола HTTP, наименование user-agent, IP адрес хоста, тип содержимого (text/xml) и длина содержимого в байтах.
POST /xmlrpc HTTP 1.1 User-Agent: mql5-rpc/1.0 Host: 10.12.10.10 Content-Type: text/xml Content-Length: 188
Тело сообщения запроса XML-RPC представляет собой XML-документ. Корневой элемент дерева XML должен иметь имя methodCall. Он содержит один исполняемый элемент methodName, который, в свою очередь, может содержать один элемент params (или не содержать совсем).
Элемент params содержит одно или более значений, массив или элементы структуры. Все значения кодируются в соответствии с типом данных (табл. 1).
В данном примере представлен запрос на исполнение метода multiply, в качестве параметров которого передается пара значений типа double.
<?xml version="1.0"?> <methodCall> <methodName>multiply</methodName> <params> <param> <value><double>8654.41</double></value> </param> <param> <value><double>7234.00</double></value> </param> </params> </methodCall>
Заголовок и тело сообщения передаются серверу, принимающему данные по протоколу HTTP. Если сервер доступен, он проверяет имя метода и список параметров и выполняет указанный метод. После завершения он подготавливает структуру ответа XML-RPC, которая может быть прочитана клиентом.
Формат структур ответа XML-RPC
Как и запрос XML-RPC, ответ XML-RPC содержит заголовок (header) и тело сообщения (message payload). Заголовок представляет собой текст, а тело сообщения - XML-документ. Если запрос был корректным, в первой строке заголовка указывается версия протокола и информация о том, что сервер был найден (код 200). Заголовок также обязательно должен содержать информацию о типе содержимого - Content-Type: text/xml и длину сообщения Content-Length (в байтах).
HTTP/1.1 200 OK Date: Tue, 08 Nov 2011 23:00:01 GMT Server: Unix Connection: close Content-Type: text/xml Content-Length: 124
Тело сообщения ответа также является XML-документом.
Корневой элемент дерева XML должен называться methodResponse. Он содержит один элемент params в случае успеха или элемент fault в случае ошибки. Элемент params содержит ровно один элемент param. Элемент param содержит ровно один элемент value.
Пример успешного ответа представлен ниже:
<?xml version="1.0"?> <methodResponse> <params> <param> <value><double>62606001.94</double></value> </param> </params> </methodResponse>
Ответное сообщение об ошибки составляется в случае, если в процессе обработки запроса XML-RPC произошла ошибка.
Элемент fault, как и элемент params, имеет только одно выходное значение:
<?xml version="1.0"?> <methodResponse> <fault> <value><string>No such method!</string></value> </fault> </methodResponse>
Поскольку в XML-RPC спецификация кодов ошибок отсутствует, сообщения об ошибках зависят от конкретной реализации.
Взаимодействие MQL5-RPC
Прочитав статьи Алексея Сергеева "Использование WinInet.dll для обмена данными между терминалами через Интернет" и "Использование WinInet в MQL5. Часть 2: POST-запросы и файлы", я понял, что смогу реализовать клиента XML-RPC для MetaTrader 5. После ознакомления со спецификациями, я с нуля написал собственного клиента. В текущей версии вся спецификация не поддерживается (в ближайшем будущем будет добавлена поддержка base64), но уже сейчас в MetaTrader 5 вы можете использовать большую часть функционала XML-RPC.
Модель данных MQL5-RPC
Для меня самым трудным этапом в реализации оказалась разработка корректной модели данных для MQL5. Было решено, что она должна быть простой настолько, насколько это возможно, поэтому я написал несколько классов, содержащих функционал модели данных. Первым решением было сделать данные запроса в виде одного указателя на класс CObject. Этот указатель хранит массив указателей на массивы классов, порожденных от класса CObject.
Для хранения массивов CObject существуют классы CArrayInt, CArrayDouble, CArrayString, поэтому для полноты на их базе были реализованы CArrayBool, CArrayDatetime и CArrayMqlRates для работы с массивом структур. Отсутствует только тип base64, его поддержка планируется в ближайшем будущем. Если массив содержит лишь один элемент, в XML он представляется в виде элемента с одним значением.
Я написал пример, показывающий как добавлять различные массивы в массив объектов CObject* и выводить полностью содержимое массива массивов различных типов. Он приведен ниже.
//+------------------------------------------------------------------+ //| ArrayObjTest.mq5 | //| Copyright 2011, Investeo.pl | //| http://Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http://Investeo.pl" #property version "1.00" //--- #include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayInt.mqh> #include <Arrays\ArrayDouble.mqh> #include <Arrays\ArrayString.mqh> #include <Arrays\ArrayBool.mqh> #include <Arrays\ArrayMqlRates.mqh> #include <Arrays\ArrayDatetime.mqh> //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- CArrayObj* params = new CArrayObj; CArrayInt* arrInt = new CArrayInt; arrInt.Add(1001); arrInt.Add(1002); arrInt.Add(1003); arrInt.Add(1004); CArrayDouble* arrDouble = new CArrayDouble; arrDouble.Add(1001.0); arrDouble.Add(1002.0); arrDouble.Add(1003.0); arrDouble.Add(1004.0); CArrayString* arrString = new CArrayString; arrString.Add("s1001.0"); arrString.Add("s1002.0"); arrString.Add("s1003.0"); arrString.Add("s1004.0"); CArrayDatetime* arrDatetime = new CArrayDatetime; arrDatetime.Add(TimeCurrent()); arrDatetime.Add(TimeTradeServer()+3600); arrDatetime.Add(TimeCurrent()+3600*24); arrDatetime.Add(TimeTradeServer()+3600*24*7); CArrayBool* arrBool = new CArrayBool; arrBool.Add(false); arrBool.Add(true); arrBool.Add(true); arrBool.Add(false); CArrayMqlRates* arrRates = new CArrayMqlRates; MqlRates rates[]; ArraySetAsSeries(rates,true); int copied=CopyRates(Symbol(),0,0,4,rates); arrRates.Add(rates[0]); arrRates.Add(rates[1]); arrRates.Add(rates[2]); arrRates.Add(rates[3]); params.Add(arrInt); params.Add(arrDouble); params.Add(arrString); params.Add(arrDatetime); params.Add(arrBool); params.Add(arrRates); Print("params содержит " + IntegerToString(params.Total()) + " массивов."); for (int p=0; p<params.Total(); p++) { int type = params.At(p).Type(); switch (type) { case TYPE_INT: { CArrayInt *arr = params.At(p); for (int i=0; i<arr.Total(); i++) PrintFormat("%d %d %d", p, i, arr.At(i)); break; } case TYPE_DOUBLE: { CArrayDouble *arr = params.At(p); for (int i=0; i<arr.Total(); i++) PrintFormat("%d %d %f", p, i, arr.At(i)); break; } case TYPE_STRING: { CArrayString *arr = params.At(p); for (int i=0; i<arr.Total(); i++) PrintFormat("%d %d %s", p, i, arr.At(i)); break; } case TYPE_BOOL: { CArrayBool *arr = params.At(p); for (int i=0; i<arr.Total(); i++) if (arr.At(i) == true) PrintFormat("%d %d true", p, i); else PrintFormat("%d %d false", p, i); break; } case TYPE_DATETIME: { CArrayDatetime *arr = params.At(p); for (int i=0; i<arr.Total(); i++) PrintFormat("%d %d %s", p, i, TimeToString(arr.At(i), TIME_DATE|TIME_MINUTES)); break; } case TYPE_MQLRATES: { // CArrayMqlRates *arr = params.At(p); for (int i=0; i<arr.Total(); i++) PrintFormat("%d %d %f %f %f %f", p, i, arr.At(i).open, arr.At(i).high, arr.At(i).low, arr.At(i).close); break; } }; }; delete params; } //+------------------------------------------------------------------+
Все должно быть понятно - есть 6 массивов: массив чисел типа integer, массив чисел типа double, массив строк типа string, массив типа datetime, массив значений типа boolean и массив структур типа MqlRates.
ArrayObjTest (EURUSD,H1) 23:01:54 params содержит 6 массивов. ArrayObjTest (EURUSD,H1) 23:01:54 0 0 1001 ArrayObjTest (EURUSD,H1) 23:01:54 0 1 1002 ArrayObjTest (EURUSD,H1) 23:01:54 0 2 1003 ArrayObjTest (EURUSD,H1) 23:01:54 0 3 1004 ArrayObjTest (EURUSD,H1) 23:01:54 1 0 1001.000000 ArrayObjTest (EURUSD,H1) 23:01:54 1 1 1002.000000 ArrayObjTest (EURUSD,H1) 23:01:54 1 2 1003.000000 ArrayObjTest (EURUSD,H1) 23:01:54 1 3 1004.000000 ArrayObjTest (EURUSD,H1) 23:01:54 2 0 s1001.0 ArrayObjTest (EURUSD,H1) 23:01:54 2 1 s1002.0 ArrayObjTest (EURUSD,H1) 23:01:54 2 2 s1003.0 ArrayObjTest (EURUSD,H1) 23:01:54 2 3 s1004.0 ArrayObjTest (EURUSD,H1) 23:01:54 3 0 2011.11.11 23:00 ArrayObjTest (EURUSD,H1) 23:01:54 3 1 2011.11.12 00:01 ArrayObjTest (EURUSD,H1) 23:01:54 3 2 2011.11.12 23:00 ArrayObjTest (EURUSD,H1) 23:01:54 3 3 2011.11.18 23:01 ArrayObjTest (EURUSD,H1) 23:01:54 4 0 false ArrayObjTest (EURUSD,H1) 23:01:54 4 1 true ArrayObjTest (EURUSD,H1) 23:01:54 4 2 true ArrayObjTest (EURUSD,H1) 23:01:54 4 3 false ArrayObjTest (EURUSD,H1) 23:01:54 5 0 1.374980 1.374980 1.374730 1.374730 ArrayObjTest (EURUSD,H1) 23:01:54 5 1 1.375350 1.375580 1.373710 1.375030 ArrayObjTest (EURUSD,H1) 23:01:54 5 2 1.374680 1.375380 1.373660 1.375370 ArrayObjTest (EURUSD,H1) 23:01:54 5 3 1.375270 1.377530 1.374360 1.374690
Возможно, вам интересно то, как я реализовал массивы других типов данных. В случае с CArrayBool и CArrayDatetime я сделал их по аналогии с CArrayInt, но для CArrayMqlRates это реализовано немного иначе, поскольку структура должна передаваться по ссылке, а заданного типа TYPE_MQLRATES не было.
Ниже приведен фрагмент кода класса CArrayMqlRates. Другие классы можно найти в приложении к статье.
//+------------------------------------------------------------------+ //| ArrayMqlRates.mqh | //| Copyright 2011, Investeo.pl | //| http://Investeo.pl | //| Revision 2011.03.03 | //+------------------------------------------------------------------+ #include "Array.mqh" //+------------------------------------------------------------------+ //| Класс CArrayMqlRates. | //| Предназначение: Класс динамического массива структур | //| типа of MqlRates. | //| Наследник класса CArray. | //+------------------------------------------------------------------+ #define TYPE_MQLRATES 7654 class CArrayMqlRates : public CArray { protected: MqlRates m_data[]; // массив данных public: CArrayMqlRates(); ~CArrayMqlRates(); //--- метод идентификации virtual int Type() const { return(TYPE_MQLRATES); } //--- методы работы с файлами virtual bool Save(int file_handle); virtual bool Load(int file_handle); //--- методы управления динамической памятью bool Reserve(int size); bool Resize(int size); bool Shutdown(); //--- методы заполнения массивов bool Add(MqlRates& element); bool AddArray(const MqlRates &src[]); bool AddArray(const CArrayMqlRates *src); bool Insert(MqlRates& element,int pos); bool InsertArray(const MqlRates &src[],int pos); bool InsertArray(const CArrayMqlRates *src,int pos); bool AssignArray(const MqlRates &src[]); bool AssignArray(const CArrayMqlRates *src); //--- методы доступа к элементам массива MqlRates At(int index) const; //--- методы модификации bool Update(int index,MqlRates& element); bool Shift(int index,int shift); //--- методы удаления bool Delete(int index); bool DeleteRange(int from,int to); protected: int MemMove(int dest,int src,int count); }; //+------------------------------------------------------------------+ //| Конструктор класса CArrayMqlRates. | //+------------------------------------------------------------------+ void CArrayMqlRates::CArrayMqlRates() { //--- initialize protected data m_data_max=ArraySize(m_data); } //+------------------------------------------------------------------+ //| Деструктор класса CArrayMqlRates. | //+------------------------------------------------------------------+ void CArrayMqlRates::~CArrayMqlRates() { if(m_data_max!=0) Shutdown(); } ... //+------------------------------------------------------------------+ //| Метод добавления элемента в конец массива | //| INPUT: element - добавляемый элемент. | //| OUTPUT: true в случае удачи, иначе false | //+------------------------------------------------------------------+ bool CArrayMqlRates::Add(MqlRates& element) { //--- проверка/резервирование элементов массива if(!Reserve(1)) return(false); //--- добавление m_data[m_data_total++]=element; m_sort_mode=-1; //--- return(true); } //+------------------------------------------------------------------+ //| Добавляет элементы в конец массива из другого массива | //| INPUT: src - массив элементов для добавления. | //| OUTPUT: true в случае удачи, иначе false | //+------------------------------------------------------------------+ bool CArrayMqlRates::AddArray(const MqlRates &src[]) { int num=ArraySize(src); //--- проверка/резервирование элементов массива if(!Reserve(num)) return(false); //--- добавляем элементы for(int i=0;i<num;i++) m_data[m_data_total++]=src[i]; m_sort_mode=-1; //--- return(true); } ...
Перед отсылкой запроса все данные MQL5 должны быть преобразованы в XML-значения, поэтому я написал CXMLRPCEncoder, класс-помощник, который кодирует данные в виде XML-строки.
class CXMLRPCEncoder { public: CXMLRPCEncoder(){}; string header(string path,int contentLength); string fromInt(int param); string fromDouble(double param); string fromBool(bool param); string fromString(string param); string fromDateTime(datetime param); string fromMqlRates(MqlRates ¶m); };
Приведем ниже реализацию трех методов. Все они принимают один параметр (типа bool, string, datetime) и возвращают XML-строку для протокола XML-RPC с правильным типом данных.
//+------------------------------------------------------------------+ //| fromBool | //+------------------------------------------------------------------+ string CXMLRPCEncoder::fromBool(bool param) { CString s_bool; s_bool.Clear(); s_bool.Append(VALUE_B); s_bool.Append(BOOL_B); if(param==true) s_bool.Append("1"); else s_bool.Append("0"); s_bool.Append(BOOL_E); s_bool.Append(VALUE_E); return s_bool.Str(); } //+------------------------------------------------------------------+ //| fromString | //+------------------------------------------------------------------+ string CXMLRPCEncoder::fromString(string param) { CString s_string; s_string.Clear(); s_string.Append(VALUE_B); s_string.Append(STRING_B); s_string.Append(param); s_string.Append(STRING_E); s_string.Append(VALUE_E); return s_string.Str(); } //+------------------------------------------------------------------+ //| fromDateTime | //+------------------------------------------------------------------+ string CXMLRPCEncoder::fromDateTime(datetime param) { CString s_datetime; s_datetime.Clear(); s_datetime.Append(VALUE_B); s_datetime.Append(DATETIME_B); CString s_iso8601; s_iso8601.Assign(TimeToString(param, TIME_DATE|TIME_MINUTES)); s_iso8601.Replace(" ", "T"); s_iso8601.Remove(":"); s_iso8601.Remove("."); s_datetime.Append(s_iso8601.Str()); s_datetime.Append(DATETIME_E); s_datetime.Append(VALUE_E); return s_datetime.Str(); }
Возможно, вы заметили, что есть некоторые константы с суффиксом "_B", означающие начало тега и константы с суффиксом "_E", означающие конец тега.
Для хранения наименований XML-тегов я решил использовать .mqh-файл, поскольку это делает реализацию гораздо более прозрачной.
//+------------------------------------------------------------------+ //| xmlrpctags.mqh | //| Copyright 2011, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http://www.investeo.pl" #define HEADER_1a "POST" #define HEADER_1b "HTTP/1.1" #define HEADER_2 "User-Agent: MQL5RPC/1.1" #define HEADER_3 "Host: host.com" #define HEADER_4 "Content-Type: text/xml" #define HEADER_5 "Content-Length: " #define HEADER_6 "<?xml version='1.0'?>" #define METHOD_B "<methodCall>" #define METHOD_E "</methodCall>" #define METHOD_NAME_B "<methodName>" #define METHOD_NAME_E "</methodName>" #define RESPONSE_B "<methodResponse>" #define RESPONSE_E "</methodResponse>" #define PARAMS_B "<params>" #define PARAMS_E "</params>" #define PARAM_B "<param>" #define PARAM_E "</param>" #define VALUE_B "<value>" #define VALUE_E "</value>" #define INT_B "<int>" #define INT_E "</int>" #define I4_B "<i4>" #define I4_E "</i4>" #define BOOL_B "<boolean>" #define BOOL_E "</boolean>" #define DOUBLE_B "<double>" #define DOUBLE_E "</double>" #define STRING_B "<string>" #define STRING_E "</string>" #define DATETIME_B "<dateTime.iso8601>" #define DATETIME_E "</dateTime.iso8601>" #define BASE64_B "<base64>" #define BASE64_E "</base64>" #define ARRAY_B "<array>" #define ARRAY_E "</array>" #define DATA_B "<data>" #define DATA_E "</data>" #define STRUCT_B "<struct>" #define STRUCT_E "</struct>" #define MEMBER_B "<member>" #define MEMBER_E "</member>" #define NAME_B "<name>" #define NAME_E "</name>" //+------------------------------------------------------------------+
Задав модель данных MQL5-RPC, теперь мы можем приступить к построению полного запроса XML-RPC.
Запрос MQL5-RPC
Как уже упоминалось ранее, запрос XML-RPC состоит из заголовка запроса (request header) и тела сообщения (XML payload). Я написал класс CXMLRPCQuery, который автоматически строит объект запроса (строку типа CString) из массивов данных MQL5. Класс использует CXMLRPCEncoder для построения XML и добавляет имя метода внутри тега "methodName".
class CXMLRPCQuery { private: CString s_query; void addValueElement(bool start,bool array); public: CXMLRPCQuery() {}; CXMLRPCQuery(string method="",CArrayObj *param_array=NULL); string toString(); };
Конструктор класса имеет два параметра: имя метода и указатель на массив объектов CArrayObj, содержащий параметры вызываемого метода. Все параметры преобразуются в XML способом, описанном в предыдущем разделе, затем добавляется заголовок. Полный текст XML-запроса может быть получен при помощи метода toString().
CXMLRPCQuery::CXMLRPCQuery(string method="",CArrayObj *param_array=NULL) { //--- составляет один XMLRPC-запрос this.s_query.Clear(); CXMLRPCEncoder encoder; this.s_query.Append(HEADER_6); this.s_query.Append(METHOD_B); this.s_query.Append(METHOD_NAME_B); this.s_query.Append(method); this.s_query.Append(METHOD_NAME_E); this.s_query.Append(PARAMS_B); for(int i=0; i<param_array.Total(); i++) { int j=0; this.s_query.Append(PARAM_B); int type=param_array.At(i).Type(); int elements=0; switch(type) { case TYPE_INT: { CArrayInt *arr=param_array.At(i); elements=arr.Total(); if(elements==1) addValueElement(true,false); else addValueElement(true,true); for(j=0; j<elements; j++) this.s_query.Append(encoder.fromInt(arr.At(j))); break; } case TYPE_DOUBLE: { CArrayDouble *arr=param_array.At(i); elements=arr.Total(); if(elements==1) addValueElement(true,false); else addValueElement(true,true); for(j=0; j<elements; j++) this.s_query.Append(encoder.fromDouble(arr.At(j))); break; } case TYPE_STRING: { CArrayString *arr=param_array.At(i); elements=arr.Total(); if(elements==1) addValueElement(true,false); else addValueElement(true,true); for(j=0; j<elements; j++) this.s_query.Append(encoder.fromString(arr.At(j))); break; } case TYPE_BOOL: { CArrayBool *arr=param_array.At(i); elements=arr.Total(); if(elements==1) addValueElement(true,false); else addValueElement(true,true); for(j=0; j<elements; j++) this.s_query.Append(encoder.fromBool(arr.At(j))); break; } case TYPE_DATETIME: { CArrayDatetime *arr=param_array.At(i); elements=arr.Total(); if(elements==1) addValueElement(true,false); else addValueElement(true,true); for(j=0; j<elements; j++) this.s_query.Append(encoder.fromDateTime(arr.At(j))); break; } case TYPE_MQLRATES: { CArrayMqlRates *arr=param_array.At(i); elements=arr.Total(); if(elements==1) addValueElement(true,false); else addValueElement(true,true); for(j=0; j<elements; j++) { MqlRates tmp=arr.At(j); this.s_query.Append(encoder.fromMqlRates(tmp)); } break; } }; if(elements==1) addValueElement(false,false); else addValueElement(false,true); this.s_query.Append(PARAM_E); } this.s_query.Append(PARAMS_E); this.s_query.Append(METHOD_E); }
Рассмотрим тестовый пример запроса. Он не будет самым простым, поскольку теперь я хочу показать, что также осуществлять вызов сложных методов.
Входные параметры: массив значений типа double, массив значений типа int, массив типа string, массив значений типа bool, одно значение типа datetime и массив структур типа MqlRates.
//+------------------------------------------------------------------+ //| MQL5-RPC_query_test.mq5 | //| Copyright 2011, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" #include <MQL5-RPC.mqh> #include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayInt.mqh> #include <Arrays\ArrayDouble.mqh> #include <Arrays\ArrayString.mqh> #include <Arrays\ArrayBool.mqh> #include <Arrays\ArrayDatetime.mqh> #include <Arrays\ArrayMqlRates.mqh> //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- тестовый запрос CArrayObj* params = new CArrayObj; CArrayDouble* param1 = new CArrayDouble; CArrayInt* param2 = new CArrayInt; CArrayString* param3 = new CArrayString; CArrayBool* param4 = new CArrayBool; CArrayDatetime* param5 = new CArrayDatetime; CArrayMqlRates* param6 = new CArrayMqlRates; for (int i=0; i<4; i++) param1.Add(20000.0 + i*100.0); params.Add(param1); for (int i=0; i<4; i++) param2.Add(i); params.Add(param2); param3.Add("first_string"); param3.Add("second_string"); param3.Add("third_string"); params.Add(param3); param4.Add(false); param4.Add(true); param4.Add(false); params.Add(param4); param5.Add(TimeCurrent()); params.Add(param5); const int nRates = 3; MqlRates rates[3]; ArraySetAsSeries(rates,true); int copied=CopyRates(Symbol(),0,0,nRates,rates); if (copied==nRates) { param6.AddArray(rates); params.Add(param6); } CXMLRPCQuery query("sampleMethodname", params); Print(query.toString()); delete params; } //+------------------------------------------------------------------+
Ниже приведен полученный текст запроса. Обычно он представляет собой одну строку, но для лучшего понимания и распознавания разделов XML-RPC и соответствующих значений, здесь он представлен в другом виде.
<?xml version="1.0" ?> <methodCall> <methodName>sampleMethodname</methodName> <params> <param> <value> <array> <data> <value> <double>20000.0000000000000000</double> </value> <value> <double>20100.0000000000000000</double> </value> <value> <double>20200.0000000000000000</double> </value> <value> <double>20300.0000000000000000</double> </value> </data> </array> </value> </param> <param> <value> <array> <data> <value> <int>0</int> </value> <value> <int>1</int> </value> <value> <int>2</int> </value> <value> <int>3</int> </value> </data> </array> </value> </param> <param> <value> <array> <data> <value> <string>first_string</string> </value> <value> <string>second_string</string> </value> <value> <string>third_string</string> </value> </data> </array> </value> </param> <param> <value> <array> <data> <value> <boolean>0</boolean> </value> <value> <boolean>1</boolean> </value> <value> <boolean>0</boolean> </value> </data> </array> </value> </param> <param> <value> <dateTime.iso8601>20111111T2042</dateTime.iso8601> </value> </param> <param> <value> <array> <data> <value> <struct> <member> <name>open</name> <value> <double>1.02902000</double> </value> </member> <member> <name>high</name> <value> <double>1.03032000</double> </value> </member> <member> <name>low</name> <value> <double>1.02842000</double> </value> </member> <member> <name>close</name> <value> <double>1.02867000</double> </value> </member> <member> <name>time</name> <value> <dateTime.iso8601> <value> <dateTime.iso8601>20111111T1800</dateTime.iso8601> </value> </dateTime.iso8601> </value> </member> <member> <name>tick_volume</name> <value> <double>4154.00000000</double> </value> </member> <member> <name>real_volume</name> <value> <double>0.00000000</double> </value> </member> <member> <name>spread</name> <value> <double>30.00000000</double> </value> </member> </struct> </value> <value> <struct> <member> <name>open</name> <value> <double>1.02865000</double> </value> </member> <member> <name>high</name> <value> <double>1.02936000</double> </value> </member> <member> <name>low</name> <value> <double>1.02719000</double> </value> </member> <member> <name>close</name> <value> <double>1.02755000</double> </value> </member> <member> <name>time</name> <value> <dateTime.iso8601> <value> <dateTime.iso8601>20111111T1900</dateTime.iso8601> </value> </dateTime.iso8601> </value> </member> <member> <name>tick_volume</name> <value> <double>3415.00000000</double> </value> </member> <member> <name>real_volume</name> <value> <double>0.00000000</double> </value> </member> <member> <name>spread</name> <value> <double>30.00000000</double> </value> </member> </struct> </value> <value> <struct> <member> <name>open</name> <value> <double>1.02760000</double> </value> </member> <member> <name>high</name> <value> <double>1.02901000</double> </value> </member> <member> <name>low</name> <value> <double>1.02756000</double> </value> </member> <member> <name>close</name> <value> <double>1.02861000</double> </value> </member> <member> <name>time</name> <value> <dateTime.iso8601> <value> <dateTime.iso8601>20111111T2000</dateTime.iso8601> </value> </dateTime.iso8601> </value> </member> <member> <name>tick_volume</name> <value> <double>1845.00000000</double> </value> </member> <member> <name>real_volume</name> <value> <double>0.00000000</double> </value> </member> <member> <name>spread</name> <value> <double>30.00000000</double> </value> </member> </struct> </value> </data> </array> </value> </param> </params> </methodCall>
Это XML-дерево сложнее, протокол достаточно гибкий для вызова более сложных методов.
Ответ MQL5-RPC
Как и запрос, ответ XML-RPC также состоит из заголовка и тела сообщения, но порядок обработки обратный - XML-ответ должен быть преобразован в данные MQL5. Чтобы справиться с этой задачей, был разработан класс CXMLRPCResult.
После вызова желаемого метода результат может быть получен в виде строки, передаваемой конструктору класса или автоматически запрашиваемой из класса CXMLServerProxy. В случаях, когда результат содержит структуры, которые заранее неизвестны, метод parseXMLResponseRAW() анализирует все теги <value> и возвращает указатель на массив CArrayObj, содержащий массив всех найденных элементов.
class CXMLRPCResult { private: CArrayObj *m_resultsArr; CString m_cstrResponse; CArrayString m_params; bool isValidXMLResponse(); bool parseXMLValuesToMQLArray(CArrayString *subArr,CString &val); bool parseXMLValuesToMQLArray(CArrayDouble *subArr,CString &val); bool parseXMLValuesToMQLArray(CArrayInt *subArr,CString &val); bool parseXMLValuesToMQLArray(CArrayBool *subArr,CString &val); bool parseXMLValuesToMQLArray(CArrayDatetime *subArr,CString &val); bool parseXMLValuesToMQLArray(CArrayMqlRates *subArr,CString &val); bool parseXMLResponse(); public: CXMLRPCResult() {}; ~CXMLRPCResult(); CXMLRPCResult(string resultXml); CArrayObj *getResults(); bool parseXMLResponseRAW(); string toString(); };
Конструктор класса сканирует каждый заголовок XML-ответа и вызывает private-метод parseXMLValuesToMQLArray(), делающий за кулисами тяжелую работу по конвертации содержимого XML в MQL5-данные. Он распознает, является ли элемент param массивом (или единственным элементом) и заполняет соответствующие массивы, добавляя их в результирующий массив-контейнер типа CArrayObj.
bool CXMLRPCResult::parseXMLResponse() { CArrayObj *results=new CArrayObj; m_params.Clear(); //--- находим параметры и размещаем их в массиве m_params int tagStartIdx= 0; int tagStopIdx = 0; while((tagStartIdx!=-1) && (tagStopIdx!=-1)) { tagStartIdx= m_cstrResponse.Find(tagStartIdx,PARAM_B); tagStopIdx = m_cstrResponse.Find(tagStopIdx,PARAM_E); if((tagStartIdx!=-1) && (tagStopIdx!=-1)) { m_params.Add(m_cstrResponse.Mid(tagStartIdx+StringLen(PARAM_B),tagStopIdx-tagStartIdx-StringLen(PARAM_B))); tagStartIdx++; tagStopIdx++; }; }; for(int i=0; i<m_params.Total(); i++) { CString val; val.Assign(m_params.At(i)); //--- обработка тега value val.Assign(val.Mid(StringLen(VALUE_B),val.Len()-StringLen(VALUE_B)-StringLen(VALUE_E))); //--- проверка первого тега и его обработка string param_type=val.Mid(0,val.Find(0,">")+1); if(param_type==INT_B || param_type==I4_B) { val.Assign(m_params.At(i)); CArrayInt *subArr=new CArrayInt; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(param_type==BOOL_B) { val.Assign(m_params.At(i)); CArrayBool *subArr=new CArrayBool; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(param_type==DOUBLE_B) { val.Assign(m_params.At(i)); CArrayDouble *subArr=new CArrayDouble; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(param_type==STRING_B) { val.Assign(m_params.At(i)); CArrayString *subArr=new CArrayString; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(param_type==DATETIME_B) { val.Assign(m_params.At(i)); CArrayDatetime *subArr=new CArrayDatetime; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(param_type==ARRAY_B) { val.Assign(val.Mid(StringLen(ARRAY_B)+StringLen(DATA_B),val.Len()-StringLen(ARRAY_B)-StringLen(DATA_E))); //--- поиск первого типа и определение массива string array_type=val.Mid(StringLen(VALUE_B),val.Find(StringLen(VALUE_B)+1,">")-StringLen(VALUE_B)+1); if(array_type==INT_B || array_type==I4_B) { CArrayInt *subArr=new CArrayInt; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(array_type==BOOL_B) { CArrayBool *subArr=new CArrayBool; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(array_type==DOUBLE_B) { CArrayDouble *subArr=new CArrayDouble; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(array_type==STRING_B) { CArrayString *subArr=new CArrayString; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } else if(array_type==DATETIME_B) { CArrayDatetime *subArr=new CArrayDatetime; bool isValid=parseXMLValuesToMQLArray(subArr,val); if(isValid==true) results.Add(subArr); } } }; m_resultsArr=results; return true; }
Конвертация производится внутри перегруженных методов parseXMLValuesToMQLArray. Строковые значения выбираются из XML и преобразуются в переменные базовых типов MQL5.
Ниже приведена реализация трех методов, использующихся для конвертации.
//+------------------------------------------------------------------+ //| parseXMLValuesToMQLArray | //+------------------------------------------------------------------+ bool CXMLRPCResult::parseXMLValuesToMQLArray(CArrayBool *subArr,CString &val) { //--- обрабатывает XML-значения и размещает их в массиве int tagStartIdx=0; int tagStopIdx=0; while((tagStartIdx!=-1) && (tagStopIdx!=-1)) { tagStartIdx= val.Find(tagStartIdx,VALUE_B); tagStopIdx = val.Find(tagStopIdx,VALUE_E); if((tagStartIdx!=-1) && (tagStopIdx!=-1)) { CString e; e.Assign(val.Mid(tagStartIdx+StringLen(VALUE_B)+StringLen(BOOL_B), tagStopIdx-tagStartIdx-StringLen(VALUE_B)-StringLen(BOOL_B)-StringLen(BOOL_E))); if(e.Str()=="0") subArr.Add(false); if(e.Str()=="1") subArr.Add(true); tagStartIdx++; tagStopIdx++; }; } if(subArr.Total()<1) return false; return true; } //+------------------------------------------------------------------+ //| parseXMLValuesToMQLArray | //+------------------------------------------------------------------+ bool CXMLRPCResult::parseXMLValuesToMQLArray(CArrayInt *subArr,CString &val) { //--- обрабатывает XML-значения и размещает их в массиве int tagStartIdx=0; int tagStopIdx=0; while((tagStartIdx!=-1) && (tagStopIdx!=-1)) { tagStartIdx= val.Find(tagStartIdx,VALUE_B); tagStopIdx = val.Find(tagStopIdx,VALUE_E); if((tagStartIdx!=-1) && (tagStopIdx!=-1)) { CString e; e.Assign(val.Mid(tagStartIdx+StringLen(VALUE_B)+StringLen(INT_B), tagStopIdx-tagStartIdx-StringLen(VALUE_B)-StringLen(INT_B)-StringLen(INT_E))); subArr.Add((int)StringToInteger(e.Str())); tagStartIdx++; tagStopIdx++; }; } if(subArr.Total()<1) return false; return true; } //+------------------------------------------------------------------+ //| parseXMLValuesToMQLArray | //+------------------------------------------------------------------+ bool CXMLRPCResult::parseXMLValuesToMQLArray(CArrayDatetime *subArr,CString &val) { //--- обрабатывает XML-значения и размещает их в массиве int tagStartIdx=0; int tagStopIdx=0; while((tagStartIdx!=-1) && (tagStopIdx!=-1)) { tagStartIdx= val.Find(tagStartIdx,VALUE_B); tagStopIdx = val.Find(tagStopIdx,VALUE_E); if((tagStartIdx!=-1) && (tagStopIdx!=-1)) { CString e; e.Assign(val.Mid(tagStartIdx+StringLen(VALUE_B)+StringLen(DATETIME_B), tagStopIdx-tagStartIdx-StringLen(VALUE_B)-StringLen(DATETIME_B)-StringLen(DATETIME_E))); e.Replace("T"," "); e.Insert(4,"."); e.Insert(7,"."); subArr.Add(StringToTime(e.Str())); tagStartIdx++; tagStopIdx++; }; } if(subArr.Total()<1) return false; return true; }
Как можно видеть, строковые значения выделяются из тегов XML-RPC при помощи методов Assign(), Mid() и Find() класса CString и затем преобразуются в типы MQL5 при помощи функций StringToTime(), StringToInteger(), StringToDouble() или специально разработанных методов как в случае с переменными типа bool.
После того как парсинг данных завершен, все доступные данные могут быть выведены при помощи метода toString(). Для разделения значений я использовал запятые.
//+------------------------------------------------------------------+ //| toString | //| Возвращает строковое представление массивов | //+------------------------------------------------------------------+ string CXMLRPCResult::toString(void) { //--- CString r; for(int i=0; i<m_resultsArr.Total(); i++) { int rtype=m_resultsArr.At(i).Type(); switch(rtype) { case(TYPE_STRING) : { CArrayString *subArr=m_resultsArr.At(i); for(int j=0; j<subArr.Total(); j++) { r.Append(subArr.At(j)+":"); } break; }; case(TYPE_DOUBLE) : { CArrayDouble *subArr=m_resultsArr.At(i); for(int j=0; j<subArr.Total(); j++) { r.Append(DoubleToString(NormalizeDouble(subArr.At(j),8))+":"); } break; }; case(TYPE_INT) : { CArrayInt *subArr=m_resultsArr.At(i); for(int j=0; j<subArr.Total(); j++) { r.Append(IntegerToString(subArr.At(j))+":"); } break; }; case(TYPE_BOOL) : { CArrayBool *subArr=m_resultsArr.At(i); for(int j=0; j<subArr.Total(); j++) { if(subArr.At(j)==false) r.Append("false:"); else r.Append("true:"); } break; }; case(TYPE_DATETIME) : { CArrayDatetime *subArr=m_resultsArr.At(i); for(int j=0; j<subArr.Total(); j++) { r.Append(TimeToString(subArr.At(j),TIME_DATE|TIME_MINUTES|TIME_SECONDS)+" : "); } break; }; }; } return r.Str(); }
Для того чтобы в явном виде показать преобразование параметров XML-RPC в данные MQL5, приведу тестовый пример.
//+------------------------------------------------------------------+ //| MQL5-RPC_result_test.mq5 | //| Copyright 2011, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" //--- #include <MQL5-RPC.mqh> #include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayInt.mqh> #include <Arrays\ArrayDouble.mqh> #include <Arrays\ArrayString.mqh> #include <Arrays\ArrayBool.mqh> #include <Arrays\ArrayDatetime.mqh> #include <Arrays\ArrayMqlRates.mqh> //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- string sampleXMLResult = "<?xml version='1.0'?>" "<methodResponse>" + "<params>" + "<param>" + "<value><array><data>" + "<value><string>Xupypr</string></value>" + "<value><string>anuta</string></value>" + "<value><string>plencing</string></value>" + "<value><string>aharata</string></value>" + "<value><string>beast</string></value>" + "<value><string>west100</string></value>" + "<value><string>ias</string></value>" + "<value><string>Tim</string></value>" + "<value><string>gery18</string></value>" + "<value><string>ronnielee</string></value>" + "<value><string>investeo</string></value>" + "<value><string>droslan</string></value>" + "<value><string>Better</string></value>" + "</data></array></value>" + "</param>" + "<param>" + "<value><array><data>" + "<value><double>1.222</double></value>" + "<value><double>0.456</double></value>" + "<value><double>1000000000.10101</double></value>" + "</data></array></value>" + "</param>" + "<param>" + "<value><array><data>" + "<value><boolean>1</boolean></value>" + "<value><boolean>0</boolean></value>" + "<value><boolean>1</boolean></value>" + "</data></array></value>" + "</param>" + "<param>" + "<value><array><data>" + "<value><int>-1</int></value>" + "<value><int>0</int></value>" + "<value><int>1</int></value>" + "</data></array></value>" + "</param>" + "<param>" + "<value><array><data>" + "<value><dateTime.iso8601>20021125T02:20:04</dateTime.iso8601></value>" + "<value><dateTime.iso8601>20111115T00:00:00</dateTime.iso8601></value>" + "<value><dateTime.iso8601>20121221T00:00:00</dateTime.iso8601></value>" + "</data></array></value>" + "</param>" + "<param><value><string>Single string value</string></value></param>" + "<param><value><dateTime.iso8601>20111115T00:00:00</dateTime.iso8601></value></param>" + "<param><value><int>-111</int></value></param>" + "<param><value><boolean>1</boolean></value></param>" + "</params>" + "</methodResponse>"; CXMLRPCResult* testResult = new CXMLRPCResult(sampleXMLResult); Print(testResult.toString()); delete testResult; } //+------------------------------------------------------------------+
Результат:
MQL5-RPC__result_test (EURUSD,H1) 23:16:57 Xupypr:anuta:plencing:aharata:beast:west100:ias:Tim:gery18:ronnielee:investeo: droslan:Better:1.22200000:0.45600000:1000000000.10099995:true:false:true:-1:0: 1:2002.11.25 02:20:04 : 2011.11.15 00:00:00 : 2012.12.21 00:00:00 : Single string value:2011.11.15 00:00:00 :-111:true:
Здесь показан массив массивов с различными типами MQL5, преобразованный из XML. Как видите, данные типов string, double, boolean, integer и datetime могут быть доступны из одного указателя на объект типа CArrayObj.
Прокси-класс для MQL5-RPC
CXMLRPCServerProxy является основным классом взаимодействия по протоколу HTTP. Для реализации функционала HTTP я использовал статью "Использование WinInet в MQL5. Часть 2: POST-запросы и файлы", добавив заголовок в соответствии со спецификацией XML-RPC.
CXMLRPCServerProxy::CXMLRPCServerProxy(string s_proxy,int timeout=0) { CString proxy; proxy.Assign(s_proxy); //--- поиск пути запроса int sIdx = proxy.Find(0,"/"); if (sIdx == -1) m_query_path = "/"; else { m_query_path = proxy.Mid(sIdx, StringLen(s_proxy) - sIdx) + "/"; s_proxy = proxy.Mid(0, sIdx); }; //--- поиск порта запроса. значение по умолчанию 80 int query_port = 80; int pIdx = proxy.Find(0,":"); if (pIdx != -1) { query_port = (int)StringToInteger(proxy.Mid(pIdx+1, sIdx-pIdx)); s_proxy = proxy.Mid(0, pIdx); }; //Print(query_port); //Print(proxy.Mid(pIdx+1, sIdx-pIdx)); if(InternetAttemptConnect(0)!=0) { this.m_connectionStatus="InternetAttemptConnect failed."; this.m_session=-1; this.m_isConnected=false; return; } string agent = "Mozilla"; string empty = ""; this.m_session=InternetOpenW(agent,OPEN_TYPE_PRECONFIG,empty,empty,0); if(this.m_session<=0) { this.m_connectionStatus="InternetOpenW failed."; this.m_session=-2; this.m_isConnected=true; return; } this.m_connection=InternetConnectW(this.m_session,s_proxy,query_port,empty,empty,SERVICE_HTTP,0,0); if(this.m_connection<=0) { this.m_connectionStatus="InternetConnectW failed."; return; } this.m_connectionStatus="Connected."; }
Объект класса CXMLRPCQuery должен быть передан методу execute() класса CXMLRPCServerProxy для запуска вызова XML-RPC.
Этот метод возвращает указатель на объект класса CXMLRPCResult, который далее используется в скрипте, осуществляющем вызов XML-RPC.
CXMLRPCResult *CXMLRPCServerProxy::execute(CXMLRPCQuery &query) { //--- подготовка дескриптора запроса string empty_string = ""; string query_string = query.toString(); string query_method = HEADER_1a; string http_version = HEADER_1b; uchar data[]; StringToCharArray(query.toString(),data); int ivar=0; int hRequest=HttpOpenRequestW(this.m_connection,query_method,m_query_path,http_version, empty_string,0,FLAG_KEEP_CONNECTION|FLAG_RELOAD|FLAG_PRAGMA_NOCACHE,0); if(hRequest<=0) { Print("-Err OpenRequest"); InternetCloseHandle(this.m_connection); return(new CXMLRPCResult); } //-- отсылка запроса CXMLRPCEncoder encoder; string header=encoder.header(m_query_path,ArraySize(data)); int aH=HttpAddRequestHeadersW(hRequest,header,StringLen(header),HTTP_ADDREQ_FLAG_ADD|HTTP_ADDREQ_FLAG_REPLACE); bool hSend=HttpSendRequestW(hRequest,empty_string,0,data,ArraySize(data)-1); if(hSend!=true) { int err=0; err=GetLastError(); Print("-Err SendRequest= ",err); } string res; ReadPage(hRequest,res,false); CString out; out.Assign(res); out.Remove("\n"); //--- закрываем все хендлы InternetCloseHandle(hRequest); InternetCloseHandle(hSend); CXMLRPCResult* result = new CXMLRPCResult(out.Str()); return result; }
Пример 1 - Доступ к Web-сервису
Первым рабочим примером MQL5-RPC будет вызов внешнего web-сервиса. Пример, который я нашел, использует текущий курс обмена для конвертации заданного количества одной валюты в другую. С полной спецификацией параметров метода можно ознакомиться по ссылке. Web-сервис использует метод foxrate.currencyConvert, который принимает 3 параметра: два строковых и один типа float.
- Исходная валюта/From currency (например: USD) = string
- Валюта конвертации/To currency (например: GBP) = string
- Количество/Amount to convert (например: 100.0) = float
Реализация занимает всего несколько строк кода.
//+------------------------------------------------------------------+ //| MQL5-RPC_ex1_ExternalWebService.mq5 | //| Copyright 2011, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" #include <MQL5-RPC.mqh> #include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayDouble.mqh> #include <Arrays\ArrayString.mqh> //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- //--- тест web-сервиса CArrayObj* params = new CArrayObj; CArrayString* from = new CArrayString; CArrayString* to = new CArrayString; CArrayDouble* amount = new CArrayDouble; from.Add("GBP"); to.Add("USD"); amount.Add(10000.0); params.Add(from); params.Add(to); params.Add(amount); CXMLRPCQuery query("foxrate.currencyConvert", params); Print(query.toString()); CXMLRPCServerProxy s("foxrate.org/rpc"); CXMLRPCResult* result; result = s.execute(query); result.parseXMLResponseRAW(); Print(result.toString()); delete params; delete result; } //+------------------------------------------------------------------+
Поскольку метод возвращает сложную структуру, результат парсируется при помощи метода parseXMLResponseRAW().
В этом примере запрос XML-RPC выглядит следующим образом:
<?xml version="1.0" ?> <methodCall> <methodName>foxrate.currencyConvert</methodName> <params> <param> <value> <string>GBP</string> </value> </param> <param> <value> <string>USD</string> </value> </param> <param> <value> <double>10000.00000000</double> </value> </param> </params> </methodCall>
Ответ возвращается в виде структуры, обернутой в XML.
<?xml version="1.0" ?> <methodResponse> <params> <param> <value> <struct> <member> <name>flerror</name> <value> <int>0</int> </value> </member> <member> <name>amount</name> <value> <double>15773</double> </value> </member> <member> <name>message</name> <value> <string>cached</string> </value> </member> </struct> </value> </param> </params> </methodResponse>
Результат toString() показывает, что ответ состоит из 3 значений: 0-нет ошибки, второе значение - количество базовой валюты, необходимой для конвертации и третий параметр - время последнего обменного курса (здесь "cached")
0:15773.00000000:cached:
Давайте продолжим и рассмотрим более интересные приложения.
Пример 2 - Анализ ATC 2011 на базе XML-RPC
Представьте, что вы захотели получать статистику Automated Trading Championship 2011 и использовать эту информацию в терминале MetaTrader 5. Если вы хотите знать, как этого реализовать, читайте дальше.
Поскольку MetaQuotes Software Corp. разрабатывает сервис "Сигналы", который позволит подписываться на торговые сигналы конкретных советников, я думаю, комбинация этого сервиса с системой анализа будет очень мощным решением для детального анализа и откроет новые возможности использования ATC. Фактически, при помощи данного метода вы сможете захватывать данные с нескольких различных источников и на их базе сделать сложный советник.
Сервер системы анализа данных ATC 2011 на базе XML-RPC
Первым шагом к созданию сервера анализа является подготовка выходных данных. Скажем, мы хотим получать данные всех участников, сумма средств на счете (Equity) которых больше некоторого порогового значения. Кроме того, нас интересуют их текущие позиции и статистика по объемам их позиций и валютным парам.
Для получения и обработки (парсинга) данных я использовал язык Python и библиотеку BeautifulSoup. Если ранее вы не использовали Python, я настоятельно рекомендую вам зайти на http:/Python.org и прочитать о возможностях этого языка, вы не пожалеете.
Если же вы хотите быстро получить систему анализа данных Чемпионата, скачайте Python 2.7.1 installer и setup_tools и установите их. После этого запустите консоль Windows, зайдите в каталог C:\Python27\Scripts (или тот, куда вы установили Python).
Затем для установки пакета BeautifulSoup запустите команду "easy_install BeautifulSoup":
C:\Python27\Scripts>easy_install BeautifulSoup Searching for BeautifulSoup Reading http://pypi.python.org/simple/BeautifulSoup/ Reading http://www.crummy.com/software/BeautifulSoup/ Reading http://www.crummy.com/software/BeautifulSoup/download/ Best match: BeautifulSoup 3.2.0 Downloading http://www.crummy.com/software/BeautifulSoup/download/3.x/BeautifulS oup-3.2.0.tar.gz Processing BeautifulSoup-3.2.0.tar.gz Running BeautifulSoup-3.2.0\setup.py -q bdist_egg --dist-dir c:\users\przemek\ap pdata\local\temp\easy_install-zc1s2v\BeautifulSoup-3.2.0\egg-dist-tmp-hnpwoo zip_safe flag not set; analyzing archive contents... C:\Python27\lib\site-packages\setuptools\command\bdist_egg.py:422: UnicodeWarnin g: Unicode equal comparison failed to convert both arguments to Unicode - interp reting them as being unequal symbols = dict.fromkeys(iter_symbols(code)) Adding beautifulsoup 3.2.0 to easy-install.pth file Installed c:\python27\lib\site-packages\beautifulsoup-3.2.0-py2.7.egg Processing dependencies for BeautifulSoup Finished processing dependencies for BeautifulSoup C:\Python27\Scripts>
Далее нужно запустить консоль Python и выполнить команду 'import BeautifulSoup':
Python 2.7.1 (r271:86832, Nov 27 2010, 18:30:46) [MSC v.1500 32 bit (Intel)] on win32 Type "copyright", "credits" or "license()" for more information. >>> import BeautifulSoup >>>
После этого запустите прилагаемую к статье систему анализа при помощи команды 'python ContestantParser.py'.
Программа проанализирует содержимое web-сайта по некоторым ключевым словам и выведет результат в консольном окне Windows.
import re import urllib from BeautifulSoup import BeautifulSoup class ContestantParser(object): URL_PREFIX = 'https://championship.mql5.com/2011/en/users/' def __init__(self, name): print "User:", name url = ContestantParser.URL_PREFIX + name feed = urllib.urlopen(url) s = BeautifulSoup(feed.read()) account = s.findAll('td', attrs={'class' : 'alignRight'}) self.balance = float(account[0].contents[0].replace(' ', '')) self.equity = float(account[2].contents[0].replace(' ', '')) terminals = s.findAll('table', attrs={'class': 'terminal'}) terminal = terminals[0] trs = terminal.findAll('tr') pairs = terminal.findAll('span', attrs={'class':'stateText'}) self.stats = {} for i in range(len(pairs)-1): if len(pairs[i].string)> 6: break self.stats[str(pairs[i].string)] = str(trs[i+1].contents[7].string) def __str__(self): for k in self.stats.keys(): print k, self.stats[k] return "Bal " + str(self.balance) + " Equ " + str(self.equity) def position(self, pair): if pair in self.stats.keys(): return self.stats[pair] else: return "none" class ContestantsFinder(object): URL_PREFIX = "https://championship.mql5.com/2011/en/users/index/page" URL_SUFFIX = "?orderby=equity&dir=down" def __init__(self, min_equity): self.min_equity = min_equity self.user_list = [] self.__find() def __find(self): isLastPage = False pageCnt = 1 while isLastPage == False: url = ContestantsFinder.URL_PREFIX + str(pageCnt) + \ ContestantsFinder.URL_SUFFIX feed = urllib.urlopen(url) s = BeautifulSoup(feed.read()) urows = s.findAll('tr', attrs={'class' : re.compile('row.*')}) for row in urows: user_name = row.contents[5].a.string equity = float(row.contents[19].string.replace(' ','')) if equity <= self.min_equity: isLastPage = True break self.user_list.append(str(user_name)) print user_name, equity pageCnt += 1 def list(self): return self.user_list if __name__ == "__main__": # find all contestants with equity larger than threshold contestants = ContestantsFinder(20000.0) print contestants.list() # display statistics print "* Statistics *" for contestant in contestants.list(): u = ContestantParser(contestant) print u print u.position('eurusd') print '-' * 60
Результат, представленный ниже, был получен путем анализа и нахождения взаимосвязей между тегами в коде HTML-документа.
Если этот код выполняется напрямую из консоли Python, будут выведены имена участников, их баланс и текущие открытые позиции.
Ниже приведен результат простого запроса:
* Statistics * User: Tim Bal 31459.2 Equ 31459.2 none ------------------------------------------------------------ User: enivid eurusd sell euraud sell Bal 26179.98 Equ 29779.89 sell ------------------------------------------------------------ User: ias eurjpy sell usdchf sell gbpusd buy eurgbp buy eurchf sell audusd buy gbpjpy sell usdjpy buy usdcad buy euraud buy Bal 15670.0 Equ 29345.66 none ------------------------------------------------------------ User: plencing eurusd buy Bal 30233.2 Equ 29273.2 buy ------------------------------------------------------------ User: anuta audusd buy usdcad sell gbpusd buy Bal 28329.85 Equ 28359.05 none ------------------------------------------------------------ User: gery18 Bal 27846.7 Equ 27846.7 none ------------------------------------------------------------ User: tornhill Bal 27402.4 Equ 27402.4 none ------------------------------------------------------------ User: rikko eurusd sell Bal 25574.8 Equ 26852.8 sell ------------------------------------------------------------ User: west100 eurusd buy Bal 27980.5 Equ 26255.5 buy ------------------------------------------------------------ ...
Выглядит неплохо, не так ли? С этими данными мы можем собрать интересную статистику и реализовать сервис XML-RPC, который предоставит их по требованию. При необходимости эту статистику может получить клиент сервиса XML-RPC из платформы MetaTrader 5.
Для создания XMLRPC-сервера я использовал библиотеку SimpleXMLRPC языка Python. Для внешнего мира со стороны сервера доступны два метода listContestants и getStats. В то время как первый выводит только имена участников (с размером Equity, больше некоторого порога), последний показывает, сколько из них имеют открытые позиции по заданной валютной паре и отношение buy/sell по этим позициям.
Согласитесь, вместе с сервисом "Сигналы" и копировщиком сделок, ваша торговля имеет больше шансов стать прибыльной.
from SimpleXMLRPCServer import SimpleXMLRPCServer from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler from PositionGrabberTest import ContestantParser, ContestantsFinder class RequestHandler( SimpleXMLRPCRequestHandler ): rpc_path = ( '/RPC2', ) class ATC2011XMLRPCServer(SimpleXMLRPCServer): def __init__(self): SimpleXMLRPCServer.__init__( self, ("192.168.235.168", 6666), requestHandler=RequestHandler, logRequests = False) self.register_introspection_functions() self.register_function( self.xmlrpc_contestants, "listContestants" ) self.register_function( self.xmlrpc_get_position_stats, "getStats" ) def xmlrpc_contestants(self, min_equity): try: min_equity = float(min_equity) except ValueError as error: print error return [] self.contestants = ContestantsFinder(min_equity) return self.contestants.list() def xmlrpc_get_position_stats(self, pair): total_users = len(self.contestants.list()) holding_pair = 0 buy_positions = 0 for username in self.contestants.list(): u = ContestantParser(username) position = u.position(pair) if position != "none": holding_pair += 1 if position == "buy": buy_positions += 1 return [ total_users, holding_pair, buy_positions ] if __name__ == '__main__': server = ATC2011XMLRPCServer() server.serve_forever()
Доступ к этому серверу возможен напрямую с консоли Python.
При запуске этого скрипта не забудьте заменить адрес "http://192.168.235.168:666" на ваш IP-адрес.
C:\Program Files\MetaTrader 5\MQL5>python Python 2.7.2 (default, Jun 12 2011, 15:08:59) [MSC v.1500 32 bit (Intel)] on win 32 Type "help", "copyright", "credits" or "license" for more information. >>> from xmlrpclib import ServerProxy >>> u = ServerProxy('http://192.168.235.168:6666') >>> u.listContestants(20000.0) ['lf8749', 'ias', 'aharata', 'Xupypr', 'beast', 'Tim', 'tmt0086', 'yyy999', 'bob sley', 'Diubakin', 'Pirat', 'notused', 'AAA777', 'ronnielee', 'samclider-2010', 'gery18', 'p96900', 'Integer', 'GradyLee', 'skarkalakos', 'zioliberato', 'kgo', 'enivid', 'Loky', 'Gans-deGlucker', 'zephyrrr', 'InvestProm', 'QuarkSpark', 'ld7 3', 'rho2011', 'tornhill', 'botaxuper'] >>>
Возможно, когда вы читаете эту статью, список участников полностью изменился, но общая идея ясна.
Вызов метода getStats() с параметром "eurusd" возвращает 3 числа:
In : u.getStats("eurusd") Out: [23, 12, 9]
Первое значение - число участников с Equity выше заданного порогового значения, второе число - количество участников, имеющих открытые позиции по EURUSD, а третье число - количество участников, имеющих длинные позиции по EURUSD. В данном случае 2/3 наиболее успешных участников имеют открытые позиции по EURUSD, поэтому это может служить подтверждающим сигналом для случаев, когда ваш робот или индикаторы сгенерировали сигнал на открытие длинных позиций по EURUSD.
Клиент системы анализа ATC 2011 на базе MQL5-RPC
Пришло время использовать MQL5-RPC для получения этих данных по запросу из платформы MetaTrader 5.
Посмотрев на исходный код, легко разобраться в том, как это работает.
//+------------------------------------------------------------------+ //| MQL5-RPC_ex2_ATC2011AnalyzerClient.mq5 | //| Copyright 2011, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" #include <MQL5-RPC.mqh> #include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayInt.mqh> #include <Arrays\ArrayDouble.mqh> #include <Arrays\ArrayString.mqh> //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- //--- тест ATC 2011 analyzer CXMLRPCServerProxy s("192.168.235.168:6666"); CXMLRPCResult* result; //--- получаем список участников CArrayObj* params1 = new CArrayObj; CArrayDouble* amount = new CArrayDouble; amount.Add(20000.0); params1.Add(amount); CXMLRPCQuery query("listContestants", params1); Print(query.toString()); result = s.execute(query); Print(result.toString()); delete result; //--- получаем статистику по позициям CArrayObj* params2 = new CArrayObj; CArrayString* pair = new CArrayString; pair.Add("eurusd"); params2.Add(pair); CXMLRPCQuery query2("getStats", params2); Print(query2.toString()); result = s.execute(query2); CArrayObj* resultArray = result.getResults(); CArrayInt* stats = resultArray.At(0); Print("Участников = " + IntegerToString(stats.At(0)) + ". EURUSD позиций = " + IntegerToString(stats.At(1)) + ". BUY позиций = " + IntegerToString(stats.At(2))); delete params1; delete params2; delete result; } //+------------------------------------------------------------------+
При запуске скрипта происходит следующее. Запрос оборачивается в XML:
<?xml version="1.0" ?> <methodCall> <methodName>listContestants</methodName> <params> <param> <value> <double>20000.00000000</double> </value> </param> </params> </methodCall>
а затем через некоторое время приходит ответ от XMLRPC-сервера.
В моем случае использовался Linux, на котором был запущен сервис XML-RPC на Python, который вызывался из Windows VirtualBox с запущенным MetaTrader 5.
<?xml version="1.0" ?> <methodResponse> <params> <param> <value> <array> <data> <value> <string>lf8749</string> </value> <value> <string>ias</string> </value> <value> <string>Xupypr</string> </value> <value> <string>aharata</string> </value> <value> <string>beast</string> </value> <value> <string>Tim</string> </value> <value> <string>tmt0086</string> </value> <value> <string>yyy999</string> </value> <value> <string>bobsley</string> </value> <value> <string>Diubakin</string> </value> <value> <string>Pirat</string> </value> <value> <string>AAA777</string> </value> <value> <string>notused</string> </value> <value> <string>ronnielee</string> </value> <value> <string>samclider-2010</string> </value> <value> <string>gery18</string> </value> <value> <string>Integer</string> </value> <value> <string>GradyLee</string> </value> <value> <string>p96900</string> </value> <value> <string>skarkalakos</string> </value> <value> <string>Loky</string> </value> <value> <string>zephyrrr</string> </value> <value> <string>Medvedev</string> </value> <value> <string>Gans-deGlucker</string> </value> <value> <string>InvestProm</string> </value> <value> <string>zioliberato</string> </value> <value> <string>QuarkSpark</string> </value> <value> <string>rho2011</string> </value> <value> <string>ld73</string> </value> <value> <string>enivid</string> </value> <value> <string>botaxuper</string> </value> </data> </array> </value> </param> </params> </methodResponse>
Результат первого запроса, показанного при помощи метода toString() выглядит следующим образом:
lf8749:ias:Xupypr:aharata:beast:Tim:tmt0086:yyy999:bobsley:Diubakin:Pirat:
AAA777:notused:ronnielee:samclider-2010:gery18:Integer:GradyLee:p96900:
skarkalakos:Loky:zephyrrr:Medvedev:Gans-deGlucker:InvestProm:zioliberato:
QuarkSpark:rho2011:ld73:enivid:botaxuper:
Второй запрос используется для вызова метода getStats().
<?xml version="1.0" ?> <methodCall> <methodName>getStats</methodName> <params> <param> <value> <string>eurusd</string> </value> </param> </params> </methodCall>
Ответ XML-RPC простой, он содержит лишь 3 значения типа integer.
<?xml version="1.0" ?> <methodResponse> <params> <param> <value> <array> <data> <value> <int>31</int> </value> <value> <int>10</int> </value> <value> <int>3</int> </value> </data> </array> </value> </param> </params> </methodResponse>
На этот раз я использовал другой подход для доступа к значениям, переданным в MQL5-переменные.
MQL5-RPC_ex2_ATC2011AnalyzerClient (AUDUSD,H1) 12:39:23 lf8749:ias:Xupypr:aharata:beast:Tim:tmt0086:yyy999:bobsley:Diubakin:Pirat: AAA777:notused:ronnielee:samclider-2010:gery18:Integer:GradyLee:p96900: skarkalakos:Loky:zephyrrr:Medvedev:Gans-deGlucker:InvestProm:zioliberato: QuarkSpark:rho2011:ld73:enivid:botaxuper: MQL5-RPC_ex2_ATC2011AnalyzerClient (AUDUSD,H1) 12:39:29 Участников = 31. EURUSD позиций = 10. BUY позиций = 3
Как видите, на выходе имеем данные в понятном виде.
Заключение
В этой статье я предложил MQL5-RPC framework, который позволяет MetaTrader 5 осуществлять удаленный вызов процедур по протоколу XML-RPCl. Представлены два способа его использования, первый - доступ к Web-сервису, второй - система анализа статистики Automated Trading Championship 2011. Эти два примера могут служить основой для дальнейших экспериментов.
Я считаю, что MQL5-RPC является очень мощным инструментом, который можно использовать во многих случаях. Я решил сделать этот проект открытым (Open Source) и опубликовал его (лицензия GPL) на сайте http://code.google.com/p/mql5-rpc/. Если вы захотите помочь в улучшении кода, провести его рефакторинг или исправить ошибки, присоединяйтесь к проекту. Исходные коды всех примеров также доступны в приложении к статье.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/342





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Ну вот честно не пойму чем это всё круче обычного html-парсера, питон зачем то сюда приплели, WinInet.dll, а цель то какая? для чего это всё?
какая сверхзадача?
Анализ данных? так по моему проще написать парсер, сохранить результаты парсинга в класс CTable и уже из него сортируй анализируй как душе угодно.
ЗЫ Может моё непонимание от того что у нас с XML полное взаимопонимание, я его не трогаю и он меня не беспокоит :)
Urain:
Well, honestly I do not understand what it is steeper normal html-parser, python why then dragged here, WinInet.dll, and then what purpose? what does it all?
What important task?
Analysis of the data? so in my opinion easier to write a parser to save the results of parsing the class CTable and already out of it Sort analyzed as you want.
Threat Can my misunderstanding of the fact that we have full understanding of XML, I do not touch it and it does not bother me:)
This serves only as an example and was mentioned only because ATC is in progress. The true underlying purpose of this method is that you can have access to cloud computing via XML-RPC. Think of possible solutions by yourself. You will also see a different issue that will be presented in my next article.
cheers.
Urain:
This serves only as an example and was mentioned only because ATC is in progress. The true underlying purpose of this method is that you can have access to cloud computing via XML-RPC. Think of possible solutions by yourself. You will also see a different issue that will be presented in my next article.
cheers.
A remark - by cloud computing I do not mean MQL5 cloud but a group of servers that would compute complex stuff from several resources and return it as a XML-RPC.
Now something that emerges.
I wanted to say cheers, but obviously not destiny, your remark has changed everything.
If you implement a management target setting MQL5 Cloud Network it would be a huge breakthrough.
Now something that emerges.
I wanted to say cheers, but obviously not destiny, your remark has changed everything.
If you implement a management target setting MQL5 Cloud Network it would be a huge breakthrough.
MQL5 cloud is rather closed solution since it is closely integrated with terminal and payments and I do not suspect MetaQuotes to release API for that :)
cheers.
MQL5 cloud is rather closed solution since it is closely integrated with terminal and payments and I do not suspect MetaQuotes to release API for that :)
cheers.
Yes, I talked about it with MQ, they do not plan to give the API.
I thought that maybe there is a solution in areas of little-known to me.