Uso de los repositorios en la nube para el intercambio de datos entre los terminales

Dmitriy Gizlyk | 14 septiembre, 2017


Introducción

Las tecnologías en la nube se difunden ampliamente en el mundo moderno. Podemos usar los repositorios de diferentes volúmenes, tanto de pago, como gratuitos. ¿Podemos usarlos en el trading real? En este artículo se propone la tecnología para el intercambio de datos entre los terminales con el uso de los repositorios en la nube.

Usted preguntará, ¿por qué tenemos que usar los repositorios en la nube para eso?  Es que ya existen las tecnologías de conexión directa entre los terminales. Pero yo creo que este enfoque tiene una serie de ventajas. En primer lugar, es el anonimato del proveedor: los usuarios de la información no acceden a su ordenador, sino al servidor en la nube. De esta manera, el ordenador del proveedor queda protegido contra los ataques de los virus, y no tiene que estar conectado siempre a la Red. Basta con conectarse sólo para enviar los mensajes al servidor. En segundo lugar, en la «nube» puede haber prácticamente una cantidad infinita de los proveedores. En tercer lugar, el aumento del número de usuarios no requerirá el aumento de las potencias del proveedor.

Como ejemplo, usaremos el repositorio gratuito en la nube de la empresa Google de 15 Gb. Será más que suficiente para nuestros propósitos.

1. Un poco de teoría

La autorización en Google drive esta organizada a través del protocolo OAuth 2.0. Es un protocolo abierto de la autorización que permite proporcionar a las aplicaciones ajenas y sitios web el acceso a los recursos protegidos de los usuarios autorizados sin necesidad de traspasar sus credenciales. El escenario base de acceso a través de OAuth 2.0 se compone de 4 etapas.

  1. Primero, es necesario obtener los datos para la autorización (identificador del usuario y la contraseña del usuario). Estos datos se generan por el sitio web , por consiguiente, el sitio web y la aplicación los sabe.
  2. Antes de que la aplicación pueda acceder a los datos personales, tiene que obtener el token del acceso. Un token puede proporcionar diferente grado de acceso que se determina por la variable scope. Durante la solicitud del token de acceso, la aplicación puede enviar uno o varios valores en el parámetro scope. Para crear esta solicitud, la aplicación puede usar tanto el navegador de sistema, como las solicitudes de los servicios web. Algunas solicitudes requieren la fase de autenticación en la que el usuario entra usando su cuenta de usuario. Después de entrar en el sistema, al usuario se le pregunta si está dispuesto a dar los permisos solicitados por la aplicación. Este proceso se llama el consentimiento del usuario. Si el usuario da su conformidad, el servidor de la autorización facilitará el código de autorización a la aplicación, según el cual la aplicación puede obtener el token del acceso. Si el usuario no da su permiso, el servidor devuelve el error.
  3. Una vez obtenido el token del acceso por la aplicación, ella envía el token en el encabezado de la autorización HTTP. Los puntos de acceso son válidos sólo para el conjunto de operaciones y recursos descritos en el parámetro scope de la solicitud. Por ejemplo, si ha sido generado el token de acceso para Google Drive, él no proporciona el acceso a los contactos de Google. No obstante, la aplicación puede enviar este token de acceso a Google Drive varias veces para ejecutar las operaciones permitidas.
  4. Los tokens tienen una duración de vida limitada. Si la aplicación necesita el acceso después del vencimiento de un token de acceso, puede obtener el token de la actualización que permite a la aplicación obtener nuevos tokens de acceso.

Escenario de acceso

2. Organización de acceso a Google Drive

 Para trabajar con Google drive vamos a necesitar una cuenta de usuario en Google, una nueva, o bien una que ya tenemos.

Antes de empezar a desarrollar el código de nuestro programa, vamos a realizar un trabajo preparatorio en el sitio web de Google. Para eso pasamos a la consola de los desarrolladores (para acceder a esa consola habrá que volver a autorizar la cuenta de usuario).

Creamos nuevo proyecto para nuestra aplicación. Vamos al panel de proyectos (botón "Select a project" o combinación de teclas "Ctrl + O"). Creamos un nuevo proyecto (botón "+») en el panel de proyectos.

Panel de proyectos

En la página que se abre, ponemos el nombre de nuestro proyecto, aceptamos las condicione de uso y confirmamos la creación.

Nuevo proyecto

Luego, lo seleccionamos en el panel de proyectos y le conectamos Google Drive API. Para eso, seleccionamos "Drive API" en la librería API del gestor, y luego activamos API indicada en la página que se abre usando "Enable".

Librería APIActivar API

En la página que se abre, se nos dice que para usar la API necesitamos las credenciales. Vamos a generarlas (botón "Create credentials").

Advertencia

La consola Google propondrá usar un Asistente para seleccionar el tipo de autenticación, pero no necesitamos hacer eso. Pulsamos directamente en "client ID". En la siguiente ventana, Google volverá a mostrar una advertencia sobre la necesidad de configurar la página de confirmación del acceso, lo haremos haciendo clic en el botón "Configure consent screen".

Advertencia

En la página abierta, se puede dejar todos los campos por defecto, llenando sólo el campo "Product name shown to users" (Nombre del programa que se muestra para el usuario). Después de eso, especificamos el tipo de nuestra aplicación como "Other", damos el nombre al cliente y pulsamos el botón "Create" (Crear). Como respuesta a nuestras acciones, el servidor generará los códigos "client ID" y "client secret". Se puede copiarlos pero no es necesario, ya el servidor permite descargarlos como un archivo json. Pulsamos "Ok" y descargamos el archivo json con los datos de acceso al disco local.

Pues, con eso terminamos el trabajo preparatorio en el lado del servidor, y podemos a empezar el desarrollo de nuestros programas.

3. Creamos el puente entre los programas locales y Google drive

Para resolver este problema, he creado un programa separado, en su especie un puente, que va a recibir las solicitudes y los datos de parte del Asesor Experto (EA) e indicador de MetaTrader, procesarlos, interactuar con Google drive y devolver los datos de nuevo a los programas de MetaTrader. Lo atractivo de este enfoque consiste primero en que Google proporciona las librerías para trabajar con Google drive en C#. Eso facilita considerablemente el desarrollo. Segundo, el uso de un programa ajeno libra al terminal de las operaciones «pesadas» del intercambio con el servicio externo. Tercero, eso desvincula nuestro programa de la plataforma haciéndolo multiplataforma, con posibilidad de trabajar con las aplicaciones tanto a base de MetaTrader 4, como para MetaTrader 5.

Como ya he dicho antes, el programa puente será escrito en C# , usando las librerías de Google. Vamos a crear el proyecto Windows Form en VisualStudio, y enseguida le añadimos la librería Google.Apis.Drive.v3 a través de NuGet.

Luego, creamos la clase GoogleDriveClass para trabajar con Google drive.

class GoogleDriveClass
    {
        static string[] Scopes = { DriveService.Scope.DriveFile };  //Array para trabajar con archivos
        static string ApplicationName = "Google Drive Bridge";      //Nombre del programa
        public static UserCredential credential = null;             //Claves de autorización
        public static string extension = ".gdb";                    //Extensión para los archivos a guardar
    }

Primero, vamos a crear la función de autorización en el servicio. Ahí va a usarse el archivo json con credenciales que hemos guardado antes. En mi caso es "client-secret.json". Si Usted ha guardado el archivo con otro nombre, ponga el nombre correspondiente en el código de la función. Después de cargar los datos para la autorización, será llamada la función asincrónica de autorización en el servicio. En caso de la autorización con éxito, en el objeto credential se guardará token para el acceso posterior. Durante el trabajo en C#, no olvide del procesamiento de las excepciones: en caso de la aparición de una excepción, el objeto credential será reseteado. 

        public bool Authorize()
        {
            using (System.IO.FileStream stream =
                     new System.IO.FileStream("client-secret.json", System.IO.FileMode.Open, System.IO.FileAccess.Read))
            {
                try
                {
                    string credPath = System.Environment.CurrentDirectory.ToString();
                    credPath = System.IO.Path.Combine(credPath, "drive-bridge.json");

                    credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                        GoogleClientSecrets.Load(stream).Secrets,
                        GoogleDriveClass.Scopes,
                        "example.bridge@gmail.com",
                        CancellationToken.None,
                        new FileDataStore(credPath, true)).Result;
                }
                catch (Exception)
                {
                    credential = null;
                }

            }
            return (credential != null);
        }

