Forma avanzada de crear indicadores: IndicatorCreate

La creación de un indicador mediante la función iCustom o una de las funciones que componen un conjunto de indicadores integrado requiere conocer la lista de parámetros en la fase de codificación. Sin embargo, en la práctica, a menudo es necesario escribir programas lo suficientemente flexibles como para sustituir un indicador por otro.

Por ejemplo, al optimizar un Asesor Experto en el probador, tiene sentido seleccionar no sólo el período de la media móvil, sino también el algoritmo para su cálculo. Por supuesto, si construimos el algoritmo sobre un único indicador iMA, puede ofrecer la posibilidad de especificar ENUM_MA_METHOD en la configuración de su método. Sin embargo, a alguien probablemente le gustaría ampliar la elección cambiando entre media móvil doble exponencial, triple exponencial y fractal. A primera vista, esto podría hacerse utilizando switch con una llamada de DEMA, iTEMA y iFrAMA, respectivamente. Sin embargo, ¿qué hay acerca de incluir indicadores personalizados en esta lista?

Aunque el nombre del indicador puede sustituirse fácilmente en la llamada a iCustom, la lista de parámetros puede diferir significativamente. En el caso general, un Asesor Experto puede necesitar generar señales basadas en una combinación de cualquier indicador que no se conozca de antemano, y no sólo medias móviles.

Para estos casos, MQL5 dispone de un método universal para crear un indicador técnico arbitrario utilizando la función IndicatorCreate.

int IndicatorCreate(const string symbol, ENUM_TIMEFRAMES timeframe, ENUM_INDICATOR indicator, int count = 0, const MqlParam &parameters[] = NULL)

La función crea una instancia de indicador para el símbolo y el marco temporal especificados. El tipo de indicador se establece mediante el parámetro indicator. Su tipo es la enumeración ENUM_INDICATOR (véase más adelante) que contiene identificadores para todos los indicadores integrados, así como una opción para iCustom. El número de parámetros indicadores y sus descripciones se pasan, respectivamente, en el argumento count y en el array de estructuras MqlParam (véase más abajo).

Cada elemento de este array describe el parámetro de entrada correspondiente del indicador que se está creando, por lo que el contenido y el orden de los elementos deben corresponderse con el prototipo de la función integrada del indicador o, en el caso de un indicador personalizado, con las descripciones de las variables de entrada en su código fuente.

El incumplimiento de esta regla puede dar lugar a un error en la fase de ejecución del programa (véase el ejemplo siguiente) y a la incapacidad de crear un manejador. En el peor de los casos, los parámetros pasados se interpretarán incorrectamente y el indicador no se comportará como se espera; sin embargo, debido a la falta de errores, esto no es fácil de notar. La excepción es pasar un array vacío o no pasarlo en absoluto (porque los argumentos count y parameters son opcionales): en este caso, el indicador se creará con la configuración predeterminada. Además, en el caso de los indicadores personalizados, puede omitir un número arbitrario de parámetros del final de la lista.

La estructura MqlParam ha sido especialmente diseñada para pasar parámetros de entrada cuando se crea un indicador utilizando IndicatorCreate o para obtener información sobre los parámetros de un indicador de terceros (realizado en el gráfico) utilizando IndicatorParameters.

struct MqlParam 

   ENUM_DATATYPE type;          // input parameter type
   long          integer_value// field for storing an integer value
   double        double_value;  // field for storing double or float values
   string        string_value;  // field for storing a value of string type
};

El valor real del parámetro debe establecerse en uno de los campos integer_value, double_value, string_value, según el valor del primer campo type. A su vez, el campo type se describe mediante la enumeración ENUM_DATATYPE que contiene identificadores para todos los tipos MQL5 integrados.

Identificador

Tipo de datos

TYPE_BOOL

bool

TYPE_CHAR

char

TYPE_UCHAR

uchar

TYPE_SHORT

short

TYPE_USHORT

ushort

TYPE_COLOR

color

TYPE_INT

int

TYPE_UINT

uint

TYPE_DATETIME

datetime

TYPE_LONG

long

TYPE_ULONG

ulong

TYPE_FLOAT

float

TYPE_DOUBLE

double

TYPE_STRING

string

Si algún parámetro del indicador tiene un tipo de enumeración, debe utilizar el valor TYPE_INT en el campo type para describirlo.

La enumeración ENUM_INDICATOR utilizada en el tercer parámetro IndicatorCreate para indicar el tipo de indicador contiene las siguientes constantes:

Identificador

Indicador

IND_AC

Oscilador Acelerador

IND_AD

Acumulación/Distribución

IND_ADX

Índice direccional medio

IND_ADXW

ADX de Welles Wilder

IND_ALLIGATOR

Alligator

IND_AMA

Media móvil adaptativa

IND_AO

Oscilador Impresionante

IND_ATR

Rango medio verdadero

IND_BANDS

Bandas de Bollinger

IND_BEARS

Bears Power

IND_BULLS

Bulls Power

IND_BWMFI

Índice de facilitación del mercado

IND_CCI

Índice del Canal de Materias Primas

IND_CHAIKIN

Oscilador Chaikin

IND_CUSTOM

Indicador personalizado

IND_DEMA

Media móvil exponencial doble

IND_DEMARKER

DeMarker

IND_ENVELOPES

Envelopes

IND_FORCE

Índice de Fuerza

IND_FRACTALS

Fractales

IND_FRAMA

Media móvil adaptativa fractal

IND_GATOR

Oscilador Gator

IND_ICHIMOKU

Ichimoku Kinko Hyo

IND_MA

Media móvil

IND_MACD

MACD

IND_MFI

Índice de flujo de dinero

IND_MOMENTUM

Momentum

IND_OBV

Volumen de balance

IND_OSMA

OsMA

IND_RSI

Índice de Fuerza Relativa

IND_RVI

Índice de Vigor Relativo

IND_SAR

SAR parabólico

IND_STDDEV

Desviación típica

IND_STOCHASTIC

Oscilador estocástico

IND_TEMA

Media móvil exponencial triple

IND_TRIX

Oscilador de medias móviles exponenciales triples

IND_VIDYA

Media Móvil con Periodo de Promediación Dinámico

IND_VOLUMES

Volúmenes

IND_WPR

Rango Porcentual de Williams

Es importante tener en cuenta que si se pasa el valor IND_CUSTOM como tipo de indicador, entonces el primer elemento del array de parámetros debe tener el campo type con el valor TYPE_STRING, y el campo string_value debe contener el nombre (ruta) del indicador personalizado.

Si tiene éxito, la función IndicatorCreate devuelve un manejador del indicador creado, y en caso de fallo devuelve INVALID_HANDLE. El código de error se proporcionará en _LastError.

Recuerde que, para probar programas MQL que crean indicadores personalizados cuyos nombres no se conocen en la fase de compilación (lo que también ocurre cuando se utiliza IndicatorCreate), debe vincularlos explícitamente mediante la directiva:

#property tester_indicator "indicator_name.ex5"

Esto permite al probador enviar los indicadores auxiliares necesarios a los agentes de pruebas, pero limita el proceso a sólo los indicadores conocidos de antemano.

Veamos algunos ejemplos. Empecemos con una aplicación sencilla IndicatorCreate como alternativa a funciones ya conocidas, y después, para demostrar la flexibilidad del nuevo enfoque, crearemos un indicador universal envolvente para visualizar indicadores arbitrarios integrados o personalizados.

El primer ejemplo de UseEnvelopesParams1.mq5 crea una copia insertada del indicador Envelopes. Para ello, describimos dos búferes, dos trazados, arrays para ellos, y parámetros de entrada que repiten los parámetros de iEnvelopes.

#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
   
