Desarrollando una utilidad para la selección y navegación de instrumentos en los lenguajes MQL5 y MQL4

Roman Klymenko | 19 marzo, 2019

Introducción

Para el tráder avanzado no es un secreto que la mayor parte del tiempo que ocupa el comercio no se invierte en la apertura o acompañamiento de transacciones. Lo que más tiempo ocupa es la selección de instrumentos y la búsqueda de puntos de entrada.

Está calro que si usted trabaja con 1-2 instrumentos, este problema no le resultará familiar. Sin embargo, si su arsenal comercial incluye cientos de acciones del mercado de valores y decenas de parejas de divisas del mercado Fórex, solo en la búsqueda de puntos de entrada se le irá bastante más de una hora diaria.

En este artículo vamos a tratar de crear un asesor que le permita simplificar el proceso de búsqueda de acciones. Nos podrá ayuda de tres formas:

Plantilla inicial del asesor

Crearemos inicialmente el asesor en el lenguaje MQL5. Sin embargo, puesto que muchos brókeres hasta ahora no tienen en su arsenal cuentas en MetaTrader 5, al final de artículo reharemos el asesor de tal forma que también funcione en la plataforma MetaTrader 4.

Bien, vamos a comenzar la escritura del asesor por la plantilla, que prácticamente no se diferencia de la que se puede generar con el Wizard MQL5:

//+------------------------------------------------------------------+
//|                                                     _finder.mq5  |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://logmy.net"
#property version   "1.00"
ç#property strict

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

//--- create timer
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
  }
//+------------------------------------------------------------------+

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
  { 

}

En esta plantilla vamos a registrar el temporizador al crear un asesor. Nuestro temporizador se activará cada segundo. Es decir, se llamará la función estándar OnTimer una vez por segundo.

La única línea en esta plantilla que se diferencia de la plantilla que podría crear el wizard MQL5, es #property strict. Es necesaria para que nuestro asesor en el futuro funcione corerectamente en MetaTrader 4. Puesto que no influye de forma significativa en MetaTrader 5, la introduciremos en nuestra plantilla.

Bien, vamos a usar las siguientes funciones estándar:

Guardaremos la lista de símbolos que cumplen nuestras condiciones en el objeto de tipo CArrayString. Por eso, incluiremos en nuestro asesor el archivo MQH con la descripción de este objeto:

#include <Arrays\ArrayString.mqh>

Asimismo, para trabajar con los gráficos, necesitaremos el objeto del tipo CChart. Vamos a incluir también su definición:

#include <Charts\Chart.mqh>

Todo esto se hace al principio de nuestra plantilla, fuera de los límites de cualquier función, después del bloque de líneas #property.

A continuación, deberemos decidir qué anchura y altura tendrán todos los botones creados por el asesor. Introduciremos estos valores en las constantes, indicándolos después del bloque de líneas #include:

#define BTN_HEIGHT                        (20)
#define BTN_WIDTH                         (100)

Parámetros de entrada

La gestión del asesor se realizará a través de los parámetros de entrada. Vamos a echarles un vistazo, para que se vea al momento qué funcionalidad implementaremos más tarde en el artículo :

sinput string        delimeter_01="";        // --- Ajustes de filtrado ---
input bool           noSYMBmarketWath=true;  // Ocultar, si no hay en la Observación del Mercado
input bool           noSYMBwithPOS=true;     // Ocultar, si hay posiciones
input ValueOfSpread  hide_SPREAD=spread_b1;  // Ocultar, si hay un spread
input uint           hide_PRICE_HIGH=0;      // Cerrar, si el precio es superior
input uint           hide_PRICE_LOW=0;       // Cerrar, si el precio es inferior
input bool           hideProhibites=true;    // Cerrar, si el comercio está prohibido
input bool           hideClosed=true;        // Ocultar, si el mercado está cerrado
input StartHour      hide_HOURS=hour_any;    // Mostrar, si hay una hora de apertura
input double         hideATRcents=0.00;      // Ocultar, si ATR es inferior al valor establecido en dólares
sinput string        delimeter_02="";        // --- Ajustes de los gráficos ---
input bool           addInfoWatch=false;     // Añadir gráfico a la Observación del mercado
input bool           viewCandle=true;        // Abrir gráficos en forma de velas
input bool           viewVolumes=true;       // Mostrar volúmenes de ticks
input bool           showInfoSymbol=true;    // Mostrar la dirección del movimiento
input bool           showNameSymbol=true;    // Mostrar el nombre del símbolo

Podemos destacar al instante que los 2 parámetros de entrada tienen carácter personalizado. Por eso, antes de los parámetros de entrada, añadiremos la definición de los datos de los tipos. Ambos tipos personalizados son enumeraciones.

La enumeración ValueOfSpread determina las posibles condiciones del tamaño del spread de los símbolos que serán mostrados por el asesor:

enum ValueOfSpread
  {
   spread_no,//No
   spread_b05,// > 0.05%
   spread_b1,// > 0.1% 
   spread_b15,// > 0.15% 
   spread_l15,// < 0.15% 
   spread_l1,// < 0.1% 
  }; 

Un spread superior al 0.1% del precio se considerará elevado. Por eso, vamos a mostrar por defecto solo los símbolos en los que el spread sea menor al 0.1%. Es decir, el valor del parámetro de entrada Ocultar símbolos cuyo spread tiene un valor > 0.1%. Pero si la lista de estos símbolos para su bróker es muy pequeña, podrá elegir otro valor para este parámetro.

