Trabalhando com as funções de rede ou MySQL sem DLL: Parte II - Programa para monitorar as alterações nas propriedades do sinal

Serhii Shevchuk | 19 maio, 2020

Conteúdo


Introdução

No artigo anterior, nós consideramos a implementação do conector MySQL. Agora é hora de considerarmos os exemplos de sua aplicação. O mais simples e mais óbvio é a coleta dos valores de propriedades de sinal com a capacidade de visualizar suas alterações adicionais. Mais de 100 sinais estão disponíveis no terminal para a maioria das contas, enquanto cada sinal possui mais de 20 parâmetros. Isso significa que nós teremos dados suficientes. O exemplo implementado tem sentido prático se os usuários precisarem observar alterações nas propriedades que não são exibidas na página da web do sinal. Essas mudanças podem incluir a alavancagem, classificação, número de assinantes e muito mais.

Para coletar os dados, vamos implementar o serviço que solicita periodicamente as propriedades de sinal, as compara com os valores anteriores e envia todo o array ao banco de dados, caso sejam detectadas diferenças.

Para visualizar a dinâmica da propriedade, nós escrevemos um EA que exibe uma alteração em uma propriedade selecionada como um gráfico. Além disso, vamos implementar a capacidade de classificar os sinais pelos valores de algumas propriedades usando as consultas condicionais ao banco de dados.

Durante a implementação, nós descobriremos por que é desejável usar o modo de conexão constante do Keep Alive e várias consultas em alguns casos.


Serviço de coleta de dados

Os objetivos do serviço são os seguintes:

  1. Solicitar periodicamente as propriedades de todos os sinais disponíveis na plataforma
  2. Comparar seus valores com os anteriores
  3. Se diferenças forem detectadas, escrevemos todo o array de valores no banco de dados
  4. Informar um usuário em caso de erros

Criamos um novo serviço no editor e nomeamos ele para signs_to_db.mq5. Os parâmetros de entrada são os seguintes:

input string   inp_server     = "127.0.0.1";          // MySQL server address
input uint     inp_port       = 3306;                 // TCP port
input string   inp_login      = "admin";              // Login
input string   inp_password   = "12345";              // Password
input string   inp_db         = "signals_mt5";        // Database name
input bool     inp_creating   = true;                 // Allow creating tables
input uint     inp_period     = 30;                   // Signal loading period
input bool     inp_notifications = true;              // Send error notifications

Além das configurações de rede, existem várias opções aqui:


Obtendo os valores das propriedades do sinal

Para que o serviço funcione corretamente, é importante saber quando os valores das propriedades do sinal são atualizados na plataforma. Isso acontece em dois casos:

Pelo menos, é o que acontece na versão da plataforma até o momento de sua escrita.

As propriedades do sinal que nós estamos interessados são de quatro tipos:

O tipo ENUM_SIGNAL_BASE_DATETIME é criado para detectar as propriedades ENUM_SIGNAL_BASE_INTEGER que devem ser convertidas na string como um horário e não como um número inteiro.

Por conveniência, decompomos os valores da enumeração das propriedades do mesmo tipo através dos arrays (quatro tipos de propriedade — quatro arrays). Cada enumeração é acompanhada por uma descrição em texto da propriedade, que também é o nome do campo correspondente na tabela do banco de dados. Para conseguir isso, criaremos as seguintes estruturas:

//--- Structures of signal properties description for each type
struct STR_SIGNAL_BASE_DOUBLE
  {
   string                     name;
   ENUM_SIGNAL_BASE_DOUBLE    id;
  };
struct STR_SIGNAL_BASE_INTEGER
  {
   string                     name;
   ENUM_SIGNAL_BASE_INTEGER   id;
  };
struct STR_SIGNAL_BASE_DATETIME
  {
   string                     name;
   ENUM_SIGNAL_BASE_INTEGER   id;
  };
struct STR_SIGNAL_BASE_STRING
  {
   string                     name;
   ENUM_SIGNAL_BASE_STRING    id;
  };

Em seguida, declaramos os arrays da estrutura (abaixo está um exemplo para ENUM_SIGNAL_BASE_DOUBLE, que é semelhante para os outros tipos):

const STR_SIGNAL_BASE_DOUBLE tab_signal_base_double[]=
  {
     {"Balance",    SIGNAL_BASE_BALANCE},
     {"Equity",     SIGNAL_BASE_EQUITY},
     {"Gain",       SIGNAL_BASE_GAIN},
     {"Drawdown",   SIGNAL_BASE_MAX_DRAWDOWN},
     {"Price",      SIGNAL_BASE_PRICE},
     {"ROI",        SIGNAL_BASE_ROI}
  };
Agora, para receber os valores das propriedades de sinal selecionadas, nós precisamos apenas nos mover pelos quatro laços:
   //--- Read signal properties
   void              Read(void)
     {
      for(int i=0; i<6; i++)
         props_double[i] = SignalBaseGetDouble(ENUM_SIGNAL_BASE_DOUBLE(tab_signal_base_double[i].id));
      for(int i=0; i<7; i++)
         props_int[i] = SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_integer[i].id));
      for(int i=0; i<3; i++)
         props_datetime[i] = datetime(SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_datetime[i].id)));
      for(int i=0; i<5; i++)
         props_str[i] = SignalBaseGetString(ENUM_SIGNAL_BASE_STRING(tab_signal_base_string[i].id));
     }

No exemplo acima, Read() é um método da estrutura SignalProperties com todos os recursos necessários para trabalhar com as propriedades do sinal. Estes são os buffers para cada um dos tipos, bem como os métodos para ler e comparar os valores atuais com os anteriores:

//--- Structure for working with signal properties
struct SignalProperties
  {
   //--- Property buffers
   double            props_double[6];
   long              props_int[7];
   datetime          props_datetime[3];
   string            props_str[5];
   //--- Read signal properties
   void              Read(void)
     {
      for(int i=0; i<6; i++)
         props_double[i] = SignalBaseGetDouble(ENUM_SIGNAL_BASE_DOUBLE(tab_signal_base_double[i].id));
      for(int i=0; i<7; i++)
         props_int[i] = SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_integer[i].id));
      for(int i=0; i<3; i++)
         props_datetime[i] = datetime(SignalBaseGetInteger(ENUM_SIGNAL_BASE_INTEGER(tab_signal_base_datetime[i].id)));
      for(int i=0; i<5; i++)
         props_str[i] = SignalBaseGetString(ENUM_SIGNAL_BASE_STRING(tab_signal_base_string[i].id));
     }
   //--- Compare signal Id with the passed value
   bool              CompareId(long id)
     {
      if(id==props_int[0])
         return true;
      else
         return false;
     }
   //--- Compare signal property values with the ones passed via the link
   bool              Compare(SignalProperties &sig)
     {
      for(int i=0; i<6; i++)
        {
         if(props_double[i]!=sig.props_double[i])
            return false;
        }
      for(int i=0; i<7; i++)
        {
         if(props_int[i]!=sig.props_int[i])
            return false;
        }
      for(int i=0; i<3; i++)
        {
         if(props_datetime[i]!=sig.props_datetime[i])
            return false;
        }
      return true;
     }
   //--- Compare signal property values with the one located inside the passed buffer (search by Id)
   bool              Compare(SignalProperties &buf[])
     {
      int n = ArraySize(buf);
      for(int i=0; i<n; i++)
        {
         if(props_int[0]==buf[i].props_int[0])  // Id
            return Compare(buf[i]);
        }
      return false;
     }
  };


Adicionando ao banco de dados

Para trabalhar com o banco de dados, nós primeiro precisamos declarar a instância da classe CMySQLTransaction:

//--- Include MySQL transaction class
#include  <MySQL\MySQLTransaction.mqh>
CMySQLTransaction mysqlt;

Em seguida, definimos os parâmetros de conexão na função OnStart(). Para fazer isso, chamamos o método Config:

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Configure MySQL transaction class
   mysqlt.Config(inp_server,inp_port,inp_login,inp_password);
   
   ...
  }

O próximo passo é criar o nome da tabela. Como o conjunto de sinais depende de uma corretora e um tipo de conta de negociação, esses parâmetros devem ser usados para isso. No nome do servidor, substituímos o ponto, hífen e espaço por um sublinhado, adicionamos o login da conta e substituímos todas as letras por minúsculas. Por exemplo, no caso do servidor MetaQuotes-Demo e o login de número 17273508, o nome da tabela será metaquotes_demo__17273508.

O código terá o seguinte aspecto:

//--- Assign a name to the table
//--- to do this, get the trade server name
   string s = AccountInfoString(ACCOUNT_SERVER);
//--- replace the space, period and hyphen with underscores
   string ss[]= {" ",".","-"};
   for(int i=0; i<3; i++)
      StringReplace(s,ss[i],"_");
//--- assemble the table name using the server name and the trading account login
   string tab_name = s+"__"+IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN));
//--- set all letters to lowercase
   StringToLower(tab_name);
//--- display the result in the console
   Print("Table name: ",tab_name);

Em seguida, realizamos a leitura dos últimos dados de entrada no banco de dados. Isso é feito para que seja possível comparar as propriedades obtidas com algo para detectar as diferenças ao reiniciar o serviço.

A função DB_Read() lê as propriedades do banco de dados.

//+------------------------------------------------------------------+
//| Read signal properties from the database                         |
//+------------------------------------------------------------------+
bool  DB_Read(SignalProperties &sbuf[],string tab_name)
  {
//--- prepare a query
   string q="select * from `"+inp_db+"`.`"+tab_name+"` "+
            "where `TimeInsert`= ("+
            "select `TimeInsert` "+
            "from `"+inp_db+"`.`"+tab_name+"` order by `TimeInsert` desc limit 1)";
//--- send a query
   if(mysqlt.Query(q)==false)
      return false;
//--- if the query is successful, get the pointer to it
   CMySQLResponse *r = mysqlt.Response();
   if(CheckPointer(r)==POINTER_INVALID)
      return false;
//--- read the number of rows in the accepted response
   uint rows = r.Rows();
//--- prepare the array
   if(ArrayResize(sbuf,rows)!=rows)
      return false;
//--- read property values to the array
   for(uint n=0; n<rows; n++)
     {
      //--- read the pointer to the current row
      CMySQLRow *row = r.Row(n);
      if(CheckPointer(row)==POINTER_INVALID)
         return false;
      for(int i=0; i<6; i++)
        {
         if(row.Double(tab_signal_base_double[i].name,sbuf[n].props_double[i])==false)
            return false;
        }
      for(int i=0; i<7; i++)
        {
         if(row.Long(tab_signal_base_integer[i].name,sbuf[n].props_int[i])==false)
            return false;
        }
      for(int i=0; i<3; i++)
         sbuf[n].props_datetime[i] = MySQLToDatetime(row[tab_signal_base_datetime[i].name]);
      for(int i=0; i<5; i++)
         sbuf[n].props_str[i] = row[tab_signal_base_string[i].name];
     }
   return true;
  }
