Trabajando con las funciones de red, o MySQL sin DLL: Parte II - El programa para monitorear los cambios de las propiedades de las señales

14 mayo 2020, 11:34
Serhii Shevchuk
0
1 123

Contenido

Introducción

En el artículo anterior, nos familiarizamos con la implementación del conector MySQL. Ha llegado el momento de desarollar ejemplos para su uso. Entre estos, el más sencillo y visual consiste en la recopilación de los valores de las propiedades de las señales con la posterior visualización de sus cambios. Para la mayoría de las cuentas en el terminal, están disponibles más de 100 señales, y cada señal tiene más de 20 parámetros, por lo que habrá una cantidad de datos considerable. Además, el ejemplo implementado puede tener un sentido práctico en el caso de que el usuario necesite observar los cambios de las propiedades que no se representan en la página web de la señal. Puede ser el gráfico de cambio del apalancamiento, el rating, el número de suscriptores y mucho más.

Para recopilar los datos, escribiremos un servicio que solicitará periódicamente las propiedades de las señales, comparándolas con los valores anteriores. De existir diferencias, el servicio enviará la matriz completa a la base de datos.

Para observar la dinámica de las propiedades, escribiremos un asesor que representará el cmabio de la propiedad seleccionada en forma de gráfico. Y también para ofrecer la posibilidad de filtrar las señales según los valores de ciertas propiedades con la ayuda de solicitudes condicionales a la base de datos.

Durante la implementación, hemos notado que, en ciertos casos, resulta deseable usar el modo de conexión conestante Keep Alive y solicitudes múltiples.


Servicio de recopilación de datos

Bien, la tarea del servicio consiste en lo siguiente:

  1. Preguntar periódicamente las propiedades de todas las señales disponibles en el terminal
  2. Comparar sus valores con los anteriores
  3. En el caso de detectarse diferencias, escribir la matriz de valores completa en la base de datos
  4. Informar al usuario en caso de que surjan errores

Vamos a crear el nuevo servicio en el editor y a darle el nombre "signals_to_db.mq5". Los parámetros de entrada serán los siguientes:

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

Aquí, además de los ajustes de red, existen varias opciones:

  • inp_creating — permiso para crear recuadros en la base de datos. Si el servicio recurre a un recuadro que no existe, podrá crearlo si este parámetro tien el valor true
  • inp_period — periodo de solicitud de las propiedades de las señales en segundos
  • inp_notifications — permiso para enviar notificaciones sobre los errores en el trabajo con el servidor MySQL


Obteniendo los valores de las propiedades de las señales

Para que el servicio funcione correctamente, es importante saber en qué momento se actualizan las propiedades de las señales en el terminal. Y esto sucede en dos casos:
  • Al iniciarse el terminal.
  • De forma periódica durante el funcionamiento del terminal, con la condición de que la pestaña "Señales" en la ventana "Herramientas" esté activa. En este caso, la periodicidad de la actualización de los datos es de 3 horas.

Por lo menos, así sucede en la versión del terminal más actual al momento de escribir el presente artículo.

Las propiedades de las señales que vamos a tratar son de cuatro tipos:

El tipo ENUM_SIGNAL_BASE_DATETIME se ha creado para diferenciar las propiedades ENUM_SIGNAL_BASE_INTEGER que deberán transformarse en una línea como tiempo, y no como número entero.

Para que el trabajo resulte más cómodo, distribuiremos los valores de las enumeraciones con las propiedades de un mismo tipo por matrices (cuatro tipos de propiedades, cuatro matrices). Formando una pareja con cada enumeración se encontrará una descripción de texto de la propiedad, que también supondrá el nombre del campo correspondiente en el recuadro de la base de datos; para conseguir esto, crearemos la siguiente estructura:

//--- Estructuras de descripción de las propiedades de las señales para cada tipo
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;
  };

A continuación, declaramos las matrices de las estructuras (más abajo, se muestra un ejemplo para ENUM_SIGNAL_BASE_DOUBLE, para el resto de los ejemplos será igual):

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}
  };
Ahora, para obtener los valores de las propiedades de la señal elegida, simplemente deberemos pasar por cuatro ciclos:
   //--- Leyendo las propiedades de la señal
   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));
     }

En el ejemplo de arriba, Read() es el método de la estructura SignalProperties, en la que se reúne todo lo necesario para trabajar con las propiedades de las señales. Nos referimos a los búferes de cada uno de los tipos, así como a los métodos de comparación de los valores actuales con los anteriores:

//--- Estructura para trabajar con las propiedades de la señal
struct SignalProperties
  {
   //--- Búferes de las propiedades
   double            props_double[6];
   long              props_int[7];
   datetime          props_datetime[3];
   string            props_str[5];
   //--- Leyendo las propiedades de la señal
   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));
     }
   //--- Comparando la Id de la señal con el valor transmitido
   bool              CompareId(long id)
     {
      if(id==props_int[0])
         return true;
      else
         return false;
     }
   //--- Comparando los valores de las propiedades de la señal con los transmitidos mediante enlace
   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;
     }
   //--- Comparando los valores de las propiedades de la señal con los mismos que se hallan dentro del búfer transmitido (búsqueda por 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;
     }
  };