La enumeración StartHour contiene una lista con los principales periodos en los que tiene lugar la apertura de diferentes mercados:

enum StartHour
  {
   hour_any, //Cualquier hora
   hour_9am, // 9 de la mañana
   hour_10am,// 10 de la mañana 
   hour_4pm, // 16 de la tarde 
   hour_0am, // Medianoche
  }; 

El valor 9 de la mañana (o culquier otro) no significa que se vayan a mostrar solo símbolos que se abren exactamente a la hora indicada. Esto significa que se representarán los símbolos que se abren en esta hora. Por ejemplo, no solo a las 9:00, sino a las 9:05.

Por consiguiente, a las 16 horas significa que se mostrarán solo las acciones del mercado de valores americano que se abran a las 16:30.

El valor 10 de la mañana se refiere a las acciones de los mercados de valores ruso y europeo.

A las 9 de la mañana se abren ciertos índices.

Y a Medianoche se refiere a la hora de apertura del mercado Fórex.

Variables globales

Antes de comenzar a trabajar con el contenido de las funciones estándar, nos queda solo declarar una serie de variables cuya zona de visibilidad es nuestro asesor al completo. Las vamos a añadir después de los parámetros de entrada:

// prefijo que se añadirá a los nombres de todos los objetos gráficos creados por el asesor:
string exprefix="finder";
// matriz de símbolos que cumplen con nuestras condiciones:
CArrayString arrPanel1;
// índice del símbolo actual en la matriz arrPanel1:
int panel1val;
// matriz en la que vamos a ubicar los gráficos creados por el asesor (uno, por el momento):
CChart charts[];
// matriz en la que vamos a ubicar los punteros a los gráficos creados por el asesor (uno, por el momento):
long curChartID[];

Por los comentarios se deberá comprender para qué son necesarias estas variables.

Bien, todos los preparativos están listos. Ya podemos proceder al desarrollo del asesor. Pero, en primer lugar, vamos a ver lo que tenemos:

//+------------------------------------------------------------------+
//|                                                     _finder.mq5  |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://logmy.net"
#property version   "1.00"
ç#property strict

#include <Arrays\ArrayString.mqh>
#include <Charts\Chart.mqh>

#define BTN_HEIGHT                        (20)
#define BTN_WIDTH                         (100)

enum ValueOfSpread
  {
   spread_no,//No
   spread_b05,// > 0.05 %
   spread_b1,// > 0.1 %
   spread_b15,// > 0.15 %
   spread_l15,// < 0.15 %
   spread_l1,// < 0.1 %
  }; 
enum StartHour
  {
   hour_any,//Cualquier hora
   hour_9am,// 9 hora
   hour_10am,// 10 de la mañana 
   hour_4pm,// 16 de la tarde 
   hour_0am,// Medianoche
  }; 

input bool           noSYMBmarketWath=true; // Ocultar los símbolos que no estén en el panel de Observación del mercado
input bool           noSYMBwithPOS=true;    // Ocultar los símbolos de los que haya posiciones
input ValueOfSpread  hide_SPREAD=spread_b1; // Ocultar los símbolos de los que haya spread
input uint           hide_PRICE_HIGH=0;     // Ocultar los símbolos cuyo precio sea superior (0 - no ocultar)
input uint           hide_PRICE_LOW=0;      // Ocultar los símbolos cuyo precio sea inferior (0 - no ocultar)
input bool           hideProhibites=true;   // Ocultar los símbolos cuyo comercio esté prohibido
input StartHour      hide_HOURS=hour_any;   // Mostrar solo los símbolos que se abran en
input bool           viewCandle=true;       // Abrir los gráficos en forma de velas

// prefijo que se añadirá a los nombres de todos los objetos gráficos creados por el asesor:
string exprefix="finder";
// matriz de símbolos que cumplen con nuestras condiciones:
CArrayString arrPanel1;
// índice del símbolo actual en la matriz arrPanel1:
int panel1val;
// matriz en la que vamos a ubicar los gráficos creados por el asesor (uno, por el momento):
CChart charts[];
// matriz en la que vamos a ubicar los punteros a los gráficos creados por el asesor (uno, por el momento):
long curChartID[];

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- create timer
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }
  
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(reason!=REASON_CHARTCHANGE){
      ObjectsDeleteAll(0, exprefix);
   }
   EventKillTimer();
  }

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
  {
  }

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
  { 

}

Función de filtrado de símbolos

Vamos a comenzar por la función que mostrará en el gráfico los botones de los símbolos que cumplen nuestras condiciones. Llamaremos a esta función start_symbols. La llamada de esta función se ejecutará dentro de la función OnInit. Como resultado, la función OnInit tomará su aspecto final:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   start_symbols();

//--- create timer
   EventSetTimer(1);
      
//---
   return(INIT_SUCCEEDED);
  }

Es posible le surja la pregunta, ¿para qué crear una función aparte, si podemos escribir todo dentro de la función OnInit? Todo es muy sencillo. Llamaremos esta función no solo al iniciar el asesor, sino también al pulsar la tecla R. De esta forma, podremos actualizar con facilidad la lista de símbolos sin necesidad de eliminar el asesor del gráfico y abrirlo de nuevo.

Y tendremos que actualizar la lista con bastante frecuencia, ya que el spread cambia constantemente. Aparte de ello, la presencia de posiciones abiertas de diferentes símbolos también cambia. Por eso, antes de usar de nuevo un asesor iniciado anteriormente, no se olvide de actualizar la lista de símbolos con la tecla R, para ver los datos actuales.

Bien, vamos a ver la función start_symbols. Por cierto, también es un envoltorio para iniciar otras funciones:

void start_symbols(){
   // ponemos a cero (el primer símbolo en la matriz) el índice del símbolo actual en la lista de símbolos:
   panel1val=0;
   // preparamos la lista de símbolos:
   prepare_symbols();
   // eliminamos del gráfico los botones de los símbolos creados anteriormente:
   ObjectsDeleteAll(0, exprefix);
   // mostramos la lista de símbolos:
   show_symbols();
   // actualizamos el gráfico para ver los cambios:
   ChartRedraw(0);
}

Hemos encontrado otras 2 funciones personalizadas: prepare_symbols y show_symbols. La primera forma la matriz de símbolos que cumplen con nuestras condiciones. La segunda, muestra los botones de estos símbolos en el gráfico en el que está iniciado el asesor.

No hay nada complicado a la hora de mostrar los botones en el gráfico. Primero encontramos las coordenadas X e Y, conforme a las cuales debemos mostrar el siguiente botón de manera que no se superponga a otros botones. Y luego simplemente lo mostramos:

void show_symbols(){
   
   // inicializamos las variables para definir las coordenadas X e Y
   int btn_left=0;
   int btn_line=1;
   int btn_right=(int) ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-77;
   
   // para cada símbolo en la matriz mostramos el botón en el gráfico
   // en el botón escribiremos el nombre del símbolo
   for( int i=0; i<arrPanel1.Total(); i++ ){
      if( btn_left>btn_right-BTN_WIDTH ){
         btn_line++;
         btn_left=0;
      }
      
      ObjectCreate(0, exprefix+"btn"+(string) i, OBJ_BUTTON, 0, 0, 0);
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XDISTANCE,btn_left); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YDISTANCE,BTN_HEIGHT*btn_line); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_XSIZE,BTN_WIDTH); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_YSIZE,BTN_HEIGHT); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_FONTSIZE,8); 
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_COLOR,clrBlack); 
      ObjectSetString(0,exprefix+"btn"+(string) i,OBJPROP_TEXT,arrPanel1.At(i));    
      ObjectSetInteger(0,exprefix+"btn"+(string) i,OBJPROP_SELECTABLE,false);
      
      
      btn_left+=BTN_WIDTH;
   }
   
}

Como resultado, en el gráfico aparecerán los símbolos que cumplan con nuestras condiciones:

Mostrando los botones de los símbolos en el gráfico

Ahora vamos a pasar a la formación de las propias condiciones según las cuales se seleccionarán los símbolos. Es decir, a la función prepare_symbols. Para comenzar, simplemente vamos a añadir todos los símbolos a una lista:

void prepare_symbols(){
   // variable para el guardado temporal del nombre del símbolo
   string name;
   // variable para el guardado de las últimas cotizaciones del símbolo
   MqlTick lastme;
   
   // reseteamos la matriz de símbolos, si en ella ha habido algún valor
   arrPanel1.Resize(0);
   
   // formamos la matriz temporal tmpSymbols
   // esta contendrá todos los símbolos disponibles
   CArrayString tmpSymbols;
   for( int i=0; i<SymbolsTotal(noSYMBmarketWath); i++ ){
      tmpSymbols.Add(SymbolName(i, noSYMBmarketWath));
   }
   
   // aquí tendrá lugar la comprobación de las condiciones
   // y la colocación del símbolo en la lista de símbolos,
   // si este cumple las condiciones
   for( int i=0; i<tmpSymbols.Total(); i++ ){
      name=tmpSymbols[i];
      
      // limpiamos el nombre del símbolo de espacios sobrantes
      // quién sabe de dónde podría haber aparecido
      StringTrimLeft(name);
      StringTrimRight(name);
      if( !StringLen(name) ){
         continue;
      }
      
      // a continuación, tendrá lugar el filtrado principal de símbolos
      // ...

      
      // si el símbolo cumple todas nuestras condiciones, lo añadimos a la lista
      arrPanel1.Add(name);
   }
}

Primero ubicamos todos los símbolos en una matriz temporal. En esta etapa ya tiene lugar el filtrado inicial según el parámetro inicial Ocultar los símbolos que no estén en el panel de Observación del mercado.

En principio, podríamos no realizar la ubicación de símbolos en la matriz temporal. En lugar de ello, es posible colocar los símbolos que necesitemos en la lista principal. Pero entonces deberíamos reescribir el código, si quisiéramos, por ejemplo, añadir un parámetro de entrada en el que introducir solo aquellos símbolos que deben representarse en la lista. Es decir, usar símbolos propios en el orden que necesitemos, en lugar de tomar todos los símbolos ofrecidos por el bróker.

Por esto mismo, en el ciclo que itera todos los símbolos de la matriz temporal, en primer lugar se limpian los nombres de los símbolos de espacios. Y es que si, a pesar de todo usted quiere implementar el parámetro de entrada descrito más arriba, no habrá otro remedio que filtrar la entrada personalizada.

