Usando armazenamento em nuvem para intercâmbio de dados entre os terminais

10 outubro 2017, 08:05
Dmitriy Gizlyk
0
1 061

Introdução

As tecnologias baseadas em nuvem estão se tornando mais populares no mundo moderno. Podemos usar serviços de armazenamento pagos ou gratuitos de vários tamanhos. Mas será que é possível usá-los na negociação prática? Este artigo apresenta uma tecnologia para intercâmbio de dados entre terminais através de serviços de armazenamento em nuvem.

Você pode perguntar: "por que precisamos de um armazenamento em nuvem para isso quando já temos soluções para uma conexão direta entre os terminais?" Eu acho que essa abordagem tem uma série de vantagens. Em primeiro lugar, o provedor permanece anônimo, quer dizer, os usuários obtêm acesso a um servidor em nuvem em vez do PC do provedor. Assim, o computador do provedor está protegido contra ataques de vírus e não precisa estar conectado permanentemente à internet. Basta se conectar apenas a fim de enviar mensagens para o servidor. Em segundo lugar, uma nuvem pode conter um número praticamente ilimitado de provedores. E em terceiro lugar, à medida que o número de usuários aumenta, os provedores não precisam melhorar suas capacidades de computação.

Usaremos o armazenamento gratuito em nuvem de 15 GB fornecido pela empresa Google como exemplo. Isso é mais que suficiente para nossos objetivos.

1. Um pouco de teoria

A autorização no Google Drive é organizada através do protocolo OAuth 2.0. Este é um protocolo de autorização aberto que permite que aplicativos e sites de terceiros tenham acesso limitado a recursos protegidos de usuários autorizados sem a necessidade de credenciais. O cenário básico de acesso ao protocolo OAuth 2.0 consiste em 4 etapas.

  1. Primeiro, é preciso obter dados para autorização (ID do cliente e segredo do cliente). Esses dados são gerados pelo site e, portanto, são conhecidos pelo site e pelo aplicativo.
  2. Antes que o aplicativo possa acessar os dados pessoais, ele deve receber um token de acesso. Um desses tokens pode fornecer diferentes níveis de acesso definidos pela variável scope. Quando o token de acesso é solicitado, o aplicativo pode enviar um ou mais valores para o parâmetro scope. Para criar esta solicitação, o aplicativo pode usar o navegador do sistema e solicitações de serviço Web. Alguns pedidos exigem uma etapa de autenticação, na qual os usuários efetuam login com sua conta. Uma vez logado, pergunta-se ao usuário se está pronto para conceder as permissões solicitadas pelo aplicativo. O processo é chamado de consentimento do usuário. Se o usuário conceder, o servidor de autorização fornecerá ao aplicativo o código de autorização, que permite que o aplicativo obtenha o token de acesso. Se o usuário não conceder permissão, o servidor retornará um erro.
  3. Depois que o aplicativo recebe o token de acesso, ele o envia para o cabeçalho da autorização http. Os pontos de acesso são válidos apenas para o conjunto de operações e recursos descritos no parâmetro scope da solicitação. Por exemplo, se o token de acesso do Google Drive for emitido, ele não fornecerá acesso aos contatos do Google. No entanto, o aplicativo pode enviar este token de acesso ao Google Drive várias vezes para executar as operações permitidas
  4. Os tokens têm uma vida útil limitada. Se o aplicativo precisar de acesso após a vida útil do token de acesso, ele pode receber um token de atualização que permite que o aplicativo receba novos tokens de acesso.

Cenário de acesso

2. Organizando o acesso ao Google Drive

 Para trabalhar com o Google Drive, precisamos de uma conta do Google.

Antes de desenvolver o código de nosso aplicativo, realizaremos o trabalho preparatório no site do Google. Para fazer isso, acessamos a consola de desenvolvedores (é necessário fazer login na conta novamente para acessá-la).

Criamos um novo projeto para nosso aplicativo. Avançamos para o painel de projetos (botão "Select a project" ou Ctrl + O). Criamos um novo projeto (botão "+").