Adición a la base de datos

Para trabajar con la base de datos, en primer lugar, declaramos un ejemplar de la clase CMySQLTransaction:

//--- Incluyendo la clase transacción MySQL
#include  <MySQL\MySQLTransaction.mqh>
CMySQLTransaction mysqlt;

A continuación, debemos establecer los parámetros de conexión en la función OnStart(). Para ello, llamamos al método Config:

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Configuramos la clase de transacción MySQL
   mysqlt.Config(inp_server,inp_port,inp_login,inp_password);
   
   ...
  }

El siguiente paso consistirá en crear el nombre del recuadro. Dado que el conjunto de señales depende del bróker y del tipo de la cuenta comercial, deberemos implicar a estos en el proceso. En el nombre del servidor, sustituimos el punto, el guión y el espacio en blanco por guión bajo, añadiéndole el login de la cuenta y cambiando todas las letras por minúsculas. De esta forma, teniendo el servidor del bróker "MetaQuotes-Demo" y el login " 17273508", el nombre del recuadro que obtendremos será "metaquotes_demo__17273508".

En el código, esto tendrá el aspecto siguiente:

//--- Añadiendo el nombre del recuadro
//--- para ello, obtenemos el nombre del servidor comercial
   string s = AccountInfoString(ACCOUNT_SERVER);
//--- sustituimos el espacio en blanco, el punto y el guión por guiones bajos
   string ss[]= {" ",".","-"};
   for(int i=0; i<3; i++)
      StringReplace(s,ss[i],"_");
//--- formamos el nombre del recuadro a partir del nombre del servidor 
   string tab_name = s+"__"+IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN));
//--- transformamos todas las letras en minúsculas
   StringToLower(tab_name);
//--- mostramos el resultado en la consola
   Print("Table name: ",tab_name);

A continuación, debemos leer los datos de la última entrada en la base. Esto se hace para que, al reiniciar el servicio, tengamos con qué comparar las propiedades obtenidas, para así detectar diferencias.

La función DB_Read() se ocupa de leer las propiedades de la base.

//+------------------------------------------------------------------+
//| Leyendo las propiedades de las señales de la base de datos       |
//+------------------------------------------------------------------+
bool  DB_Read(SignalProperties &sbuf[],string tab_name)
  {
//--- creando la solicitud
   string q="select * from `"+inp_db+"`.`"+tab_name+"` "+
            "where `TimeInsert`= ("+
            "select `TimeInsert` "+
            "from `"+inp_db+"`.`"+tab_name+"` order by `TimeInsert` desc limit 1)";
//--- enviando la solicitud
   if(mysqlt.Query(q)==false)
      return false;
//--- si la solicitud se ha ejecutado con éxito, obtenemos el puntero a la misma
   CMySQLResponse *r = mysqlt.Response();
   if(CheckPointer(r)==POINTER_INVALID)
      return false;
//--- leemos el número de filas en la respuesta recibida
   uint rows = r.Rows();
//--- preparamos la matriz
   if(ArrayResize(sbuf,rows)!=rows)
      return false;
//--- leemos los valores de las propiedades en la matriz
   for(uint n=0; n<rows; n++)
     {
      //--- obtenemos el puntero a la fila actual
      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;
  }
Los argumentos de la función son el enlace al búfer de señales y al nombre del recuadro que hemos formado al realizar la inicialización. En el cuerpo de la función, en primer lugar, creamos la solicitud. En este caso, deberemos leer todas las propiedades de aquellas señales en las que la adición temporal a la base de datos tiene el valor máximo. Dado que escribimos una matriz con todos los valores al mismo tiempo, para resolver este punto, bastará con encontrar la hora máxima en el recuadro y leer todas las líneas donde la hora de adición sea igual a la hallada. Por ejemplo, si el nombre en la base de datos es signals_mt5, y el nombre del recuadro es metaquotes_demo__17273508, la solicitud tendrá el aspecto siguiente:
select * 
from `signals_mt5`.`metaquotes_demo__17273508`
where `TimeInsert`= (
        select `TimeInsert`
        from `signals_mt5`.`metaquotes_demo__17273508` 
        order by `TimeInsert` desc limit 1)

En rojo se destaca la solicitud incorporada que retorna el valor máximo de la columna `TimeInsert`, es decir la hora de la última adición a la base de datos. La solicitud destacada en verde retorna todas las líneas donde el valor `TimeInsert` se corresponde con el hallado.

Si la transacción ha tenido éxito, procedemos a leer los datos recibidos. Para ello, primero obtenemos el puntero a la clase de la respuesta del servidor CMySQLResponse, y después el número de filas en la respuesta, partiendo del cual, modificaremos el tamaño del búfer de señales.

Ahora podemos leer las propiedades. Para ello, obtenemos el puntero a la fila actual, usando el índice. Después de ello, leemos los valores para cada tipo de propiedad. Por ejemplo, para leer las propiedades ENUM_SIGNAL_BASE_DOUBLE, usamos el método CMySQLRow::Double(), donde el primer argumento — el nombre del campo — es el nombre de texto de la propiedad.

Vamos a analizar el caso en el que el envío de una solicitud ha finalizado con error. Para ello, retornamos al código fuente de la función OnStart().
//--- Declaramos el búfer de propiedades de las señales
   SignalProperties sbuf[];
//--- Pasamos los datos de la base al búfer
   bool exit = false;
   if(DB_Read(sbuf,tab_name)==false)
     {
      //--- si la función de lectura ha retornado error
      //--- es posible que no haya recuadro
      if(mysqlt.GetServerError().code==ER_NO_SUCH_TABLE && inp_creating==true)
        {
         //--- si necesitamos crear un recuadro y esto está permitido en los ajustes
         if(DB_CteateTable(tab_name)==false)
            exit=true;
        }
      else
         exit=true;
     }

Si aparece un error, comprobamos en primer lugar que esto no haya sucedido debido a la ausencia del recuadro. Esto ocurre si el recuadro aún no ha sido creado, o bien si ha sido eliminado o renombrado por terceras personas durante el funcionamiento. Si obtenemos el error ER_NO_SUCH_TABLE, significará que debemos crear un recuadro (con la condición de que esté permitido).

La función de creación de recuadros DB_CteateTable() es bastante sencilla:

//+------------------------------------------------------------------+
//| Creando un recuadro                                              |
//+------------------------------------------------------------------+
bool  DB_CteateTable(string name)
  {
//--- creando la solicitud
   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";
//--- enviando la solicitud
   if(mysqlt.Query(q)==false)
      return false;
   return true;
  }
En el propio proceso, entre los nombres de los campos que también son nombres de las propiedades de las señales, se encuentra igualmente la hora de adición de los datos `TimeInsert`. Se trata de la hora local del terminal en el momento de la obtención de las propiedades actualizadas. Además, tenemos la clave única para los campos `TimeInsert` y `Id`, así como los índices necesarios para acelerar la ejecución de las solicitudes.

Si no hemos logrado crear el recuadro, salimos de la descripción del error y finalizamos el funcionamiento del servicio.

   if(exit==true)
     {
      if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR))
        {
         // si se trata de un error del servidor
         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;
     }

Aquí, debemos considerar que podemos tener tres tipos de errores.

  • El error retornado por el servidor MySQL (no hay recuadro, no hay base de datos, el login o la contraseña es incorrecto)
  • El error de tiempo de ejecución (host prohibido, error de conexión)
  • El error ENUM_TRANSACTION_ERROR

Del tipo de error dependerá cómo se forma su descripción. La metodología de determinación del error es la siguiente.

Si la transacción ha tenido lugar sin errores, entraremos en el ciclo principal del programa:

//--- establecemos la marca de tiempo de la anterior lectura de las propiedades de las señales
   datetime chk_ts = 0;

   ...
//--- Ciclo principal de funcionamiento del servicio
   do
     {
      if((TimeLocal()-chk_ts)<inp_period)
        {
         Sleep(1000);
         continue;
        }
      //--- es momento de leer la propiedades de las señales
      chk_ts = TimeLocal();

      ...

     }
   while(!IsStopped());

Precisamente en este ciclo infinito se encontrará nuestro servicio, hasta que sea descargado. Con la periodicidad establecida, tendrá lugar la lecura de las propiedades, la comparación con los valores anteriores y el registro en la base de datos, en caso necesario.

