Creación flexible de indicadores con IndicatorCreate

Después de familiarizarnos con una nueva forma de crear indicadores, pasemos a una tarea más cercana a la realidad. IndicatorCreate suele utilizarse en los casos en que el indicador llamado no se conoce de antemano. Tal necesidad, por ejemplo, surge al escribir Asesores Expertos universales capaces de operar con señales arbitrarias configuradas por el usuario. E incluso los nombres de los indicadores pueden ser fijados por el usuario.

Todavía no estamos preparados para desarrollar Asesores Expertos, por lo que estudiaremos esta tecnología utilizando el ejemplo de un indicador envolvente UseDemoAll.mq5, capaz de mostrar los datos de cualquier otro indicador.

El proceso debería tener el siguiente aspecto. Cuando ejecutamos UseDemoAll en el gráfico, aparece una lista en el cuadro de diálogo de propiedades en la que debemos seleccionar uno de los indicadores integrados o uno personalizado, y en este último caso, además, tendremos que especificar su nombre en el campo de entrada. En otro parámetro de cadena, podemos introducir una lista de parámetros separados por comas. Los tipos de parámetros se determinarán automáticamente en función de su ortografía. Por ejemplo, un número con punto decimal (10.0) se tratará como un doble, un número sin punto (15) como un entero y algo encerrado entre comillas («texto») como una cadena.

Estos son sólo los ajustes básicos de UseDemoAll, pero no todos los posibles. Más adelante estudiaremos otras configuraciones.

Tomemos la enumeración ENUM_INDICATOR como base para la solución: ya dispone de elementos para todos los tipos de indicadores, incluidos los personalizados (IND_CUSTOM). A decir verdad, en su forma pura, no encaja por varias razones. En primer lugar, es imposible obtener de él metadatos sobre un indicador concreto, como el número y los tipos de argumentos, el número de búferes y en qué ventana se muestra el indicador (principal o subventana). Esta información es importante para la correcta creación y visualización del indicador. En segundo lugar, si definimos una variable de entrada del tipo ENUM_INDICATOR para que el usuario pueda seleccionar el indicador deseado, en el cuadro de diálogo de propiedades éste se representará mediante una lista desplegable, donde las opciones contienen únicamente el nombre del elemento. En realidad, sería deseable proporcionar pistas al usuario en esta lista (al menos sobre los parámetros). Por ello, describiremos nuestra propia enumeración IndicatorType. Recordemos que MQL5 permite que cada elemento especifique un comentario a la derecha, que se muestra en la interfaz.

En cada elemento de la enumeración IndicatorType, codificaremos no sólo el identificador (ID) correspondiente de ENUM_INDICATOR, sino también el número de parámetros (P), el número de búferes (B) y el número de la ventana de trabajo (W). Para ello se han desarrollado las siguientes macros:

#define MAKE_IND(P,B,W,ID) (int)((W << 24) | ((B & 0xFF) << 16) | ((P & 0xFF) << 8) | (ID & 0xFF))
#define IND_PARAMS(X)   ((X >> 8) & 0xFF)
#define IND_BUFFERS(X)  ((X >> 16) & 0xFF)
#define IND_WINDOW(X)   ((uchar)(X >> 24))
#define IND_ID(X)       ((ENUM_INDICATOR)(X & 0xFF))

La macro MAKE_IND toma todas las características anteriores como parámetros y las empaqueta en diferentes bytes de un único entero de 4 bytes, formando así un código único para el elemento de la nueva enumeración. Las 4 macros restantes permiten realizar la operación inversa, es decir, calcular todas las características del indicador utilizando el código.

No proporcionaremos aquí toda la enumeración de IndicatorType, sino sólo una parte. El código fuente completo se encuentra en el archivo AutoIndicator.mqh.

enum IndicatorType
{
   iCustom_ = MAKE_IND(000IND_CUSTOM), // {iCustom}(...)[?]
   