Durante el trabajo en Google drive, nuestro «puente» debe ejecutar dos funciones: escribir los datos en el disco y leer el archivo necesario del disco. Vamos a considerarlas con más detalles. Para implementar estas funciones, tan sencillas a primera vista, tendremos que escribir toda una serie de procedimientos. Es que el sistema de archivos de Google drive se diferencia del sistema con el que estamos acostumbrados a trabajar. Aquí los nombres y las extensiones existen como unas entradas separadas sólo para mantener la representación a la que el usuario está acostumbrado. En realidad, cuando se guarda un archivo, se le asigna un identificador único con el que va a almacenarse. De esta manera, el usuario puede guardar una cantidad ilimitada de archivos con el mismo nombre y la misma extensión. Por consiguiente, antes de acceder al archivo, tenemos que averiguar su identificador en el repositorio en la nube. Para eso, cargaremos la lista de todos los archivos en el disco y compararemos sus nombres uno por uno con el nombre establecido.

La función GetFileList se encargará de obtener la lista del archivo, ella devolverá la lista de las clases Google.Apis.Drive.v3.Data.File. Para obtener la lista de archivos desde Google drive, usaremos la clase Google.Apis.Drive.v3.DriveService desde las librerías descargadas anteriormente. Al inicializar esta clase, le pasaremos el token obtenido durante la autorización y el nombre de nuestro proyecto. Guardaremos la lista obtenida en la variable devuelta result, y en caso de excepciones, la variable será reseteada. En otras funciones de nuestro programa, vamos a solicitar y procesar la lista de archivos, según vaya surgiendo la necesidad.

using File = Google.Apis.Drive.v3.Data.File;
        public IList<File> GetFileList()
        {
            IList<File> result = null;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }
            // Create Drive API service.
            using (Google.Apis.Drive.v3.DriveService service = new Google.Apis.Drive.v3.DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            }))
            {
                try
                {
                    // Define parameters of request.
                    FilesResource.ListRequest listRequest = service.Files.List();
                    listRequest.PageSize = 1000;
                    listRequest.Fields = "nextPageToken, files(id, name, size)";

                    // List files.
                    result = listRequest.Execute().Files;
                }
                catch (Exception)
                {
                    return null;
                }
            }
            return result;
        }


3.1. Escritura de datos en el repositorio en la nube

Para escribir el archivo en el repositorio en la nube, crearemos la función FileCreate. El nombre del archivo y su contenido son los parámetros de entrada de esta función. Ella va a devolver el valor lógico sobre el resultado de la operación, y el identificador del archivo en el disco en caso de su creación con éxito. La clase Google.Apis.Drive.v3.DriveService que ya conocemos se encargará de la creación del archivo, y para enviar la solicitud, usaremos la clase Google.Apis.Drive.v3.FilesResource.CreateMediaUpload. En los parámetros del archivo, indicamos que será un simple archivo de texto y damos permiso para su copiado.

       public bool FileCreate(string name, string value, out string id)
        {
            bool result = false;
            id = null;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }
            using (var service = new Google.Apis.Drive.v3.DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            }))

            {
                var body = new File();
                body.Name = name;
                body.MimeType = "text/json";
                body.ViewersCanCopyContent = true;

                byte[] byteArray = Encoding.Default.GetBytes(value);
                using (var stream = new System.IO.MemoryStream(byteArray))
                {
                    Google.Apis.Drive.v3.FilesResource.CreateMediaUpload request = service.Files.Create(body, stream, body.MimeType);
                    if (request.Upload().Exception == null)
                    { id = request.ResponseBody.Id; result = true; }
                }
            }
            return result;
        }

El siguiente paso tras la creación del archivo será la función de su actualización. Vamos a recordar los objetivos de nuestro programa y las particularidades del sistema de archivos de Google drive. Escribimos un programa para el intercambio de datos entre varios terminales que se encuentran instalados en diferentes ordenadores. No podemos saber en qué momento y cuántos terminales necesitarán nuestra información. Pero las particularidades del sistema de archivos del repositorio en la nube nos permiten crear varios archivos con los mimos nombres y extensiones. Eso nos permite crear primero el nuevo archivo con datos nuevos, y sólo después, eliminar los datos obsoletos del repositorio en la nube. Pues, eso es lo que va a hacer nuestra nueva función FileUpdate. Sus parámetros de entrada serán el nombre del archivo y su contenido, ella va a devolver el valor lógico sobre el resultado de la operación.