Os argumentos de função são referências ao buffer de sinal e ao nome da tabela que formamos durante a inicialização. A primeira coisa que nós fazemos no corpo da função é preparar uma consulta. Nesse caso, nós precisamos ler todas as propriedades dos sinais com o tempo máximo de adição ao banco de dados. Como nós escrevemos no array de todos os valores simultaneamente, nós precisamos encontrar o tempo máximo na tabela e ler todas as strings em que o tempo de adição seja igual ao que foi encontrado. Por exemplo, se o nome do banco de dados for signs_mt5, enquanto o nome da tabela é metaquotes_demo__17273508, a consulta terá a seguinte aparência:
select * 
from `signals_mt5`.`metaquotes_demo__17273508`
where `TimeInsert`= (
        select `TimeInsert`
        from `signals_mt5`.`metaquotes_demo__17273508` 
        order by `TimeInsert` desc limit 1)

A subconsulta retornará o valor máximo da coluna `TimeInsert`, ou seja, a hora da última adição ao banco de dados, que é destacada em vermelho. A consulta destacada em verde retorna todas as strings onde o valor `TimeInsert` corresponde ao valor encontrado.

Se a transação for bem sucedida, começamos a ler os dados obtidos. Para fazer isso, colocamos o ponteiro na classe de resposta do servidor CMySQLResponse e obtemos o número de linhas na resposta. Dependendo desse parâmetro, alteramos o tamanho do buffer de sinal.

Agora nós precisamos ler as propriedades. Para fazer isso, recebemos o ponteiro para a linha atual usando o índice. Depois disso, lemos os valores para cada tipo de propriedade. Por exemplo, para ler as propriedades ENUM_SIGNAL_BASE_DOUBLE, usamos o método CMySQLRow::Double() onde o primeiro argumento (nome do campo) é o nome de texto da propriedade.

Vamos considerar o caso em que o envio de uma consulta termina com um erro. Para fazer isso, retornamos ao código fonte da função OnStart().
//--- Declare the buffer of signal properties
   SignalProperties sbuf[];
//--- Raise the data from the database to the buffer
   bool exit = false;
   if(DB_Read(sbuf,tab_name)==false)
     {
      //--- if the reading function returns an error,
      //--- the table is possibly missing
      if(mysqlt.GetServerError().code==ER_NO_SUCH_TABLE && inp_creating==true)
        {
         //--- if we need to create a table and this is allowed in the settings
         if(DB_CteateTable(tab_name)==false)
            exit=true;
        }
      else
         exit=true;
     }

Em caso de erro, verificamos primeiro se isso não foi causado pela ausência da tabela. Este erro ocorre se a tabela ainda não foi criada, removida ou renomeada. O erro ER_NO_SUCH_TABLE significa que a tabela deve ser criada (se permitido).

A função de criação de tabelas DB_CteateTable() é bastante simples:

//+------------------------------------------------------------------+
//| Create the table                                                 |
//+------------------------------------------------------------------+
bool  DB_CteateTable(string name)
  {
//--- prepare a query
   string q="CREATE TABLE `"+inp_db+"`.`"+name+"` ("+
            "`PKey`                        BIGINT(20)   NOT NULL AUTO_INCREMENT,"+
            "`TimeInsert`     DATETIME    NOT NULL,"+
            "`Id`             INT(11)     NOT NULL,"+
            "`Name`           CHAR(50)    NOT NULL,"+
            "`AuthorLogin`    CHAR(50)    NOT NULL,"+
            "`Broker`         CHAR(50)    NOT NULL,"+
            "`BrokerServer`   CHAR(50)    NOT NULL,"+
            "`Balance`        DOUBLE      NOT NULL,"+
            "`Equity`         DOUBLE      NOT NULL,"+
            "`Gain`           DOUBLE      NOT NULL,"+
            "`Drawdown`       DOUBLE      NOT NULL,"+
            "`Price`          DOUBLE      NOT NULL,"+
            "`ROI`            DOUBLE      NOT NULL,"+
            "`Leverage`       INT(11)     NOT NULL,"+
            "`Pips`           INT(11)     NOT NULL,"+
            "`Rating`         INT(11)     NOT NULL,"+
            "`Subscribers`    INT(11)     NOT NULL,"+
            "`Trades`         INT(11)     NOT NULL,"+
            "`TradeMode`      INT(11)     NOT NULL,"+
            "`Published`      DATETIME    NOT NULL,"+
            "`Started`        DATETIME    NOT NULL,"+
            "`Updated`        DATETIME    NOT NULL,"+
            "`Currency`       CHAR(50)    NOT NULL,"+
            "PRIMARY KEY (`PKey`),"+
            "UNIQUE INDEX `TimeInsert_Id` (`TimeInsert`, `Id`),"+
            "INDEX `TimeInsert` (`TimeInsert`),"+
            "INDEX `Currency` (`Currency`, `TimeInsert`),"+
            "INDEX `Broker` (`Broker`, `TimeInsert`),"+
            "INDEX `AuthorLogin` (`AuthorLogin`, `TimeInsert`),"+
            "INDEX `Id` (`Id`, `TimeInsert`)"+
            ") COLLATE='utf8_general_ci' "+
            "ENGINE=InnoDB "+
            "ROW_FORMAT=DYNAMIC";
//--- send a query
   if(mysqlt.Query(q)==false)
      return false;
   return true;
  }
A própria consulta apresenta o tempo de adição `TimeInsert` dos dados entre os nomes dos campos, que são os nomes das propriedades do sinal. Esta é a hora local da plataforma no momento de receber as propriedades atualizadas. Além disso, existe uma chave única para os campos `TimeInsert` e "Id", bem como os índices necessários para acelerar a execução da consulta.

Se a criação da tabela falhar, exibimos a descrição do erro e encerramos o serviço.

   if(exit==true)
     {
      if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR))
        {
         // in case of a server error
         Print("MySQL Server Error: ",mysqlt.GetServerError().code," (",mysqlt.GetServerError().message,")");
        }
      else
        {
         if(GetLastError()>=ERR_USER_ERROR_FIRST)
            Print("Transaction Error: ",EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST)));
         else
            Print("Error: ",GetLastError());
        }
      return;
     }

Nós podemos ter três tipos de erros.

O tipo de erro define como sua descrição é formada. O erro é definido da seguinte maneira.

Se a transação passa sem erros, nós inserimos o loop principal do programa:

//--- set the time label of the previous reading of signal properties
   datetime chk_ts = 0;

   ...
//--- Main loop of the service operation
   do
     {
      if((TimeLocal()-chk_ts)<inp_period)
        {
         Sleep(1000);
         continue;
        }
      //--- it is time to read signal properties
      chk_ts = TimeLocal();

      ...

     }
   while(!IsStopped());

Nosso serviço deve estar localizado nesse laço infinito até que ele seja descarregado. As propriedades são lidas, as comparações com os valores anteriores são realizadas e a escrita no banco de dados é feita (se necessário) com uma periodicidade especificada.

Suponha que nós recebemos as propriedades de sinal diferentes dos valores anteriores. A seguir, acontece o seguinte:

      if(newdata==true)
        {
         bool bypass = false;
         if(DB_Write(buf,tab_name,chk_ts)==false)
           {
            //--- if we need to create a table and this is allowed in the settings
            if(mysqlt.GetServerError().code==ER_NO_SUCH_TABLE && inp_creating==true)
              {
               if(DB_CteateTable(tab_name)==true)
                 {
                  //--- if the table is created successfully, send the data
                  if(DB_Write(buf,tab_name,chk_ts)==false)
                     bypass = true; // sending failed
                 }
               else
                  bypass = true; // failed to create the table
              }
            else
               bypass = true; // there is no table and it is not allowed to create one
           }
         if(bypass==true)
           {
            if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR))
              {
               // in case of a server error
               PrintNotify("MySQL Server Error: "+IntegerToString(mysqlt.GetServerError().code)+" ("+mysqlt.GetServerError().message+")");
              }
            else
              {
               if(GetLastError()>=ERR_USER_ERROR_FIRST)
                  PrintNotify("Transaction Error: "+EnumToString(ENUM_TRANSACTION_ERROR(GetLastError()-ERR_USER_ERROR_FIRST)));
               else
                  PrintNotify("Error: "+IntegerToString(GetLastError()));
              }
            continue;
           }
        }
      else
         continue;

Aqui nós podemos ver o familiar fragmento de código que apresenta a verificação da ausência da tabela e sua criação subsequente. Isso permite o tratamento correto da exclusão da tabela por terceiros enquanto o serviço está sendo executado. Observe também que a função Print() é substituída por PrintNotify(). Esta função duplica a string exibida no console como uma notificação se isso for permitido nas entradas:

//+------------------------------------------------------------------+
//| Print to console and send notification                           |
//+------------------------------------------------------------------+
void PrintNotify(string text)
  {
//--- display in the console
   Print(text);
//--- send a notification
   if(inp_notifications==true)
     {
      static datetime ts = 0;       // last notification sending time
      static string prev_text = ""; // last notification text
      if(text!=prev_text || (text==prev_text && (TimeLocal()-ts)>=(3600*6)))
        {
         // identical notifications are sent one after another no more than once every 6 hours
         if(SendNotification(text)==true)
           {
            ts = TimeLocal();
            prev_text = text;
           }
        }
     }
  }

Ao detectar as atualizações das propriedades, chamamos a função para escrever no banco de dados:

//+------------------------------------------------------------------+
//| Write signal properties to the database                          |
//+------------------------------------------------------------------+
bool  DB_Write(SignalProperties &sbuf[],string tab_name,datetime tc)
  {
//--- prepare a query
   string q = "insert ignore into `"+inp_db+"`.`"+tab_name+"` (";
   q+= "`TimeInsert`";
   for(int i=0; i<6; i++)
      q+= ",`"+tab_signal_base_double[i].name+"`";
   for(int i=0; i<7; i++)
      q+= ",`"+tab_signal_base_integer[i].name+"`";
   for(int i=0; i<3; i++)
      q+= ",`"+tab_signal_base_datetime[i].name+"`";
   for(int i=0; i<5; i++)
      q+= ",`"+tab_signal_base_string[i].name+"`";
   q+= ") values ";
   int sz = ArraySize(sbuf);
   for(int s=0; s<sz; s++)
     {
      q+=(s==0)?"(":",(";
      q+= "'"+DatetimeToMySQL(tc)+"'";
      for(int i=0; i<6; i++)
         q+= ",'"+DoubleToString(sbuf[s].props_double[i],4)+"'";
      for(int i=0; i<7; i++)
         q+= ",'"+IntegerToString(sbuf[s].props_int[i])+"'";
      for(int i=0; i<3; i++)
         q+= ",'"+DatetimeToMySQL(sbuf[s].props_datetime[i])+"'";
      for(int i=0; i<5; i++)
         q+= ",'"+sbuf[s].props_str[i]+"'";
      q+=")";
     }
//--- send a query
   if(mysqlt.Query(q)==false)
      return false;
//--- if the query is successful, get the pointer to it
   CMySQLResponse *r = mysqlt.Response(0);
   if(CheckPointer(r)==POINTER_INVALID)
      return false;
//--- the Ok type packet should be received as a response featuring the number of affected rows; display it
   if(r.Type()==MYSQL_RESPONSE_OK)
      Print("Added ",r.AffectedRows()," entries");
//
   return true;
  }

Tradicionalmente, o código da função começa com uma formação de consulta. Devido ao fato de decompormos as propriedades do mesmo tipo em arrays, a obtenção da lista de campos e valores é realizada em laços, parecendo bem compacta no código.

Depois de enviar uma consulta, nós esperamos a resposta do tipo Ok do servidor. A partir dessa resposta, nós obtemos o número de linhas afetadas usando o método AffectedRows(). Esse número é exibido no console. Em caso de falha, a função retorna false implicando que a mensagem de erro seja exibida no console e enviada como uma notificação, se isso for permitido nas configurações. As propriedades obtidas não são copiadas para o buffer principal. Após o período especificado, uma nova alteração de seus valores é detectada e é feita uma tentativa de gravá-los no banco de dados.

Serviço de coleta das propriedades de sinal

Fig. 1. Início do serviço de coleta das propriedades de sinal

A Fig. 1 exibe o início do serviço signs_to_db como é visto na janela Navegador. Não se esqueça de selecionar a guia Sinais, conforme mencionado acima, caso contrário, o serviço não receberá os novos dados.


Aplicação para a visualização da dinâmica das propriedades

Na seção anterior, nós implementamos o serviço adicionando ao banco de dados os valores das propriedades do sinal quando uma alteração é detectada. A próxima etapa é preparar um aplicativo destinado a exibir a dinâmica da propriedade selecionada dentro de um intervalo de tempo especificado como um gráfico. Também será possível selecionar apenas os sinais nos quais estamos interessados em usar os filtros de determinados valores de propriedade.

Como o aplicativo deve ter uma GUI avançada, a Biblioteca EasyAndFastGUI para criar interfaces gráficas de Anatoli Kazharski deve ser usada como base.

Visualizando o aplicativo

a)

Página web do sinal

b)

Fig. 2. A interface do usuário do programa: gráfico da propriedade Equity do sinal selecionado (a); o mesmo gráfico na página web do sinal (b)

A Fig. 2a apresenta a aparência da interface do usuário do programa. A parte esquerda contém o período, enquanto a parte direita tem o gráfico da propriedade Equity do sinal selecionado. Para comparação, a Fig. 2b apresenta a captura de tela de uma página web do sinal com o gráfico da propriedade Equity. O motivo das pequenas discrepâncias está nos "buracos" do banco de dados formados durante o tempo ocioso do PC, bem como no período relativamente grande de atualização dos valores das propriedades do sinal na plataforma.


Definindo uma tarefa

Portanto, o aplicativo terá a seguinte funcionalidade:


Implementação

De todos os elementos gráficos, nós precisamos de um bloco de dois calendários para definir as datas "de" e "para", um grupo de caixas de combinação para selecionar os valores das listas e o bloco de campos de entrada para editar os valores extremos das propriedades, caso um intervalo deva ser definido. Para desativar as condições, usamos para as listas o valor da chave "All", localizada no início. Além disso, podemos equipar os blocos do campo de entrada com uma caixa de seleção que está desativada por padrão.

O intervalo das datas deve ser especificado em todos os momentos. Todo o resto pode ser personalizado conforme necessário. A Fig. 2a mostra que uma moeda e um corretora são definidas rigidamente no bloco de propriedades do tipo string, enquanto o nome do autor do sinal não é regulado (valor All)

Cada lista de caixas de combinação é formada usando os dados obtidos ao manipular uma consulta. Isso também se aplica aos valores extremos dos campos de entrada. Após formar a lista de IDs de sinal e selecionar alguns de seus elementos, a consulta de dados para plotar um gráfico de uma propriedade especificada é enviada.

Para obter mais informações sobre como o programa interage com o servidor MySQL, exibimos os contadores de bytes recebidos e enviados, bem como o horário da última transação (Fig. 2) na barra de status. Se uma transação falhar, exibimos o código de erro (Fig. 3).


Exibindo o erro na barra de status

Fig. 3. Exibe o código de erro na barra de status e a mensagem na guia Experts

Como a maioria das descrições textuais de erros do servidor não se encaixa na barra de progresso, exibimos elas na guia Experts.