   iAC_ = MAKE_IND(011IND_AC), // iAC( )[1]*
   iAD_volume = MAKE_IND(111IND_AD), // iAD(volume)[1]*
   iADX_period = MAKE_IND(131IND_ADX), // iADX(period)[3]*
   iADXWilder_period = MAKE_IND(131IND_ADXW), // iADXWilder(period)[3]*
   ...
   iMomentum_period_price = MAKE_IND(211IND_MOMENTUM), // iMomentum(period,price)[1]*
   iMFI_period_volume = MAKE_IND(211IND_MFI), // iMFI(period,volume)[1]*
   iMA_period_shift_method_price = MAKE_IND(410IND_MA), // iMA(period,shift,method,price)[1]
   iMACD_fast_slow_signal_price = MAKE_IND(421IND_MACD), // iMACD(fast,slow,signal,price)[2]*
   ...
   iTEMA_period_shift_price = MAKE_IND(310IND_TEMA), // iTEMA(period,shift,price)[1]
   iVolumes_volume = MAKE_IND(111IND_VOLUMES), // iVolumes(volume)[1]*
   iWPR_period = MAKE_IND(111IND_WPR// iWPR(period)[1]*
};

Los comentarios, que se convertirán en elementos de la lista desplegable visible para el usuario, indican prototipos con parámetros con nombre, el número de búferes entre corchetes y las marcas de estrella de los indicadores que se muestran en su propia ventana. Los propios identificadores también se convierten en informativos, ya que son los que convierte en texto la función EnumToString que se utiliza para enviar mensajes al registro.

La lista de parámetros es especialmente importante, ya que el usuario tendrá que introducir los valores apropiados separados por comas en la variable de entrada reservada a tal efecto. También podríamos mostrar los tipos de los parámetros, pero por simplicidad, se ha decidido dejar sólo los nombres con un significado, del que también se puede deducir el tipo. Por ejemplo, period, fast, slow son enteros con un periodo (número de barras), method es el método de cálculo de medias ENUM_MA_METHOD, price es el tipo de precio ENUM_APPLIED_PRICE, volume es el tipo de volumen ENUM_APPLIED_VOLUME.

Para comodidad del usuario (de manera que no tenga que recordar los valores de los elementos de enumeración), el programa admitirá los nombres de todas las enumeraciones. En concreto, el identificador sma denota MODO_SMA, ema denota MODO_EMA, y así sucesivamente. El precio close se convertirá en PRICE_CLOSE, open se convertirá en PRICE_OPEN, y otros tipos de precios se comportarán igual, por la última palabra (después del subrayado) en el identificador del elemento de enumeración. Por ejemplo, para la lista de parámetros del indicador iMA (iMA_period_shift_method_price), puede escribir la siguiente línea: 11,0,sma,close. No es necesario entrecomillar los identificadores. Sin embargo, si es necesario, puede pasar una cadena con el mismo texto, por ejemplo, una lista 1.5,"close" contiene el número real 1.5 y la cadena «cerrar».

El tipo de indicador, así como cadenas con una lista de parámetros y, opcionalmente, un nombre (si el indicador es personalizado) son los datos principales para el constructor de la clase AutoIndicator.

class AutoIndicator
{
protected:
   IndicatorTypetype;       // selected indicator type
   string symbols;          // working symbol (optional)
   ENUM_TIMEFRAMES tf;      // working timeframe (optional)
   MqlParamBuilder builder// "builder" of the parameter array
   int handle;              // indicator handle
   string name;             // custom indicator name
   ...
public:
   AutoIndicator(const IndicatorType tconst string customconst string parameters,
      const string s = NULLconst ENUM_TIMEFRAMES p = 0):
      type(t), name(custom), symbol(s), tf(p), handle(INVALID_HANDLE)
   {
      PrintFormat("Initializing %s(%s) %s, %s",
         (type == iCustom_ ? name : EnumToString(type)), parameters,
         (symbol == NULL ? _Symbol : symbol), EnumToString(tf == 0 ? _Period : tf));
      // split the string into an array of parameters (formed inside the builder)
      parseParameters(parameters);
      // create and store the handle
      handle = create();
   }
   
   int getHandle() const
   {
      return handle;
   }
};

Aquí y más abajo se omiten algunos fragmentos relacionados con la comprobación de la corrección de los datos de entrada. El libro incluye el código fuente completo.

El proceso de análisis de una cadena con parámetros se confía al método parseParameters, que implementa el esquema descrito anteriormente con el reconocimiento de los tipos de valor y su transferencia a un objeto MqlParamBuilder, que conocimos en el ejemplo anterior.

   int parseParameters(const string &list)
   {
      string sparams[];
      const int n = StringSplit(list, ',', sparams);
      
      for(int i = 0i < ni++)
      {
         // normalization of the string (remove spaces, convert to lower case)
         StringTrimLeft(sparams[i]);
         StringTrimRight(sparams[i]);
         StringToLower(sparams[i]);
   
         if(StringGetCharacter(sparams[i], 0) == '"'
         && StringGetCharacter(sparams[i], StringLen(sparams[i]) - 1) == '"')
         {
            // everything inside quotes is taken as a string
            builder << StringSubstr(sparams[i], 1StringLen(sparams[i]) - 2);
         }
         else
         {
            string part[];
            int p = StringSplit(sparams[i], '.', part);
            if(p == 2// double/float
            {
               builder << StringToDouble(sparams[i]);
            }
            else if(p == 3// datetime
            {
               builder << StringToTime(sparams[i]);
            }
            else if(sparams[i] == "true")
            {
               builder << true;
            }
            else if(sparams[i] == "false")
            {
               builder << false;
            }
            else // int
            {
               int x = lookUpLiterals(sparams[i]);
               if(x == -1)
               {
                  x = (int)StringToInteger(sparams[i]);
               }
               builder << x;
            }
         }
      }
      
      return n;
   }

La función de ayuda lookUpLiterals proporciona la conversión de identificadores a constantes de enumeración estándar.

   int lookUpLiterals(const string &s)
   {
      if(s == "sma"return MODE_SMA;
      else if(s == "ema"return MODE_EMA;
      else if(s == "smma"return MODE_SMMA;
      else if(s == "lwma"return MODE_LWMA;
      
      else if(s == "close"return PRICE_CLOSE;
      else if(s == "open"return PRICE_OPEN;
      else if(s == "high"return PRICE_HIGH;
      else if(s == "low"return PRICE_LOW;
      else if(s == "median"return PRICE_MEDIAN;
      else if(s == "typical"return PRICE_TYPICAL;
      else if(s == "weighted"return PRICE_WEIGHTED;
   
      else if(s == "lowhigh"return STO_LOWHIGH;
      else if(s == "closeclose"return STO_CLOSECLOSE;
   
      else if(s == "tick"return VOLUME_TICK;
      else if(s == "real"return VOLUME_REAL;
      
      return -1;
   }

Una vez reconocidos y guardados los parámetros en el array interno del objeto MqlParamBuilder, se llama al método create. Su propósito es copiar los parámetros al array local, complementarlo con el nombre del indicador personalizado (si lo hay), y llamar a la función IndicatorCreate.

   int create()
   {
      MqlParam p[];
      // fill 'p' array with parameters collected by 'builder' object
      builder >> p;
      
      if(type == iCustom_)
      {
         // insert the name of the custom indicator at the very beginning
         ArraySetAsSeries(ptrue);
         const int n = ArraySize(p);
         ArrayResize(pn + 1);
         p[n].type = TYPE_STRING;
         p[n].string_value = name;
         ArraySetAsSeries(pfalse);
      }
      
      return IndicatorCreate(symboltfIND_ID(type), ArraySize(p), p);
   }

El método devuelve el manejador recibido.

Resulta especialmente interesante la forma en que se inserta un parámetro de cadena adicional con el nombre del indicador personalizado al principio del array. En primer lugar, el array se asigna al orden de indexación «como en las series temporales» (véase ArraySetAsSeries), por lo que el índice del último elemento (físicamente, por ubicación en memoria) pasa a ser igual a 0, y los elementos se cuentan de derecha a izquierda. A continuación, se aumenta el tamaño del array y se escribe el nombre del indicador en el elemento añadido. Debido a la indexación inversa, esta adición no se produce a la derecha de los elementos existentes, sino a la izquierda. Por último, devolvemos el array a su orden de indexación habitual, y en el índice 0 está el nuevo elemento con la cadena que acaba de ser la última.

Opcionalmente, la clase AutoIndicator puede formar un nombre abreviado del indicador integrado a partir del nombre de un elemento de enumeración.

   ...
   string getName() const
   {
      if(type != iCustom_)
      {
         const string s = EnumToString(type);
         const int p = StringFind(s"_");
         if(p > 0return StringSubstr(s0p);
         return s;
      }
      return name;
   }
};

Ahora todo está listo para ir directamente al código fuente UseDemoAll.mq5. Pero empecemos con una versión ligeramente simplificada UseDemoAllSimple.mq5.

En primer lugar, vamos a definir el número de búferes de indicadores. Dado que el número máximo de búferes entre los indicadores integrados es de cinco (para Ichimoku), lo tomamos como limitador. Asignaremos el registro de este número de arrays como búferes a la clase que ya conocemos, BufferArray (véase la sección Indicadores multidivisa y de marco temporal múltiple, ejemplo IndUnityPercent).

#define BUF_NUM 5
   
#property indicator_chart_window
#property indicator_buffers BUF_NUM
#property indicator_plots   BUF_NUM
   
#include <MQL5Book/IndBufArray.mqh>
 
BufferArray buffers(5);

Es importante recordar que un indicador puede diseñarse para que aparezca en la ventana principal o en una ventana aparte. MQL5 no permite combinar dos modos. No obstante, no sabemos de antemano qué indicador elegirá el usuario, por lo que tenemos que inventar algún tipo de «solución». Por ahora, vamos a colocar nuestro indicador en la ventana principal, y abordaremos el problema de una ventana separada más adelante.

Desde un punto de vista puramente técnico, no hay ningún obstáculo para copiar los datos de los búferes de indicadores con la propiedad indicator_separate_window en sus búferes que se muestran en la ventana principal. Sin embargo, hay que tener en cuenta que el rango de valores de estos indicadores a menudo no coincide con la escala de precios, por lo que es poco probable que pueda verlos en el gráfico (las líneas estarán en algún lugar más allá de la zona visible, en la parte superior o inferior), aunque los valores se seguirán mostrando en Data window.

Con la ayuda de las variables de entrada, seleccionaremos el tipo de indicador, el nombre del indicador personalizado y la lista de parámetros. También añadiremos variables para el tipo de representación y el ancho de línea. Dado que los búferes se conectarán para funcionar dinámicamente, dependiendo del número de búferes del indicador fuente, no describiremos los estilos de búfer de forma estática utilizando directivas y lo haremos en OnInit mediante llamadas a las funciones Plot integradas.

input IndicatorType IndicatorSelector = iMA_period_shift_method_price// Built-in Indicator Selector
input string IndicatorCustom = ""// Custom Indicator Name
input string IndicatorParameters = "11,0,sma,close"// Indicator Parameters (comma,separated,list)
input ENUM_DRAW_TYPE DrawType = DRAW_LINE// Drawing Type
input int DrawLineWidth = 1// Drawing Line Width

Vamos a definir una variable global para almacenar el descriptor del indicador.

int Handle;

En el manejador OnInit, utilizamos la clase AutoIndicator presentada anteriormente, para analizar un dato de entrada, preparar el array MqlParam y obtener un manejador basado en él.

#include <MQL5Book/AutoIndicator.mqh>
   
int OnInit()
{
   AutoIndicator indicator(IndicatorSelectorIndicatorCustomIndicatorParameters);
   Handle = indicator.getHandle();
   if(Handle == INVALID_HANDLE)
   {
      Alert(StringFormat("Can't create indicator: %s",
         _LastError ? E2S(_LastError) : "The name or number of parameters is incorrect"));
      return INIT_FAILED;
   }
   ...

Para personalizar los trazados, describimos un conjunto de colores y obtenemos el nombre abreviado del indicador del objeto AutoIndicator. También calculamos el número de búferes n utilizados del indicador integrado utilizando la macro IND_BUFFERS, y para cualquier indicador personalizado (que no se conoce de antemano), a falta de una solución mejor, incluiremos todos los búferes. Además, en el proceso de copia de datos, las llamadas innecesarias a CopyBuffer simplemente devolverán un error, y dichos arrays pueden llenarse con valores vacíos.

   ...
   static color defColors[BUF_NUM] = {clrBlueclrGreenclrRedclrCyanclrMagenta};
   const string s = indicator.getName();
   const int n = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
   ...

En el bucle, estableceremos las propiedades de los gráficos, teniendo en cuenta el limitador n: los búferes por encima de él están ocultos.

   for(int i = 0i < BUF_NUM; ++i)
   {
      PlotIndexSetString(iPLOT_LABELs + "[" + (string)i + "]");
      PlotIndexSetInteger(iPLOT_DRAW_TYPEi < n ? DrawType : DRAW_NONE);
      PlotIndexSetInteger(iPLOT_LINE_WIDTHDrawLineWidth);
      PlotIndexSetInteger(iPLOT_LINE_COLORdefColors[i]);
      PlotIndexSetInteger(iPLOT_SHOW_DATAi < n);
   }
   
   Comment("DemoAll: ", (IndicatorSelector == iCustom_ ? IndicatorCustom : s),
      "("IndicatorParameters")");
   
   return INIT_SUCCEEDED;
}

En la esquina superior izquierda del gráfico, el comentario mostrará el nombre del indicador con los parámetros.

En el manejador OnCalculate, cuando los datos del manejador están listos, los leemos en nuestros arrays.

int OnCalculate(ON_CALCULATE_STD_SHORT_PARAM_LIST)
{
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   const int m = (IndicatorSelector != iCustom_) ? IND_BUFFERS(IndicatorSelector) : BUF_NUM;
   for(int k = 0k < m; ++k)
   {
      // fill our buffers with data form the indicator with the 'Handle' handle
      const int n = buffers[k].copy(Handle,
         k0rates_total - prev_calculated + 1);
         
      // in case of error clean the buffer
      if(n < 0)
      {
         buffers[k].empty(EMPTY_VALUEprev_calculatedrates_total - prev_calculated);
      }
   }
   
   return rates_total;
}

La implementación anterior está simplificada y coincide con el archivo original UseDemoAllSimple.mq5. Nos ocuparemos de su ampliación más adelante, pero por ahora comprobaremos el comportamiento de la versión actual. En la siguiente imagen se muestran 2 instancias del indicador: la línea azul con la configuración por defecto (iMA_period_shift_method_price, opciones «11,0,sma,close»), y la roja iRSI_period_price con los parámetros «11 close»:

Dos instancias del indicador UseDemoAllSimple con lecturas iMA e iRSI

Dos instancias del indicador UseDemoAllSimple con lecturas iMA e iRSI

El gráfico USDRUB se ha elegido de forma intencionada para la demostración, porque los valores de las cotizaciones aquí coinciden más o menos con el rango del indicador RSI (que debería haberse mostrado en una ventana aparte). En la mayoría de los gráficos de otros símbolos, no notaríamos el RSI. Si sólo le importa el acceso programático a los valores, entonces esto no es gran cosa, pero si tiene requisitos de visualización, se trata de un problema que debe resolverse.

Por lo tanto, debe proporcionar de alguna manera una visualización por separado de los indicadores destinados a la subventana. Básicamente, existe una petición popular de la comunidad de desarrolladores de MQL para permitir la visualización de gráficos tanto en la ventana principal como en una subventana al mismo tiempo. Vamos a presentar una de las soluciones, pero para ello primero tiene que familiarizarse con algunas de las nuevas funciones.