Ahora vamos a filtrar los símbolos obtenidos partiendo de los parámetros de entrada que tenemos. Añadiremos los bloques de código mostrados más abajo por debajo de la línea de comentario, a continuación, tendrá lugar el filtrado adicional de símbolos de la función prepare_symbols (en el ciclo de adición de símbolos a la lista).

Ocultar los símbolos de los que hay posiciones:

      // Ocultar los símbolos de los que hay posiciones
      bool isskip=false;
      if( noSYMBwithPOS ){
         // miramos la lista de todas las posiciones abiertas
         int cntMyPos=PositionsTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            // si hay posiciones del símbolo actual, omitimos
            if(PositionGetSymbol(ti) == name ){
               isskip=true;
               break;
            }
         }
         if(!isskip){
            int cntMyPosO=OrdersTotal();
            if(cntMyPosO>0){
               for(int ti=cntMyPosO-1; ti>=0; ti--){
                  ulong orderTicket=OrderGetTicket(ti);
                  if( OrderGetString(ORDER_SYMBOL) == name ){
                     isskip=true;
                     break;
                  }
               }
            }
         }
      }

Primero, comparamos si hay una posición del símbolo. Si no la hay, comprobamos de forma adicional si hay una orden límite del símbolo. Si hay una posición abierta u orden límite, omitimos el símbolo.

Ocultar los símbolos de los que hay spread:

      // si están activos los parámetros de entrada que usan el valor del precio actual del instrumento
      // intentamos obtener los valores actuales
      if(hide_PRICE_HIGH>0 || hide_PRICE_LOW>0 || hide_SPREAD>0 ){
         SymbolInfoTick(name, lastme);
         if( lastme.bid==0 ){
            Alert("No se ha logrado obtener el valor BID. Algunas funciones de filtrado podrían no funcionar.");
         }
      }
      if(hide_SPREAD>0 && lastme.bid>0){
         switch(hide_SPREAD){
            // si el spread actual es superior al 0.05% del precio, omitimos el símbolo
            case spread_b05:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.05 ){
                  isskip=true;
               }
               break;
            // si el spread actual es superior al 0.01% del precio, omitimos el símbolo
            case spread_b1:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.1 ){
                  isskip=true;
               }
               break;
            // si el spread actual es superior al 0.15% del precio, omitimos el símbolo
            case spread_b15:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 > 0.15 ){
                  isskip=true;
               }
               break;
            // si el spread actual es menor al 0.15% del precio, omitimos el símbolo
            case spread_l15:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.15 ){
                  isskip=true;
               }
               break;
            // si el spread actual es menor al 0.1% del precio, omitimos el símbolo
            case spread_l1:
               if( ((SymbolInfoInteger(name, SYMBOL_SPREAD)*SymbolInfoDouble(name, SYMBOL_POINT))/lastme.bid)*100 < 0.1 ){
                  isskip=true;
               }
               break;
         }
      }
      if(isskip){
         continue;
      }

Cuanto menor sea el spread, mejor. Desde este punto de vista, lo mejor es trabajar con símbolos cuyo spread sea menor al 0.05% del precio. Pero no todos los brókeres tienen unas condiciones tan buenas. Sobre todo si usted comercia en el mercado de valores.

Ocultar los símbolos cuyo precio es superior (0 - no ocultar):

      // Ocultar los símbolos cuyo precio es superior (0 - no ocultar)<
      if(hide_PRICE_HIGH>0 && lastme.bid>0 && lastme.bid>hide_PRICE_HIGH){
         continue;
      }

Ocultar los símbolos cuyo precio es inferior (0 - no ocultar)<:

      if(hide_PRICE_LOW>0 && lastme.bid>0 && lastme.bid<hide_PRICE_LOW){
         continue;
      }

Ocultar los símbolos cuyo comercio está prohibido:

      if(hideProhibites){
         // si el nivel de posición mínimo del símbolo es igual a 0, omitimos
         if( SymbolInfoDouble(name, SYMBOL_VOLUME_MIN)==0 ) continue;
         // si está prohibido abrir posiciones, omitimos
         if(SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_DISABLED || SymbolInfoInteger(name, SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_CLOSEONLY ){
            continue;
         }
      }

Claro que podríamos ocultar los símbolos cuyo comercio está prohibido, sin condición alguna en cuanto a los parámetros de entrada. Pero podría ser que alguien necesitara estos símbolos en la lista. Para ellos, vamos a añadir este parámetro de entrada.

Mostrar solo los símbolos que se abren a una hora concreta:

      // obtenemos el día actual en la variable curDay
      MqlDateTime curDay;
      TimeCurrent(curDay);
      MqlDateTime curDayFrom;
      datetime dfrom;
      datetime dto;
      // si no se ha establecido una limitación temporal de apertura de mercados  
      // y obtenemos la hora de apertura de la acción actual para el día de hoy, entonces...
      if( hide_HOURS!=hour_any && SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto)){
         TimeToStruct(dfrom, curDayFrom);
         if(hide_HOURS==hour_9am && curDayFrom.hour != 9){
            continue;
         }
         if(hide_HOURS==hour_10am && curDayFrom.hour != 10){
            continue;
         }
         if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){
            continue;
         }
         if(hide_HOURS==hour_0am && curDayFrom.hour != 0){
            continue;
         }
      }

Ocultar, si el mercado está cerrado. Si usted ha iniciado este asesor un domingo, entonces no querrá ver con su ayuda las acciones del mercado de valores. Lo más probable es que usted quiera ver solo aquellos símbolos que funcionan el domingo. Por ejemplo, el índice TA25 o las criptomonedas. Este parámetro de entrada nos permitirrá hacerlo.

Claro que podríamos no implementar el parámetro de entrada, sino representar por defecto solo los símbolos que se comercian hoy. Pero, ¿qué podemos hacer si, aun así, deseamos prepararnos un domingo para comerciar al día siguiente? Seleccionar las acciones convenientes, etc. Para tener esta posibilidad, la implementaremos en forma de parámetro de entrada.

Para determinar si se abrirá hoy el mercado, vamos a necesitar de nuevo la función SymbolInfoSessionTrade. Si esta retorna el valor false, significa que, por lo visto, hoy está cerrado el mercado de un instrumento. Para no llamar esta función dos veces, también deberemos reescribir el código que muestra solo los símbolos que se abren en:

      MqlDateTime curDay;
      TimeCurrent(curDay);
      MqlDateTime curDayFrom;
      datetime dfrom;
      datetime dto;
      
      bool sessionData=SymbolInfoSessionTrade(name, (ENUM_DAY_OF_WEEK) curDay.day_of_week, 0, dfrom, dto);

      // ocultar el símbolo, si el mercado está hoy cerrado
      if( hideClosed && !sessionData ){
         continue;
      }
      
      // Mostrar solo los símbolos que se abren en
      // obtenemos el día actual en la variable curDay
      // si no se ha establecido una limitación temporal de apertura de mercados  
      // y obtenemos la hora de apertura de la acción actual para el día de hoy, entonces...
      if( hide_HOURS!=hour_any && sessionData){
         TimeToStruct(dfrom, curDayFrom);
         if(hide_HOURS==hour_9am && curDayFrom.hour != 9){
            continue;
         }
         if(hide_HOURS==hour_10am && curDayFrom.hour != 10){
            continue;
         }
         if(hide_HOURS==hour_4pm && curDayFrom.hour != 16){
            continue;
         }
         if(hide_HOURS==hour_0am && curDayFrom.hour != 0){
            continue;
         }
      }

Ocultar, si ATR es inferior al valor establecido en dólares. Si usted comercia dentro del día y espera movimientos de precio como mínimo de 50-90 centavos, difícilmente necesitará símbolos que, estadísticamente, se mueven menos de 30 centavos al día en los últimos tiempos. Con la ayuda de este parámetro, podemos filtrar estos símbolos, indicando el tamaño mínimo del movimiento de precio diario que necesitamos:

      // Ocultar, si ATR es inferior al valor establecido en dólares.
      if(hideATRcents>0){
         MqlRates rates[];
         ArraySetAsSeries(rates, true);
         double atr;
         if(CopyRates(name, PERIOD_D1, 1, 5, rates)==5){
            atr=0;
            for(int j=0; j<5; j++){
               atr+=rates[j].high-rates[j].low;
            }
            atr/=5;
            if( atr>0 && atr<hideATRcents ){
               continue;
            }
         }
      }

Probablemente, esto sea suficiente para filtrar con garantías la mayoría de las señales. Pero si usted necesita otras condiciones para el filtrado, siempre podrá terminar de escribirlas dentro del ciclo de la función prepare_symbols.

Mecanismo de apertura de los gráficos

Ya hemos aprendido a mostrar en el gráfico los botones de los símbolos que cumplen nuestras condiciones. Claro que podríamos habernos limitado a ello. Y abrir los gráficos de los símbolos obtenidos manualmente. Pero estará usted de acuerdo con que esto no resulta muy cómodo. Si podemos simplificar este proceso, ¿por qué no hacerlo? Además, los botones presuponen precisamente que debemos pulsarlos. Entonces, ¿por qué no abrir el gráfico que necesitamos al pulsar un botón?

Para que al pulsar un botón suceda alguna acción, es necesario describir esta acción en la función estándar del lenguaje MQL OnChartEvent. Esta función captura cualquier evento sucedido en el gráfico. Es decir, se llama al darse cualquier evento que suceda en el gráfico.

La función OnChartEvent tiene 4 parámetros de entrada. El parámetro más a la derecha, id, contiene el identificador del evento que en este momento ha sido capturado por la función OnChartEvent. Para entender que la función OnChartEvent ha sido llamada precisamente tras pulsar algún botón del gráfico, debemos comparar el valor de este parámetro con el que necesitamos.

El evento de pulsación en el gráfico tiene el identificador CHARTEVENT_OBJECT_CLICK. De esta forma, para procesar la pulsación de los botones en el gráfico, escribiremos en la función OnChartEvent el siguiente código:

void OnChartEvent(const int id,         // event ID   
                  const long& lparam,   // event parameter of the long type 
                  const double& dparam, // event parameter of the double type 
                  const string& sparam) // event parameter of the string type 
  { 
   switch(id){
      case CHARTEVENT_OBJECT_CLICK:
         // código que se ejecutará al pulsar el botón
         break;
   }

}

¿Cómo podemos entender qué botón ha sido pulsado precisamente? A esto nos ayudará un parámetro de la función OnChartEvent: el parámetro sparam. Para el evento CHARTEVENT_OBJECT_CLICK contiene el nombre del botón que ha pulsado el usuario. Así que nos queda solo comparar este nombre con aquellos generados por nuestro asesor, y si se trata de un botón de nuestro asesor, abrimos el gráfico del símbolo necesario. El nombre del símbolo que debemos abrir lo tomaremos del texto que está escrito en el botón. Como resultado, obtendremos el siguiente código, que debemos ubicar dentro de la condición case CHARTEVENT_OBJECT_CLICK:

         // si en el nombre del botón se halla la línea que se encuentra en todos los objetos gráficos 
         // creados por nuestro asesor, entonces...
         if( StringFind(sparam, exprefix+"btn")>=0 ){
            // ubicamos en la variable panel1val el número del botón actual
            // es decir, la posición del símbolo actual en la lista de símbolos
            string tmpme=sparam;
            StringReplace(tmpme, exprefix+"btn", "");
            panel1val=(int) tmpme;
            
            // abrimos el gráfico del símbolo dado
            showcharts(ObjectGetString(0,sparam,OBJPROP_TEXT));
         }

La apertura del gráfico del símbolo elegido se realiza con la ayuda de la función personalizada showcharts. Esta función no solo abre el gráfico necesario, sino que de manera adicional:

void showcharts(string name){
   // si se han abierto anteriormente gráficos, los cerramos
   closecharts();
   
   // Añadimos un símbolo al panel de Observación del mercado, si no se encuentra allí
   // y si el parámetro de entrada "Añadir el gráfico a la Observación de mercado" es igual a true
   if( addInfoWatch ){
      SymbolSelect(name, true);
   }
   
   // abrimos el gráfico y ubicamos en la matriz curChartID el identificador de este gráfico
   curChartID[ArrayResize(curChartID,ArraySize(curChartID)+1)-1]=charts[(uchar) ArrayResize(charts,ArraySize(charts)+1)-1].Open( name, PERIOD_D1 );
   
   // si el parámetro de entrada "Abrir los gráficos en forma de velas" es igual a true,
   // pasamos el gráfico al modo de representación de velas japonesas
   if(viewCandle){
      ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_MODE, CHART_CANDLES);
   }
   // si el parámetro de entrada "Mostrar los volúmenes de ticks" es igual a true,
   // mostramos los volúmenes de ticks
   if(viewVolumes){
      ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SHOW_VOLUMES, CHART_VOLUME_TICK);
   }
   // cambiamos la escala del gráfico a una más cómoda
   ChartSetInteger( curChartID[ArraySize(curChartID)-1], CHART_SCALE, 2);
      
   // esperamos un tercio de segundo, para que todos los cambios tengan tiempo de introducirse
   Sleep(333);
   // actualizamos el gráfico abierto, para introducir en él todos los cambios
   ChartRedraw(curChartID[ArraySize(curChartID)-1]);
   
}

El cierre de los gráficos abiertos anteriormente por el asesor corre a cargo de closecharts. Su código es muy sencillo:
void closecharts(){
   // si la matriz de los gráficos abiertos por el asesor no está vacía, entonces...
   if(ArraySize(charts)){
      // cerramos todos los gráficos de forma consecutiva
      for( int i=0; i<ArraySize(charts); i++ ){
         charts[i].Close();
      }
      // limpiamos la matriz de gráficos
      ArrayFree(charts);
   }
   // si la matriz de identificadores de los gráficos abiertos por el asesor no está vacía, la limpiamos
   if(ArraySize(curChartID)){
      ArrayFree(curChartID);
   }
}

Mostrando información adicional sobre los símbolos

Estaría bien no solo abrir el gráfico de un instrumento, sino también representar distinta información auxiliar sobre el mismo. Aunque sea la descripción de dicho símbolo (en algunos brókeres, los nombres de los símbolos son tan incomprensibles que paracen cifras), así como la dirección del movimiento del símbolo en el día de hoy y en la última hora.

Podríamos mostrar esta información con la ayuda de objetos gráficos. Pero actuaremos de una forma bastante más sencilla. Simplemente la mostraremos en el comentario al gráfico.

Para ello, añadiremos a la función showcharts, antes de la llamada de la función Sleep, el siguiente código:

   //mostramos información adicional en el gráfico
   string msg="";
   if(showNameSymbol){
      StringAdd(msg, getmename_symbol(name)+"\r\n");
   }
   if(showInfoSymbol){
      StringAdd(msg, getmeinfo_symbol(name, false)+"\r\n");
   }
   if( StringLen(msg)>0 ){
      ChartSetString(curChartID[ArraySize(curChartID)-1], CHART_COMMENT, msg);
   }

Si el parámetro de entrada showNameSymbol es igual a true, llamamos la función getmename_symbol, que nos devolverá la línea con el nombre del símbolo. Si el parámetro de entrada showInfoSymbol es igual a true, llamamos la función getmeinfo_symbol, que nos devolverá la línea con el nombre del símbolo:

string getmename_symbol(string symname){
   return SymbolInfoString(symname, SYMBOL_DESCRIPTION);
}
string getmeinfo_symbol(string symname, bool show=true){
   MqlRates rates2[];
   ArraySetAsSeries(rates2, true);
   string msg="";

   if(CopyRates(symname, PERIOD_D1, 0, 1, rates2)>0){
      if(show){
         StringAdd(msg, (string) symname+": ");
      }
      StringAdd(msg, "D1 ");
      if( rates2[0].close > rates2[0].open ){
         StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2) +"%");
      }else{
         if( rates2[0].close < rates2[0].open ){
            StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2) +"%");
         }else{
            StringAdd(msg, "0%");
         }
      }
   }
   if(CopyRates(symname, PERIOD_H1, 0, 1, rates2)>0){
      StringAdd(msg, ", H1 ");
      if( rates2[0].close > rates2[0].open ){
         StringAdd(msg, "+"+DoubleToString(((rates2[0].close-rates2[0].open)/rates2[0].close)*100, 2)+"% (+"+DoubleToString(rates2[0].close-rates2[0].open, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")");
      }else{
         if( rates2[0].close < rates2[0].open ){
            StringAdd(msg, "-"+DoubleToString(((rates2[0].open-rates2[0].close)/rates2[0].close)*100, 2)+"% (-"+DoubleToString(rates2[0].open-rates2[0].close, (int) SymbolInfoInteger(symname, SYMBOL_DIGITS))+" "+SymbolInfoString(symname, SYMBOL_CURRENCY_PROFIT)+")");
         }else{
            StringAdd(msg, "0%");
         }
      }
   }
   
   return msg;
}

Como resultado, veremos la siguiente información en el gráfico que se abrirá:

Mostrando la información adicional del símbolo

Gestionando el asesor con el teclado

Mientras aún nos encontramos en la función OnChartEvent, vamos a añadir allí la reacción a la pulsación de varias teclas de nuestro teclado:

El responsable de la pulsación de las teclas del teclado es el evento con la id CHARTEVENT_KEYDOWN. En este caso, además, el código de pulsación se transmite en el parámetro sparam, que ya conocemos. Así que solo necesitamos añadir al operador switch la siguiente condición:

      case CHARTEVENT_KEYDOWN:
         switch((int) sparam){
            case 45: //x
               ExpertRemove();
               break;
            case 19: //r
               start_symbols();
               break;
         }
         break;

Como podemos ver, al pulsar la tecla R, simplemente llamaremos la función start_symbols, creada anteriormente.

Añadiendo la navegación por los gráficos

Ya sabemos, no solo mostrar los botones de los símbolos que cumplen nuestras condiciones, sino también abrir los gráficos de los instrumentos necesarios al clicar en los botones dados. Pero aún nos falta un poco para lograr la perfección. Nuestra utilidad sigue siendo incómoda de usar. Y es que, después de que el gráfico del símbolo esté abierto, necesitaremos cerrarlo manualmente tras clicar en el botón del próximo gráfico. Y esto cada vez que debamos pasar al siguiente símbolo. En general, se trata de un proceso bastante pesado. ¿Por qué no añadimos a los gráficos abiertos botones para la navegación por la lista de símbolos? Eso es lo que vamos a hacer ahora.

Vamos a añadir un total de 3 botones: para pasar al siguiente gráfico, para pasar al gráfico anterior y para cerrar el gráfico.

Solo nos queda decidir cómo hacerlo. Podríamos añadir botones al gráfico directamente en la función showcharts, al crear un nuevo gráfico. Pero, ¿y si aumenta el número de botones en el futuro? Entonces el estado de los botones y otros objetos gráficos puede ralentizar el proceso de apertura del gráfico, lo cual no es deseable.

Por eso, crearemos los botones en la función estándar OnTimer. Es decir, comprobaremos periódicamente si el asesor ha abierto algún gráfico. Si hay abierto un gráfico, entonces veremos si hay botones en este. Y solo si no hay botones, los crearemos:

void OnTimer()
  {
   // si en la matriz de identificadores de los gráficos abiertos hay valores, entonces...
   uchar tmpCIDcnt=(uchar) ArraySize(curChartID);
   if(tmpCIDcnt>0 ){
      // si el último identificador en la matriz no está estropeado, entonces...
      if(curChartID[tmpCIDcnt-1]>0){
         // si en el gráfico con este identificador no hay botones, los creamos
         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }
      }
   }
   
  }

La creación de los botones en el gráfico se realiza en la función personalizada createBTNS. Su código es muy sencillo:

void createBTNS(long CID){
   ObjectCreate(CID, exprefix+"_p_btn_prev", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YDISTANCE,90); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_prev",OBJPROP_TEXT,"Prev chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_prev",OBJPROP_SELECTABLE,false); 
      
   ObjectCreate(CID, exprefix+"_p_btn_next", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YDISTANCE,65); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_next",OBJPROP_TEXT,"Next chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_next",OBJPROP_SELECTABLE,false); 
      
   ObjectCreate(CID, exprefix+"_p_btn_close", OBJ_BUTTON, 0, 0, 0);
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XDISTANCE,110); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YDISTANCE,40); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_XSIZE,BTN_WIDTH); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_YSIZE,BTN_HEIGHT); 
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_CORNER,CORNER_LEFT_LOWER); 
   ObjectSetString(CID,exprefix+"_p_btn_close",OBJPROP_TEXT,"Close chart");
   ObjectSetInteger(CID,exprefix+"_p_btn_close",OBJPROP_SELECTABLE,false); 
   
   // actualizamos el gráfico para ver los cambios introducidos
   ChartRedraw(CID);
}