Como o artigo atual não tem nada a ver com os gráficos, eu não vou insistir na implementação da interface do usuário aqui. O trabalho com a biblioteca é descrito em detalhes pelo seu autor na série de artigos. Eu fiz algumas alterações em alguns arquivos de exemplo que foram tirados como base:

    • MainWindow.mqh — construindo uma interface gráfica
    • Program.mqh — interagindo com a interface gráfica
    • Main.mqh — trabalhando com o banco de dados (adicionado)


    Consultas múltiplas

    As consultas ao banco de dados usadas ao executar o programa podem ser divididas aproximadamente em três grupos:

    • Consultas para obter os valores da lista de caixas de combinação
    • Consultas para obter os valores extremos dos blocos de campos de entrada
    • Consultas dos dados para a criação de um gráfico

    Enquanto nos dois últimos casos, uma única consulta SELECT é suficiente, a primeira exige o envio de uma consulta separada para cada uma das listas. Em algum momento, nós não podemos aumentar o tempo para a obtenção dos dados. Idealmente, todos os valores devem ser atualizados simultaneamente. Além disso, é impossível atualizar apenas uma parte das listas. Para fazer isso, usamos uma consulta múltipla. Mesmo se uma transação (incluindo sua manipulação e transferência) for atrasada, a interface será atualizada somente depois que todas as respostas do servidor forem aceitas. Em caso de erro, a atualização parcial das listas de elementos gráficos da interface é desativada.

    Abaixo está uma amostra de várias consultas enviadas imediatamente ao iniciar o programa.

    select `Currency` from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59'  
    group by `Currency`; 
    select `Broker` from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59'  
    group by `Broker`; 
    select `AuthorLogin` from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59'  
    group by `AuthorLogin`; 
    select `Id` from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59'  
    group by `Id`; 
    select  Min(`Equity`) as EquityMin,             Max(`Equity`) as EquityMax, 
            Min(`Gain`) as GainMin,                 Max(`Gain`) as GainMax, 
            Min(`Drawdown`) as DrawdownMin,         Max(`Drawdown`) as DrawdownMax, 
            Min(`Subscribers`) as SubscribersMin,   Max(`Subscribers`) as SubscribersMax 
    from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59'
    

    Como nós podemos ver, esta é uma sequência de cinco consultas "SELECT" separadas por ";". As quatro primeiras solicitam as listas contendo os valores únicos das propriedades especificadas (Currency, Broker, AuthorLogin e Id) em um intervalo de tempo especificado. A quinta consulta foi projetada para receber os valores mínimo e máximo de quatro propriedades (Equity, Gain, Drawdown e Subscribers) a partir do mesmo intervalo de tempo.

    Se nós observarmos a troca de dados com o servidor MySQL, nós podemos ver que: a consulta (1) foi enviada em um único pacote TCP, enquanto as respostas dela (2) foram entregues em pacotes TCP diferentes (veja a Figura 4).

    A consulta múltipla no analisador de tráfego

    Fig. 4. A consulta múltipla no analisador de tráfego

    Observe que se uma das consultas "SELECT" aninhadas causar um erro, as subsequentes não serão tratadas. Em outras palavras, o servidor MySQL lida com consultas até o primeiro erro.


    Filtros

    Para maior comodidade, vamos adicionar os filtros, reduzindo a lista de sinais, deixando apenas os que atendem aos requisitos definidos. Por exemplo, nós estamos interessados em sinais com uma determinada moeda base, um intervalo de crescimento especificado ou um número diferente de zero assinantes. Para fazer isso, aplicamos o operador WHERE na consulta:

    select `Broker` from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' AND `Currency`='USD' AND `Gain`>='100' AND `Gain`<='1399'  
    group by `Broker`; 
    select `AuthorLogin` from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' AND `Currency`='USD' AND `Gain`>='100' AND `Gain`<='1399'  
    group by `AuthorLogin`; 
    select `Id` from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' AND `Currency`='USD' AND `Gain`>='100' AND `Gain`<='1399'  
    group by `Id`; 
    select  Min(`Equity`) as EquityMin,             Max(`Equity`) as EquityMax, 
            Min(`Gain`) as GainMin,                 Max(`Gain`) as GainMax, 
            Min(`Drawdown`) as DrawdownMin,         Max(`Drawdown`) as DrawdownMax, 
            Min(`Subscribers`) as SubscribersMin,   Max(`Subscribers`) as SubscribersMax 
    from `signals_mt5`.`metaquotes_demo__17273508` 
    where `TimeInsert`>='2019-11-01 00:00:00' AND `TimeInsert`<='2019-12-15 23:59:59' AND `Currency`='USD' AND `Gain`>='100' AND `Gain`<='1399' 
    

    A consulta acima tem como objetivo obter as listas de caixas de combinação e valores extremos dos campos de entrada, desde que a moeda base seja USD, enquanto o valor do crescimento está na faixa de 100-1399. Aqui, nós precisamos primeiro prestar atenção à ausência de uma consulta por valores da lista Currency. Isso é lógico, pois nós excluímos todos os valores ao selecionar um específico na lista da caixa de combinação. Ao mesmo tempo, a consulta de valores sempre é executada para os campos de entrada, mesmo que sejam usadas a condição. Isso é feito para permitir que um usuário veja um intervalo real de valores. Suponha que nós introduzimos o valor mínimo de crescimento de 100. No entanto, considerando o conjunto de dados que atende aos critérios selecionados, o valor mínimo mais próximo é 135. Isso significa que, após receber a resposta do servidor, o valor de 100 é substituído por 135.

    Após fazer uma consulta com os filtros especificados, a lista dos valores da caixa de combinação ID Signal é reduzida significativamente. É possível selecionar um sinal e acompanhar as alterações de suas propriedades no gráfico.


    Modo de conexão constante Keep Alive

    Se nós olharmos atentamente para a Fig. 4, nós podemos ver que não há encerramento de conexão lá. A razão para isso é que o programa para visualizar a dinâmica das propriedades do sinal aplica o modo de conexão constante, que nós vamos considerar aqui.

    Ao desenvolver o serviço de coleta de dados, nós deixamos o parâmetro "constant connection" desativado. Os dados raramente são registrados e não havia sentido em manter a conexão. Este não é o caso aqui. Suponha que um usuário esteja procurando um sinal adequado usando o gráfico dinâmico de uma determinada propriedade. A consulta é enviada ao banco de dados toda vez que qualquer um dos elementos de controle é modificado. Não seria totalmente correto estabelecer e fechar a conexão todas as vezes neste caso.

    Para ativar o modo de conexão constante, definimos o tempo limite igual a 60 segundos.

       if(CheckPointer(mysqlt)==POINTER_INVALID)
         {
          mysqlt = new CMySQLTransaction;
          mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,60000);
          mysqlt.PingPeriod(10000);
         }
    

    Isso significa que a conexão será fechada se um usuário permanecer inativo por mais de 60 segundos.

    Vamos ver como isso fica na prática. Suponha que um usuário altere um determinado parâmetro permanecendo ocioso por um minuto depois disso. A captura de pacotes de rede terá a seguinte aparência:

    O ping no modo de conexão constante

    Fig. 5. Capturando os pacotes ao trabalhar no modo Keep Alive

    A imagem mostra a consulta (1), a série de ping com o período de 10 segundos (2) e o fechamento da conexão até um minuto após a consulta (3). Se o usuário tivesse continuado o trabalho e as consultas tivessem sido enviadas mais de uma vez por minuto, a conexão não teria sido fechada.

    A especificação de parâmetros da classe de transação também foi acompanhada pelo período de ping igual a 10 segundos. Por que nós precisamos disso? Antes de tudo, é necessário que o servidor não feche a conexão de acordo com o tempo limite definido na configuração, desde que o valor do tempo limite possa ser obtido usando a seguinte consulta:

    show variables 
            where `Variable_name`='interactive_timeout'
    

    Na maioria das vezes, são 3 600 segundos. Teoricamente, é suficiente enviar um ping com um período menor que o tempo limite do servidor, impedindo que a conexão seja fechada do seu lado. Mas, neste caso, nós somos capazes de saber sobre a perda de conexão apenas ao enviar a próxima consulta. Pelo contrário, quando o valor de 10 segundos é definido, nós somos capazes de saber sobre a perda de conexão quase imediatamente.


    Recuperação de dados

    Vamos dar uma olhada na resposta do servidor a uma consulta múltipla usando a implementação do método GetData como exemplo. O método foi desenvolvido para atualizar o conteúdo das listas suspensas, valores extremos do campos de entrada, bem como o gráfico dinâmico de uma propriedade selecionada.
    void CMain::GetData(void)
      {
       if(CheckPointer(mysqlt)==POINTER_INVALID)
         {
          mysqlt = new CMySQLTransaction;
          mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,60000);
          mysqlt.PingPeriod(10000);
         }
    //--- Save signal id
       string signal_id = SignalId();
       if(signal_id=="Select...")
          signal_id="";
    //--- Make a query
       string   q = "";
       if(Currency()=="All")
         {
          q+= "select `Currency` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Currency`; ";
         }
       if(Broker()=="All")
         {
          q+= "select `Broker` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Broker`; ";
         }
       if(Author()=="All")
         {
          q+= "select `AuthorLogin` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `AuthorLogin`; ";
         }
       q+= "select `Id` from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition()+" group by `Id`; ";
       q+= "select Min(`Equity`) as EquityMin, Max(`Equity`) as EquityMax";
       q+= ", Min(`Gain`) as GainMin, Max(`Gain`) as GainMax";
       q+= ", Min(`Drawdown`) as DrawdownMin, Max(`Drawdown`) as DrawdownMax";
       q+= ", Min(`Subscribers`) as SubscribersMin, Max(`Subscribers`) as SubscribersMax from `"+m_mysql_db+"`.`"+m_mysql_table+"` where "+Condition();
    //--- Display the transaction result in the status bar
       if(UpdateStatusBar(mysqlt.Query(q))==false)
          return;
    //--- Set accepted values in the combo box lists and extreme values of the input fields
       uint responses = mysqlt.Responses();
       for(uint j=0; j<responses; j++)
         {
          if(mysqlt.Response(j).Fields()<1)
             continue;
          if(UpdateComboBox(m_currency,mysqlt.Response(j),"Currency")==true)
             continue;
          if(UpdateComboBox(m_broker,mysqlt.Response(j),"Broker")==true)
             continue;
          if(UpdateComboBox(m_author,mysqlt.Response(j),"AuthorLogin")==true)
             continue;
          if(UpdateComboBox(m_signal_id,mysqlt.Response(j),"Id",signal_id)==true)
             continue;
          //
          UpdateTextEditRange(m_equity_from,m_equity_to,mysqlt.Response(j),"Equity");
          UpdateTextEditRange(m_gain_from,m_gain_to,mysqlt.Response(j),"Gain");
          UpdateTextEditRange(m_drawdown_from,m_drawdown_to,mysqlt.Response(j),"Drawdown");
          UpdateTextEditRange(m_subscribers_from,m_subscribers_to,mysqlt.Response(j),"Subscribers");
         }
       GetSeries();
      }
    

    Primeiro, formamos uma consulta. Em relação às listas das caixas de combinação, somente as listas com o valor atualmente selecionado de All estão incluídos na consulta. As condições são montadas no método separado Condition():

    string CMain::Condition(void)
      {
    //--- Add the time interval
       string s = "`TimeInsert`>='"+time_from(TimeFrom())+"' AND `TimeInsert`<='"+time_to(TimeTo())+"' ";
    //--- Add the remaining conditions if required
    //--- For drop-down lists, the current value should not be equal to All
       if(Currency()!="All")
          s+= "AND `Currency`='"+Currency()+"' ";
       if(Broker()!="All")
         {
          string broker = Broker();
          //--- the names of some brokers contain characters that should be escaped
          StringReplace(broker,"'","\\'");
          s+= "AND `Broker`='"+broker+"' ";
         }
       if(Author()!="All")
          s+= "AND `AuthorLogin`='"+Author()+"' ";
    //--- A checkbox should be set for input fields
       if(m_equity_from.IsPressed()==true)
          s+= "AND `Equity`>='"+m_equity_from.GetValue()+"' AND `Equity`<='"+m_equity_to.GetValue()+"' ";
       if(m_gain_from.IsPressed()==true)
          s+= "AND `Gain`>='"+m_gain_from.GetValue()+"' AND `Gain`<='"+m_gain_to.GetValue()+"' ";
       if(m_drawdown_from.IsPressed()==true)
          s+= "AND `Drawdown`>='"+m_drawdown_from.GetValue()+"' AND `Drawdown`<='"+m_drawdown_to.GetValue()+"' ";
       if(m_subscribers_from.IsPressed()==true)
          s+= "AND `Subscribers`>='"+m_subscribers_from.GetValue()+"' AND `Subscribers`<='"+m_subscribers_to.GetValue()+"' ";
       return s;
      }
    

    Se a transação for bem-sucedida, obtemos o número de respostas que serão analisados no laço.

    O método UpdateComboBox() foi desenvolvido para atualizar os dados nas caixas de combinação. Ele recebe um ponteiro para a resposta e o nome do campo correspondente. Se o campo existir na resposta, os dados serão incluídos na lista de caixas de combinação e o método retornará true. O argumento set_value contém o valor da lista anterior selecionada pelo usuário durante a consulta. Ele deve ser encontrado na nova lista e definido como o atual. Se o valor especificado não estiver presente na nova lista, o valor de índice igual a 1 será definido (após "Select...").

    bool CMain::UpdateComboBox(CComboBox &object, CMySQLResponse *p, string name, string set_value="")
      {
       int col_idx = p.Field(name);
       if(col_idx<0)
          return false;
       uint total = p.Rows()+1;
       if(total!=object.GetListViewPointer().ItemsTotal())
         {
          string tmp = object.GetListViewPointer().GetValue(0);
          object.GetListViewPointer().Clear();
          object.ItemsTotal(total);
          object.SetValue(0,tmp);
          object.GetListViewPointer().YSize(18*((total>16)?16:total)+3);
         }
       uint set_val_idx = 0;
       for(uint i=1; i<total; i++)
         {
          string value = p.Value(i-1,col_idx);
          object.SetValue(i,value);
          if(set_value!="" && value==set_value)
             set_val_idx = i;
         }
    //--- if there is no specified value, but there are others, select the topmost one
       if(set_value!="" && set_val_idx==0 && total>1)
          set_val_idx=1;
    //---
       ComboSelectItem(object,set_val_idx);
    //---
       return true;
      }
    

    O método UpdateTextEditRange() atualiza os valores extremos dos campos de entrada de texto.

    bool CMain::UpdateTextEditRange(CTextEdit &obj_from,CTextEdit &obj_to, CMySQLResponse *p, string name)
      {
       if(p.Rows()<1)
          return false;
       else
          return SetTextEditRange(obj_from,obj_to,p.Value(0,name+"Min"),p.Value(0,name+"Max"));
      }
    

    Antes de sair do GetData(), o método GetSeries() é chamado, selecionando os dados por um ID do sinal e um nome da propriedade:

    void CMain::GetSeries(void)
      {
       if(SignalId()=="Select...")
         {
          // if a signal is not selected
          ArrayFree(x_buf);
          ArrayFree(y_buf);
          UpdateSeries();
          return;
         }
       if(CheckPointer(mysqlt)==POINTER_INVALID)
         {
          mysqlt = new CMySQLTransaction;
          mysqlt.Config(m_mysql_server,m_mysql_port,m_mysql_login,m_mysql_password,60000);
          mysqlt.PingPeriod(10000);
         }
       string   q = "select `"+Parameter()+"` ";
       q+= "from `"+m_mysql_db+"`.`"+m_mysql_table+"` ";
       q+= "where `TimeInsert`>='"+time_from(TimeFrom())+"' AND `TimeInsert`<='"+time_to(TimeTo())+"' ";
       q+= "AND `Id`='"+SignalId()+"' order by `TimeInsert` asc";
    
    //--- Send a query
       if(UpdateStatusBar(mysqlt.Query(q))==false)
          return;
    //--- Check the number of responses
       if(mysqlt.Responses()<1)
          return;
       CMySQLResponse *r = mysqlt.Response(0);
       uint rows = r.Rows();
       if(rows<1)
          return;
    //--- copy the column to the graph data buffer (false - do not check the types)
       if(r.ColumnToArray(Parameter(),y_buf,false)<1)
          return;
    //--- form X axis labels
       if(ArrayResize(x_buf,rows)!=rows)
          return;
       for(uint i=0; i<rows; i++)
          x_buf[i] = i;
    //--- Update the graph
       UpdateSeries();
      }
    

    Geralmente, sua implementação é semelhante ao método GetData() discutido acima. Mas há duas coisas que merecem atenção:

    O método mencionado foi projetado exatamente para os casos em que os dados da coluna devem ser copiados para o buffer. No caso atual, a verificação de tipos está desativada já que os dados da coluna podem ser do tipo inteiro ou real. Nos dois casos, eles devem ser copiados para o buffer 'double'.

    Os métodos GetData() e GetSeries() são chamados quando qualquer um dos elementos gráficos é alterado:

    //+------------------------------------------------------------------+
    //| Handler of the value change event in the "Broker" combo box      |
    //+------------------------------------------------------------------+
    void CMain::OnChangeBroker(void)
      {
       m_duration=0;
       GetData();
      }
    
    ...
    
    //+------------------------------------------------------------------+
    //| Handler of the value change event in the "SignalID" combo box    |
    //+------------------------------------------------------------------+
    void CMain::OnChangeSignalId(void)
      {
       m_duration=0;
       GetSeries();
      }
    

    Os códigos-fonte do manipulador das caixas de combinação Broker e Signal ID são exibidos acima. Os demais são implementados de maneira semelhante. Ao selecionar outra corretora, o método GetData() é chamado, e a partir daí, GetSeries(). Ao selecionar outro sinal, GetSeries() é chamado imediatamente.

    Na variável m_duration, o tempo total de manipulação de todas as consultas é acumulado, incluindo a transferência, e é exibido na barra de status. O tempo de execução da consulta é um parâmetro importante. Seus valores crescentes indicam os erros na otimização do banco de dados.

    O aplicativo em ação é exibido na Fig. 6.

    O aplicativo em ação

    Fig. 6. O programa para visualizar a dinâmica das propriedades do sinal em ação



    Conclusão

    Neste artigo, nós consideramos os exemplos de aplicação do Conector MySQL que analisamos com detalhes anteriormente. Ao implementar as tarefas, nós descobrimos que o uso da conexão constante durante as consultas frequentes ao banco de dados é a solução mais razoável. Nós também destacamos a importância de um ping para impedir o término da conexão do lado do servidor.

    Quanto às funções de rede, trabalhar com o MySQL é apenas uma pequena parte do que pode ser implementado com a ajuda deles, sem recorrer as bibliotecas dinâmicas. Nós vivemos na era das tecnologias de rede, e a adição do grupo de funções Socket é, sem dúvida, um marco significativo no desenvolvimento da linguagem MQL5.

    Conteúdo dos arquivos em anexo: