MQL5-RPC. Chiamate di procedura remota da MQL5: Accesso al servizio web e analizzatore ATC XML-RPC per divertimento e profitto
Introduzione
Questo articolo descriverà il framework MQL5-RPC che ho creato nelle ultime settimane. Copre gli accessi di base XML-RPC, la descrizione dell'implementazione MQL5 e due esempi di utilizzo del mondo reale MQL5-RPC. Il primo sarà una chiamata di procedura remota su un servizio web di un sito esterno di forex e il secondo sarà un client per il nostro server XML-RPC utilizzato per interpretare, analizzare e fornire risultati raccolti dall’ Automated Trading Championship 2011. Se sei interessato a come implementare e analizzare diverse statistiche da ATC 2011 in tempo reale, questo articolo fa per te.
Nozioni di base su XML-RPC
Cominciamo con le basi di XML-RPC. XML-RPC sta per XML Remote Procedure Call. Questo è un protocollo di rete che utilizza XML per codificare e decodificare i parametri passati per chiamare un metodo esterno. Utilizza il protocollo HTTP come meccanismo di trasporto per scambiare dati. Per metodo esterno intendo un altro programma per computer o un servizio web che espone procedure remote.
Il metodo esposto può essere chiamato da qualsiasi linguaggio di computer da qualsiasi macchina connessa alla rete a condizione che utilizzi anche lo stack di protocollo XML-RPC e abbia accesso di rete al server. Ciò significa anche che XML-RPC può essere utilizzato per chiamare un metodo sulla stessa macchina scritto in un altro linguaggio di programmazione. Questo verrà mostrato nella seconda parte dell'articolo.
Modello dati XML-RPC
La specifica XML-RPC utilizza sei tipi di dati di base: int, double, boolean, string, datetime, base64 e due tipi di dati composti: array e struct. L'array può essere costituito da qualsiasi elemento di base e la struttura fornisce coppie nome-valore come array associativi o proprietà dell'oggetto.
Tipi di dati di base in XML-RPC | ||
---|---|---|
Tipo | Valore | Esempi |
int o i4 | Interi a 32 bit compresi tra - 2.147.483.648 e 2.147.483.647. | <int>11<int> <i4>12345<i4> |
double | Numeri in virgola mobile a 64 bit | <double>30.02354</double> <double>-1.53525</double> |
Boolean | true (1) o falsa (0) | <boolean>1</boolean> <boolean>0</boolean> |
string | Testo ASCII, molte implementazioni supportano Unicode | <string>Hello</string> <string>MQL5</string> |
dateTime.iso8601 | Date in formato ISO8601: CCYYMMDDTHH:MM:SS | <dateTime.iso8601> 20111125T02:20:04 </dateTime.iso8601> <dateTime.iso8601> 20101104T17:27:30 </dateTime.iso8601> |
base64 | Informazioni binarie codificate come definito in RFC 2045 | <base64> TDVsbG8sIFdvdwxkIE== </base64> |
Tabella 1. Tipi di dati di base in XML-RPC
L'array può contenere qualsiasi tipo di base, non necessariamente dello stesso tipo. L'elemento dell'array deve essere nidificato all'interno dell'elemento value. Contiene un elemento dati e uno o più elementi valore nell'elemento dati. L'esempio seguente mostra un array di quattro valori interi.
<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>
Il secondo esempio mostra un array di cinque valori stringa.
<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>
Sono convinto che sarai in grado di individuare somiglianze in questi due esempi per costruire un altro array XML-RPC.
Gli struct hanno un elemento struct all'interno dell'elemento value e sezioni membro all'interno dell'elemento struct. Ogni membro è costituito dal suo nome e dal valore che detiene. È quindi facile passare i valori di un array associativo o dei membri di un oggetto utilizzando le strutture.
Si prega di vedere l'esempio qui sotto.
<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>
Avendo familiarizzato con il modello dati XML-RPC, andiamo oltre alle strutture di richiesta e risposta. Ciò costituirà una base per l'implementazione del client XML-RPC in MQL5.
Strutture di richiesta XML-RPC
La richiesta XML-RPC è composta dall'intestazione del messaggio e dal payload del messaggio. L'intestazione del messaggio specifica HTTP il metodo di invio (POST), il percorso relativo al servizio XML-RPC, la versione del protocollo HTTP, il nome agente utente, l’indirizzo IP host, il tipo di contenuto (testo/xml) e la lunghezza del contenuto in byte.
POST /xmlrpc HTTP 1.1 User-Agent: mql5-rpc/1.0 Host: 10.12.10.10 Content-Type: text/xml Content-Length: 188
Il payload della richiesta XML-RPC è un documento XML. L'elemento radice dell'albero XML deve essere chiamato methodCall. Un methodCall contiene un singolo elemento methodName il cui contenuto è il nome del metodo eseguito. L'elemento MethodName contiene zero o un elemento params.
L'elemento params contiene uno o più elementi value, array o struct. Tutti i valori sono codificati di conseguenza con il tipo di dati (vedi tabella sopra). Osserva l’esempio di payload riportato di seguito e che mostra la richiesta di esecuzione del metodo "moltiplica" con due valori doppi da passare alla funzione.
<?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>
L'intestazione e il payload vengono inviati tramite HTTP al server che accetta l'input. Se il server è disponibile, controlla il nome del metodo e l'elenco dei parametri ed esegue il metodo desiderato. Al termine dell'elaborazione, prepara la struttura di risposta XML-RPC che può essere letta dal client.
Strutture di risposta XML-RPC
Simile alla richiesta XML-RPC, la risposta XML-RPC consiste in un'intestazione e un payload. L'intestazione è testo e il payload è documento XML. Se la richiesta era corretta, la prima riga dell'intestazione informa che il server è stato trovato (codice 200) e specifica la versione del protocollo. L'intestazione deve contenere anche Content-Type text/xml e Content-Length, cioè la lunghezza del payload in byte.
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
Non sorprende che anche il carico utile della risposta sia un documento XML. L'elemento radice dell'albero XML deve essere chiamato methodResponse. L'elemento methodResponse contiene un parametro in caso di successo o un elemento di errore in caso di fallimento. L'elemento params contiene esattamente un elemento param. L'elemento param contiene esattamente un elemento value.
L'esempio di una risposta di successo è presentato di seguito:
<?xml version="1.0"?> <methodResponse> <params> <param> <value><double>62606001.94</double></value> </param> </params> </methodResponse>
La risposta di errore viene preparata se si è verificato un problema nell'elaborazione della richiesta XML-RPC.
L'elemento fault, come l'elemento params, ha un solo valore di output.
<?xml version="1.0"?> <methodResponse> <fault> <value><string>No such method!</string></value> </fault> </methodResponse>
Poiché XML-RPC non standardizza i codici di errore, i messaggi di errore dipendono quindi dall'implementazione.
Introduzione a MQL5-RPC
Mi sono imbattuto in due articoli di Alex Sergeev, "Using WinInet.dll for Data Exchange between Terminals via the Internet" e "Using WinInet in MQL5. Part 2: POST Requests and Files" e ho capito che potevo implementare un client XML-RPC per MetaTrader 5. Dopo aver esaminato le specifiche, ho implementato il mio da zero. Questo è un progetto in corso e non affronta ancora l'intera specifica (il supporto base64 verrà aggiunto nel prossimo futuro), ma puoi già usarlo per effettuare un ampio sottoinsieme di chiamate XML-RPC da MetaTrader 5.
Modello dati MQL5-RPC
La parte più difficile dell'implementazione per me è stata quella di trovare un modello di dati corretto per MQL5. Ho deciso che doveva essere il più semplice possibile per l'utente del framework, quindi ho creato diverse classi che ne incapsulano le funzionalità. La prima decisione è stata quella di effettuare una richiesta di dati param come un singolo puntatore CObject*. Questo puntatore mantiene una matrice di puntatori a matrici derivate dalla classe CObject.
Esistono classi standard che contengono array CObject; CArrayInt, CArrayDouble, CArrayString, quindi mi sono basato su questo e ho implementato CArrayBool, CArrayDatetime per completare i tipi di dati di base e CArrayMqlRates per aggiungere array di strutture. Manca solo il tipo base64 per ora, ma sarà supportato nel prossimo futuro. Se l'array contiene un solo elemento, viene incapsulato in XML come elemento a valore singolo.
Ho scritto un esempio su come aggiungere array diversi all'array CObject* e visualizzare l'intero array di array di tipi diversi. È disponibile di seguito.
//+------------------------------------------------------------------+ //| 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 has " + IntegerToString(params.Total()) + " arrays."); 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; } //+------------------------------------------------------------------+
Il risultato dovrebbe essere chiaro: ci sono 6 sotto-array: array di valori interi, array di valori double, array di stringhe, array di datetime, array di valori booleani e array di MqlRates.
ArrayObjTest (EURUSD,H1) 23:01:54 params has 6 arrays. 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
Potresti essere interessato a come ho implementato array di altri tipi di dati. In CArrayBool e CArrayDatetime mi sono basato semplicemente su CArrayInt, ma in CArrayMqlRates era leggermente diverso poiché la struttura deve essere passata come riferimento e non è stato definito TYPE_MQLRATES.
Di seguito puoi trovare un codice sorgente parziale della classe CArrayMqlRates. Altre classi sono disponibili come allegato all'articolo.
//+------------------------------------------------------------------+ //| ArrayMqlRates.mqh | //| Copyright 2011, Investeo.pl | //| http://Investeo.pl | //| Revision 2011.03.03 | //+------------------------------------------------------------------+ #include "Array.mqh" //+------------------------------------------------------------------+ //| Class CArrayMqlRates. | //| Purpose: Class of dynamic array of structs | //| of MqlRates type. | //| Derived from CArray class. | //+------------------------------------------------------------------+ #define TYPE_MQLRATES 7654 class CArrayMqlRates : public CArray { protected: MqlRates m_data[]; // data array public: CArrayMqlRates(); ~CArrayMqlRates(); //--- method of identifying the object virtual int Type() const { return(TYPE_MQLRATES); } //--- methods for working with files virtual bool Save(int file_handle); virtual bool Load(int file_handle); //--- methods of managing dynamic memory bool Reserve(int size); bool Resize(int size); bool Shutdown(); //--- methods of filling the array 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); //--- method of access to the array MqlRates At(int index) const; //--- methods of changing bool Update(int index,MqlRates& element); bool Shift(int index,int shift); //--- methods of deleting bool Delete(int index); bool DeleteRange(int from,int to); protected: int MemMove(int dest,int src,int count); }; //+------------------------------------------------------------------+ //| Constructor CArrayMqlRates. | //| INPUT: no. | //| OUTPUT: no. | //| REMARK: no. | //+------------------------------------------------------------------+ void CArrayMqlRates::CArrayMqlRates() { //--- initialize protected data m_data_max=ArraySize(m_data); } //+------------------------------------------------------------------+ //| Destructor CArrayMqlRates. | //| INPUT: no. | //| OUTPUT: no. | //| REMARK: no. | //+------------------------------------------------------------------+ void CArrayMqlRates::~CArrayMqlRates() { if(m_data_max!=0) Shutdown(); } ... //+------------------------------------------------------------------+ //| Adding an element to the end of the array. | //| INPUT: element - variable to be added. | //| OUTPUT: true if successful, false if not. | //| REMARK: no. | //+------------------------------------------------------------------+ bool CArrayMqlRates::Add(MqlRates& element) { //--- checking/reserve elements of array if(!Reserve(1)) return(false); //--- adding m_data[m_data_total++]=element; m_sort_mode=-1; //--- return(true); } //+------------------------------------------------------------------+ //| Adding an element to the end of the array from another array. | //| INPUT: src - source array. | //| OUTPUT: true if successful, false if not. | //| REMARK: no. | //+------------------------------------------------------------------+ bool CArrayMqlRates::AddArray(const MqlRates &src[]) { int num=ArraySize(src); //--- checking/reserving elements of array if(!Reserve(num)) return(false); //--- adding for(int i=0;i<num;i++) m_data[m_data_total++]=src[i]; m_sort_mode=-1; //--- return(true); } ...
Tutti i dati MQL5 devono essere convertiti in valori XML prima di inviarli come richiesta RPC, quindi ho progettato una classe helper CXMLRPCEncoder, la quale prende un valore e lo codifica come stringa 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); };
Ho incollato tre dei metodi implementati di seguito. Tutti accettano un parametro (bool, string, datetime) e restituiscono una stringa, che è un tipo di dati valido XML per il protocollo 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(); }
Potresti notare che ci sono alcuni tag con il suffisso _B che significa "inizio tag" e il suffisso _E che significa "fine tag".
Ho deciso di utilizzare un file di intestazione che conserva i nomi dei tag e delle intestazioni XML poiché ha reso l'implementazione molto più trasparente
//+------------------------------------------------------------------+ //| 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>" //+------------------------------------------------------------------+
Dopo aver definito il modello dati di MQL5-RPC possiamo procedere alla costruzione di una richiesta XML-RPC completa.
Richiesta MQL5-RPC
Come accennato in precedenza, la richiesta XML-RPC consiste in un'intestazione della richiesta e un payload XML. Ho progettato la classe CXMLRPCQuery che costruisce automaticamente un oggetto query da array di dati MQL5. La classe utilizza CXMLRPCEncoder per incapsulare i dati in XML e aggiunge il nome del metodo all'interno del tag methodName.
class CXMLRPCQuery { private: CString s_query; void addValueElement(bool start,bool array); public: CXMLRPCQuery() {}; CXMLRPCQuery(string method="",CArrayObj *param_array=NULL); string toString(); };
Il costruttore della classe ha due parametri: nome del metodo e puntatore a CArrayObj, che contiene i parametri per chiamare il metodo. Tutti i parametri sono racchiusi in XML come descritto nella sezione precedente e viene aggiunta un'intestazione di query. L'intera query XML può essere visualizzata utilizzando il metodo toString().
CXMLRPCQuery::CXMLRPCQuery(string method="",CArrayObj *param_array=NULL) { //--- constructs a single XMLRPC Query 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); }
Osserva l’esempio di test di query di seguito. Questo non è il più semplice, poiché voglio mostrare che è possibile chiamare metodi abbastanza complessi.
I parametri di input sono: array di valori double, array di valori interi, array di valori string, array di valori bool, un singolo valore datetime e array di strutture 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() { //--- query test 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; } //+------------------------------------------------------------------+
La query risultante è la seguente. Normalmente questo è un valore di stringa di una riga, ma lo presento come un testo a schede per distinguere facilmente le sezioni XML-RPC e i singoli valori.
<?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>
Questo è un albero XML abbastanza sviluppato e il protocollo è flessibile per chiamare metodi ancora più complessi.
Risposta MQL5-RPC
Come con la richiesta XML-RPC, la risposta XML-RPC è costituita da intestazioni e payload XML, ma questa volta l'ordine di elaborazione deve essere invertito, ovvero la risposta XML deve essere convertita in dati MQL5. La classe CXMLRPCResult è stata progettata per far fronte a questa attività.
Il risultato può essere ricevuto come una stringa passata al costruttore della classe o può essere recuperato automaticamente dalla classe CXMLServerProxy dopo l'esecuzione del metodo desiderato. In situazioni in cui il risultato contiene strutture che potrebbero non essere note in anticipo, il metodo parseXMLResponseRAW() può cercare tutti i tag <value> e restituisce il puntatore CArrayObj, il quale contiene l'array di tutti gli elementi di valore trovati.
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(); };
Il costruttore della classe esegue la scansione di ogni intestazione nell'XML di risposta e chiama il metodo privato parseXMLValuesToMQLArray(), il quale esegue il duro lavoro di conversione dei dati XML in MQL5 dietro le quinte.
Riconosce se param è un array o un singolo elemento e riempie gli array appropriati aggiunti al risultato dell'array CArrayObj.
bool CXMLRPCResult::parseXMLResponse() { CArrayObj *results=new CArrayObj; m_params.Clear(); //--- find params and put them in m_params array 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)); //--- parse value tag val.Assign(val.Mid(StringLen(VALUE_B),val.Len()-StringLen(VALUE_B)-StringLen(VALUE_E))); //--- now check first tag and handle it approprietaly 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))); //--- find first type and define array 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; }
La conversione avviene all'interno di metodi parseXMLValuesToMQLArray sovraccaricati. Il valore della stringa viene estratto da XML e convertito in variabili MQL5 di base.
Tre dei metodi utilizzati per la conversione sono incollati di seguito per riferimento.
//+------------------------------------------------------------------+ //| parseXMLValuesToMQLArray | //+------------------------------------------------------------------+ bool CXMLRPCResult::parseXMLValuesToMQLArray(CArrayBool *subArr,CString &val) { //--- parse XML values and populate MQL array 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) { //--- parse XML values and populate MQL array 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) { // parse XML values and populate MQL array 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; }
Come puoi vedere, i valori stringa vengono estratti dai tag XML-RPC utilizzando i metodi disponibili nella classe CString, Assign(), Mid(), Find() e vengono ulteriormente convertiti in MQL5 utilizzando StringToTime(), StringToInteger(), StringToDouble() o un metodo personalizzato come nel caso delle variabili bool.
Dopo che tutti i valori sono stati analizzati, tutti i dati disponibili possono essere visualizzati utilizzando il metodo toString(). Uso semplicemente i due punti per separare i valori.
string CXMLRPCResult::toString(void) { // returns results array of arrays as a string 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(); }
Ho implementato un test dei risultati per farti vedere esplicitamente come i parametri dei risultati XML-RPC vengono convertiti in 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; } //+------------------------------------------------------------------+
Puoi trovare il risultato qui sotto:
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:
Questo è un array visualizzato di array di diversi valori MQL5 convertiti da XML. Come puoi vedere ci sono stringhe, valori doppi, valori booleani, valori interi, valori datetime a cui è possibile accedere dal singolo puntatore CArrayObj.
Classe proxy MQL5-RPC
Il proxy CXMLRPCServer è una classe principale per la comunicazione HTTP. Ho usato "Using WinInet in MQL5. Part 2: POST Requests and Files" per implementare la funzionalità HTTP e l’aggiunta di un'intestazione personalizzata coerente con la specifica XML-RPC.
CXMLRPCServerProxy::CXMLRPCServerProxy(string s_proxy,int timeout=0) { CString proxy; proxy.Assign(s_proxy); //--- find query path 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); }; //--- find query port. 80 is default 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."; }
L'oggetto CXMLRPCQuery deve essere passato al metodo CXMLRPCServerProxy execute() per attivare la chiamata XML-RPC.
I metodi restituiscono il puntatore all'oggetto CXMLRPCResult che può essere ulteriormente utilizzato nello script che ha chiamato la chiamata XML-RPC.
CXMLRPCResult *CXMLRPCServerProxy::execute(CXMLRPCQuery &query) { //--- creating descriptor of the request 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); } //-- sending the request 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"); //--- closing all handles InternetCloseHandle(hRequest); InternetCloseHandle(hSend); CXMLRPCResult* result = new CXMLRPCResult(out.Str()); return result; }
Esempio 1 - Accesso al servizio web
Il primo esempio funzionante di MQL5-RPC è una chiamata a un servizio web esterno. L'esempio che ho trovato utilizza il tasso di cambio corrente per convertire un importo specifico di una valuta in un'altra. La specifica exact dei parametri del metodo è disponibile online.
Il servizio web espone il metodo foxrate.currencyConvert, il quale accetta tre parametri, due stringhe e un valore float:
- dalla valuta (es: USD) = string;
- alla valuta (es: GBP) = string;
- importo da convertire (es:100.0) = float.
L'implementazione è piuttosto breve e richiede solo un paio di righe.
//+------------------------------------------------------------------+ //| 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 service test 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; } //+------------------------------------------------------------------+
Poiché il metodo restituisce una struttura complessa, ho analizzato il risultato utilizzando il metodo parseXMLResponseRAW().
La query XML-RPC in questo esempio ha il seguente aspetto:
<?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>
E la risposta viene restituita come una struttura racchiusa in 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>
L'output di toString() rivela che nel risultato sono disponibili tre valori: 0 - nessun errore, il secondo valore è la quantità di valuta di base necessaria per lo scambio e il terzo parametro è l'ora dell'ultimo tasso di cambio (è stato memorizzato nella cache).
0:15773.00000000:cached:
Continuiamo con casi d'uso più interessanti.
Esempio 2 - Analizzatore XML-RPC ATC 2011
Immagina di voler ottenere statistiche dall’ Automated Trading Championship 2011 (io l'ho fatto) e di utilizzare queste informazioni nel terminale MetaTrader 5. Se vuoi sapere come ottenerlo, continua a leggere.
Poiché MetaQuotes Software Corp. sta preparando un Servizio che segue il segnale, il quale consentirà di sottoscrivere segnali da particolari Expert Advisor, ritengo che la combinazione di quel servizio con l'analizzatore sarà un modo molto potente per eseguire analisi sofisticate e fornire nuovi modi per beneficiare dell’ATC. In effetti, potresti prendere dati da alcune fonti diverse usando questo metodo e fare un Expert Advisor complesso basato su di esso.
Server analizzatore XML-RPC ATC 2011
Il primo passo per creare un server ATC Analyzer è preparare l'analisi dell'output. Diciamo che vogliamo catturare tutti i partecipanti i cui conti delle equità sono al di sopra di una certa soglia e sono interessati alle posizioni che tutti detengono attualmente e visualizzare la quantità di statistiche sulle posizioni di acquisto e vendita della coppia di valute a cui siamo interessati.
Ho usato il linguaggio Python e la libreria BeautifulSoup per recuperare e analizzare i dati. Se non hai mai usato Python prima, ti consiglio vivamente di puntare il tuo browser su http:/Python.org e leggere cosa ha da offrire questo linguaggio: non te ne pentirai.
Se invece desideri mettere rapidamente le mani sull'analizzatore, scarica Python 2.7.1 installer e setup_tools e installali entrambi. Dopodiché esegui la console di Windows e cambia la directory in C:\Python27\Scripts o qualunque sia la cartella in cui hai installato Python. Quindi emetti il comando 'easy_install BeautifulSoup'. Questo installerà il pacchetto 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>
Dopodiché dovresti essere in grado di eseguire la console Python ed emettere il comando "import BeautifulSoup" senza errori.
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 >>>
Ed esegui l'analizzatore allegato con il comando 'python ContestantParser.py'. L'analizzatore recupera il sito web, esegue alcune ricerche di parole chiave e inserisce l'output sulla console.
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
Ciò è stato ottenuto semplicemente guardando all'interno del codice sorgente HTML e trovando le relazioni tra i tag. Se questo codice viene eseguito direttamente dalla console Python, restituisce i nomi dei concorrenti, il loro saldo e patrimonio e tutte le posizioni aperte correnti.
Osserva l'output di una query di esempio:
* 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 ------------------------------------------------------------ ...
Sembra buono, non è vero? Con questi dati possiamo raccogliere statistiche interessanti e implementare un servizio XML-RPC che le fornirà su richiesta. Il client XMLRPC da MetaTrader 5 potrebbe recuperare quelle statistiche quando necessario.
Per creare un server XMLRPC ho usato la libreria server SimpleXMLRPC di Python. Il server espone due metodi al mondo esterno: listContestants e getStats. Mentre il primo mostra solo i nomi dei concorrenti che hanno un'equità al di sopra di una certa soglia, il secondo mostra quanti di loro hanno una posizione aperta su una determinata coppia di valute e qual è il rapporto tra acquisto e vendita su quelle posizioni.
Insieme al servizio di segnali e/o al copiatore di trade dall'articolo, potresti voler confermare che la configurazione su cui stai effettivamente facendo trading ha più possibilità di essere redditizia.
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()
È possibile accedere a questo server direttamente dalla console Python. Quando esegui questo script, ricorda di cambiare l'indirizzo IP con quello utilizzato dal tuo sistema.
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'] >>>
Probabilmente, mentre stai leggendo, i risultati dei concorrenti sono totalmente cambiati, ma dovresti avere un'idea di ciò che stavo cercando di ottenere.
La chiamata al metodo getStats() con il parametro "eurusd" restituisce tre numeri:
In : u.getStats("eurusd") Out: [23, 12, 9]
Il primo è un numero di concorrenti che hanno un'equità al di sopra della soglia, il secondo è un numero di concorrenti che detengono una posizione EURUSD aperta e il terzo numero è un numero di concorrenti che hanno una posizione long su EURUSD. In questo caso, due terzi dei vincitori hanno aperto long EURUSD, quindi questo può servire come segnale di conferma quando il tuo robot oi tuoi indicatori hanno generato un segnale 'open eurusd long'.
Analizzatore client MQL5-RPC ATC 2011
È tempo di utilizzare il framework MQL5-RPC per recuperare questi dati su richiesta su MetaTrader 5. In effetti il suo utilizzo è autoesplicativo se segui il codice sorgente.
//+------------------------------------------------------------------+ //| 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 test CXMLRPCServerProxy s("192.168.235.168:6666"); CXMLRPCResult* result; //--- Get list of contestants 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; //--- Get position statistics 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("Contestants = " + IntegerToString(stats.At(0)) + ". EURUSD positions = " + IntegerToString(stats.At(1)) + ". BUY positions = " + IntegerToString(stats.At(2))); delete params1; delete params2; delete result; } //+------------------------------------------------------------------+
Questo è ciò che accade quando viene chiamato lo script. La query è racchiusa in XML:
<?xml version="1.0" ?> <methodCall> <methodName>listContestants</methodName> <params> <param> <value> <double>20000.00000000</double> </value> </param> </params> </methodCall>
e dopo un po' si riceve una risposta dal server XML-RPC. Nel mio caso ho usato un host Linux che esegue il servizio Python XML-RPC e l'ho chiamato dall'installazione guest di Windows VirtualBox che esegue 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>
Il risultato della prima query visualizzata utilizzando il metodo toString() è il seguente:
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:
La seconda query viene utilizzata per chiamare il metodo getStats().
<?xml version="1.0" ?> <methodCall> <methodName>getStats</methodName> <params> <param> <value> <string>eurusd</string> </value> </param> </params> </methodCall>
La risposta XML-RPC è semplice e contiene solo tre valori interi.
<?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>
Questa volta ho adottato un altro approccio e ho avuto accesso ai valori restituiti come variabili 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 Contestants = 31. EURUSD positions = 10. BUY positions = 3
Come puoi vedere, l'output presenta una forma facilmente comprensibile.
Conclusione
Ho presentato un nuovo framework MQL5-RPC che consente a MetaTrader 5 di eseguire chiamate di procedura remota utilizzando il protocollo XML-RPC. Sono stati presentati due casi d'uso, il primo relativo all'accesso a un servizio web e il secondo a un analizzatore personalizzato per l’ Automated Trading Championship 2011. Questi due esempi dovrebbero servire come base per ulteriori esperimenti.
Credo che MQL5-RPC sia una funzionalità molto potente che può essere utilizzata in molti modi diversi. Ho deciso di rendere il framework Open Source e di pubblicarlo sotto licenza GPL su http://code .google.com/p/mql5-rpc/. Se qualcuno volesse aiutare a rendere il codice a prova di proiettile, rifattorizzate o correggete i bug: le porte sono aperte per unirsi al progetto. Il codice sorgente con tutti gli esempi è disponibile anche come allegato all'articolo.
Tradotto dall’inglese da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/en/articles/342
- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso