Desarrollando un EA comercial desde cero (Parte 17): Acceso a los datos en la web (III)

Daniel Jose | 6 julio, 2022

1.0 - Introducción

En el artículo anterior Desarrollando un EA comercial desde cero (Parte 16): Acceso a los datos de la web (II), expliqué los problemas e implicaciones que conlleva capturar información de la web para utilizarla en un EA, di 3 posibles soluciones para realizar la tarea, cada una tiene sus pros y sus contras.

En la primera solución, que consistía en obtener la información directamente a través del EA, hablé del problema que puede surgir si el servidor remoto tarda en responder y, asimismo, de las implicaciones que esto puede traer al sistema comercial.

En la segunda solución implementamos un canal, según un modelo cliente servidor, donde el cliente era el EA, el servidor era un script y el canal de comunicación era un objeto. Este modelo es muy bueno hasta el momento en el que se necesita cambiar el marco temporal, entonces empieza a ser algo incómodo, pero al margen de este hecho, es el mejor sistema presentado, ya que el hecho de que utilicemos un modelo cliente servidor nos asegura que el EA no va a estar esperando a que el servidor remoto responda, simplemente va y lee la información que hay en el objeto, sin importar de donde haya venido la información.

En la tercera y última solución, hemos mejorado el sistema cliente-servidor, utilizando un servicio para ello. Y con ello hemos empezado a utilizar un recurso poco explorado de la plataforma MetaTrader 5, que son las variables globales de terminal, en esta solución hemos corregido el problema del cambio de marco temporal que es el mayor inconveniente del modelo que utiliza un script. Sin embargo, tenemos un nuevo problema, el sistema de variables globales de terminal, que nos permiten utilizar sólo el tipo double, y muchos no saben cómo sortear esto y, así, pasan información diferente, como por ejemplo un extracto de texto a través del canal que proporciona MetaTrader 5.

Este artículo tratará precisamente de eso, de cómo sortear esta limitación, pero no esperes milagros, hacer que la cosa funcione como posiblemente necesites requerirá mucho trabajo duro.

Así que pongamos manos a la obra, o más bien a empezar a codificar un sistema alternativo.


2.0 - Planificación

Para empezar, sabemos que sólo podemos utilizar variables de tipo double en el sistema de canales proporcionado por MetaTrader 5 y que este tipo está compuesto por 8 bytes. Pero luego piensas: «¿¡Y qué!?». ¿Cómo me ayudará esto? Calma, vamos a entender una cosa:

Los sistemas informáticos funcionan con bytes, aunque muchos hayan olvidado este concepto, es importante y necesario, y cada byte está compuesto por 8 bits. Y 1 bit es la cantidad más pequeña posible en un sistema computacional, y en este caso el tipo más pequeño y simple presente en un lenguaje es el tipo booleano, que está compuesto exactamente por este 1 solo bit. Esto es lo más básico de lo básico.

Pues bien, cualquier información, por complicada que sea, estará contenida en 1 byte. De nuevo, no importa lo complicada que sea la información, siempre estará dentro de 1 byte, que está formado por 8 bits. Cuando unimos 2 bytes tendremos el primer conjunto compuesto en un sistema, este primer conjunto se conoce como WORD, el segundo como DWORD, que sería 2 WORD, y el tercero sería QWORD, que sería 2 DWORD, esta es una nomenclatura utilizada en assembly que es la lengua madre de todos los lenguajes modernos, luego gran parte de los sistemas utilizan los mismos tipos, pero existe un detalle que es el nombre que recibirán estos tipos.

Espero que estés consiguiendo seguir el razonamiento hasta este punto, y para facilitar las cosas a los que están empezando, echen un vistazo a las imágenes de abajo:

          

                         

En estas figuras de arriba tenemos los principales tipos disponibles hasta ahora, y cubren desde 1 bit hasta 64 bits. Pero puedes pensar: «¿¡Por qué explicas esto!?». El detalle es que es importante conocer esto, para poder entender lo que vamos a hacer hasta el final del artículo, ya que vamos a manipular estos tipos para poder transferir información con diferentes propiedades internas.

Pues bien, cada uno de estos tipos puede recibir diferentes nombres dependiendo del lenguaje que se utilice, en el caso de MQL5 siguen la siguiente tabla:

Nombre Número de bytes Nombre basado en el lenguaje ensamblador (imágenes de arriba) 
bool   Sólo se utiliza 1 bit, puede haber 8 bools en un byte  Sólo se utiliza 1 bit, puede haber 8 bools en un byte
char 1
 Byte
short 2  Word
int  4  DWord
long 8  QWord

esta tabla cubre valores enteros con signos, para más detalles dentro de MQL5 consulta tipos enteros, allí son definidos otros nombres además de estos. Los tipos tipos reales tienen cierta similitud con los tipos enteros, pero tienen su propia forma de formateo y modelado interno, un ejemplo de formateo y modelado se puede ver en double precision floating point format, pero básicamente seguirá la tabla de abajo:

Nombre Número de bytes Nombre basado en el lenguaje ensamblador (imagen de arriba) 
Float 4  DWord
Doble 8  QWord

Observe una cosa interesante aquí, tanto los modelos de punto flotante como los modelos de enteros utilizan la misma base de datos, pero con diferentes longitudes, por lo que estamos llegando al punto que en realidad nos interesa. Si entiendes la lógica de la cosa acabarás llegando a la siguiente conclusión que se puede ver en la imagen de abajo:

Es decir, un QWORD representa 8 bytes, por lo que un valor double nos permite poner 8 bytes de información, y si se utiliza la tabla ASCII para transferir información, se pueden poner 8 caracteres imprimibles en una variable global del terminal, y se obtendrá el resultado de la comunicación entre un servicio y el EA, que se puede ver a continuación.

Es cierto que los datos están en orden, y creo que se pudo entender la idea en sí. El gran detalle es que si el mensaje tiene más de 8 caracteres imprimibles, habrá que fragmentarlo en más partes, si la entrega tiene que hacerse rápidamente, es decir, en 1 ciclo, habrá que utilizar tantas variables globales de terminal como sean necesarias para transferir el mensaje en un ciclo, y luego unirlas de forma adecuada hasta reconstruir el mensaje original. Pero si él se puede entregar en paquetes, habrá que crear una forma de que el servidor, en este caso el servicio, sepa que el cliente, que en este caso es el EA, ha leído el mensaje publicado y esperará el siguiente bloque.

Este tipo de problemas tiene varias soluciones, y quienes deseen entender o implementar dichas soluciones no necesitan crear la cuestión desde cero, se puede utilizar el mismo modelado que se hace en los protocolos de comunicación de red, como TCP/IP o UDP, y adaptar la idea al sistema de transferencia de información utilizando variables globales de terminal, una vez entendido cómo funcionan los protocolos, esta tarea deja de ser complicada y se convierte en una cuestión de habilidad y conocimiento del lenguaje que se va a utilizar. Se trata de un tema muy amplio que merece un estudio específico para cada tipo de situación y problema a resolver.


3.0 - Implementación

Bien, ahora que entendemos la idea que vamos a utilizar, podemos hacer una implementación inicial, para comprobar cómo el sistema transferirá la información entre el servicio y el EA, pero vamos a transferir sólo caracteres imprimibles.

3.0.1 - Modelo base

En el sistema del artículo anterior, vamos a modificar los archivos, empezando por el archivo de cabecera, ahora tendrá el siguiente contenido, y está por completo en el código de abajo:

//+------------------------------------------------------------------+
#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_GlobalNameChannel   "InnerChannel"
//+------------------------------------------------------------------+
union uDataServer
{
        double  value;
        char    Info[sizeof(double)];
};
//+------------------------------------------------------------------+

Esta cabecera es básica, en ella tenemos la declaración de la variable global de terminal, como se ha visto antes, y tenemos una nueva estructura, una unión. Una unión es diferente de un struct, la estructura es una combinación de datos sin que se intercalen, mientras que en la unión se produce el intercalado, donde un dato menor estará dentro de otro mayor. En el caso anterior tendremos como base el valor double, y dentro de este valor 8 bytes, pero fíjate que he utilizado un sistema de captura de la longitud del tipo sizeof, de esta forma en caso de que en el futuro tengamos un tipo double más grande, cosa poco probable, este código se adaptará automáticamente a ello.

Así que al final tendremos la siguiente imagen:

Vean que se parece a la que se ve arriba, pero esto es lo que hace la unión.

El siguiente código a modificar es el EA, que corresponde al cliente, el código se puede ver completo a continuación:

#property copyright "Daniel Jose"
#property description "Testing internal channel\nvia terminal global variable"
#property version "1.04"
//+------------------------------------------------------------------+
#include <Inner Channel.mqh>
//+------------------------------------------------------------------+
int OnInit()
{
        EventSetTimer(1);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+
void OnTimer()
{
        uDataServer loc;
        
        if (GlobalVariableCheck(def_GlobalNameChannel))
        {
                GlobalVariableGet(def_GlobalNameChannel, loc.value);
                Print(CharArrayToString(loc.Info, 0, sizeof(uDataServer)));
        }
}
//+------------------------------------------------------------------+

Observemos que aquí usamos la función CharArrayToString para convertir un array uchar en una string, pero fíjate que obtenemos un valor double, que es el único posible de recibir desde una variable global de terminal, pero una string en el lenguaje MQL5 sigue los mismos principios que una en C/C++, por lo que no podemos utilizar ningún carácter, salvo el hecho de crear el nuestro propio, pero esto es otra historia, no entraré en detalle aquí en cómo hacerlo, ya que es posible que se utilice un modelado en el que se compriman los datos, para así superar el límite de 8 bytes.

Pero aún nos falta un programa que es el servidor, en este caso, nuestro servidor es un servicio y el código para probar el sistema será el que se muestra a continuación:

//+------------------------------------------------------------------+
#property service
#property copyright "Daniel Jose"
#property version   "1.03"
//+------------------------------------------------------------------+
#include <Inner Channel.mqh>
//+------------------------------------------------------------------+
void OnStart()
{
        uDataServer loc;
        char car = 33;
        
        while (!IsStopped())
        {
                if (!GlobalVariableCheck(def_GlobalNameChannel)) GlobalVariableTemp(def_GlobalNameChannel);
                for (char c0 = 0; c0 < sizeof(uDataServer); c0++)
                {
                        loc.Info[c0] = car;
                        car = (car >= 127 ? 33 : car + 1);
                }
                GlobalVariableSet(def_GlobalNameChannel, loc.value);
                Sleep(1000);
        }
}
//+------------------------------------------------------------------+

Mira que es algo bastante sencillo, pero extremadamente eficaz y funcional.

Al ejecutar esto en la plataforma obtendremos exactamente el siguiente resultado


Tal vez esto te parezca una tontería y algo sin sentido, pero alguien con un poco de creatividad puede hacer que este sistema sea bastante útil a la hora de hacer algunas cosas que no se imaginan los demás.

Para demostrarlo modificaré el sistema y mostraré una cosa muy sencilla, sólo para despertar la curiosidad y quizás hacer que alguien imagine una funcionalidad muy exótica para este sistema de comunicación.


3.0.2 - Intercambio de estampitas

Este intercambio de estampitas es una forma que tengo de referirme a un intercambio de información entre el cliente y el servidor para que el servidor sepa qué tipo de información va a querer el cliente, para que el servidor pueda empezar a producir o buscar esta información.

El concepto es bastante simple de entender, pero implementarlo puede ser una tarea bastante desafiante, más aún cuando se trata de un modelado de datos en el que sólo tenemos 8 bytes disponibles, y debemos pasar toda y cualquier información dentro de este canal.

3.0.2.1 - Prueba de la comunicación cliente-servidor

Hagamos lo siguiente, mira el código del servicio que se puede ver en su totalidad a continuación:

#property service
#property copyright "Daniel Jose"
#property version   "1.03"
//+------------------------------------------------------------------+
#include <Inner Channel.mqh>
//+------------------------------------------------------------------+
void OnStart()
{
        uDataServer loc, loc1, loc2;
        char car = 33;
        
        while (!IsStopped())
        {
                if (!GlobalVariableCheck(def_GlobalValueInChannel))
                {
                        GlobalVariableTemp(def_GlobalValueInChannel);
                        GlobalVariableTemp(def_GlobalMaskInfo);
                        GlobalVariableTemp(def_GlobalPositionInfos);
                }
                for (char c0 = 0; c0 < sizeof(uDataServer); c0++)
                {
                        loc.Info[c0] = car;
                        car = (car >= 127 ? 33 : car + 1);
                }
                GlobalVariableSet(def_GlobalValueInChannel, loc.value);
                GlobalVariableGet(def_GlobalMaskInfo, loc1.value);
                GlobalVariableGet(def_GlobalPositionInfos, loc2.value);
                Print(CharArrayToString(loc1.Info, 0, sizeof(uDataServer)), "   ",loc2.Position[0], "    ", loc2.Position[1]);
                Sleep(1000);
        }
}
//+------------------------------------------------------------------+

Observe algunas cosas interesantes en este nuevo código de servicio o servidor. Ahora no vamos a tratar con 1 variable, sino con 3 variables, y funcionan para crear un canal lo suficientemente grande para que el cliente, en este caso el EA, pueda hablar con el servidor, que en este caso es el servicio, pero fíjate en la siguiente línea:

Print(CharArrayToString(loc1.Info, 0, sizeof(uDataServer)), "   ",loc2.Position[0], "    ", loc2.Position[1]);

Aquí tenemos los datos que fueron publicados por el cliente, nota que estamos usando 2 variables para pasar 3 informaciones diferentes, pero ¿¡cómo es posible!? Para entenderlo, es necesario ver el código de la cabecera, que se muestra a continuación en su totalidad.

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_GlobalValueInChannel        "Inner Channel"
#define def_GlobalMaskInfo                      "Mask Info"
#define def_GlobalPositionInfos         "Positions Infos"
//+------------------------------------------------------------------+
union uDataServer
{
        double  value;
        uint    Position[2];
        char    Info[sizeof(double)];
};
//+------------------------------------------------------------------+

Bien, tú que estás empezando puedes pensar que cada variable dentro de esta unión está aislada de la otra, si estás pensando esto, te aconsejo que mires al principio de este artículo, porque aunque tengamos variables con diferentes nombres, aquí están siendo tratadas como una única variable, y ésta tendrá 8 bytes de ancho. Pero para ser más claro veamos la imagen de abajo, ella representa exactamente lo que está sucediendo:

Este es el esquema que está dentro de la unión uDataServer.

Si el esquema anterior te parece complicado, es bueno que experimentes con las uniones durante un tiempo para entender cómo funcionan realmente, ya que tienen mucha utilidad en el campo de la programación.

Pero volviendo al sistema, lo siguiente que hay que hacer es crear el código del cliente, o mejor dicho del EA, y esto se puede ver a continuación en su totalidad.

#property copyright "Daniel Jose"
#property description "Testing internal channel\nvia terminal global variable"
#property version "1.04"
//+------------------------------------------------------------------+
#include <Inner Channel.mqh>
//+------------------------------------------------------------------+
enum eWhat {DOW_JONES, SP500};
input eWhat     user01 = DOW_JONES;     //Buscar
//+------------------------------------------------------------------+
int OnInit()
{
        EventSetTimer(1);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+
void OnTimer()
{
        uDataServer loc;
        
        SetFind();
        if (GlobalVariableCheck(def_GlobalValueInChannel))
        {
                GlobalVariableGet(def_GlobalMaskInfo, loc.value);
                Print(CharArrayToString(loc.Info, 0, sizeof(uDataServer)), "  ", GlobalVariableGet(def_GlobalValueInChannel));
        }
}
//+------------------------------------------------------------------+
inline void SetFind(void)
{
        static int b = -1;
        uDataServer loc1, loc2;
        
        if ((GlobalVariableCheck(def_GlobalValueInChannel)) && (b != user01))
        {
                b = user01;
                switch (user01)
                {
                        case DOW_JONES  :
                                StringToCharArray("INDU:IND", loc1.Info, 0, sizeof(uDataServer));
                                loc2.Position[0] = 172783;
                                loc2.Position[1] = 173474;
                                break;
                        case SP500              :
                                StringToCharArray("SPX:IND", loc1.Info, 0, sizeof(uDataServer));
                                loc2.Position[0] = 175484;
                                loc2.Position[1] = 176156;
                                break;
                }
                GlobalVariableSet(def_GlobalMaskInfo, loc1.value);
                GlobalVariableSet(def_GlobalPositionInfos, loc2.value);
        }
};
//+------------------------------------------------------------------+

Obsérvese que en este EA estamos pasando y recibiendo información, es decir, podemos controlar cómo debe funcionar el servicio, en una variable pasamos una pequeña string que dirá lo que debe buscar el servicio, y en otra variable pasamos 2 puntos de direccionamiento,

En respuesta el servicio devolverá una información. Pero para entender este primer momento veamos el resultado que está en el video de abajo:




3.1.2.2 - Creación de una versión práctica

Bien ahora que hemos visto como funciona el sistema, podemos hacer algo realmente funcional, y esta vez vamos a recoger información del servidor web, pero para ello tenemos que hacer una serie de modificaciones que nos aseguren una perfecta comprensión de lo que está ocurriendo, ya que no queremos imaginar que estamos recibiendo datos actualizados, cuando en realidad estamos utilizando basura en un análisis. Hay que tener mucho cuidado durante la fase de programación para no correr este riesgo, así que añade todas las pruebas posibles e intenta que el sistema informe de cualquier actividad extraña que pueda detectar durante su funcionamiento.

Recuerda: La información sólo le será útil si confía en ella.

Bien, lo primero que hay que hacer es modificar el archivo de cabecera, de modo que ahora tendrá este aspecto:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#define def_GlobalValueInChannel        "Inner Channel"
#define def_GlobalMaskInfo              "Mask Info"
#define def_GlobalPositionInfos         "Positions Infos"
//+------------------------------------------------------------------+
#define def_MSG_FailedConnection        "BAD"
#define def_MSG_FailedReturn            "FAILED"
#define def_MSG_FailedMask              "ERROR"
#define def_MSG_FinishServer            "FINISH"
//+------------------------------------------------------------------+
union uDataServer
{
        double  value;
        uint            Position[2];
        char            Info[sizeof(double)];
};
//+------------------------------------------------------------------+

Los puntos resaltados son códigos que usaremos para reportar alguna actividad extraña, debes usar un máximo de 8 caracteres, pero también tienes que crear una secuencia que difícilmente será creada por el mercado, es decir, la cosa no es fácil de implementar. Aunque todo parezca estar bien, siempre existe el riesgo de que el mercado genere un valor que coincida con la secuencia que estarás usando como mensajes de falla en el servidor. A pesar de todo, también puedes usar una variable global de terminal solo para este propósito, esto aumentará el número de combinaciones posibles y con esto puedes pasar muchas más cosas. Quería utilizar lo menos posible las variables globales de terminal, pero en un caso real pensaré honestamente en este tipo de cosas, y posiblemente utilizaría una variable sólo para indicar e informar de errores o actividades anormales.

Bueno, el siguiente código es el EA, se puede ver a continuación en su totalidad.

#property copyright "Daniel Jose"
#property description "Testing internal channel\nvia terminal global variable"
#property version "1.04"
//+------------------------------------------------------------------+
#include <Inner Channel.mqh>
//+------------------------------------------------------------------+
enum eWhat {DOW_JONES, SP500};
input eWhat     user01 = DOW_JONES;             //Buscar
//+------------------------------------------------------------------+
int OnInit()
{
        EventSetTimer(1);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+
void OnTimer()
{
        ClientServer();
}
//+------------------------------------------------------------------+
inline void ClientServer(void)
{
        uDataServer loc1, loc2;
        string          sz0;
        
        SetFind();
        if (GlobalVariableCheck(def_GlobalValueInChannel))
        {
                GlobalVariableGet(def_GlobalMaskInfo, loc1.value);
                loc2.value = GlobalVariableGet(def_GlobalValueInChannel);
                sz0 = CharArrayToString(loc2.Info, 0, sizeof(uDataServer));
                if (sz0 == def_MSG_FailedConnection) Print("Failed in connection."); else
                if (sz0 == def_MSG_FailedReturn) Print("Error in Server Web."); else
                if (sz0 == def_MSG_FailedMask) Print("Bad Mask or position."); else
                if (sz0 == def_MSG_FinishServer) Print("Service Stop."); else
                Print(CharArrayToString(loc1.Info, 0, sizeof(uDataServer)), "  ", loc2.value);
        }
}
//+------------------------------------------------------------------+
inline void SetFind(void)
{
        static int b = -1;
        uDataServer loc1, loc2;
        
        if ((GlobalVariableCheck(def_GlobalValueInChannel)) && (b != user01))
        {
                b = user01;
                switch (user01)
                {
                        case DOW_JONES  :
                                StringToCharArray("INDU:IND", loc1.Info, 0, sizeof(uDataServer));
                                loc2.Position[0] = 172783;
                                loc2.Position[1] = 173474;
                                break;
                        case SP500              :
                                StringToCharArray("SPX:IND", loc1.Info, 0, sizeof(uDataServer));
                                loc2.Position[0] = 175487;
                                loc2.Position[1] = 176159;
                                break;
                }
                GlobalVariableSet(def_GlobalMaskInfo, loc1.value);
                GlobalVariableSet(def_GlobalPositionInfos, loc2.value);
        }
};
//+------------------------------------------------------------------+

Las líneas resaltadas son muy importantes, y deben estar bien pensadas, porque realmente queremos saber lo que está pasando, vean que podemos informar al usuario algo mucho más detallado que esas secuencias creadas en el archivo de cabecera, para que sea fácil de programar y mantener. El resto del código apenas si ha cambiado, pero ahora veamos el código del servicio.

#property service
#property copyright "Daniel Jose"
#property version   "1.03"
//+------------------------------------------------------------------+
#include <Inner Channel.mqh>
//+------------------------------------------------------------------+
void OnStart()
{
        uDataServer loc1, loc2;
        
        while (!IsStopped())
        {
                if (!GlobalVariableCheck(def_GlobalValueInChannel))
                {
                        GlobalVariableTemp(def_GlobalValueInChannel);
                        GlobalVariableTemp(def_GlobalMaskInfo);
                        GlobalVariableTemp(def_GlobalPositionInfos);
                }
                GlobalVariableGet(def_GlobalMaskInfo, loc1.value);
                GlobalVariableGet(def_GlobalPositionInfos, loc2.value);
                if (!_StopFlag)
                {
                        GlobalVariableSet(def_GlobalValueInChannel, GetDataURL(
                                                                                "https://tradingeconomics.com/stocks",
                                                                                100,
                                                                                "<!doctype html>",
                                                                                2,
                                                                                CharArrayToString(loc1.Info, 0, sizeof(uDataServer)),
                                                                                loc2.Position[0],
                                                                                loc2.Position[1],
                                                                                0x0D
                                                                               )
                                        );
                        Sleep(1000);
                }
        }
        GlobalVariableSet(def_GlobalValueInChannel, Codification(def_MSG_FinishServer));
}
//+------------------------------------------------------------------+
double GetDataURL(const string url, const int timeout, const string szTest, int iTest, const string szFind, int iPos, int iInfo, char cLimit)
{
        string          headers, szInfo = "";
        char                    post[], charResultPage[];
        int                     counter;
   
        if (WebRequest("GET", url, NULL, NULL, timeout, post, 0, charResultPage, headers) == -1) return Codification(def_MSG_FailedConnection);
        for (int c0 = 0, c1 = StringLen(szTest); (c0 < c1) && (!_StopFlag); c0++) if (szTest[c0] != charResultPage[iTest + c0]) return Codification(def_MSG_FailedReturn);
        for (int c0 = 0, c1 = StringLen(szFind); (c0 < c1) && (!_StopFlag); c0++) if (szFind[c0] != charResultPage[iPos + c0]) return Codification(def_MSG_FailedMask);
        if (_StopFlag) return Codification(def_MSG_FinishServer);
        for (counter = 0; charResultPage[counter + iInfo] == 0x20; counter++);
        for (;charResultPage[counter + iInfo] != cLimit; counter++) szInfo += CharToString(charResultPage[counter + iInfo]);
        
        return StringToDouble(szInfo);
}
//+------------------------------------------------------------------+
inline double Codification(const string arg)
{
        uDataServer loc;
        StringToCharArray(arg, loc.Info, 0, sizeof(uDataServer));
        
        return loc.value;
}
//+------------------------------------------------------------------+

La línea resaltada es importante para asegurar que el servicio informará de que ya no se está ejecutando.

Pues bien, al ejecutar este sistema, obtendremos el siguiente resultado:



Conclusión

Y con esto creo haberte pasado la idea detrás de la investigación, búsqueda y uso de datos web en la plataforma MetaTrader 5, sé que al principio es algo que asusta a los que se inician en el arte de la programación, pero con el tiempo, la disciplina y el estudio, acabarás dominando gran parte de este material, aquí he intentado pasar al menos un poco de lo que sé, creo que lo he conseguido.