Al principio de la función declararemos la variable del texto new_id y llamaremos a la función FileCreate creada antes que nos creará el nuevo archivo de datos en la nube y devolverá el identificador del archivo nuevo en nuestra variable.

Luego, obtendremos la lista de todos los archivos en la nube desde la función GetFileList y los compararemos uno por uno con el nombre e identificador del archivo creado nuevamente. Todos los duplicados innecesarios serán eliminados del repositorio. Aquí, volveremos a usar la clase Google.Apis.Drive.v3.DriveService ya conocida, y el envío de las solicitudes vamos a realizar usando la clase Google.Apis.Drive.v3.FilesResource.DeleteRequest.

        public bool FileUpdate(string name, string value)
        {
            bool result = false;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }

            string new_id;
            if (FileCreate(name, value, out new_id))
            {
                IList<File> files = GetFileList();
                if (files != null && files.Count > 0)
                {
                    result = true;
                    try
                    {
                        using (var service = new DriveService(new BaseClientService.Initializer()
                        {
                            HttpClientInitializer = credential,
                            ApplicationName = ApplicationName,
                        }))
                        {
                            foreach (var file in files)
                            {
                                if (file.Name == name && file.Id != new_id)
                                {
                                    try
                                    {
                                        Google.Apis.Drive.v3.FilesResource.DeleteRequest request = service.Files.Delete(file.Id);
                                        string res = request.Execute();
                                    }
                                    catch (Exception)
                                    {
                                        continue;
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception)
                    {
                        return result;
                    }
                }
            }
            return result;
        }

3.2. Lectura de datos desde el repositorio en la nube

Ya hemos creado las funciones para escribir los datos en el repositorio en la nube. Ahora ha llegado el momento para leerlos al revés. Como recordamos, antes de descargar el archivo, hay que obtener su identificador en la nube. De eso se encarga la función GetFileID. Su parámetro de entrada es el nombre del archivo que se busca, y su valor devuelto es su identificador. La construcción lógica no es complicada: obtenemos la lista de archivos desde la función GetFileList y buscamos el primer archivo con el nombre buscado simplemente recorriendo la lista. Hay que comprender que seguramente será el archivo más antiguo. Existe riesgo de que precisamente en este momento se guarda un archivo nuevo con parámetros necesarios, o ha ocurrido un error durante su descarga. Aceptaremos estos riesgos con el fin de obtener la información completa. Los últimos cambios se cargan durante la siguiente actualización. Es que nos acordamos de que en la función FileUpdate se eliminan todos los duplicados no necesarios después de la creación del archivo nuevo.

        public string GetFileId(string name)
        {
            string result = null;
            IList<File> files = GetFileList();

            if (files != null && files.Count > 0)
            {
                foreach (var file in files)
                {
                    if (file.Name == name)
                    {
                        result = file.Id;
                        break;
                    }
                }
            }
            return result;
        }

Después de obtener el identificador del archivo, podemos obtener la información necesaria. Para eso, escribiremos la función FileRead, en la que pasaremos el identificador del archivo que necesitamos, y la función devolverá su contenido. En caso del fallo, la función devolverá una línea vacía. Igual como antes, necesitaremos la clase Google.Apis.Drive.v3.DriveService para crear la conexión y la clase Google.Apis.Drive.v3.FilesResource.GetRequest para crear la solicitud.

        public string FileRead(string id)
        {
            if (String.IsNullOrEmpty(id))
            {
                return ("Errore. File not found");
            }
            bool result = false;
            string value = null;
            if (credential == null)
                this.Authorize();
            if (credential != null)
            {
                using (var service = new DriveService(new BaseClientService.Initializer()
                {
                    HttpClientInitializer = credential,
                    ApplicationName = ApplicationName,
                }))
                {
                    Google.Apis.Drive.v3.FilesResource.GetRequest request = service.Files.Get(id);
                    using (var stream = new MemoryStream())
                    {
                        request.MediaDownloader.ProgressChanged += (IDownloadProgress progress) =>
                        {
                            if (progress.Status == DownloadStatus.Completed)
                                result = true;
                        };
                        request.Download(stream);

                        if (result)
                        {
                            int start = 0;
                            int count = (int)stream.Length;
                            value = Encoding.Default.GetString(stream.GetBuffer(), start, count);
                        }
                    }
                }
            }
            return value;
        }

3.3. Creación del bloque de interacción con las aplicaciones del terminal

Ahora, cuando ya hemos construido la conexión de nuestro programa con el repositorio en la nube Google Drive, ha llegado el momento para conectarlo con las aplicaciones MetaTrader. Pues, precisamente en eso consiste su tarea principal.He decidido construir esta conexión usando las tuberías nombradas. El trabajo con ellas ya fue descrito en el sitio web, y los desarrolladores ya incluyeron la clase CFilePipe para trabajar con este tipo de conexiones en el lenguaje MQL5. Eso facilitará nuestro trabajo a la hora de crear las aplicaciones.

En el terminal, se puede iniciar varias aplicaciones al mismo tiempo. Por eso, nuestro «puente» tiene que saber procesar varias aplicaciones simultáneamente. Para eso usamos los modelos de la programación asíncrona multihilo.

Desde el principio, vamos a aclarar el formato de los mensajes traspasados entre el puente y la aplicación. Para leer un archivo desde la nube, hay que enviar el comando y el nombre del archivo. Para escribir un archivo en la nube, hay que enviar el comando, el nombre del archivo y su contenido. Puesto que en la tubería vamos a transmitir los datos usando un hilo, es lógico traspasar toda la información en una línea. Para separar los campos en la línea, he usado el separador ";".

Primero, declaramos las variables globales:

  • Drive — la clase que hemos creado antes para trabajar con el repositorio en la nube;
  • numThreads — establece el número de hilos simultáneos;
  • pipeName — variable string que guarda el nombre de nuestras tuberías;
  • servers — array de hilos operacionales.
        GoogleDriveClass Drive = new GoogleDriveClass();
        private static int numThreads = 10;
        private static string pipeName = "GoogleBridge";
        static Thread[] servers;

Vamos a crear la función para iniciar los hilos operacionales PipesCreate. Ahí inicializamos el array de nuestros hilos y los iniciamos cíclicamente.Durante el arranque de cada hilo, se invoca la función ServerThread que inicializa las funciones en nuestros hilos.

        public void PipesCreate()
        {
            int i;
            servers = new Thread[numThreads];

            for (i = 0; i < numThreads; i++)
            {
                servers[i] = new Thread(ServerThread);
                servers[i].Start();
            }
        }

Además, durante el arranque de cada hilo, se crea una tubería nombrada y se inicia la función asíncrona de la espera de la conexión del cliente con la tubería.Cuando el cliente se conecta a la tubería, se invoca la función Connected, para eso creamos el delegado AsyncCallback asyn_connected. Si surge la excepción, el hilo se reinicia.

        private void ServerThread()
        {
            NamedPipeServerStream pipeServer =
                new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous);

            int threadId = Thread.CurrentThread.ManagedThreadId;
            // Wait for a client to connect
            AsyncCallback asyn_connected = new AsyncCallback(Connected);
            try
            {
                pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
            }
            catch (Exception)
            {
                servers[threadId].Suspend();
                servers[threadId].Start();
            }
        }

Cuando el cliente se conecta a la tubería nombrada, comprobamos el estado de la tubería y si surge la excepción, reiniciamos el hilo. Si la conexión es estable, iniciamos la función de la lectura de la solicitud de parte de la aplicación. Si la función de la lectura devuelve false, reiniciamos la conexión.

        private void Connected(IAsyncResult pipe)
        {
            if (!pipe.IsCompleted)
                return;
            bool exit = false;
            try
            {
                NamedPipeServerStream pipeServer = (NamedPipeServerStream)pipe.AsyncState;
                try
                {
                    if (!pipeServer.IsConnected)
                        pipeServer.WaitForConnection();
                }
                catch (IOException)
                {
                    AsyncCallback asyn_connected = new AsyncCallback(Connected);
                    pipeServer.Dispose();
                    pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous);
                    pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
                    return;
                }
                while (!exit && pipeServer.IsConnected)
                {
                    // Read the request from the client. Once the client has
                    // written to the pipe its security token will be available.

                    while (pipeServer.IsConnected)
                    {
                        if (!ReadMessage(pipeServer))
                        {
                            exit = true;
                            break;
                        }
                    }
                    //Wait for a client to connect
                    AsyncCallback asyn_connected = new AsyncCallback(Connected);
                    pipeServer.Disconnect();
                    pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
                    break;
                }
            }
            finally
            {
                exit = true;
            }
        }

La función ReadMessage va a leer y procesar la solicitud de parte de la aplicación. La referencia al objeto del hilo se traspasa a la función como parámetro. El resultado de la función es el valor lógico sobre la ejecución de la operación. Primero, la función lee la solicitud de parte de la aplicación desde la tubería nombrada y la divide en los campos. Luego, reconoce el comando y ejecuta las acciones necesarias.

He previsto 3 comandos en la función:

  • Close — cierre de la conexión actual;
  • Read — lectura del archivo desde la nube;
  • Write — escritura del archivo en la nube.

Para cerrar la conexión actual, será suficiente que la función devuelva false, la función Connected que la ha llamado hará el resto.

Para ejecutar la solicitud de la lectura del archivo desde la nube, determinamos el identificador del archivo y leemos su solicitud usando las funciones GetFileID y FileRead descritas antes.

Para ejecutar la función de la escritura del archivo en la nube, llamamos a la función FileUpdate creada antes.

Y claro que no olvidamos del procesamiento de las excepciones. En caso de la aparición de una excepción, realizamos de nuevo la autorización en Google.

        private bool ReadMessage(PipeStream pipe)
        {
            if (!pipe.IsConnected)
                return false;

            byte[] arr_read = new byte[1024];
            string message = null;
            int length;
            do
            {
                length = pipe.Read(arr_read, 0, 1024);
                if (length > 0)
                    message += Encoding.Default.GetString(arr_read, 0, length);
            } while (length >= 1024 && pipe.IsConnected);
            if (message == null)
                return true;

            if (message.Trim() == "Close\0")
                return false;

            string result = null;
            string[] separates = { ";" };
            string[] arr_message = message.Split(separates, StringSplitOptions.RemoveEmptyEntries);
            if (arr_message[0].Trim() == "Read")
            {
                try
                {
                    result = Drive.FileRead(Drive.GetFileId(arr_message[1].Trim() + GoogleDriveClass.extension));
                }
                catch (Exception e)
                {
                    result = "Error " + e.ToString();
                    Drive.Authorize();
                }
                return WriteMessage(pipe, result);
            }

            if (arr_message[0].Trim() == "Write")
            {
                try
                {
                    result = (Drive.FileUpdate(arr_message[1].Trim() + GoogleDriveClass.extension, arr_message[2].Trim()) ? "Ok" : "Error");
                }
                catch (Exception e)
                {
                    result = "Error " + e.ToString();
                    Drive.Authorize();
                }

                return WriteMessage(pipe, result);
            }
            return true;
        }

Después de procesar las solicitudes, tenemos que devolver el resultado de la ejecución de la operación a la aplicación. Creamos la función WriteMessage. La referencia al objeto de la tubería nombrada actual y el mensaje que se envía a la aplicación son sus parámetros. La función va a devolver un valor lógico sobre el resultado de la ejecución de la operación.

        private bool WriteMessage(PipeStream pipe, string message)
        {
            if (!pipe.IsConnected)
                return false;
            if (message == null || message.Count() == 0)
                message = "Empty";
            byte[] arr_bytes = Encoding.Default.GetBytes(message);
            try
            {
                pipe.Flush();
                pipe.Write(arr_bytes, 0, arr_bytes.Count());
                pipe.Flush();
            }
            catch (IOException)
            {
                return false;
            }
            return true;
        }

Ahora, cuando hemos descrito todas las funciones necesarias, nos queda iniciar la función PipesCreate. He creado el proyecto Windows Form, por eso inicio esta función desde la función Form1.

        public Form1()
        {
            InitializeComponent();
            PipesCreate();
        }

Nos queda recompilar el proyecto y compilar el archivo json con los datos para el acceso al repositorio en la nube, colocándolo en la carpeta con el programa.
  

4. Creamos las aplicaciones en MetaTrader

Vamos a la aplicación práctica de nuestro programa. Para empezar, propongo escribir un programa para copiar los objetos gráficos simples.

4.1. Clase para trabajar con los objetos gráficos

Primero, vamos a reflexionar qué información exactamente sobre el objeto tenemos que pasar para reconstruir en otro gráfico. Tal vez, en primer lugar, hay que pasar el tipo del objeto y su nombre para la identificación. También necesitaremos el color del objeto y sus coordenadas. Pues, aquí surge la primera pregunta, ¿cuántas y qué tipo de coordenadas habrá que pasar? Por ejemplo, al enviar la información sobre una línea vertical, basta con pasar sólo la fecha. Para una línea horizontal, hay que pasar el precio, en vez de la fecha Para una línea de tendencia hacen falta dos pares de coordenadas: la fecha y el precio, más la información sobre la dirección en la que continuará la línea, derecha o izquierda. Diferentes objetos tienen tanto los parámetros comunes, como los únicos. Pero en MQL5, todos los objetos se crean y se alteran por cuatro funciones: ObjectCreate, ObjectSetInteger, ObjectSetDouble y ObjectSetString. Seguiremos el mismo camino, y vamos a pasar el tipo del parámetro, la propiedad y el valor.

Crearemos la enumeración de los tipos del parámetro.

enum ENUM_SET_TYPE
  {
   ENUM_SET_TYPE_INTEGER=0,
   ENUM_SET_TYPE_DOUBLE=1,
   ENUM_SET_TYPE_STRING=2
  };

Crearemos la clase CCopyObject para procesar la información sobre los objetos. Durante la inicialización, se le pasa el parámetro string. A continuación, ella va a identificar los objetos creados en el gráfico por nuestra clase. Este valor lo guardamos en la variable de la clase s_ObjectsID.

class CCopyObject
  {
private:
   string            s_ObjectsID;

public:
                     CCopyObject(string objectsID="CopyObjects");
                    ~CCopyObject();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CCopyObject::CCopyObject(string objectsID="CopyObjects")
  {
   s_ObjectsID = (objectsID==NULL || objectsID=="" ? "CopyObjects" : objectsID);
  }

4.1.1. Bloque de las funciones para recopilar la información sobre el objeto

Primero, creamos la función CreateMessage. Su parámetro es el identificador del gráfico necesario. La función va a devolver el valor del texto para el envío al repositorio en la nube con la lista de parámetros del objeto y sus valores. La línea devuelta debe estar estructurada para poder leer estos datos luego. Quedamos en que los datos sobre cada objeto serán encerados entre las llaves, el separador entre los parámetros será el signo "|", y el separador entre el parámetro y su valor será el signo "=".  Al principio de la descripción de cada objeto, se indica su nombre y el tipo, y luego se invoca la función de la descripción del objeto que corresponde a su tipo.

string CCopyObject::CreateMessage(long chart)
  {
   string result = NULL;
   int total = ObjectsTotal(chart, 0);
   for(int i=0;i<total;i++)
     {
      string name = ObjectName(chart, i, 0);
      switch((ENUM_OBJECT)ObjectGetInteger(chart,name,OBJPROP_TYPE))
        {
         case OBJ_HLINE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_HLINE)+"|"+HLineToString(chart, name)+"}";
           break;
         case OBJ_VLINE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_VLINE)+"|"+VLineToString(chart, name)+"}";
           break;
         case OBJ_TREND:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_TREND)+"|"+TrendToString(chart, name)+"}";
           break;
         case OBJ_RECTANGLE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_RECTANGLE)+"|"+RectangleToString(chart, name)+"}";
           break;
        }
     }
   return result;
  }

Así, para la descripción de una línea horizontal se invoca la función HLineToString. Sus parámetros serán el identificador del gráfico y el nombre del objeto. La función va a devolver una línea estructurada con el parámetro del objeto. Por ejemplo, para la línea horizontal habrá que pasar el precio, color, estilo y el grosor de la línea, así como indicar si hay que mostrar la línea sobre el gráfico en el fondo. Además, antes de la propiedad del parámetro, no olvidemos indicar su tipo desde la enumeración creada antes.

string CCopyObject::HLineToString(long chart,string name)
  {
   string result = NULL;
   if(ObjectFind(chart,name)!=0)
      return result;
   
   result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=0="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_COLOR)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_COLOR,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_STYLE)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_STYLE,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_BACK)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_BACK,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_WIDTH)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_WIDTH,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TEXT)+"=0="+ObjectGetString(chart,name,OBJPROP_TEXT,0)+"|";
   result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TOOLTIP)+"=0="+ObjectGetString(chart,name,OBJPROP_TOOLTIP,0);
   return result;
  }

De la misma manera, vamos a crear las funciones de la descripción para otros tipos de objetos. En mi ejemplo, es VLineToString para la línea vertical, TrendToString para la línea de tendenci y RectangleToString para el rectángulo. Puede estudiar el código de estas funciones en el código adjunto de la clase.

4.1.2. Función de visualización de objetos en el gráfico

Ya hemos creado la función para recopilar la información. Ahora, nos centramos en la función que va a leer el mensaje y visualizar los objetos en el gráfico: DrawObjects. Sus parámetros son el identificador del gráfico para mostrar los objetos y el mensaje obtenido. La función va a devolver un valor lógico sobre la ejecución de la operación.

El algoritmo de la función incluye varios pasos:

  • división del mensaje string en el array de líneas por objetos;
  • división de cada elemento del array de objetos en el array de parámetros;
  • en el array de parámetros, buscamos el nombre y el tipo del objeto, añadimos nuestro identificador al nombre;
  • buscamos el objeto con el nombre obtenido en el gráfico; si el objeto no se encuentra en la subventana principal o su tipo se deferencia del indicado en el mensaje, él se elimina;
  • creamos nuevo objeto en el gráfico, si el objeto todavía no existe o ha sido eliminado en el punto anterior;
  • pasamos las propiedades del objeto obtenidas en el mensaje al objeto de nuestro gráfico (se ejecuta por la función adicional CopySettingsToObject);
  • eliminamos los objetos sobrantes del gráfico (se ejecuta por la función adicional DeleteExtraObjects). 
bool CCopyObject::DrawObjects(long chart,string message)
  {
   //--- Split message to objects
   StringTrimLeft(message);
   StringTrimRight(message);
   if(message==NULL || StringLen(message)<=0)
      return false;
   StringReplace(message,"{","");
   string objects[];
   if(StringSplit(message,'}',objects)<=0)
      return false;
   int total=ArraySize(objects);
   SObject Objects[];
   if(ArrayResize(Objects,total)<0)
      return false;
  
   //--- Split every object message to settings
   for(int i=0;i<total;i++)
     {
      string settings[];
      int total_settings=StringSplit(objects[i],'|',settings);
      //--- Search name and type of object
      int set=0;
      while(set<total_settings && Objects[i].name==NULL && Objects[i].type==-1)
        {
         string param[];
         if(StringSplit(settings[set],'=',param)<=1)
           {
            set++;
            continue;
           }
         string temp=param[0];
         StringTrimLeft(temp);
         StringTrimRight(temp);
         if(temp=="NAME")
           {
            Objects[i].name=param[1];
            StringTrimLeft(Objects[i].name);
            StringTrimRight(Objects[i].name);
            Objects[i].name=s_ObjectsID+Objects[i].name;
           }
         if(temp=="TYPE")
            Objects[i].type=(int)StringToInteger(param[1]);
         set++;
        }
      //--- if name or type of object not found go to next object
      if(Objects[i].name==NULL || Objects[i].type==-1)
         continue;
      //--- Search object on chart
      int subwindow=ObjectFind(chart,Objects[i].name);
      //--- if object found on chat but it not in main subwindow or its type is different we delete this oject from chart
      if(subwindow>0 || (subwindow==0 && ObjectGetInteger(chart,Objects[i].name,OBJPROP_TYPE)!=Objects[i].type))
        {
         if(!ObjectDelete(chart,Objects[i].name))
            continue;
         subwindow=-1;
        }
      //--- if object doesn't found create it on chart
      if(subwindow<0)
        {
         if(!ObjectCreate(chart,Objects[i].name,(ENUM_OBJECT)Objects[i].type,0,0,0))
            continue;
         ObjectSetInteger(chart,Objects[i].name,OBJPROP_HIDDEN,true);
         ObjectSetInteger(chart,Objects[i].name,OBJPROP_SELECTABLE,false);
         ObjectSetInteger(chart,Objects[i].name,OBJPROP_SELECTED,false);
        }      
      //---
      CopySettingsToObject(chart,Objects[i].name,settings);
     }
   //---
   DeleteExtraObjects(chart,Objects);
   return true;
  }