// drawing settings
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrBlue
#property indicator_width1  1
#property indicator_label1  "Upper"
#property indicator_style1  STYLE_DOT
   
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrRed
#property indicator_width2  1
#property indicator_label2  "Lower"
#property indicator_style2  STYLE_DOT
   
input int WorkPeriod = 14;
input int Shift = 0;
input ENUM_MA_METHOD Method = MODE_EMA;
input ENUM_APPLIED_PRICE Price = PRICE_TYPICAL;
input double Deviation = 0.1// Deviation, %
   
double UpBuffer[];
double DownBuffer[];
   
int Handle// handle of the subordinate indicator

El manejador OnInit podría tener este aspecto si utiliza la función iEnvelopes:

int OnInit()
{
   SetIndexBuffer(0UpBuffer);
   SetIndexBuffer(1DownBuffer);
   
   Handle = iEnvelopes(WorkPeriodShiftMethodPriceDeviation);
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

Los enlaces del búfer seguirán siendo los mismos, pero para crear un manejador, ahora iremos en la otra dirección. Vamos a describir el array MqlParam, rellenarlo y llamar a la función IndicatorCreate.

int OnInit()
{
   ...
   MqlParam params[5] = {};
   params[0].type = TYPE_INT;
   params[0].integer_value = WorkPeriod;
   params[1].type = TYPE_INT;
   params[1].integer_value = Shift;
   params[2].type = TYPE_INT;
   params[2].integer_value = Method;
   params[3].type = TYPE_INT;
   params[3].integer_value = Price;
   params[4].type = TYPE_DOUBLE;
   params[4].double_value = Deviation;
   Handle = IndicatorCreate(_Symbol_PeriodIND_ENVELOPES,
      ArraySize(params), params);
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

Una vez recibido el manejador, lo utilizamos en OnCalculate para rellenar dos de sus búferes.

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &data[])
{
   if(BarsCalculated(Handle) != rates_total)
   {
      return prev_calculated;
   }
   
   const int n = CopyBuffer(Handle, 0, 0, rates_total - prev_calculated + 1, UpBuffer);
   const int m = CopyBuffer(Handle, 1, 0, rates_total - prev_calculated + 1, DownBuffer);
      
   return n > -1 && m > -1 ? rates_total : 0;
}

Comprobemos cómo se ve en el gráfico el indicador creado UseEnvelopesParams1:

Indicador UseEnvelopesParams1

Indicador UseEnvelopesParams1

Arriba se ha mostrado una forma estándar pero no muy elegante de poblar las propiedades. Dado que la llamada a IndicatorCreate puede ser necesaria en muchos proyectos, tiene sentido simplificar el procedimiento para el código de llamada. Para este fin desarrollaremos una clase denominada MqlParamBuilder (véase el archivo MqlParamBuilder.mqh). Su tarea será aceptar valores de parámetros utilizando algunos métodos, determinar su tipo y añadir los elementos apropiados (estructuras correctamente rellenadas) al array.

MQL5 no soporta completamente el concepto de información sobre los tipos de tiempo de ejecución (Run-Time Type Information, RTTI). Con esto, los programas pueden pedir al tiempo de ejecución metadatos descriptivos sobre sus partes constituyentes, incluidas variables, estructuras, clases, funciones, etc. Las pocas características integradas de MQL5 que pueden clasificarse como RTTI son los operadores typename y offsetof. Dado que typename devuelve el nombre del tipo como una cadena, vamos a construir nuestro autodetector de tipos en cadenas (véase el archivo RTTI.mqh).

template<typename T>
ENUM_DATATYPE rtti(T v = (T)NULL)
{
   static string types[] =
   {
      "null",     //               (0)
      "bool",     // 0 TYPE_BOOL=1 (1)
      "char",     // 1 TYPE_CHAR=2 (2)
      "uchar",    // 2 TYPE_UCHAR=3 (3)
      "short",    // 3 TYPE_SHORT=4 (4)
      "ushort",   // 4 TYPE_USHORT=5 (5)
      "color",    // 5 TYPE_COLOR=6 (6)
      "int",      // 6 TYPE_INT=7 (7)
      "uint",     // 7 TYPE_UINT=8 (8)
      "datetime"// 8 TYPE_DATETIME=9 (9)
      "long",     // 9 TYPE_LONG=10 (A)
      "ulong",    // 10 TYPE_ULONG=11 (B)
      "float",    // 11 TYPE_FLOAT=12 (C)
      "double",   // 12 TYPE_DOUBLE=13 (D)
      "string",   // 13 TYPE_STRING=14 (E)
   };
   const string t = typename(T);
   for(int i = 0i < ArraySize(types); ++i)
   {
      if(types[i] == t)
      {
         return (ENUM_DATATYPE)i;
      }
   }
   return (ENUM_DATATYPE)0;
}

La función de plantilla rtti utiliza typename para recibir una cadena con el nombre del parámetro de tipo de plantilla y la compara con los elementos de un array que contiene todos los tipos integrados de la enumeración ENUM_DATATYPE. El orden de enumeración de los nombres en el array corresponde al valor del elemento de enumeración, por lo que cuando se encuentra una cadena coincidente, basta con convertir el índice al tipo (ENUM_DATATYPE) y devolverlo al código de llamada. Por ejemplo, la llamada a rtti(1.0) o rtti<double> () dará el valor TYPE_DOUBLE.

Con esta herramienta, podemos volver a trabajar en MqlParamBuilder. En la clase se describe el array de estructuras MqlParam y la variable n que contendrá el índice del último elemento que se va a rellenar.

class MqlParamBuilder
{
protected:
   MqlParam array[];
   int n;
   ...

Hagamos que el método público para añadir el siguiente valor a la lista de parámetros sea una plantilla. Además, lo implementamos como una sobrecarga del operador '<<' , que devuelve un puntero al propio objeto «constructor». Esto permitirá escribir múltiples valores en el array en una sola línea; por ejemplo, así: builder << WorkPeriod << PriceType << SmoothingMode.

Es en este método que aumentamos el tamaño del array, obtenemos el índice de trabajo n para rellenar, e inmediatamente restablecemos esta estructura n--ésima.

...
public:
   template<typename T>
   MqlParamBuilder *operator<<(T v)
   {
 // expand the array
      n = ArraySize(array);
      ArrayResize(arrayn + 1);
      ZeroMemory(array[n]);
      ...
      return &this;
   }

Donde haya una elipsis, seguirá la parte principal de trabajo, es decir, rellenar los campos de la estructura. Se podría suponer que determinaremos directamente el tipo del parámetro utilizando un rtti de elaboración propia. Pero debe prestar atención a un matiz: si escribimos las instrucciones array[n].type = rtti(v), no funcionará correctamente para las enumeraciones. Cada enumeración es un tipo independiente con su propio nombre, a pesar de que se almacena de la misma forma que los enteros. En el caso de las enumeraciones, la función rtti devolverá 0, por lo que deberá sustituirla explícitamente por TYPE_INT.

      ...
      // define value type
      array[n].type = rtti(v);
      if(array[n].type == 0array[n].type = TYPE_INT// imply enum
      ...

Ahora sólo tenemos que poner el valor v en uno de los tres campos de la estructura: integer_value del tipo long (nota: long es un entero largo, de ahí el nombre del campo), double_value de tipo double o string_value de tipo string. Mientras tanto, el número de tipos integrados es mucho mayor, por lo que se asume que todos los tipos integrales (incluyendo int, short, char, color, datetime y enumeraciones) deben caer en el campo integer_value, los valores float deben caer en el campo double_value, y sólo para el campo string_value tiene una interpretación inequívoca: siempre es string.

Para llevar a cabo esta tarea, implementamos varios métodos assign sobrecargados: tres con tipos específicos de float, double y string, y una plantilla para todo lo demás.

class MqlParamBuilder
{
protected:
   ...
   void assign(const float v)
   {
      array[n].double_value = v;
   }
   
   void assign(const double v)
   {
      array[n].double_value = v;
   }
   
   void assign(const string v)
   {
      array[n].string_value = v;
   }
   
   // here we process int, enum, color, datetime, etc. compatible with long
   template<typename T>
   void assign(const T v)
   {
      array[n].integer_value = v;
   }
   ...

Esto completa el proceso de llenado de estructuras, y queda la cuestión de pasar el array generado para el código de llamada. Esta acción se asigna a un método público con una sobrecarga del operador '>>', que tiene un único argumento: una referencia al array receptor MqlParam.

   // export the inner array to the outside
   void operator>>(MqlParam &params[])
   {
      ArraySwap(arrayparams);
   }

Ahora que todo está listo, podemos trabajar con el código fuente del indicador modificado UseEnvelopesParams2.mq5. Los cambios comparados con la primera versión sólo afectan al rellenado del array MqlParam en el manejador OnInit. En él, describimos el objeto «constructor», le enviamos todos los parámetros mediante '<<' y devolvemos el array terminado mediante '>>'. Todo se hace en una sola línea.

int OnInit()
{
   ...
   MqlParam params[];
   MqlParamBuilder builder;
   builder << WorkPeriod << Shift << Method << Price << Deviation >> params;
   ArrayPrint(params);
   /*
       [type] [integer_value] [double_value] [string_value]
   [0]      7              14        0.00000 null            <- "INT" period
   [1]      7               0        0.00000 null            <- "INT" shift
   [2]      7               1        0.00000 null            <- "INT" EMA
   [3]      7               6        0.00000 null            <- "INT" TYPICAL
   [4]     13               0        0.10000 null            <- "DOUBLE" deviation
   */

Para el control, enviamos el array al registro (arriba se muestra el resultado para los valores por defecto).

Si el array no está completamente lleno, la llamada a IndicatorCreate finalizará con un error. Por ejemplo, si pasa sólo 3 parámetros de los 5 requeridos para Envelopes, obtendrá el error 4002 y un manejador no válido.

   Handle = PRTF(IndicatorCreate(_Symbol_PeriodIND_ENVELOPES3params));
   // Error example:
   // indicator Envelopes cannot load [4002]   
   // IndicatorCreate(_Symbol,_Period,IND_ENVELOPES,3,params)=
      -1 / WRONG_INTERNAL_PARAMETER(4002)

Sin embargo, un array más largo que en la especificación del indicador no se considera un error: los valores adicionales simplemente no se tienen en cuenta.

Tenga en cuenta que cuando los tipos de valores difieren de los tipos de parámetros esperados, el sistema realiza una conversión implícita, y esto no genera errores obvios, aunque el indicador generado puede no funcionar como se esperaba. Por ejemplo, si en lugar de Deviation enviamos una cadena al indicador, se interpretará como el número 0, como resultado de lo cual el «sobre» se colapsará: ambas líneas se alinearán en la línea central, respecto a la cual la sangría se realiza por el tamaño de Deviation (en porcentajes). Del mismo modo, pasar un número real con una parte fraccionaria en un parámetro donde se espera un entero hará que se redondee.

Pero, por supuesto, dejar la versión correcta de la llamada IndicatorCreate y obtener un indicador de trabajo, al igual que en la primera versión.

   ...
   Handle = PRTF(IndicatorCreate(_Symbol_PeriodIND_ENVELOPES,
      ArraySize(params), params));
   // success:
   // IndicatorCreate(_Symbol,_Period,IND_ENVELOPES,ArraySize(params),params)=10 / ok
   return Handle == INVALID_HANDLE ? INIT_FAILED : INIT_SUCCEEDED;
}

Por su aspecto, el nuevo indicador no difiere del anterior.