Painel de projetos

Na página recém-aberta, definimos o nome do projeto, concordamos com os termos de uso e confirmamos a criação.

Novo projeto

Selecione o novo projeto do painel e conecte a API do Google Drive a ele. Para fazer isso, selecionamos Drive API, na biblioteca da API do gerente, e ativamos a API numa nova página clicando em "Enable".

Biblioteca APIAtivação da API

A nova página nos solicita a criação de credenciais para usar a API. Clicamos em "Create credentials" para fazer isso.

Alerta

O console do Google oferece o assistente para selecionar o tipo de autenticação, mas não precisamos disso. Clicamos em "client ID". Em seguida, o Google novamente nos avisa da necessidade de configurar a página de confirmação de acesso. Clicamos em "Configure consent screen", para fazer isso.

Alerta

Na página recém-aberta, é possível deixar todos os campos como padrão preenchendo apenas "Product name shown to users" (Nome do programa que é exibido para o usuário). Em seguida, definimos o tipo de aplicativo como "Other", especificamos o nome do cliente e clicamos em "Create" (Criar). Em resposta às nossas ações, o serviço gera os códigos "client ID" e "client secret". É possível copiá-los, mas isso não é necessário, porque se podem baixar como um arquivo json. Clicamos "Ok" e baixamos o arquivo json com os dados para acessar o disco local.

Depois disso, o trabalho preparatório no lado do serviço está completo, e podemos começar a desenvolver nossos aplicativos.

3. Criando uma ponte entre aplicativos locais e o Google Drive

Para resolver esta tarefa, desenvolvi um programa separado (um tipo de ponte) que recebe solicitações e dados de um EA ou indicador MetaTrader, processa-as, interage com o Google Drive e retorna os dados de volta para o programa MetaTrader. A vantagem dessa abordagem é, em primeiro lugar, que o Google oferece bibliotecas para trabalhar com o Google Drive em C #. Isso facilita grandemente o desenvolvimento. Em segundo lugar, o uso de um aplicativo de terceiros elimina a necessidade de o terminal consumir tempo em operações de intercâmbio com um serviço externo. Em terceiro lugar, isso desvincula nosso programa da plataforma, a fim de torná-lo uma plataforma cruzada com a capacidade de trabalhar com os aplicativos MetaTrader 4 e MetaTrader 5.

Como eu disse anteriormente, o programa ponte deve ser desenvolvido em C# usando as bibliotecas do Google. Criamos o projeto Windows Form no VisualStudio, e, imediatamente, com ajuda do NuGet, adicionamos a ele a biblioteca  Google.Apis.Drive.v3.

Em seguida, criamos a classe GoogleDriveClass, para trabalhar com o Google drive.

class GoogleDriveClass
    {
        static string[] Scopes = { DriveService.Scope.DriveFile };  //Matriz para trabalhar com arquivos
        static string ApplicationName = "Google Drive Bridge";      //Nome do programa
        public static UserCredential credential = null;             //Chave de autorização
        public static string extension = ".gdb";                    //Extensão para arquivos salvos
    }

Primeiro, criamos a função para iniciar sessão no serviço. Ele usará o arquivo json anteriormente salvo com os códigos de acesso. No meu caso, é "client-secret.json". Se você salvou o arquivo com um nome diferente, especifique-o no código da função. Depois de carregar os dados de autorização, é chamada a função de autorização assíncrona no serviço. No caso de um login bem-sucedido, no objeto credential é salvo o token, para acesso posterior. Ao trabalhar em C#, não se esqueça de tratamento de exceções, quer dizer, no caso de uma exceção, o objeto credential é reiniciado. 

        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);
        }

Ao trabalhar com o Google Drive, nossa "ponte" deve executar duas funções, isto e, gravar dados no disco e ler - a partir dele - o arquivo desejado. Vamos examiná-los em mais detalhes. Para implementar estas funções aparentemente simples, precisamos escrever uma série de procedimentos. O motivo é que o sistema de arquivos do Google Drive é diferente do que estamos acostumados. Aqui, os nomes e extensões de arquivo existem como entradas separadas apenas para manter a apresentação habitual. Na verdade, ao salvar, cada arquivo recebe uma ID exclusiva, sob a qual ela será armazenado. Assim, os usuários podem salvar um número ilimitado de arquivos com o mesmo nome e extensão. Portanto, antes de acessar o arquivo, precisamos saber seu ID no armazenamento em nuvem. Para fazer isso, carregamos a lista de todos os arquivos no disco e comparamos seus nomes um a um com o especificado.

A função GetFileList é responsável pela obtenção da lista de arquivos. Ela retorna a lista de classes Google.Apis.Drive.v3.Data.File. Vamos usar a classe Google.Apis.Drive.v3.DriveService das bibliotecas anteriormente baixadas para receber a lista de arquivos do Google Drive. Ao inicializar a classe, transferimos para ela o token obtido ao iniciar sessão junto com o nome do nosso projeto. A lista resultante é armazenada na variável result retornada. No caso de exceções, a variável é redefinida para zero. A lista de arquivos será solicitada e processada conforme necessário em outras funções do programa.

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. Registrando dados no armazenamento em nuvem

Para gravação do arquivo no armazenamento em nuvem, criamos a função FileCreate. Os parâmetros de entrada da função são o nome do arquivo e seu conteúdo. Ela retornará o valor lógico do resultado da operação e do ID do arquivo no disco no caso de ter sido criado com sucesso. A já conhecida classe Google.Apis.Drive.v3.DriveService será responsável pela criação do arquivo, enquanto a classe Google.Apis.Drive.v3.FilesResource.CreateMediaUpload deverá ser usada para enviar uma solicitação. Nos parâmetros do arquivo, indicamos que ele é um arquivo de texto simples e dá permissão para copiá-lo.

       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;
        }

O próximo passo após a criação do arquivo é a função de atualização. Lembremos os objetivos do nosso aplicativo e as características do sistema de arquivos do Google Drive. Estamos desenvolvendo um aplicativo para intercâmbio de dados entre vários terminais localizados em diferentes PCs. Nós não sabemos a que horas e quantos terminais precisam de nossa informação. Mas os recursos do sistema de arquivos da nuvem nos permitem criar vários arquivos com os mesmos nomes e extensões. Isso nos permite primeiro criar um novo arquivo com novos dados e excluir os dados obsoletos do armazenamento em nuvem depois. Isso é o que a função FileUpdate faz. Seus parâmetros de entrada são o nome do arquivo e seu conteúdo, além disso, ela retorna o valor lógico do resultado da operação.

No início da função, declaramos a variável new_id e chamamos a função FileCreate criada anteriormente, que, por sua vez, cria um novo arquivo de dados na nuvem e retorna o novo ID do arquivo para nossa variável.

Em seguida, obtemos a lista de todos os arquivos na nuvem a partir da função GetFileList e os comparamos com o nome e o identificador do arquivo recém-criado, um a um. Todas as duplicações desnecessárias são removidas do armazenamento. Aqui, usamos novamente a classe já conhecida Google.Apis.Drive.v3.DriveService, enquanto enviamos os pedidos usando a classe 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. Lendo dados do armazenamento em nuvem

Nós já criamos funções para gravar dados no armazenamento em nuvem. Agora é hora de lê-los de volta. Como lembramos, antes de baixar o arquivo, precisamos obter sua identificação na nuvem. Esta tarefa é atribuída à função GetFileID. Seu parâmetro de entrada é um nome do arquivo desejado, enquanto o valor de retorno é seu identificador. A construção lógica da função é simples, ou seja, recebemos a lista de arquivos da função GetFileList e procuramos o primeiro arquivo com o nome desejado simplesmente ordenando os arquivos. Provavelmente, será o arquivo mais antigo. Existe o risco de que, neste momento, um novo arquivo com os parâmetros necessários seja salvo ou ocorra uma falha durante o download. Tomamos esses riscos, a fim de obter informações completas. As últimas alterações são baixadas durante a próxima atualização. Como lembramos, todas as duplicações desnecessárias são removidas da função FileUpdate após a criação de um novo arquivo de dados.

        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;
        }

Depois de obter o ID do arquivo, podemos recuperar os dados que precisamos. Para fazer isso, escrevemos a função FileRead, para a qual transferimos o identificador do arquivo que precisamos, enquanto a função retorna seu conteúdo. Em caso de falha, a função retorna uma cadeia de comprimento zero. Como antes, precisamos da classe Google.Apis.Drive.v3.DriveService para criar uma conexão e a classe Google.Apis.Drive.v3.FilesResource.GetRequest para criar uma solicitação.

        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. Criando um bloco de interação com aplicativos de terminal

Agora que ligamos nosso aplicativo ao armazenamento em nuvem do Google Drive, é hora de conectá-lo ao aplicativos do MetaTrader também. Afinal, esse é o principal objetivo. Eu decidi estabelecer essa conexão usando canais nomeados. Trabalhar com eles já foi descrito no site, e a linguagem MQL5 já possui a classe CFilePipe para trabalhar com este tipo de conexão. Isso facilitará nosso trabalho ao criar aplicativos.

O terminal permite o lançamento de vários aplicativos simultaneamente. Portanto, nossa "ponte" deve ser capaz de lidar com várias conexões ao mesmo tempo. Vamos usar o módulo de programação assíncrona multifluxo [multi-threaded] para isso.

Devemos definir o formato das mensagens transmitidas entre a ponte e o aplicativo. Para ler o arquivo a partir da nuvem, devemos transferir o comando e o nome do arquivo. Para gravar um arquivo na nuvem, devemos enviar o comando, o nome do arquivo e seu conteúdo. Como os dados são transmitidos como um fluxo de dados único, é razoável passar toda a informação numa única cadeia de caracteres. Eu aplico ";" como um separador de campo na string.

Primeiro, declararemos as variáveis globais:

  • Drive — classe previamente criada para trabalhar com o armazenamento em nuvem;
  • numThreads — definir o número de fluxos simultâneos;
  • pipeName — variável de string armazenando o nome de nossos canais;
  • servers — matriz de fluxos operacionais.
        GoogleDriveClass Drive = new GoogleDriveClass();
        private static int numThreads = 10;
        private static string pipeName = "GoogleBridge";
        static Thread[] servers;

Criamos a função para inicialização dos fluxos operacionais PipesCreate. Nesta função, inicializamos a matriz de nossos fluxos e os lançamos num ciclo.  Ao iniciar cada fluxo, a função ServerThread é chamada para inicializar as funções em nossos fluxos.

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

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

Além disso, no início de cada thread, um pipe nomeado é criado e é inicializada a função assíncrona de espera para conexão de cliente com o pipe. Ao conectar o cliente ao canal, é chamada a função Connected. Para fazer isso, criamos o delegado AsyncCallback asyn_connected. Quando ocorre uma excepção o fluxo é reiniciado.

        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();
            }
        }

Quando um cliente se conecta a um pipe nomeado, verificamos o estado do canal e, no caso de uma exceção, reiniciamos o fluxo. Se a conexão for estável, executamos a função de leitura de solicitação do aplicativo. Se a função de leitura retorna false, reiniciamos a conexão.

        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;
            }
        }

A função ReadMessage lê e processa a solicitação de aplicativos. Uma referência ao objeto thread é transferida para a função como um parâmetro. O resultado da função é o valor lógico da operação. Primeiro, a função lê a solicitação do aplicativo do pipe nomeado e divide-o em campos. Em seguida, ele reconhece o comando e executa as ações necessárias.

A função possui três comandos:

  • Close — encerramento da conexão atual;
  • Read — leitura do arquivo a partir da nuvem;
  • Write — gravação do arquivo na nuvem.