Supongamos que hemos obtenido propiedades de las señales que se diferencian de los valores anteriores. Qué sucede a continuación:

      if(newdata==true)
        {
         bool bypass = false;
         if(DB_Write(buf,tab_name,chk_ts)==false)
           {
            //--- si necesitamos crear un recuadro y esto está permitido en los ajustes
            if(mysqlt.GetServerError().code==ER_NO_SUCH_TABLE && inp_creating==true)
              {
               if(DB_CteateTable(tab_name)==true)
                 {
                  //--- si la creación del recuadro ha tenido éxito, enviaremos los datos
                  if(DB_Write(buf,tab_name,chk_ts)==false)
                     bypass = true; // no se ha podido enviar
                 }
               else
                  bypass = true; // no se ha podido crear el recuadro
              }
            else
               bypass = true; // no hay recuadro, y no está permitido crear uno
           }
         if(bypass==true)
           {
            if(GetLastError()==(ERR_USER_ERROR_FIRST+MYSQL_ERR_SERVER_ERROR))
              {
               // si se trata de un error del servidor
               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;

Aquí, podemos ver el ya conocido fragmento de código con comprobación de la ausencia de recuadro y su posterior creación. Esto es necesario para procesar correctamente la situación en la terceras partes eliminan el recuadro durante el funcionamiento del servicio. Asimismo, debemos tener en cuenta que, en lugar del Print() acostumbrado, se usa PrintNotify(). Esta función duplica como notificación la línea mostrada en la consola, si lo permiten los parámetros de entrada:

//+------------------------------------------------------------------+
//| Imprimiendo en la consola y enviando una notificación            |
//+------------------------------------------------------------------+
void PrintNotify(string text)
  {
//--- mostrando en la consola
   Print(text);
//--- enviando notificación
   if(inp_notifications==true)
     {
      static datetime ts = 0;       // hora de envío de la última notificación
      static string prev_text = ""; // prueba de la última notificación
      if(text!=prev_text || (text==prev_text && (TimeLocal()-ts)>=(3600*6)))
        {
         // las notificaciones consecutivas se enviarán con una frecuencia no mayor a una vez cada 6 horas
         if(SendNotification(text)==true)
           {
            ts = TimeLocal();
            prev_text = text;
           }
        }
     }
  }

Al detectar actualizaciones en las propiedades, llamamos a la función de registro en la base de datos:

//+------------------------------------------------------------------+
//| Registrando las propiedades de las señales en la base de datos   |
//+------------------------------------------------------------------+
bool  DB_Write(SignalProperties &sbuf[],string tab_name,datetime tc)
  {
//--- creando la solicitud
   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+=")";
     }
//--- enviando la solicitud
   if(mysqlt.Query(q)==false)
      return false;
//--- si la solicitud se ha ejecutado con éxito, obtenemos el puntero a la misma
   CMySQLResponse *r = mysqlt.Response(0);
   if(CheckPointer(r)==POINTER_INVALID)
      return false;
//--- como respuesta, debe llegar un paquete del tipo "Ok" que contendrá el número de filas afectadas, lo mostramos
   if(r.Type()==MYSQL_RESPONSE_OK)
      Print("Añadidas ",r.AffectedRows()," entradas");
//
   return true;
  }

Tradicionalmente, el código de la función comienza por la formación de la solicitud. Gracias a que hemos distribuido las propiedades del mismo tipo por matrices, la obtención de la lista de campos y valores tiene lugar en los ciclos, y el código posee un aspecto muy compacto.

Después de enviar la solicitud, esperamos del servidor la respuesta del tipo "Ok", a partir de la cual, con la ayuda del método AffectedRows(), obtenemos el número de filas afectadas, que mostramos en la consola. En caso de fracaso, la función retornará false, lo cual conllevará que se registre en la consola un mensaje sobre el error y se envíe una copia en forma de notificación, si está permitido en los ajustes. En este caso, además, las propiedades obtenidas no serán copiadas en el búfer principal: detectaremos nuevamente el cambio de valores tras el periodo indicado, e intentaremos registrar estos en la base de datos.

Servicio de recopilación de propiedades de las señales

Fig. 1. Servicio de recopilación de propiedades de las señales iniciado

En la figura 1, se muestra el servicio signals_to_db iniciado con el aspecto que tiene en la ventana "Navegador". En este caso, además, no debemos olvidar que es necesario seleccionar la pestaña "Señales", como hemos mencionado anteriormente, de lo contrario, el servicio no recibirá nuevos datos.


Aplicación para visualizar la dinámica de las propiedades

En la sección anterior, escribimos un servicio que añade a la base de datos los valores de las propiedades de las señales al detectar un cambio en las mismas. El siguiente paso consistirá en crear una aplicación pensada para representar en forma de gráfico la dinámica de una propiedad seleccionada en un periodo de tiempo determinado. Además, preveremos la posibilidad de representar solo las señales que nos interesen, usando para ello los filtros de valores de ciertas propiedades.

Puesto que la aplicación debe tener una interfaz gráfica avanzada, tomaremos como ejemplo "EasyAndFastGUI, la biblioteca de creación de interfaces gráficas del " autor Anatoli Kazharski.

Programa de visualización

a)

Página web de la señal

b)

Fig. 2. Interfaz personalizada del programa: se muestra el gráfico de cambio de la propiedad Equity de la señal seleccionada (a); el mismo gráfico de la propiedad Equity en la página web de la señal (b)

En la figura 2а, se muestra el aspecto externo de la interfaz de usuario del programa. En la parte izquierda, se indica el intervalo de fechas, en la derecha, se muestra el gráfico de la propiedad Equity de la señal seleccionada. Para realizar la comprobación, en la figura 2b podemos ver la página web de esta señal con el gráfico de la propiedad Equity. El motivo de las pequeñas divergencias son algunos "agujeros" en la base de datos, formados mientras el PC estaba desconectado, así como un periodo relativamente grande de actualización de las propiedades de las señales en el terminal.