La función para asignar las propiedades obtenidas en el mensaje al objeto del gráfico es universal, y se aplica a cualquier tipo de objetos. Los parámetros que recibe son los siguientes: el identificador del gráfico, nombre del objeto y el array string de parámetros. Cada elemento del array se divide en el tipo de la operación, propiedad, modificador y el valor. Los valores obtenidos se asignan al objeto a través de la función correspondiente al tipo de la operación. 

bool CCopyObject::CopySettingsToObject(long chart,string name,string &settings[])
  {
   int total_settings=ArraySize(settings);
   if(total_settings<=0)
      return false;
   
   for(int i=0;i<total_settings;i++)
     {
      string setting[];
      int total=StringSplit(settings[i],'=',setting);
      if(total<3)
         continue;
      switch((ENUM_SET_TYPE)StringToInteger(setting[0]))
        {
         case ENUM_SET_TYPE_INTEGER:
           ObjectSetInteger(chart,name,(ENUM_OBJECT_PROPERTY_INTEGER)StringToInteger(setting[1]),(int)(total==3 ? 0 : StringToInteger(setting[2])),StringToInteger(setting[total-1]));
           break;
         case ENUM_SET_TYPE_DOUBLE:
           ObjectSetDouble(chart,name,(ENUM_OBJECT_PROPERTY_DOUBLE)StringToInteger(setting[1]),(int)(total==3 ? 0 : StringToInteger(setting[2])),StringToDouble(setting[total-1]));
           break;
         case ENUM_SET_TYPE_STRING:
           ObjectSetString(chart,name,(ENUM_OBJECT_PROPERTY_STRING)StringToInteger(setting[1]),(int)(total==3 ? 0 : StringToInteger(setting[2])),setting[total-1]);
           break;
        }
     }
   return true;
  }

Después de visualizar los objetos en el gráfico, hay que comparar los objetos existentes en el gráfico con los objetos pasados en el mensaje. Del gráfico, se eliminan los objetos «sobrantes» que contienen el identificador necesario pero que no se encuentran en el mensaje (son objetos eliminado por el proveedor). De eso se encarga la función DeleteExtraObjects. Sus parámetros son los siguientes: el identificador del gráfico y el array de estructuras que contiene el nombre y el tipo del objeto.

void CCopyObject::DeleteExtraObjects(long chart,SObject &Objects[])
  {
   int total=ArraySize(Objects);
   for(int i=0;i<ObjectsTotal(chart,0);i++)
     {
      string name=ObjectName(chart,i,0);
      if(StringFind(name,s_ObjectsID)!=0)
         continue;
      bool found=false;
      for(int obj=0;(obj<total && !found);obj++)
        {
         if(name==Objects[obj].name && ObjectGetInteger(chart,name,OBJPROP_TYPE)==Objects[obj].type)
           {
            found=true;
            break;
           }
        }
      if(!found)
        {
         if(ObjectDelete(chart,name))
            i--;
        }
     }
   return;
  }

4.2. Aplicación-proveedor

Nos acercamos poco a poco al final. Crearemos un programa-proveedor que va a recopilar y enviar los datos sobre los objetos al repositorio en la nube. Elegiremos para eso una forma del EA. Habrá sólo un parámetro externo:  la variable lógica SendAtStart que determina si hace falta enviar los datos inmediatamente después de cargar el programa en el terminal.

sinput bool       SendAtStart =  true; //Send message at Init

En la cabecera de la aplicación, incluimos las librerías que necesitamos: la clase creada antes para trabajar con objetos gráficos y la clase para trabajar con las tuberías nombradas. Además, indicaremos el nombre de la tubería a la que va a conectarse la aplicación.

#include <CopyObject.mqh>
#include <Files\FilePipe.mqh>

#define                     Connection       "\\\\.\\pipe\\GoogleBridge"

En las variables globales, declaramos la clase para trabajar con los objetos gráficos, la variable string para guardar el último mensaje enviado y el array tipo uchar donde escribiremos el comando para cerrar la conexión con el repositorio en la nube.

CCopyObject *CopyObjects;
string PrevMessage;
uchar Close[];

En la función OnInit, inicializamos las variables globales e iniciamos la función del envío de la información al repositorio en la nube (si es necesario). 

int OnInit()
  {
//---
   CopyObjects = new CCopyObject();
   PrevMessage="Init";
   StringToCharArray(("Close"),Close,0,WHOLE_ARRAY,CP_UTF8);
   if(SendAtStart)
      SendMessage(ChartID());
//---
   return(INIT_SUCCEEDED);
  }

En la función OnDeinit, eliminamos el objeto de la clase para trabajar con los objetos gráficos.

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(CopyObjects)!=POINTER_INVALID)
      delete CopyObjects;
  }

La función del envío de los mensajes informativos al repositorio en la nube se invoca desde la función OnChartEvent cuando ocurre uno de los eventos de la creación, cambio o eliminación del objeto en el gráfico.

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   int count=10;
   switch(id)
     {
      case CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_OBJECT_DELETE:
      case CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_OBJECT_DRAG:
      case CHARTEVENT_OBJECT_ENDEDIT:
        while(!SendMessage(ChartID()) && !IsStopped() && count>=0)
           {
            count--;
            Sleep(500);
           }
        break;
     }      
  }

Las operaciones principales van a ejecutarse en la función SendMessage cuyo parámetro de entrada será el identificador del gráfico para el trabajo. Su algoritmo se puede dividir en varios pasos:

  • comprobamos el estado de la clase del trabajo con los objetos gráficos; si es necesario, volvemos a inicializarla;
  • usando la función CreatMessage creada antes, formamos el mensaje para enviar a la nube. Si el mensaje está en blanco o es igual al último enviado, salimos de la función;
  • creamos el nombre del archivo para el envío a la nube a base del instrumento del gráfico;
  • abrimos la conexión con nuestro programa-puente a través de la tubería nombrada;
  • usando la conexión abierta, enviamos la orden del envío del mensaje al repositorio en la nube con la indicación del nombre del archivo;
  • después de recibir la respuesta sobre la ejecución de la orden del envío, enviamos la orden del cierre de la conexión con la nube y rompemos la tubería nombrada con la aplicación-puente;
  • antes de salir del programa, eliminamos el objeto del trabajo con las tuberías nombradas.
Sobre la marcha de las operaciones, mostramos los mensajes informativos en los comentarios para el gráfico.
bool SendMessage(long chart)
  {
   Comment("Sending message");
   if(CheckPointer(CopyObjects)==POINTER_INVALID)
     {
      CopyObjects = new CCopyObject();
      if(CheckPointer(CopyObjects)==POINTER_INVALID)
         return false;
     }
   string message=CopyObjects.CreateMessage(chart);
   if(message==NULL || PrevMessage==message)
      return true;
   
   string Name=SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_BASE)+SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_PROFIT);
   CFilePipe *pipe=new CFilePipe();
   int handle=pipe.Open(Connection,FILE_WRITE|FILE_READ);
   if(handle<=0)
     {
      Comment("Pipe doesn't found");
      delete pipe;
      return false;
     }
   uchar iBuffer[];
   int size=StringToCharArray(("Write;"+Name+";"+message),iBuffer,0,WHOLE_ARRAY,CP_UTF8);
   if(pipe.WriteArray(iBuffer)<=0)
     {
      Comment("Error of sending request");
      pipe.Close();
      delete pipe;
      return false;
     }
   ArrayFree(iBuffer);
   uint res=0;
   do
     {
      res=pipe.ReadArray(iBuffer);
     }
   while(res==0 && !IsStopped());
   
   if(res>0)
     {
      string result=CharArrayToString(iBuffer,0,WHOLE_ARRAY,CP_UTF8);
      if(result!="Ok")
        {
         Comment(result);
         pipe.WriteArray(Close);
         pipe.Close();
         delete pipe;
         return false;
        }
     }
   PrevMessage=message;
   pipe.WriteArray(Close);
   pipe.Close();
   delete pipe;
   Comment("");
   return true;
  }

4.3. Aplicación-usuario

Al final, crearemos la aplicación-usuario que va a recibir la información desde el repositorio en la nube, y luego crear y modificar los objetos gráficos en el gráfico. Como en la aplicación anterior, en la cabecera, incluimos las librerías necesarias e indicamos el nombre de la tubería usada.

#include <CopyObject.mqh>
#include <Files\FilePipe.mqh>

#define                     Connection       "\\\\.\\pipe\\GoogleBridge"

Esta aplicación va a tener tres parámetros externos: tiempo en segundos que especifica la periodicidad de la actualización de los datos del repositorio en la nube, identificador de objetos en el gráfico y el valor lógico que indica en la necesidad de eliminar todos los objetos creados del gráfico al cerrar la aplicación.

sinput int        RefreshTime =  10; //Time to refresh data, sec
sinput string     ObjectsID   =  "GoogleDriveBridge";
sinput bool       DeleteAtClose = true;   //Delete objects from chart at close program

En las variables globales de la aplicación y en la aplicación-proveedor, vamos a declarar la clase para trabajar con los objetos gráficos, la variable string para guardar el último mensaje recibido y el array tipo uchar donde escribiremos el comando para cerrar la conexión con el repositorio en la nube. Además, añadiremos una variable lógica sobre el estado del temporizador y las variables para almacenar la hora de la última actualización y la visualización del último comentario en el gráfico.

CCopyObject *CopyObjects;
string PrevMessage;
bool timer;
datetime LastRefresh,CommentStart;
uchar Close[];

Inicializamos las variables globales y el temporizador en la función OnInit.

int OnInit()
  {
//---
   CopyObjects = new CCopyObject(ObjectsID);
   PrevMessage="Init";
   timer=EventSetTimer(1);
   if(!timer)
     {
      Comment("Error of set timer");
      CommentStart=TimeCurrent();
     }
   LastRefresh=0;
   StringToCharArray(("Close"),Close,0,WHOLE_ARRAY,CP_UTF8);
   
//---
   return(INIT_SUCCEEDED);
  }

En la función de deinicialización OnDeinit, eliminamos el objeto de la clase del trabajo con los objetos gráficos, detenemos el temporizador, vaciamos los comentarios, y si hace falta, eliminamos los objetos creados por la aplicación del gráfico.

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(CopyObjects)!=POINTER_INVALID)
      delete CopyObjects;
   EventKillTimer();
   Comment("");
   if(DeleteAtClose)
     {
      for(int i=0;i<ObjectsTotal(0,0);i++)
        {
         string name=ObjectName(0,i,0);
         if(StringFind(name,ObjectsID,0)==0)
           {
            if(ObjectDelete(0,name))
               i--;
           }
        }
     }
  }