Para fechar a conexão atual, a função deve simplesmente retornar false, todo o resto é feito pela função Connected.

Para executar a solicitação de leitura de arquivos, devemos definir o identificador do arquivo e lemos seu conteúdo usando as funções GetFileID e FileRead descritas acima.

Depois de executar a função de gravação do arquivo, chamamos a função FileUpdate criada anteriormente.

Claro, não se esqueça do tratamento de exceções. Em caso de uma exceção, faça o login novamente no 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;
        }

Depois de processar os pedidos, devemos devolver o resultado da operação ao aplicativo. Criamos a função WriteMessage. Seus parâmetros são uma referência ao objeto do pipe nomeado atual e uma mensagem enviada para o aplicativo. A função retorna o valor lógico sobre o resultado da operação.

        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;
        }

Agora que descrevemos todas as funções necessárias, é hora de executar a função PipesCreate. Eu criei o projeto Windows Form, poi isso executo essa função a partir da função Form1.

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

Tudo o que temos a fazer agora é recopilar o projeto e copiar o arquivo json com os dados de acesso ao armazenamento em nuvem para a pasta do aplicativo.
  

4. Criando aplicativos MetaTrader

Procedemos à aplicação prática do nosso programa. Primeiro, sugiro que você escreva um programa para copiar objetos gráficos simples.

4.1. Classe para trabalhar com objetos gráficos

Que dados do objeto devemos passar para recriá-lo noutro gráfico? Provavelmente, devem ser o tipo de objeto e seu nome para identificação. Também precisaremos da cor do objeto e das suas coordenadas. A primeira questão é a quantidade de coordenadas que devemos passar e quais são seus valores. Por exemplo, ao transferir dados numa linha vertical, basta passar uma data. Ao lidar com uma linha horizontal, devemos passar um preço. Para a linha de tendência são necessários dois pares de coordenadas, isto é, data, preço e direção da linha (para a direita/esquerda). Objetos diferentes têm parâmetros comuns e únicos. No entanto, em MQL5, todos os objetos são criados e alterados usando quatro funções: ObjectCreate, ObjectSetInteger, ObjectSetDouble e ObjectSetString. Seguiremos o mesmo caminho e passaremos o tipo de parâmetro, propriedade e valor.

Criamos a enumeração de tipos de parâmetros.

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

Criamos a classe CCopyObject para processamento dos dados do objeto. Um parâmetro de string é transferido para ela durante a inicialização. Posteriormente, ele identifica os objetos criados pela nossa classe no gráfico. Vamos salvar esse valor para a variável de classe 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. Bloco de funções para coletar os dados do objeto

Primeiro, criamos a função CreateMessage. Seu parâmetro é o ID do gráfico necessário. A função retorna o valor de texto a ser enviado para o armazenamento em nuvem com a lista dos parâmetros do objeto e seus valores. A cadeia de caracteres retornada deve ser estruturada para que esses dados possam ser lidos. Concordamos que os dados em cada objeto são colocados em chavetas, o sinal "|" é usado como um separador entre os parâmetros e o sinal "=" separa o parâmetro e seu valor. No início da descrição de cada objeto, seu nome e tipo são indicados, e a função de descrição do objeto correspondente ao seu tipo é chamada posteriormente.

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;
  }

Por exemplo, a função HLineToString é chamada para descrever uma linha horizontal. O ID do gráfico e o nome do objeto são usados ​​como seus parâmetros. A função retornará a cadeia de caracteres estruturada com o parâmetro do objeto. Por exemplo, para uma linha horizontal, os parâmetros transferidos ​​são preço, cor, estilo e espessura da linha, exibição de linha em primeiro plano ou no plano de fundo. Não se esqueça de, antes da propriedade do parâmetro, definir seu tipo a partir da enumeração criada anteriormente.

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;
  }