Formulando la tarea

Bien, nuestra aplicación poseerá la siguiente funcionalidad:

  • Al seleccionar la muestra de datos del recuadro, tendrá la posibilidad de:
    • Establecer el intervalo de fechas
    • Establecer condiciones según los valores de las propiedades SIGNAL_BASE_CURRENCY, SIGNAL_BASE_AUTHOR_LOGIN, SIGNAL_BASE_BROKER
    • Establecer el intervalo de valores permitidos para las propiedades SIGNAL_BASE_EQUITY, SIGNAL_BASE_GAIN, SIGNAL_BASE_MAX_DRAWDOWN, SIGNAL_BASE_SUBSCRIBERS
  • Construir el gráfico de la propiedad indicada, seleccionado el valor SIGNAL_BASE_ID


Implementación

Entre los elementos gráficos, necesitaremos un bloque con dos calendarios para establer las fechas "desde" y "hasta", un grupo de cuadros combinados para seleccionar los valores de las listas, y un bloque de campos de edición para editar los valores límite de las propiedades para las que debemos establecer el intervalo. Para desactivar las condiciones, usaremos para las listas el valor clave "All", ubicado al principio, mientras que los bloques con los campos de edición podemos equiparlos con una casilla de verificación desactivada por defecto.

El intervalo de fechas debe establecerse siempre. Lo demás se puede configurar según las propias necesidades. En la figura 2a, podemos ver que en el bloque de propiedades de tipo string la divisa se ha establecido de forma rigurosa, mientras que el nombre del autor de la señal puede ser cualquiera (valor "All").

Cada lista del cuadro combinado se forma a partir de los datos obtenidos como resultado del procesamiento de la solicitud. De la misma forma que los valores límite para los campos de edición. Una vez hayamos formado la lista con los identificadores de las señales y seleccionado alguno de sus elementos, enviaremos una solicitud de datos para construir el gráfico de la propiedad indicada.

Para disponer de más información sobre la interacción de nuestro programa con el servidor MySQL, mostraremos en la barra de estado los contadores con los bytes recibidos y enviados, así como la hora de ejecución de la última transacción (figura 2). Si la transacción se ha realizado sin éxito, mostramos el código de error (figura 3).


Código de error en la barra de estado

Fig.3. Muestra del código de error en la barra de estado y mensajes en la pestaña "Expertos"

Dado que la mayoría de las descripciones de texto de los errores del servidor no cabrá en la barra de estado, mostraremos estos en la pestaña "Expertos".