Como resultado, el nuevo gráfico tendrá el siguiente aspecto:

Botones de navegación por la lista de símbolos

Añadimos la reacción a la pulsación de botones

Por el momento, los botones añadidos al gráfico son solo un adorno. Y es que, al pulsarlos, no sucede nada. Vamos a enseñarlos a reaccionar a la pulsación.

Por desgracia, la función estándar OnChartEvent no nos podrá ayudar a ello. Y es que ella reacciona solo a aquellos eventos que han sucedido en el gráfico donde ha sido iniciado nuestro asesor. Y los botones los hemos añadido al nuevo gráfico.

Es posible que haya métodos más cómodos. Pero hemos pensado solo una forma en la que se puede reaccionar a los cambios sucedidos en un tercer gráfico. A ello nos ayudará la función OnTimer. Si nuestros botones están en el gráfico, solo comprobaremos si alguno de ellos está pulsado. Si está pulsado, ejecutaremos la acción necesaria. Como resultado, la condición:

         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }

...se reescribirá de la forma siguiente:

         if(ObjectFind(curChartID[tmpCIDcnt-1], exprefix+"_p_btn_next")<0){
            createBTNS(curChartID[tmpCIDcnt-1]);
         }else{
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_prev",OBJPROP_STATE)==true ){
               prevchart();
               return;
            }
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_next",OBJPROP_STATE)==true ){
               nextchart();
               return;
            }
            if(ObjectGetInteger(curChartID[tmpCIDcnt-1],exprefix+"_p_btn_close",OBJPROP_STATE)==true ){
               closecharts();
               return;
            }
         }

Si está pulsado Prev chart, llamamos la función prevchart. Si está pulsado el botónNext chart, llamamos la función nextchart. Y al pulsar el botón Close chart, se llama la función closecharts, que ya conocemos. Las funciones prevchart y nextchart se parecenn unas a otras:

void nextchart(){
   // si en la lista de símbolos está el símbolo siguiente, abrimos su gráfico
   // de lo contrario, cerramos el gráfico actual
   if(arrPanel1.Total()>(panel1val+1)){
      panel1val++;
      showcharts(arrPanel1[panel1val]);
   }else{
      closecharts();
   }
}
void prevchart(){
   // si en la lista de símbolos está el símbolo anterior, abrimos su gráfico
   // de lo contrario, cerramos el gráfico actual
   if(arrPanel1.Total()>(panel1val-1) && (panel1val-1)>=0){
      panel1val--;
      showcharts(arrPanel1[panel1val]);
   }else{
      closecharts();
   }
}

Conclusión

Esto es todo. Estará usted de acuerdo en que no hay mucho código. ¡Pero qué útil es! Ya no será necesario crear y cerrar los gráficos, abriéndolos y cerrándolos después una y otra vez. Ahora podemos simplemente clicar en el botón necesario y todo se hará por nosotros.

Está claro que podemos pensar multitud de métodos más para mejorar nuestro asesor. Pero ya nos encontramos ante un producto plenamente funcional, capaz de facilitar sustancialmente al tráder la selección de acciones.

Trasladando la utilidad a MQL4

Ahora vamos a intentar trasladar nuestra utilidad al lenguaje MQL4 Resulta sorprendente, pero el proceso solo consistirá en reescribir un bloque de código. Y esto nos ocupará solo 5 minutos. Así que manos a la obra.

Vamos a comenzar creando un nuevo asesor en el editor MetaEditor versión 4. Después de ello, trasladamos al mismo el código fuente de nuestro asesor en MQL5.

Vamos a intentar compilar el asesor directamente. Está claro que no lo conseguiremos. Pero como resultado obtendremos una lista de errores que tenemos que corregir. Y solo hay 3 errores así:

Clique dos veces sobre el primer error para pasar a la línea del asesor donde ha sido detectado.

Error 'PositionsTotal' - function not defined. El error ha sido detectado en el siguiente bloque de código de la función prepare_symbols:

         int cntMyPos=PositionsTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            // si hay posiciones del símbolo actual, omitimos
            if(PositionGetSymbol(ti) == name ){
               isskip=true;
               break;
            }
         }
         if(!isskip){
            int cntMyPosO=OrdersTotal();
            if(cntMyPosO>0){
               for(int ti=cntMyPosO-1; ti>=0; ti--){
                  ulong orderTicket=OrderGetTicket(ti);
                  if( OrderGetString(ORDER_SYMBOL) == name ){
                     isskip=true;
                     break;
                  }
               }
            }
         }

Una de las diferencias sustanciales entre los lenguajes MQL4 y MQL5 es el trabajo con posiciones y órdenes. Por eso, para que el asesor funcione correctamente en MetaTrader 4, debemos reescribir el bloque de código dado de la siguiente forma:

         int cntMyPos=OrdersTotal();
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if(OrderSymbol() == name ){
               isskip=true;
               break;
            }
         }

Puesto que en MQL4 no se diferencia entre posiciones y órdenes, habrá bastante menos código que sustituir.

Otros errores. Quizá no locrea, pero ya hemos terminado con las correcciones. El resto de errores han sido eliminados de forma automática, puesto que estaban precisamente en el bloque de código corregido.