Da mesma forma, criamos as funções para descrever outros tipos de objeto. No meu caso, estes são VLineToString para a linha vertical, TrendToString para a linha de tendência e RectangleToString para o retângulo. Os códigos dessas funções podem ser encontrados no código de classe anexado.

4.1.2. Função para exibir objetos no gráfico

Já criamos a função de coleta de dados. Agora, vamos desenvolver a função que lê as mensagens e exibe objetos no gráfico: DrawObjects. Seus parâmetros são o ID do gráfico e a mensagem recebida. A função retorna o valor lógico da execução da operação.

O algoritmo de função inclui várias etapas:

  • divisão da mensagem de cadeia em matriz de strings por objetos;
  • divisão de cada elemento da matriz de objetos em matriz de parâmetros;
  • busca do nome e o tipo de objeto na matriz de parâmetros. Adição de nosso identificador ao nome;
  • busca de um objeto com o nome obtido no gráfico; se o objeto não estiver na sub-janela principal ou seu tipo for diferente do especificado na mensagem, ele será removido;
  • criação de um novo objeto no gráfico se ainda não houver nenhum objeto ou ele foi removido na etapa anterior;
  • transferência das propriedades do objeto recebidas na mensagem para o objeto em nosso gráfico (usando a função adicional CopySettingsToObject);
  • remoção de objetos desnecessários do gráfico (executados pela função 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;
  }

A função de atribuição de propriedades recebidas numa mensagem para um objeto de gráfico é universal e aplicável a qualquer tipo de objeto. O ID do gráfico, o nome do objeto e uma matriz de caracteres são transferidos ​​para ele como parâmetros. Cada elemento de matriz é dividido em tipo de operação, uma propriedade, um modificador e um valor. Os valores obtidos são atribuídos ao objeto através de uma função correspondente ao tipo de operação. 

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;
  }

Depois de plotar os objetos no gráfico, precisamos comparar os objetos presentes no gráfico com os transferidos ​​na mensagem. Os objetos "extra" que contém a ID necessária, mas não estão presentes na mensagem, são removidos do gráfico (estes são os objetos removidos pelo provedor). A função DeleteExtraObjects é responsável por isso. Seus parâmetros são o ID do gráfico ea matriz de estruturas que contêm o nome e o tipo do 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. Aplicativo de provedor

Estamos chegando gradualmente perto do final. Criamos o programa-provedor que irá coletar dados de objetos e enviá-los para o armazenamento na nuvem. Vamos executá-lo sob a forma de um Expert Advisor. Existe apenas um parâmetro externo, isto é, a variável lógica SendAtStart que define se os dados devem ser enviados imediatamente após o download do aplicativo para o terminal.

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

Incluímos as bibliotecas necessárias no cabeçalho do aplicativo. Elas são a classe para trabalhar com objetos gráficos descrita acima e a classe base para trabalhar com pipes nomeados. Além disso, especificamos o nome do pipe ao qual o aplicativo está conectado.

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

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

Nas variáveis ​​globais, declaramos a classe para trabalhar com objetos gráficos, variável de string para salvar a última mensagem enviada e a matriz de tipo uchar, em que escrevemos o comando para fechar a conexão ao armazenamento em nuvem.

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

Na função OnInit, inicializamos variáveis ​​globais e executamos a função para enviar dados para o armazenamento em nuvem, se necessário. 

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

Na função OnDeinit, excluímos a classe de objeto para trabalhar com objetos gráficos.

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

A função de envio de mensagens contendo informações para o armazenamento em nuvem é chamada a partir da função OnChartEvent quando um objeto é criado, modificado ou removido do 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;
     }      
  }

As operações principais são executadas na função SendMessage, aplicando o ID do gráfico como uma entrada. Seu algoritmo pode ser dividido em várias etapas:

  • verificar o status da classe para trabalhar com objetos gráficos e reinicializá-lo, se necessário;
  • gerar a mensagem a ser enviada para a nuvem usando a função CreatMessage criada anteriormente. Deve-se sair da função se a mensagem estiver vazia ou igual à última enviada;
  • criar o nome do arquivo a ser enviado para a nuvem com base no símbolo do gráfico;
  • estabelecer conexão com nosso aplicativo de ponte através do pipe nomeado;
  • transferir uma ordem para enviar uma mensagem para o armazenamento na nuvem com o nome do arquivo especificado através da conexão aberta;
  • enviar uma ordem para fechar a conexão à nuvem e quebrar o pipe nomeado para o aplicativo ponte depois de receber uma resposta sobre a execução da ordem de envio;
  • remover os objetos de trabalho com pipes nomeados antes de sair do aplicativo.
Durante a execução das operações, exibimos mensagens de informação nos comentários ao 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. Aplicativo de usuários

Como conclusão, vamos criar um aplicativo de usuário para receber dados do armazenamento na nuvem, bem como criar e modificar objetos gráficos no gráfico. Como no aplicativo anterior, devemos incluir as bibliotecas necessárias no cabeçalho e especificar o nome do canal usado.

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

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

O aplicativo deve caracterizar três parâmetros externos: tempo em segundos indicando a periodicidade da atualização de dados de armazenamento na nuvem, identificação de objetos no gráfico e o valor lógico indicando a necessidade de remover todos os objetos criados do gráfico quando o aplicativo está fechado.

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

Nas variáveis ​​globais (assim como no aplicativo do provedor), declaramos a classe para trabalhar com objetos gráficos, variável de string para salvar a última mensagem obtido e a matriz de tipo uchar, em que escrevemos o comando para fechar a conexão ao armazenamento em nuvem. Além disso, adicionamos a variável lógica sobre o estado do temporizador e as variáveis ​​para armazenar o tempo da última atualização e exibir o último comentário no gráfico.

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

Na função OnInit, realizamos a inicialização das variáveis globais e temporizador.

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);
  }

Na função de anulação da inicialização OnDeinit, eliminamos a classe de objeto para trabalhar com objetos gráficos, paramos o temporizador, limpamos os comentários e, se necessário, removemos os objetos criados pelo aplicativo a partir do 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--;
           }
        }
     }
  }

Na função OnTick, verificamos o status do temporizador e, se necessário, voltamos a reativá-lo.

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

Na função OnTimer, limpamos os comentários que estão presentes no gráfico com mais de 10 segundos e chamamos a função para ler o arquivo de dados do armazenamento na nuvem (ReadMessage). Depois que os dados são carregados com sucesso, é alterado o tempo da última atualização de dados.

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

As ações básicas para carregar dados a partir do armazenamento em nuvem e exibir objetos no gráfico são realizadas na função ReadMessage. Esta função tem apenas um parâmetro, isto é, o ID do gráfico com o qual a função vai funcionar. As operações realizadas na função podem ser divididas em várias etapas:

  • gerar o nome do arquivo segundo o símbolo do gráfico para leitura a partir da nuvem;
  • abrir um pipe nomeado para conexão com o programa ponte;
  • enviar uma solicitação de leitura de dados a partir do armazenamento em nuvem, especificando o arquivo necessário;
  • ler o resultado do processamento da solicitação;
  • enviar uma ordem para fechar a conexão à nuvem e quebrar o pipe nomeado para o aplicativo ponte;
  • comparamos o resultado obtido com a mensagem anterior. Se os dados forem semelhantes, saímos da função;
  • passar a mensagem obtida para a função DrawObjects do objeto de classe de processamento de elementos gráficos;
  • salvar a mensagem processada com sucesso na variável PrevMessage para a comparação subsequente com os dados obtidos.
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. Primeiro lançamento dos aplicativos

Depois de tanto trabalho, é hora de analisar os resultados. Inicializamos o programa ponte. Conferimos se o arquivo client-secret.json, que contém os dados (recebidos do serviço Google) para se conectar ao armazenamento em nuvem, está localizado na pasta do aplicativo. Em seguida, executamos um dos nosso aplicativos MetaTrader. Ao acessar a nuvem pela primeira vez, o programa ponte inicia o aplicativo de internet padrão com a página de login da conta do Google.

Página de acesso à Conta Google

Aqui é necessário digitar o endereço de e-mail que fornecemos ao registrar a conta do Google e avançar para a próxima página (botão NEXT). Na próxima página, digitamos a senha para acessar a conta.

Senha de acesso à conta do Google

Na próxima página, o Google solicitará confirmar as permissões de acesso do aplicativo ao armazenamento em nuvem. Devemos conferir as permissões de acesso solicitadas e confirmar (botão ALLOW).

Confirmando permissões de acesso

A subpasta drive-bridge.json é criada no diretório do programa ponte. Ele armazena o arquivo que contém o token de acesso do armazenamento em nuvem. No futuro, ao replicar o aplicativo em outros computadores, esse subdiretório também deve ser copiado junto com o programa ponte. Isso elimina a necessidade de repetir o procedimento e transferir os dados de acesso ao armazenamento em nuvem para terceiros.

Arquivo de permissões no subdiretório do programa

Fim do artigo

Neste artigo, examinamos o uso do armazenamento na nuvem para fins práticos. O programa ponte é um aplicativo universal para fazer upload de dados no armazenamento em nuvem e carregá-los novamente em nossos aplicativos. A solução proposta para a transferência de objetos gráficos permite compartilhar os resultados de análise técnica com seus colegas em tempo real. Talvez, alguém decida fornecer sinais de negociação ou organizar cursos de treinamento sobre análise técnica de gráficos desta forma.

Desejo a todos negociações bem-sucedidas.

Traduzido do russo pela MetaQuotes Software Corp.
Artigo original: https://www.mql5.com/ru/articles/3331

Arquivos anexados |
Provider.mq5 (4.71 KB)
Provider.ex5 (35.89 KB)
User.ex5 (32.23 KB)
User.mq5 (5.5 KB)
GoogleBridge.zip (2468.07 KB)
CopyObject.mqh (15.53 KB)
Interfaces gráficas XI: Caixas de Edição de Texto e Caixas de Combinação nas células da tabela (build 15) Interfaces gráficas XI: Caixas de Edição de Texto e Caixas de Combinação nas células da tabela (build 15)

Nesta atualização da biblioteca, o controle da tabela (a classe CTable) será complementado com novas opções. A gama de controles nas células da tabela foi expandida, desta vez adicionando as caixas de edição de texto e as caixas de combinação. Além disso, esta atualização também apresenta a capacidade de redimensionar a janela de uma aplicação MQL em tempo de execução.

Criação e teste de símbolos personalizados na MetaTrader 5 Criação e teste de símbolos personalizados na MetaTrader 5

A criação de símbolos personalizados empurra os limites no desenvolvimento de sistemas de negociação e análise do mercado financeiro. Agora, os traders são capazes de desenhar gráficos e testar estratégias de negociação em um número ilimitado de instrumentos financeiros.

Redes Neurais Profundas (Parte III). Seleção da amostra e redução de dimensionalidade Redes Neurais Profundas (Parte III). Seleção da amostra e redução de dimensionalidade

Este artigo é uma continuação da série de artigos sobre redes neurais profundas. Aqui, nós vamos considerar a seleção de amostras (remoção de ruído), reduzindo a dimensionalidade dos dados de entrada e dividindo o conjunto de dados nos conjuntos de train/val/test durante a preparação dos dados para treinar a rede neural.

Expert Advisor universal: indicador CUnIndicator e trabalho com ordens pendentes (parte 9) Expert Advisor universal: indicador CUnIndicator e trabalho com ordens pendentes (parte 9)

O artigo descreve o trabalho com indicadores através da classe universal do CUnIndicator. Além disso, consideram-se novas formas de trabalhar com ordens pendentes. Observe que, a partir deste ponto, a estrutura do projeto do CStrategy muda significativamente. Agora todos os arquivos são colocados num único diretório para a conveniência dos usuários.