Comprobamos el estado del temporizador en la función OnTick, y si hace falta, volvemos a activarlo.

void OnTick()
  {
//---
   if(!timer)
     {
      timer=EventSetTimer(1);
      if(!timer)
        {
         Comment("Error of set timer");
         CommentStart=TimeCurrent();
        }
      OnTimer();
     }
  }

En la función OnTimer vaciamos los comentarios que se encuentran en el gráfico más de 10 segundos, y llamamos a la función de la lectura del archivo de datos desde el repositorio en la nube (ReadMessage). Después de descargar la información con éxito, se cambia la hora de la última actualización de datos.

void OnTimer()
  {
//---
   if((TimeCurrent()-CommentStart)>10)
     {
      Comment("");
     }
   if((TimeCurrent()-LastRefresh)>=RefreshTime)
     {
      if(ReadMessage(ChartID()))
        {
         LastRefresh=TimeCurrent();
        }
     }
  }

Las acciones principales para descargar los datos desde el repositorio y para mostrar los objetos en el gráfico se ejecutan en la función ReadMessage. Esta función tiene sólo un parámetro, es el identificador del gráfico con el que ella va a trabajar.Las operaciones que se ejecutan en la función se puede dividir en varias fases:

  • según el instrumento del gráfico , formamos el nombre del archivo para la lectura desde la nube;
  • abrimos una tubería nombrada de la conexión con el programa-puente;
  • enviamos la solicitud para leer los datos desde el repositorio en la nube con la indicación del archivo necesario;
  • leemos el resultado del procesamiento de la solicitud;
  • enviamos la orden del cierre de la conexión con la nube y rompemos la tubería nombrada con la aplicación-puente;
  • comparamos el resultado obtenido con el mensaje anterior. Si los datos son idénticos, salimos de la función;
  • pasamos el mensaje obtenido en la función DrawObjects del objeto de la clase del procesamiento de los elementos gráficos;
  • después de procesar con éxito el mensaje, lo guardamos en la variable PrevMessage para compararlo después con los datos obtenidos.
bool ReadMessage(long chart)
  {
   string Name=SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_BASE)+SymbolInfoString(ChartSymbol(chart),SYMBOL_CURRENCY_PROFIT);
   CFilePipe *pipe=new CFilePipe();
   if(CheckPointer(pipe)==POINTER_INVALID)
      return false;
  
   int handle=pipe.Open(Connection,FILE_WRITE|FILE_READ);
   if(handle<=0)
     {
      Comment("Pipe doesn't found");
      CommentStart=TimeCurrent();
      delete pipe;
      return false;
     }
   Comment("Send request");
   uchar iBuffer[];
   int size=StringToCharArray(("Read;"+Name+";"),iBuffer,0,WHOLE_ARRAY,CP_UTF8);
   if(pipe.WriteArray(iBuffer)<=0)
     {
      pipe.Close();
      delete pipe;
      return false;
     }
   Sleep(10);
   ArrayFree(iBuffer);
   Comment("Read message");
   
   uint res=0;
   do
     {
      res=pipe.ReadArray(iBuffer);
     }
   while(res==0 && !IsStopped());
   
   Sleep(10);
   Comment("Close connection");
   pipe.WriteArray(Close);
   pipe.Close();
   delete pipe;
   Comment("");
      
   string result=NULL;
   if(res>0)
     {
      result=CharArrayToString(iBuffer,0,WHOLE_ARRAY,CP_UTF8);
      if(StringFind(result,"Error",0)>=0)
        {
         Comment(result);
         CommentStart=TimeCurrent();
         return false;
        }
     }
   else
     {
      Comment("Empty message");
      return false;
     }
   
   if(result==PrevMessage)
      return true;
  
   if(CheckPointer(CopyObjects)==POINTER_INVALID)
     {
      CopyObjects = new CCopyObject();
      if(CheckPointer(CopyObjects)==POINTER_INVALID)
         return false;
     }
   if(CopyObjects.DrawObjects(chart,result))
     {
      PrevMessage=result;
     }
   else
     {
      return false;
     }
   return true;
  }

5. Iniciando la aplicación por primera vez

Después de trabajar tanto, ha llegado el momento para ver los resultados de nuestro trabajo. Iniciamos el programa-puente. No olvidamos comprobar la presencia del archivo con los dados obtenidos del servidor Google para la conexión a nuestro repositorio en la nube, client-secret.json, que debe ubicarse en la carpeta con el programa. Luego, iniciamos una de nuestras aplicaciones MetaTrader. Al acceder a la nube por primera vez, el programa-puente iniciará la aplicación Internet establecida por defecto con la página de identificación del acceso a la cuenta de usuario Google.

Página de acceso a la cuenta de usuario Google

Aquí, hay que introducir el correo electrónico especificado en el momento de registrar la cuenta Google, e ir a la siguiente página (botón NEXT). En la siguiente página, introducimos la contraseña de acceso a la cuenta de usuario.

Contraseña de acceso a la cuenta de usuario Google

En la siguiente página, Google pedirá confirmar los derechos de acceso de la aplicación a nuestro repositorio en la nube. Tenemos que conocer los derechos de acceso solicitados y aceptarlos (botón ALLOW).

Confirmación de los derechos de acceso

Como resultado, en la carpeta con nuestro programa-puente será creada la subcarpeta drive-bridge.json. Ahí se almacena el archivo con el token de acceso al repositorio en la nube. Luego, al copiar la aplicación a otros ordenadores, junto con el programa-puente también debe copiarse esta subcarpeta. Eso librará de la necesitad de repetir el procedimiento y pasar los datos de acceso al repositorio a los terceros.

Archivo de los permisos en la subcarpeta del programa

Conclusión

En este artículo, hemos considerado uno de los ejemplos del uso de los repositorios en la nube para las tareas prácticas. El programa-puente que hemos creado es un medio universal para la descarga de los datos en el repositorio y la descarga inversa en nuestras aplicaciones. La solución propuesta para transmitir los objetos gráficos le permitirá intercambiar sus resultados del análisis técnico con sus colegas en tiempo real. Probablemente, de la misma manera, alguien decidirá transmitir las señales de trading o impartir clases sobre el análisis técnico de los gráficos.

¡Deseo a todos operar con éxito!