Como el tema de este artículo no está relacionado con el gráfico, no nos detendremos con detalle en la construcción de la interfaz de usuario, tanto más que el trabajo con la biblioteca utilizada se describe en el ciclo de artículos perteneciente a su autor. Respecto al ejemplo tomado como base, solo han sufrido cambios varios archivos, concretamente:

      • MainWindow.mqh — construcción de la interfaz gráfica
      • Program.mqh — interacción con la interfaz gráfica
      • Main.mqh — trabajo con la base de datos (añadido)


      Solicitudes múltiples

      Las solicitudes a la base de datos (utilizadas durante el funcionamiento del programa) se pueden dividir en tres grupos:

      • Solicitudes para obtener los valores de las listas de cuadros combinados
      • Solicitudes para obtener los valores límite de los bloques de los campos de edición
      • Solicitud de datos para la construcción del gráfico

      En los casos segundo y tercero, podemos limitarnos a una sola solicitud SELECT, pero en el primero, en cambio, debemos enviar una solicitud aparte para cada una de las listas. Al mismo tiempo, no podemos "estirar" en el tiempo los datos obtenidos: lo ideal es que los valores se actualicen al instante, al igual que no podemos actualizar solo una parte de las listas. Para ello, utilizaremos una solicitud múltiple. Incluso si la duración de la transacción -incluyendo el procesamiento y el transporte- se prolonga demasiado, la actualización de la interfaz tendrá lugar solo después de que se reciban todas las respuestas del servidor. En caso de error, no se realizará la actualización parcial de las listas de los elementos gráficos de la interfaz.

      Más abajo, podemos ver un ejemplo de solicitud múltiple enviada justo al iniciar el 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 podemos ver, se trata de una secuencia de cinco solicitudes "SELECT", separadas por el símbolo ";". Las primeras cuatro solicitan las listas de valores únicos de las propiedades solicitadas (Currency, Broker, AuthorLogin y Id) en el intervalo temporal indicado. La quinta solicitud se encarga de obtener los valores mínimo y máximo de las cuatro propiedades (Equity, Gain, Drawdown y Subscribers) de este mismo intervalo temporal.

      Si nos fijamos en la captura del intercambio de datos con el servidor MySQL, veremos que la solicitud (1) ha sido enviada en un paquete TCP, mientras que las respuestas a la misma (2) se han entregado en paquetes TCP distintos (ver figura 4).

      Solicitud múltiple en el analizador de tráfico

      Fig. 4. Solicitud múltiple en el analizador de tráfico

      Debemos tener en cuenta una peculiaridad de las solicitudes múltiples. Si alguno de los "SELECT" incorporados ha provocado un error, los siguientes no se procesarán. En otras palabras, el servidor MySQL procesa las solicitudes hasta el primer error.


      Filtros

      Para que resulte más cómodo trabajar con las señales, vamos a añadir filtros que permitirán reducir la lista de señales, dejando solo aquellas que cumplen las condiciones establecidas. Por ejemplo, supongamos que nos interesan las señales con una divisa básica determinada, un intervalo de crecimiento concreto o un número de suscriptores distinto a cero. Para ello, aplicaremos en la solicitud el operador "WHERE":

      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' 
      

      La solicitud mostrada más arriba se ha pensado para obtener las listas de cuadros combinados y los valores límite de los campos de edición con la condición de que la divisa básica sea "USD", y el valor de crecimiento se encuentre en el intervalo de 100 a 1399. Aquí, deberemos prestar atención, en primer lugar, a la ausencia de la solicitud de los valores para la lista "Currency". Es es lógico, ya que, eligiendo un valor concreto en la lista de un cuadro combinado, excluimos a los demás. Al mismo tiempo, para los campos de edición, la solicitud de los valores se realiza siempre, incluso si se usan en la condición. Esto se hace para que el usuario vea el intervalo de valores real. Supongamos que hemos introducido un valor de crecimiento de 100, pero que, partiendo del conjunto de datos que se corresponde con los criterios seleccionados, el valor mínimo más próximo es de 135. Esto significará que, después de recibir la respuesta del servidor, el valor 100 será corregido por 135.

      Después de realizar la solicitud con los filtros indicados, la lista de valores del cuadro combinado "Signal ID" se ha reducido significativamente. Podemos seleccionar alguna señal y observar cómo cambian sus propiedades en el gráfico.


      Modo de conexión constante Keep Alive

      Si miramos con atención la figura 4, notaremos que no hay cierre de la conexión. El motivo es que en el programa para visualizar la dinámica de las propiedades de las señales, se usa el modo de conexión constante, sobre el que hablaremos ahora.

      Al desarrollar el servicio de recopilación de datos, hemos dejado el parámetro "conexión constante" desactivado. Los datos se registraban raramente, por lo que no había motivo para mantener la conexión. Pero aquí sí que tiene sentido. Imaginemos que el usuario busca una señal adecuada según la imagen que necesita en el gráfico de cambio de una propiedad determinada. Con cada modificación del valor de cualquiera de los elementos de control, se envia una solicitud a la base de datos. Y en este caso, no resulta demasiado correcto establecer y cerrar una conexión cada vez.

      Para activar el modo de conexión constante, asignaremos al mismo un timeout de 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);
           }
      

      Esto significa que si el usuario permanece inactivo durante más de 60 segundos, la conexión se cerrará.

      Veamos qué aspecto tendrá en acción. Supongamos que el usuario ha cambiado un parámetro, y no ha tocado nada más durante un minuto. La captura de los paquetes de red tendrá el aspecto siguiente:

      Ping en el modo de conexión constante

      Fig. 5. Captura de paquetes trabajando en el modo Keep Alive

      En la figura, podemos ver la solicitud (1), una serie de pings con un periodo de 10 segundos (2) y el cierre de la conexión un minuto después de realizarse la solicitud (3). Si el usuario continuara trabajando con la aplicación, y las solicitudes se enviaran con mayor frecuencia de una vez por minuto, la conexión no se cerraría.

      Al establecer los parámetros de la clase de la transacción, también hemos establecido un perido de ping igual a 10 segundos. ¿Para qué lo necesitamos? En primer lugar, para que el servidor no cierre la conexión desde su lado según la configuración de timeout establecida, cuyo valor podemos obtener usando la siguiente solicitud:

      show variables 
              where `Variable_name`='interactive_timeout'
      

      Normalmente, este valor es igual a 3600 segundos. Y, en teoría, basta con enviar un ping con un periodo inferior al timeout del servidor, para de esta forma prevenir el cierre de la conexión desde su lado. Pero, en este caso, solo sabremos que se ha perdido la conexión en el momento del envío de la siguiente solicitud. Y estableciendo un valor de 10 segundos para la interrupción de la conexión, si esto sucediera, lo sabríamos prácticamente en el acto.


      Obteniendo los datos

      Vamos a analizar la descomposición de la respuesta del servidor a una solicitud múltiple usando como ejemplo la implementación del método GetData, pensado para actualizar el contenido de las listas desplegables y los valores límite de los campos de edición, así como el gráfico de cambio de la propiedad seleccionada a lo largo del tiempo.
      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);
           }
      //--- Guardamos signal id
         string signal_id = SignalId();
         if(signal_id=="Select...")
            signal_id="";
      //--- Creamos la lista
         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();
      //--- Mostramos el resultado de la transacción en la barra de estado
         if(UpdateStatusBar(mysqlt.Query(q))==false)
            return;
      //--- Establecemos los valores recibidos en la lista de cuadros combinados y valores límite de los campos de edición
         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();
        }
      

      En primer lugar, formamos la solicitud. En este caso, en lo que se refiere a las listas de cuadros combinados, en la solicitud entrarán solo aquellas en las que el valor seleccionado actual sea - "All". La composición de las condiciones se ha implementado en el método aparte Condition():

      string CMain::Condition(void)
        {
      //--- Añadiendo los límites de tiempo
         string s = "`TimeInsert`>='"+time_from(TimeFrom())+"' AND `TimeInsert`<='"+time_to(TimeTo())+"' ";
      //--- Añadiendo el resto de condiciones, si fuera necesario
      //--- Para las listas desplegables, el valor actual no deberá ser igual a "All"
         if(Currency()!="All")
            s+= "AND `Currency`='"+Currency()+"' ";
         if(Broker()!="All")
           {
            string broker = Broker();
            //--- en los nombres de algunos brókeres entran símbolos que deberemos configurar como secuencia de escape
            StringReplace(broker,"'","\\'");
            s+= "AND `Broker`='"+broker+"' ";
           }
         if(Author()!="All")
            s+= "AND `AuthorLogin`='"+Author()+"' ";
      //--- Para los campos de edición, deberá establecerse una casilla de verificación
         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;
        }
      

      Si la transacción ha tenido éxito, obtenedremos el número de respuestas que analizaremos en el ciclo a continuación.

      El método UpdateComboBox() ha sido pensado para actualizar los datos en los cuadros combinados. Este obtiene el puntero a la respuesta, así como los campos que le corresponden. Si el campo existe en la respuesta, los datos se registrarán en la lista del cuadro combinado, y el método retornará true. El argumento set_value contiene el valor de la lista anterior, seleccionado por el usuario en el momento de la solicitud. Deberemos encontrarlo en la nueva lista y establecerlo como el actual. Si en la nueva lista no se encuentra el valor indicado, se establecerá el valor con el índice 1 (el que sigue a "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;
           }
      //--- si no está el indicado, pero existen otros, seleccionamos el superior
         if(set_value!="" && set_val_idx==0 && total>1)
            set_val_idx=1;
      //---
         ComboSelectItem(object,set_val_idx);
      //---
         return true;
        }
      

      El método UpdateTextEditRange() actualiza los valores límite del grupo de campos de edición del 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 salir de GetData(), llamaremos al método GetSeries(), que selecciona los datos según el identificador de la señal y el nombre de la propiedad:

      void CMain::GetSeries(void)
        {
         if(SignalId()=="Select...")
           {
            // si la señal no ha sido seleccionada
            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";
      
      //--- Enviamos la solicitud
         if(UpdateStatusBar(mysqlt.Query(q))==false)
            return;
      //--- Comprobamos el número de respuestas
         if(mysqlt.Responses()<1)
            return;
         CMySQLResponse *r = mysqlt.Response(0);
         uint rows = r.Rows();
         if(rows<1)
            return;
      //--- copiamos la columna en el búfer de datos del gráfico (false - no se comprueban los tipos)
         if(r.ColumnToArray(Parameter(),y_buf,false)<1)
            return;
      //--- formamos los rótulos del eje X
         if(ArrayResize(x_buf,rows)!=rows)
            return;
         for(uint i=0; i<rows; i++)
            x_buf[i] = i;
      //--- Actualizamos el gráfico
         UpdateSeries();
        }
      

      En general, su implementación es similar al método GetData() analizado anteriormente. Pero hay dos cosas a las que debemos prestar atención:

      • Si la señal no ha sido seleccionada (el valor del cuadro combinado es igual a "Select..."), el gráfico quedará limpio y no sucederá nada más.
      • El uso del método ColumnToArray()

      El método mencionado ha sido diseñado precisamente para copiar los datos de una columna en un búfer cuando sea necesario. En este caso, está desactivada la comprobación de la correspondencia de tipos, dado que los datos en la columna pueden ser tanto de tipo entero como real, mientras que en ambos casos deberán ser copiados a un búfer del tipo double.

      Los métodos GetData() y GetSeries() se llaman cuando cualquiera de los valores de los elementos gráficos cambia:

      //+------------------------------------------------------------------+
      //| Manejador del evento de cambio de un valor en el cuadro combinado "Broker" 
      //+------------------------------------------------------------------+
      void CMain::OnChangeBroker(void)
        {
         m_duration=0;
         GetData();
        }
      
      ...
      
      //+------------------------------------------------------------------+
      //| Manejador del evento de cambio de un valor en el cuadro combinado "SignalId"   
      //+------------------------------------------------------------------+
      void CMain::OnChangeSignalId(void)
        {
         m_duration=0;
         GetSeries();
        }
      

      Más arriba se muestran los códigos fuente del cuadro combinado "Broker" y "Signal ID", los demás se implementan de forma análoga. Al seleccionar otro bróker, se llamará al método GetData(), y ya desde él, a GetSeries(). Al seleccionar otra señal, se llamará de inmediato a GetSeries().

      En la variable m_duration, se acumula la duración total de todas las solicitudes, incluyendo el transporte, que después se muestra en la barra de estado. El tiempo de ejecución de la solicitud es un parámetro importante. Si sus valores son elevados, esto nos indicará que hay un error en la optimización de la base de datos.

      En la figura 6, podemos ver una demostración del funcionamiento de la aplicación.

      Demostrando el funcionamiento de la aplicación

      Fig. 6. Demostración del funcionamiento de la aplicación para visualizar la dinámica de las propiedades de las señales



      Conclusión

      En el presente artículo, hemos analizado una aplicación del conector MySQL, cuya implementación habíamos analizado con detalle anteriormente. Durante la implementación de las tareas planteadas, hemos descubierto que al realizar solicitudes frecuentes a la base de datos, la solución más adecuada será usar la conexión constante. También hemos resaltado la importancia del ping para evitar el cierre de la conexión por parte del servidor.

      En lo que respecta a las funciones de servicio, el trabajo con MySQL supone solo una pequeña parte de lo que podemos implmentar con su ayuda, sin tener que recurrir al uso de bibliotecas dinámicas. Vivimos en la época de la tecnologías de red, y la adición del grupo de funciones de Socket representa un gran paso en el desarrollo del lenguaje MQL5.

      Contenido del fichero adjunto:

      • Archivo Services\signals_to_db.mq5 — código fuente del servicio de recopilación de datos
      • Carpeta Experts\signals_from_db\ — códigos fuente del programa de visualización de la dinámica de las propiedades de las señales
      • Carpeta Include\MySQL\ — códigos fuente del conector MySQL
      • Carpeta Include\EasyAndFastGUI\biblioteca de creación de interfaces gráficas (según su estado a la fecha en que se ha escrito el artículo)


      Traducción del ruso hecha por MetaQuotes Software Corp.
      Artículo original: https://www.mql5.com/ru/articles/7495

      Archivos adjuntos |
      MQL5.zip (355.92 KB)
      Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXXIV): Solicitudes comerciales pendientes - Eliminación de órdenes y posiciones según condiciones Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte XXXIV): Solicitudes comerciales pendientes - Eliminación de órdenes y posiciones según condiciones

      En el presente artículo, finalizaremos la descripción del concepto de trabajo con solicitudes pendientes y crearemos la funcionalidad para eliminar órdenes pendientes y posiciones según una condición. De esta forma, dispondremos de toda una funcionalidad con la que podremos crear estrategias de usuario sencillas, para ser más exactos, una cierta lógica de comportamiento que el asesor activará al cumplirse las condiciones establecidas por el usuario.

      Cómo crear gráficos 3D en DirectX en MetaTrader 5 Cómo crear gráficos 3D en DirectX en MetaTrader 5

      Los gráficos en 3D resultan de gran ayuda a la hora de analizar grandes volúmenes de datos, ya que permiten visualizar regularidades ocultas. Estas tareas también se pueden resolver directamente en MQL5: las funciones de trabajo con DireсtX permiten MetaTrader 5. Comience el estudio dibujando figuras de volumen sencillas.

      Trabajando con las series temporales en la biblioteca DoEasy (Parte 35): El objeto "Barra" y la lista de serie temporal del símbolo Trabajando con las series temporales en la biblioteca DoEasy (Parte 35): El objeto "Barra" y la lista de serie temporal del símbolo

      Con este artículo, comenzamos una nueva serie en la descripción de la biblioteca "DoEasy" para la creación rápida y sencilla de programas. Hoy, empezaremos a preparar la funcionalidad de la biblioteca para acceder a los datos de las series temporales de los símbolos y trabajar con los mismos. Asimismo, crearemos el objeto "Barra", encargado de guardar los datos tanto básicos como ampliados de la barra de la serie temporal, y también ubicaremos los objetos de barra en la lista de serie temporal para que resulte más cómodo buscar y clasificar dichos objetos.

      Pronosticación de series temporales (Parte 2): el método de los mínimos cuadrados de los vectores de soporte (LS-SVM) Pronosticación de series temporales (Parte 2): el método de los mínimos cuadrados de los vectores de soporte (LS-SVM)

      En el artículo se analiza la teoría y el uso práctico del algoritmo de pronosticación de series temporales usando como base el método de vectores de soporte. Asimismo, presentamos su implementación en MQL, además de varios indicadores de prueba y expertos. Esta tecnología todavía no ha sido implementada en MQL. Vamos a comenzar familiarizándonos con el aparato matemático.