Oscilador universal con interfaz gráfica

Dmitry Fedoseev | 11 enero, 2017


Contenido

Introducción

Con frecuencia, la elección de un indicador adecuado para el sistema comercial se hace después observar cuidadosamente diferentes indicadores en el gráfico e iterar y experimentar con los parámetros. Si hacemos esto, arrastrando cada vez el indicador desde la ventana del indicador y abriendo la ventana de propiedades para cada cambio de parámetros, la tarea puede prolongarse mucho. No estaría mal acelerar este proceso.

Creando una interfaz gráfica que estará disponible directamente en el gráfico, se podrán cambiar rápidamente los parámetros del indicador y observa directamente el efecto de dicho cambio. La unión de varios indicadores distintos en uno común, con su interfaz gráfica, permitirá cambiar los propios indicadores de una forma igualmente rápida.

Análisis de la tarea

En sí mismo, la creación de un oscilador universal no es una tarea especialmente complicada. Necesitaremos un poco de programación orientada a objetos: la clase básica y multitud de subclases.

Los parámetros de cada indicador completo se transmitirán al constructor de la subclase. En este caso, al crear un objeto, el editor MetaEditor abrirá una indicación con una lista de parámetros que le facilitarán significativamente el proceso de desarrollo (fig. 1).

 
Fig. 1. Indicación con los parámetros del constructor al crear un objeto

La principal complicación podría surgir en el uso práctico de este indicador. El asunto es que en osciladores distintos los conjuntos de parámetros se diferencian intensamente. Si para cada oscilador hacemos nuestros propios parámetros, que se diferencien por el prefijo, entonces será posible usar el indicador manualmente, pero a la hora de usar  la función iCustom() o IndicatorCreate() podría no resultar adecuado, debido a la gran cantidad de parámetros. A la función IndicatorCreate() se le pueden transmitir no más de 256 parámetros, y a la función iCustom(), no más de 64. En esta cantidad se cuentan también los parámetros generales del tipo de símbolo o el nombre del indicador, así que el número real de parámetros disponibles es mucho menor. Es posible usar un pequeño conjunto universal de parámetros, pero entonces ya no sería cómodo usar el indicador: tendríamos que llamar constantemente la guía de ayuda para saber qué parámetros están implicados para este o aquel indicador.

El uso de la interfaz gráfica soluciona este problema significativamente: en su ventana se pueden representar los controles que corresponden solo al indicador concreto elegido. La posibilidad de llamar este indicador con la función iCustom() o IndicatorCreate() también deberá estar disponible, por eso en la ventana de propiedades del indicador se representan también los parámetros externos, pero como se ha escrito más arriba, se tratará de un pequeño conjunto universal.

Conjunto de parámetros

Vamos a establecer qué incluye el conjunto mínimo de parámetros. Miramos en el terminal la lista de osciladores: Menú principal - Insertar - Indicadores - Osciladores, los introducimos en el recuadro.

Recuadro 1. Todos los osciladores del terminal

FunciónNombreBúferesParámetros
iATRAverage True Range1. línea1. int ma_period — periodo de promediación
iBearsPowerBears Power1. histograma1. int ma_period — periodo de promediación
iBullsPowerBulls Power1. línea1. int ma_period — periodo de promediación
iCCICommodity Channel Index1. línea1. int ma_period — periodo de promediación
2. ENUM_APPLIED_PRICE applied_price — tipo de precio
iChaikinChaikin Oscillator1. línea1. int fast_ma_period — periodo rápido  
2. int slow_ma_period — periodo lento
3. ENUM_MA_METHOD ma_method — tipo de suavizado
4. ENUM_APPLIED_VOLUME applied_volume — volumen utilizado 
iDeMarkerDeMarker1. línea1. int ma_period — periodo de promediación
iForceForce Index1. línea1. int ma_period — periodo de promediación  
2. ENUM_MA_METHOD ma_method — tipo de suavizado  
3. ENUM_APPLIED_VOLUME applied_volume — tipo de volumen para el cálculo 
iMomentumMomentum1. línea1. int mom_period — periodo de promediación
2. ENUM_APPLIED_PRICE applied_price — tipo de precio
iMACDMoving Averages Convergence-Divergence1. histograma
2. línea
1. int fast_ema_period — periodo de la media rápida
2. int slow_ema_period — periodo de la media lenta
3. int signal_period — periodo de promediación de la diferencia
4. ENUM_APPLIED_PRICE  applied_price — tipo de precio
iOsMAMoving Average of Oscillator (MACD histogram)1. histograma1. int fast_ema_period — periodo de la media rápida 
2. int slow_ema_period — periodo de la media lenta
3. int signal_period — periodo de promediación de la diferencia 
4. ENUM_APPLIED_PRICE  applied_price — tipo de precio
iRSIRelative Strength Index1. línea1. int ma_period — periodo de promediación 
2. ENUM_APPLIED_PRICE applied_price — tipo de precio
iRVIRelative Vigor Index1. línea
2. línea
1. int ma_period — periodo de promediación
iStochasticStochastic Oscillator1. línea
2. línea
1. int Kperiod — periodo K (número de barras para los cálculos)
2. int Dperiod — periodo D (periodo de suavizado primario)
3. int slowing — suavizado final
4. ENUM_MA_METHOD ma_method — tipo de suavizado
5. ENUM_STO_PRICE price_field — método de cálculo del estocástico 
iTriXTriple Exponential Moving Averages Oscillator1. línea1. int ma_period — periodo de promediación 
2. ENUM_APPLIED_PRICE applied_price — tipo de precio
iWPRWilliams' Percent Range1. línea1. int calc_period — periodo de suavizado

En la columna "Parámetros" hacemos una lista de los tipos de parámetros y definimos su número máximo.

Recuadro 2. Tipo y número de parámetros 

TipoNúmero
int3
ENUM_APPLIED_PRICE1
ENUM_MA_METHOD1
ENUM_APPLIED_VOLUME1
ENUM_STO_PRICE1
En la columna "Búferes" vemos que solo se pueden usar en total dos búferes de indicador, y también que para diferentes indicadores se hacen necesarios distintos tipos de dibujado. Por supuesto, los podemos dibujar todos con líneas, pero puesto que algunos de ellos se suelen representar en forma de histograma y las  capacidades del terminal nos permiten hacer esto, al cambiar el tipo de indicador intentaremos posibilitar el cambio de tipo de dibujado. Asimismo, será necesario pensar también en el dibujado de los niveles horizontales, ya que para algunos indicadores (RSI, CCI, etcétera) es necesario que estén presentes en el gráfico.

Plan de trabajo

Cuanto mayor sea el número de tareas independientes en el que se puede dividir una tarea general, más sencillo y cómodo será realizarla. Por eso, todo el trabajo constará de tres etapas:

  1. Crear las clases para un oscilador universal y crear dicho oscilador sin interfaz gráfica.
  2. Crear las clases para la interfaz gráfica.
  3. Unir el oscilador universal y la interfaz gráfica. 

Uno de los momentos más importantes a los que hay que prestar atención se refiere a los valores por defecto. Los parámetros deberán establecerse tanto a través de la interfaz gráfica, como simplemente a través de la ventana de propiedades (para dotar al indicador creado de un carácter lo más universal posible). Al definir los parámetros a través de una ventana de propiedades que disponga de un pequeño conjunto universal de parámetros, hay que tener en cuenta que con los parámetros por defecto todos los indicadores en general se correspondan con su aspecto natural.

Vamos a ver los valores por defecto de los distintos osciladores. Por ejemplo, Stochastic tiene unos periodos de 5, 3, 3 (el primer parámetro es mayor que el segundo), y MACD: 12, 26, 9 (el primer parámetro es menor que el segundo). Para el indicador MACD, el primer parámetro es el periodo de la media rápida, y el segundo, el periodo de la media lenta; es decir, que el primer parámetro debe ser menor que el segundo. Esa misma proporción entre el primer y segundo parámetro será adecuada para el oscilador Chaikhin (para este también se definen los periodos de la media rápida y lenta). Para el indicador Stochastic no es tan importante esta proporción, en cualquier caso, tendrá el aspecto que le corresponda en función del movimiento del precio. Si para el indicador MACD definimos el primer parámetro como superior al segundo, el indicador se dirigirá al lado opuesto al movimento del precio (al establecer los parámetros por defecto conviene evitar esto).

Al usar la interfaz gráfica querríamos que los indicadores comenzaran a trabajar, en primer lugar, con el conjunto de parámetros por defecto: MACD con los periodos 12, 26, 9, Stochastic con los periodos 5, 3, 3, etcétera. Además, sería deseable tener la posibilidad de que al elegir un nuevo indicador, este comenzara a trabajar con los parámetros por defecto, o bien con los mismos parámetros con los que ha funcionado el indicador anterior. Por ejemplo, se investigan los indicadores RSI y CCI, y a nosotros nos interesa cómo cambia el tipo de línea de diferentes indicadores, pero con un mismo valor de periodo. Esto se deberá tener en cuenta en lo sucesivo al desarrollar las clases.

Creando la clase básica del indicador

En la carpeta Include, crearemos la carpeta "UniOsc", en ella se ubicarán todos los archivos adicionales del indicador. El conjunto de osciladores utilizados se ha definido más arriba, en el recuadro 1. De acuerdo con ellos, crearemos una enumeración para elegir el tipo de oscilador. Es posible que la enumeración sea necesaria no solo en el archivo del oscilador, por eso la ubicaremos en un archivo aparte, con el nombre "UniOscDefines.mqh" (el archivo se encuentra en la carpeta "UniOsc"): 

enum EOscUnyType{
   OscUni_ATR,
   OscUni_BearsPower,
   OscUni_BullsPower,  
   OscUni_CCI,
   OscUni_Chaikin,
   OscUni_DeMarker,
   OscUni_Force,
   OscUni_Momentum,
   OscUni_MACD,
   OscUni_OsMA,
   OscUni_RSI,
   OscUni_RVI,
   OscUni_Stochastic,
   OscUni_TriX,
   OscUni_WPR
};

En este archivo no habrá nada más.

Creamos el archivo "CUniOsc.mqh" para la clase del indicador, registramos en ella la plantilla de la clase COscUni:

class COscUni{
   protected:
    
   public:

};

En la plantilla se ha definido la sección protected, puesto que los miembros de la clase deberán estar protegidos, pero disponibles para los descendientes de la clase (los miembros de la sección private están protegidos, pero no están disponibles para los descendientes).

El método principal de la clase básica es el método que corresponde a la función OnCalculate() del indicador, lo llamaremos Calculate(). Los primeros dos parámetros del método se corresponderán con los parámetros análogos de la función OnCalculate(): rates_total (número total de barras) y prew_calculate (número de barras calculadas). No hay que transmitir ningunas matrices con datos al método Calculate(), puesto que se usan los datos de otro indicador. Pero hay que transmitir dos búferes de indicador, que se rellenarán con datos. Incluso usando indicadores con un solo búfer, será necesario asegurarse de que el segundo búfer esté limpio, por eso, en cualquier caso, se le transmitirán al método Calculate() dos búferes de indicador. El código del método Calculate() dependerá del tipo de oscilador utilizado, de si tiene un búfer con datos o dos. Esto significa que el método Calculate() será virtual:

virtual int Calculate( const int rates_total,
               const int prev_calculated,
               double & buffer0[],
               double & buffer1[]
){
   return(rates_total);
}

Al cargar diversos indicadores, necesitaremos una variable para el manejador del indicador. Lo declaramos en la sección protected.

Aparte, necesitaremos algunas variables más para diversas propiedades de representación de los búferes. Estas propiedades se definirán al cargarse cada indicador concreto, es decir, los valores se adjudicarán a estas variables en las subclases:

int m_handle;           // manejador del indicador
int m_bufferscnt;       // número de búferes utilizados    
string m_name;          // nombre del indicador      
string m_label1;        // nombre del búfer 1    
string m_label2;        // nombre del búfer 2
int m_drawtype1;        // tipo de dibujado del búfer 1    
int m_drawtype2;        // tipo de dibujado del búfer 2      
string m_help;          // pequeña guía de los parámetros del indicador
int m_digits;           // número de decimales tras la coma en los valores del indicador
int m_levels_total;     // número de niveles
double m_level_value[]; // matriz para los valores de los niveles

Después de cargar el indicador, se deberá comprobar su éxito, es decir, será necesario el método correspondiente para comprobar el manejador del indicador:

bool CheckHandle(){
   return(m_handle!=INVALID_HANDLE);
}

Al cambiar de oscilador a través de la interfaz gráfica, será necesario determinar si se ha calculado por completo el indicador. Esto se realiza con la función BarsCalculated(), para llamarla, se deberá usar el manejador del indicador. Eso significa que tenemos que añadir el método para obtener el manejador:  

int Handle(){
    return(m_handle);
}

En el constructor de la clase inicializamos el manejador, y lo comprobamos en el desctructor en caso necesario con la llamada IndicatorRelease():

void COscUni(){
   m_handle=INVALID_HANDLE;
}

void ~COscUni(){
   if(m_handle!=INVALID_HANDLE){
      IndicatorRelease(m_handle);
   }
} 

Proporcionamos acceso al resto de las variables que definen la representación de diferentes indicadores, creamos los métodos para obtener sus valores:

string Name(){ // nombre del oscilador
   return(m_name);
}    
  
int BuffersCount(){ // número de búferes de los osciladores
   return(m_bufferscnt);
}

string Label1(){ // nombre del primer búfer
   return(m_label1);
}

string Label2(){ // nombre del segundo búfer
   return(m_label2);
}

int DrawType1(){ // tipo de dibujado del primer búfer
   return(m_drawtype1);
}

int DrawType2(){ // tipo de dibujado del segundo búfer
   return(m_drawtype2);
}  

string Help(){ // indicación sobre el uso de los parámetros
   return(m_help);
}

int Digits(){ // número de decimales tras la coma en el indicador
   return(m_digits);
}

int LevelsTotal(){ // número de niveles del indicador
   return(m_levels_total);
}

double LevelValue(int index){ // obteniendo los valores del nivel según el índice
   return(m_level_value[index]);
}

Todos estos métodos retornan el valor de la variable correspondiente, y los valores se asignarán a las variables en las subclases de los osciladores.  

Subclases Calculate

Crearemos dos subclases: para los indicadores con un búfer y para los indicadores con dos búferes. Para los indicadores con un búfer:

class COscUni_Calculate1:public COscUni{
   public:
      void COscUni_Calculate1(){
         m_bufferscnt=1;
      }
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & buffer0[],
                     double & buffer1[]
      ){
        
         int cnt,start;
        
         if(prev_calculated==0){
            cnt=rates_total;
            start=0;
         }
         else{
            cnt=rates_total-prev_calculated+1;
            start=prev_calculated-1;
         }  
        
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
        
         for(int i=start;i<rates_total;i++){
            buffer1[i]=EMPTY_VALUE;
         }        
        
         return(rates_total);
      }
};

Vamos a echar un vistazo a esta clase. La clase tiene el constructor "COscUni_Calculate1", en el constructor se configura el número de búferes, en este caso, 1. En el método Calculate(), dependiendo de los valores de las variables rates_total y prev_calculate, se calcula el número de elementos del búfer para el copiado (variable cnt) y el índice de la barra desde el que se debe limpiar el segundo búfer (variable start). En el caso de que los datos no se hayan copiado con éxito (con la llamada de la función CopyBuffer()), desde el método se retorna 0, para que en el siguiente tick todos los cálculos se ejecuten desde el principio. Al final del método, como siempre, se retorna rates_total.

Subclase para los indicadores con dos búferes:

class COscUni_Calculate2:public COscUni{
   public:
      void COscUni_Calculate2(){
         m_bufferscnt=2;
      }  
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & buffer0[],
                     double & buffer1[]
      ){
         int cnt;
         if(prev_calculated==0){
            cnt=rates_total;
         }
         else{
            cnt=rates_total-prev_calculated+1;
         }          
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
         if(CopyBuffer(m_handle,1,0,cnt,buffer1)<=0){
            return(0);
         }
         return(rates_total);
      }
};

Esta clase es más sencilla aún que la clase para los indicadores con un búfer. Al inicio del método Calculate(), se calcula el número de elementos (variable cnt) para el copiado y se copian los búferes. 

Subclases de los indicadores

Ahora creamos las subclases para los propios osciladores. Estas clases serán subclases para la clase COscUni_Calculate1 o COscUni_Calculate2. En cada una de estas clases solo habrá un constructor. Al constructor de cada clase se transmitirán los parámetros que correspondan al propio oscilador de esta clase, además de otro par de parámetros adicionales al principio. Los parámetros adicionales determinarán si hay que usar los valores de los parámetros transmitidos al constructor, o bien establecer los valores por defecto (variable con el nombre use_default). El segundo parámetro con el nombre keep_previous determina si hay que asignar los valores por defecto a todos los parámetros o solo a aquellos aún no se han usado en absoluto.

El primer indicador de la lista es ATR, comenzaremos a escribir la subclase para él. En primer lugar, la plantilla de clase:

class COscUni_ATR:public COscUni_Calculate1{
   public:
   void COscUni_ATR(bool use_default,bool keep_previous,int & ma_period){

   }
};

Preste atención: el parámetro ma_period se transmite por enlace, para que al configurar los parámetros por defecto del indicador podamos tener acceso a sus valores en el oscilador universal creado.

Escribimos el código en el constructor:

if(use_default){
   if(keep_previous){
      if(ma_period==-1)ma_period=14;
   }
   else{
      ma_period=14;
   }      
}  

En esta parte del código, si use_default=true, se configuran los valores por defecto. Si keep_previous=true, el valor por defecto se establece solo si el parámetro obtenido tiene el valor -1, es decir, si no se ha usado con anterioridad. Por consiguiente, al inicializar el oscilador universal, habrá que asignar el valor -1 a todas las variables para los parámetros.

Ahora, la línea más importante de código en el constructor de la subclase, la carga del indicador:

m_handle=iATR(Symbol(),Period(),ma_period);

Al final, hay varias líneas para establecer los parámetros de representación:

m_name=StringFormat("ATR(%i)",ma_period); // nombre del indicador
m_label1="ATR"; // nombre del búfer
m_drawtype1=DRAW_LINE;   // tipo de dibujado
m_help=StringFormat("ma_period - Period1(%i)",ma_period); // indicación  
m_digits=_Digits+1; // número de decimales tras la coma en los valores  
m_levels_total=0; // número de niveles    

Veamos unas cuantas etapas en la creación de la subclase para un indicador más complicado, MACD. El principio de creación es el mismo, solo que el código será un poco mayor. Por eso, analizaremos los fragmentos. Configurando los parámetros por defecto:

if(use_default){
   if(keep_previous){
      if(fast_ema_period==-1)fast_ema_period=12;
      if(slow_ema_period==-1)slow_ema_period=26;
      if(signal_period==-1)signal_period=9;
      if(applied_price==-1)applied_price=PRICE_CLOSE;            
   }
   else{
      fast_ema_period=12;
      slow_ema_period=26;
      signal_period=9;
      applied_price=PRICE_CLOSE;
   }      
}

Configurando los parámetros de representación:

m_handle=iMACD(Symbol(),
               Period(),
               fast_ema_period,
               slow_ema_period,
               signal_period,
               (ENUM_APPLIED_PRICE)applied_price);

m_name=StringFormat( "iMACD(%i,%i,%i,%s)",
                     fast_ema_period,
                     slow_ema_period,
                     signal_period,
                     EnumToString((ENUM_APPLIED_PRICE)applied_price));
                    
m_label1="Main";
m_label2="Signal";      
m_drawtype1=DRAW_HISTOGRAM;            
m_drawtype2=DRAW_LINE;

m_help=StringFormat( "fast_ema_period - Period1(%i), "+
                     "slow_ema_period - Period2(%i), "+
                     "signal_period - Period3(%i), "+
                     "applied_price - Price(%s)",
                     fast_ema_period,
                     slow_ema_period,
                     signal_period,
                     EnumToString((ENUM_APPLIED_PRICE)applied_price));  
                    
m_digits=_Digits+1;

Veamos con más detalle los parámetros del constructor:

void COscUni_MACD(bool use_default,
                  bool keep_previous,
                  int & fast_ema_period,
                  int & slow_ema_period,
                  int & signal_period,
                  long & applied_price
){

Preste atención a que la variable applied_price para la enumeración estándar ENUM_APPLIED_PRICE se ha anunciado como long. Esto se ha hecho para tener la posibilidad de asignarle a la variable el valor -1, que significa que el parámetro aún no se ha utilizado.

Vamos a ver un fragmento más de la clase para el indicador RSI: la parte del código en la que se establecen los niveles: 

m_levels_total=3;
ArrayResize(m_level_value,3);
m_level_value[0]=30;
m_level_value[1]=50;
m_level_value[2]=70;

Se establece el número de niveles, se modifica el tamaño de la matriz y se llena con los valores de los niveles.

No vamos a mostrar con detalle cómo se han creado las clases del resto de los osciladores. En los archivos adjuntos al artículo hay una clase completamente preparada con un conjunto completo de osciladores (el nombre del archivo es "CUniOsc.mqh").

Creando un oscilador universal (inicio)

Puesto que las clases de los osciladores ya están preparadas, ya podemos crear usándolas un oscilador universal, pero por el momento sin interfaz gráfica.

Cree su indicador, por ejemplo, con el nombre "iUniOsc". Después, en el wizard de creación del indicador, elija el tipo de función OnCalculate(...open,high,low,close), cree solo una variable externa (para encontrar después sitio con mayor facilidad para las variables externas) y dos búferes del tipo Line.

Antes de la variable externa, incluya los archivos con la enumeración y con las clases de los osciladores: 

#include <UniOsc/UniOscDefines.mqh>
#include <UniOsc/CUniOsc.mqh>

Cree la variable externa para elegir el tipo de oscilador:

input EOscUnyType          Type        =  OscUni_ATR;

Variables UseDefault y KeepPrevious:

input bool                 UseDefault  =  true;
input bool                 KeepPrev    =  true;

Variables universales para los propios parámetros del oscilador:

input int                  Period1     =  14;
input int                  Period2     =  14;
input int                  Period3     =  14;
input ENUM_MA_METHOD       MaMethod    =  MODE_EMA;
input ENUM_APPLIED_PRICE   Price       =  PRICE_CLOSE;  
input ENUM_APPLIED_VOLUME  Volume      =  VOLUME_TICK;  
input ENUM_STO_PRICE       StPrice     =  STO_LOWHIGH;

Algunos indicadores dibujan una sola línea, otros, dos. El primer búfer a veces se representa con una línea, y a veces con un histograma. Estaría bien que el búfer en forma de línea tuviese un color llamativo, y que el histograma fuese gris, por eso crearemos tres variables para el color:

input color                ColorLine1  =  clrLightSeaGreen;
input color                ColorLine2  =  clrRed;
input color                ColorHisto  =  clrGray;

Dado que planeamos crear una interfaz gráfica, podremos cambiar el tipo de oscilador y los valores de los parámetros sin reiniciar el indicador, por eso crearemos duplicados de la variable Type y de las variables para los parámetros: 

int                  _Period1;
int                  _Period2;
int                  _Period3;
long                 _MaMethod;
long                 _Price;  
long                 _Volume;  
long                 _StPrice;
EOscUnyType          _Type;

Declaramos la variable-puntero para el objeto del oscilador universal:

COscUni * osc;

Declaramos también otro par de variables:

string ProgName;
string ShortName;

Estas variables serán adecuadas para formar el nombre del indicador, representado en la esquina superior izquierda de la subventana. 

Ahora añadiremos código al final de la función OnInit(), pero primero realizaremos trabajo preparatorio. Prepararemos los parámetros de los osciladores de acuerdo con los valores de las variables UseDefault y KeepPrevious (también asignaremos el valor de la variable _Type), lo registraremos en forma de función, para estructurar el código cómodamente:

void PrepareParameters(){

   _Type=Type;

   if(UseDefault && KeepPrev){
      _Period1=-1;
      _Period2=-1;
      _Period3=-1;
      _MaMethod=-1;
      _Volume=-1;
      _Price=-1;  
      _StPrice=-1;
   }
   else{  
      _Period1=Period1;
      _Period2=Period2;
      _Period3=Period3;
      _MaMethod=MaMethod;
      _Volume=Volume;
      _Price=Price;  
      _StPrice=StPrice;
   }
}

Si se usan UseDefault y KeepPrevious, a todas las variables se les asignan los valores -1, para que en el constructor de la clase podamos distinguir las variables que no se han usado aún y establecer los valores por defecto solo para ellas. En el resto de los casos se asignarán los valores de la ventana de propiedades, que o bien se usarán como están, o bien serán sustituidos por los valores por defecto al crear el objeto.  

Después de preparar los parámetros, cargamos el oscilador elegido. El código de la carga también se ha implementado en forma de función, aquí mostramos un fragmento:

void LoadOscillator(){
   switch(_Type){
      case OscUni_ATR:
         osc=new COscUni_ATR(UseDefault,KeepPrev,_Period1);
      break;
      case OscUni_BearsPower:
         osc=new COscUni_BearsPower(UseDefault,KeepPrev,_Period1);
      break;
      case OscUni_BullsPower:
         osc=new COscUni_BullsPower(UseDefault,KeepPrev,_Period1);
      break;      
      ...
   }  
}

Después de cargar el oscilador, comprobamos el manejador:

if(!osc.CheckHandle()){
   Alert("Error al cargar el indicador "+osc.Name());
   return(INIT_FAILED);
}

Si la descarga ha finalizado, establecemos los estilos de dibujado, obteniéndolos a través de los métodos de objeto correspondientes. Esta parte del código también se ha ejecutado en forma de función, el código se muestra al completo:

void SetStyles(){  

   // configuración de los estilos
   if(osc.BuffersCount()==2){
      PlotIndexSetInteger(0,PLOT_DRAW_TYPE,osc.DrawType1());
      PlotIndexSetInteger(1,PLOT_DRAW_TYPE,osc.DrawType2());
      PlotIndexSetInteger(0,PLOT_SHOW_DATA,true);
      PlotIndexSetInteger(1,PLOT_SHOW_DATA,true);
      PlotIndexSetString(0,PLOT_LABEL,osc.Label1());
      PlotIndexSetString(1,PLOT_LABEL,osc.Label2());
      if(osc.DrawType1()==DRAW_HISTOGRAM){
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorHisto);
      }
      else{
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorLine1);        
      }
      PlotIndexSetInteger(1,PLOT_LINE_COLOR,ColorLine2);  
   }
   else{
      PlotIndexSetInteger(0,PLOT_DRAW_TYPE,osc.DrawType1());
      PlotIndexSetInteger(1,PLOT_DRAW_TYPE,DRAW_NONE);  
      PlotIndexSetInteger(0,PLOT_SHOW_DATA,true);
      PlotIndexSetInteger(1,PLOT_SHOW_DATA,false);  
      PlotIndexSetString(0,PLOT_LABEL,osc.Label1());
      PlotIndexSetString(1,PLOT_LABEL,"");
      if(osc.DrawType1()==DRAW_HISTOGRAM){
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorHisto);
      }
      else{
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorLine1);        
      }        
   }
  
   // configuración de digits
   IndicatorSetInteger(INDICATOR_DIGITS,osc.Digits());

   // configuración de los niveles
   int levels=osc.LevelsTotal();
   IndicatorSetInteger(INDICATOR_LEVELS,levels);
   for(int i=0;i<levels;i++){
      IndicatorSetDouble(INDICATOR_LEVELVALUE,i,osc.LevelValue(i));
   }

}    

Primero, dependiendo del número de búferes del oscilador, se ejecuta una de las dos variantes de configuración de estilos. Además, si el primer búfer es un histograma, se establece el tipo correspondiente de búfer. A continuación, se establece el número de decimales tras la coma en los valores del indicador. Al final se definen los niveles.

Más abajo mostramos el código completo de la función OnInit() con la llamada de las funciones recién creadas:

int OnInit(){

   SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
   SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA);

   PrepareParameters();

   LoadOscillator();
  
   if(!osc.CheckHandle()){
      Alert("Error al cargar el indicador "+osc.Name());
      return(INIT_FAILED);
   }

   SetStyles();
  
   Print("Parameters matching: "+osc.Help());
  
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
  
   return(INIT_SUCCEEDED);
}

Preste atención: al final de la función, se ejecuta la llamada de Print con una indicación sobre los parámetros utilizados de la ventana de propiedades, después se define el nombre corto del indicador.

Con esto ya hemos finalizado por completo la primera parte del funcionamiento del oscilador, hemos obtenido un indicador con cuya ayuda podemos poner a prueba las clases anteriormente creadas. A continuación, crearemos la clase de la interfaz gráfica.

En los anexos al artículo, disponemos de un indicador completamente preparado con el nombre "iUniOsc" (después se introducirá en su código un pequeño cambio, por lo que se diferenciará un poco del indicador obtenido en esta etapa). 

Plan de creación de la interfaz gráfica

Para crear una interfaz gráfica podemos simplemente usar objetos gráficos: para introducir valores numéricos, hay que usar el objeto gráfico "campo de edición", para los parámetros del tipo de enumeraciones (listas desplegables) varios botones. Sin embargo, este enfoque exige mucho trabajo. En la actualidad, en MQL5 hay escritas muchas bibliotecas para crear una interfaz gráfica. Las bibliotecas permiten crear controles estándar: ventanas de diálogo, campos de edición con botones para aumentar o disminuir los valores (spinbox), listas desplegables y mucho más. En el complejo del terminal entra un conjunto de clases estándar para crear paneles y ventanas de diálogo. En el apartado "Artículos" hay una enorme serie de artículos dedicados a la creación de interfaces gráficas.

Asimismo, en los artículos también hay una pequeña  serie de tres publicaciones (artículo 1, artículo 2, artículo 3), dedicada a la creación rápida y sencilla de interfaces gráficas. Aparte del análisis de la teoría, en estos artículos se crea una biblioteca para trabajar rápidamente con los objetos gráficos y para creer una interfaz gráfica. Todas las variantes enumeradas más arriba tienen sus ventajas y desventajas, ya se han visto con mucho detalle al escribir este artículo. Se ha elegido la última variante de las enumeradas (biblioteca incGUI).

El terminal MetaTrader 5 se desarrolla y perfecciona de forma muy activa, por eso ciertos controles de la biblioteca propuesta se pueden considerar anacrónicos desde un punto de vista moral (en concreto, las franjas de desplazamiento),  pero, no obstante, podemos utilizarlas. Para comenzar a utilizar la biblioteca, descargue los anexos al artículo "Controles gráficos personalizados. Parte 3. Formularios para MetaTrader 5", descomprímalos, coloque el archivo "incGUI_v3.mqh" en la carpeta Include, ubicada en la carpeta de datos del terminal.

Clase de formulario

Realizaremos todo el trabajo de creación de la interfaz gráfica en un archivo aparte "UniOscGUI.mqh". Lo primero que haremos es incluir la biblioteca:

#include <IncGUI_v3.mqh>

Realizamos una compilación de control. En la compilación se mostrarán varias advertencias sobre las que el compilador no había prevenido con anterioridad. Ahora, el compilador perfeccionado permitirá detectar estos defectos en el código e introducir los cambios correspondientes. En los anexos al artículo se puede encontrar el archivo corregido "inc_GUI_v4". En lugar de "IncGUI_v3.mqh", tenemos que incluir "IncGUI_v4.mqh" y "UniOscDefines.mqh". 

#include <IncGUI_v4.mqh>
#include <UniOsc/UniOscDefines.mqh>

Guardamos una copia del indicador "iUniOsc" con el nombre "iUniOscGUI". Después de ello, podemos editar un poco el indicador "iUniOsc", ocultando los parámetros UseDefault y KeepPrev. En un indicador sin interfaz gráfica no tienen sentido, debemos asignarles el valor false:

bool                 UseDefault  =  false;
bool                 KeepPrev    =  false;

Con esto, podemos considerar el indicador "iUniOsc" finalizado por completo.

Vamos a continuar con el indicador "iUniOscGUI". Incluimos en él el archivo "UniOscGUI.mqh". En total se deben incluir tres archivos:

#include <UniOsc/UniOscDefines.mqh>
#include <UniOsc/CUniOsc.mqh>
#include <UniOsc/UniOscGUI.mqh>

Al compilar el indicador, se podrá comprobar el código y ver de inmediato la interfaz gráfica en el gráfico. Pero por el momento se ejecutará en el archivo "UniOscGUI.mqh". 

La interfaz gráfica consituirá una ventana de diálogo, en cuya parte superior se ubicará una lista desplageble para elegir el oscilador. Más abajo tendremos un conjunto de controles que se corresponderá con cada oscilador concreto. Eso significa que en el archivo se ubicarán la clase para crear el formulario y un grupo de clases (clase padre y varias subclases) para crear controles en este formulario.

Comenzaremos por el formulario. Podrá encontrar una descripción paso a paso del proceso de creación del formulario en el artículo "Controles gráficos personalizados. Parte 3. Formularios para MetaTrader 5". Aquí vamos a ejecutar este proceso aplicado a nuestra tarea.

1. Del archivo "IncGUI_v4.mqh" copiamos la clase CFormTemplate al archivo "UniOscGUI.mqh" y cambiamos el nombre a CUniOscForm.

2. Establecemos las propiedades. Esto se hace en el método MainProperties() de la clase CUniOscForm. Configuramos las siguientes propiedades:

void MainProperties(){
      m_Name         =  "UniOscForm";
      m_Width        =  FORM_WIDTH;
      m_Height       =  150;
      m_Type         =  0;
      m_Caption      =  "UniOsc";
      m_Movable      =  true;
      m_Resizable    =  true;
      m_CloseButton  =  true;
}

Preste atención: a la variable m_Heigh se le adjudica el valor de la constante FORM_WIDTH. En la etapa final del trabajo, habrá que elegir el tamaño adecuado del formulario y los controles, por eso, en la parte superior del archivo añadimos varias constantes:

#define FORM_WIDTH 210        // anchura del formulario
#define SPIN_BOX_WIDTH 110    // anchura de la caja de rotación
#define COMBO_BOX_WIDTH 110   // anchura de la lista desplegable

Después de ello, podemos aplicar el formulario en el indicador. En el indicador, declaramos la variable externa UseGUI con el valor por defecto true (al principio de la ventana de propiedades):

input bool                 UseGUI      =  true;

A continuación, después de las variables externas, declaramos la variable que es un puntero para la clase del formulario:

CUniOscForm * frm;

En la función OnInit() del indicador, si el valor de la variable UseGUI es igual a true, creamos un objeto y lo preparamos para su uso, llamando los métodos necesarios para establecer las propiedades adicionales:

frm=new CUniOscForm();  // creación del objeto
frm.Init();             // inicialización
frm.SetSubWindow(0);    // configurando la subventana en la que se representa el formulario
frm.SetPos(10,30);      // configurando la posición inicial del formulario
frm.Show();             // activando la visibilidad del formulario

En la función OnDeinit() ocultamos el formulario y eliminamos el objeto:

if(CheckPointer(frm)==POINTER_DYNAMIC){
   frm.Hide();
   delete(frm);
}

Desde la función OnChartEvent() llamamos el método Event():

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   frm.Event(id,lparam,dparam,sparam);  
}

Si colocamos ahora el indicador en el gráfico, podremos ver el formulario (fig. 2).


Fig. 2. Formulario creado por la clase CUniOscForm al colocar el indicador iUniOscGUI en el gráfico 


Todos los botones del formulario funcionan: el formulario puede moverse con la ayuda del botón en la ventana superior izquierda (pulsar el botón e indicar la nueva ubicación del formulario con un click del ratón), también se puede minimizar (botón con el rectángulo en la esquina superior derecha). Al pulsar el botón con la cruceta, el formulario se cerrará, y además habrá que eliminar el indicador del gráfico. Para eliminar el indicador del gráfico, se usa la función ChartIndicatorDelete(). Para utilizar esta función, hay que conocer el número de la subventana del indicador. Dicho número se puede descubrir con la función ChartWindowFind(), y para ello, a su vez, hay que usar el nombre corto del indicador.

Al pulsar el botón de cierre del formulario, el método Event() retornará el valor 1. Comprobamos el valor retornado y, en caso necesario, eliminamos el indicador del gráfico:

int win=ChartWindowFind(0,ShortName);  // búsqueda de la subventana
ChartIndicatorDelete(0,win,ShortName); // eliminando el indicador
ChartRedraw();                         // acelerando el redibujado del gráfico

Ahora, al pulsar el botón con cruceta, se ejecutará no solo el cierre del formulario, sino también la eliminación del indicador del gráfico.  

Añadimos ahora al formulario el principal control: una lista desplegable para elegir el tipo de oscilador. Para crearla, se usa la clase CComBox. Añadimos el código a la clase CUniOscForm. Declaramos la variable para el objeto:

CComBox m_cmb_main;

Después, en el método OnInitEvent() llamamos el método Init() de la clase:

m_cmb_main.Init("cb_main",100," select oscillator");

Al método se transmite el nombre del control (el prefijo para los nombres de los objetos gráficos), la anchura del control y el rótulo junto al mismo. 

En el método OnShowEvent() llamamos el método Show():

m_cmb_main.Show(aLeft+10,aTop+10);

Al llamar el método se indican las coordenadas de ubicación del elemento en el formulario (con una sangría de 10 píxeles a partir del ángulo superior izquierdo del espacio de usario del formulario). 

En el método OnHideEvent() llamamos el método Hide():

m_cmb_main.Hide();

Si tiene lugar el evento de cambio de elección, en la lista principal será necesario cargar otro indicador. Esto se realizará con mayor comodidad en el archivo del indicador, por eso, el método Event() de la lista de osciladores se llamará, no desde el método EventsHandler() del formulario, sino desde la función OnChartEvent() del indicador. Procesamos el evento de inmediato:

int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
if(me==1){
   Alert(frm.m_cmb_main.SelectedText());
}  

Al método se le transmiten los parámetros estándar de evento del gráfico. En caso de que el método retorne 1, se abrirá la ventana de mensajes.

Es necesario rellenar la lista con variantes. Aquí podemos adoptar varios enfoques:

  • hacerlo todo en el método OnInitEvent() del formulario
  • añadir a la clase del formulario un método adicional y llamarlo desde el indicador tras el método Init()
  • invocar los métodos de la lista directamente desde el indicador.

Usaremos la tercera variante (pues exige la menor cantidad de código). En primer lugar, crearemos en el indicador una matriz con las variantes de los osciladores:

EOscUniType osctype[]={
   OscUni_ATR,
   OscUni_BearsPower,
   OscUni_BullsPower,  
   OscUni_CCI,
   OscUni_Chaikin,
   OscUni_DeMarker,
   OscUni_Force,
   OscUni_Momentum,
   OscUni_MACD,
   OscUni_OsMA,
   OscUni_RSI,
   OscUni_RVI,
   OscUni_Stochastic,
   OscUni_TriX,
   OscUni_WPR
};

A continuación, después de llamar frm.Init(), rellenamos la lista en el indicador y establecemos el punto elegido por defecto:

for(int i=0;i<ArraySize(osctype);i++){
   frm.m_cmb_main.AddItem(EnumToString(osctype[i]));
}
frm.m_cmb_main.SetSelectedIndex(0);

En esta etapa, podemos ralizar la comprobación. En el formulario debe representarse una lista desplegable con los tipos de oscilador, y al cambiar el punto elegido se deberá abrir una ventana con el texto correspondiente (fig. 3):

 
Fig. 3. Formulario con la lista de osciladores y la ventana de mensajes después de cambiar la elección en la lista 

Controles en el formulario

Al principio del artículo se determinó el número máximo de parámetros externos por tipos (tres para introducir valores numéricos y cuatro para las enumeraciones estándar). Para introducir valores numéricos, usamos el elemento CSpinInputBox (campo de edición con botones) de la biblioteca incGUI, para las enumeraciones estándar, el elemento CComBox (lista desplegable).

Al inicio del archivo con la clase de la interfaz gráfica declaramos las matrices con los valores de las enumeraciones estándar:

ENUM_APPLIED_PRICE e_price[]={   PRICE_CLOSE,
                                 PRICE_OPEN,
                                 PRICE_HIGH,
                                 PRICE_LOW,
                                 PRICE_MEDIAN,
                                 PRICE_TYPICAL,
                                 PRICE_WEIGHTED
};

ENUM_MA_METHOD e_method[]={MODE_SMA,MODE_EMA,MODE_SMMA,MODE_LWMA};

ENUM_APPLIED_VOLUME e_volume[]={VOLUME_TICK,VOLUME_REAL};

ENUM_STO_PRICE e_sto_price[]={STO_LOWHIGH,STO_CLOSECLOSE};

En la clase del formulario, declaramos las variables para los controles (tres del tipo CSpinInputBox y cuatro CComBox): 

CSpinInputBox m_value1;
CSpinInputBox m_value2;      
CSpinInputBox m_value3;
      
CComBox m_price;
CComBox m_method;
CComBox m_volume
CComBox m_sto_price;

En la clase del formulario, en el método OnInitEvent() inicializamos las listas desplegables (objetos de la clase CComBox) y las rellenamos usando las matrices declaradas anteriormente:

m_price.Init("price",COMBO_BOX_WIDTH," price");
m_method.Init("method",COMBO_BOX_WIDTH," method");
m_volume.Init("volume",COMBO_BOX_WIDTH," volume");
m_sto_price.Init("sto_price",COMBO_BOX_WIDTH," price");              

for(int i=0;i<ArraySize(e_price);i++){
   m_price.AddItem(EnumToString(e_price[i]));            
}
for(int i=0;i<ArraySize(e_method);i++){
   m_method.AddItem(EnumToString(e_method[i]));              
}            
for(int i=0;i<ArraySize(e_volume);i++){
   m_volume.AddItem(EnumToString(e_volume[i]));            
}            
for(int i=0;i<ArraySize(e_sto_price);i++){
   m_sto_price.AddItem(EnumToString(e_sto_price[i]));            
}

Puesto que para los diferentes indicadores en el formulario deberán representarse diferentes conjuntos de controles, crearemos las clases (básicas y subclases) para formar los conjuntos. La clase básica es CUniOscControls, más abajo se muestra su plantilla:

class CUniOscControls{
   protected:
      CSpinInputBox * m_value1;
      CSpinInputBox * m_value2;      
      CSpinInputBox * m_value3;
      CComBox * m_price;
      CComBox * m_method;
      CComBox * m_volume;
      CComBox * m_sto_price;
   public:
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CSpinInputBox & value3,
                        CComBox & price,
                        CComBox & method,
                        CComBox & volume,
                        CComBox & sto_price){
       ...
   }
   void Hide(){
       ...
   }
   int Event(int id,long lparam,double dparam,string sparam){
      ...
      return(0);
   }
   virtual void InitControls(){
   }  
   virtual void Show(int x,int y){
   }  
   virtual int FormHeight(){
      return(0);
   }
};

Al comenzar a usar un objeto de esta clase, se llamará el método SetPointers(), a dicho método se le transmiten los punteros a todos los controles, y en el método se guardan en sus propias variables de clase: 

void SetPointers(CSpinInputBox & value1,
                     CSpinInputBox & value2,      
                     CSpinInputBox & value3,
                     CComBox & price,
                     CComBox & method,
                     CComBox & volume,
                     CComBox & sto_price){
   m_value1=GetPointer(value1);
   m_value2=GetPointer(value2);      
   m_value3=GetPointer(value3);            
   m_price=GetPointer(price);
   m_method=GetPointer(method);
   m_volume=GetPointer(volume);
   m_sto_price=GetPointer(sto_price);
}

Estos punteros se usan para ocultar todos los controles (método Hide()):

void Hide(){
   m_value1.Hide();
   m_value2.Hide();
   m_value3.Hide();
   m_price.Hide();
   m_method.Hide();
   m_volume.Hide();
   m_sto_price.Hide();
}

Se procesarán sus eventos (método Event()):

int Event(int id,long lparam,double dparam,string sparam){
   int e1=m_value1.Event(id,lparam,dparam,sparam);
   int e2=m_value2.Event(id,lparam,dparam,sparam);
   int e3=m_value3.Event(id,lparam,dparam,sparam);
   int e4=m_price.Event(id,lparam,dparam,sparam);
   int e5=m_method.Event(id,lparam,dparam,sparam);
   int e6=m_volume.Event(id,lparam,dparam,sparam);
   int e7=m_sto_price.Event(id,lparam,dparam,sparam);
   if(e1!=0 || e2!=0 || e3!=0 || e4!=0 || e5!=0 ||e6!=0 || e7!=0){
      return(1);
   }
   return(0);
}

Los métodos restantes son virtuales, cada oscilador tendrá su código en las subclases. El método Show() se usará para representar los controles. El método FormHeight() retornará la altura del formulario. El método InitControls() se usa solo para cambiar los rótulos junto a los controles (fig. 4).


Fig. 4. Diferentes rótulos junto a un mismo control en diferentes osciladores 

El asunto es que los controles de la biblioteca incGUI disponen solo del conjunto mínimo de métodos y no tienen métodos para cambiar los rótulos. Sin embargo, las clases se han desarrollado de tal forma que en caso necesario, el rótulo se podrá modificar llamando el método Init(). Puesto que el cambio de rótulo se ejecuta con el método Init(), el método se llama InitControls().  

Vamos a ver varias subclases. La más sencilla de ellas es para el indicador ATR, la más compleja, para Stochastic.

Para ATR:

class CUniOscControls_ATR:public CUniOscControls{
   void InitControls(){
      m_value1.Init("value1",SPIN_BOX_WIDTH,1," ma_period");
   }
   void Show(int x,int y){
      m_value1.Show(x,y);
   }  
   int FormHeight(){
      return(70);
   }  
};

En el método InitContrlos() se ejecuta la llamada del método Init() del control, lo más importante (para ello se ha tenido que hacer este método virtual) es transmitir el texto del rótulo "ma_period", que se representará a la derecha del elemento de control.

En el método Show() de la clase del formulario se ejecuta la llamada del método Show() de la clase CUniOscControls, cuando se da la llamada, se indican las coordenadas de la esquina superior izquierda del primer elemento (superior) de control. El método FormHeight() simplemente retorna el valor.

Para Stochastic:

class CUniOscControls_Stochastic:public CUniOscControls{
   void InitControls(){
      m_value1.Init("value1",SPIN_BOX_WIDTH,1," Kperiod");
      m_value2.Init("value2",SPIN_BOX_WIDTH,1," Dperiod");  
      m_value3.Init("value3",SPIN_BOX_WIDTH,1," slowing");          
   }
   void Show(int x,int y){
      m_value1.Show(x,y);
      m_value2.Show(x,y+20);      
      m_value3.Show(x,y+40);
      m_method.Show(x,y+60);      
      m_sto_price.Show(x,y+80);
   }
   int FormHeight(){
      return(150);
   }    
};

En el método Show() se ejecuta el cálculo de las coordenadas para cada control, el resto debería resultar comprensible.

Y al fin, vamos a analizar propiamente la adición de controles al formulario. En la clase del formulario declaramos la variable-puntero a la clase con los controles:

CUniOscControls * m_controls;

En el destructor, eliminamos el objeto:

void ~CUniOscForm(){
   delete(m_controls);
}

Añadimos a la clase del formulario el método SetType(). Este método se llamará para indicar el tipo de oscilador usado. 

      void SetType(long type){
         if(CheckPointer(m_controls)==POINTER_DYNAMIC){
            delete(m_controls);
            m_controls=NULL;
         }
        
         switch((EOscUniType)type){
            case OscUni_ATR:
               m_controls=new CUniOscControls_ATR();
            break;
            case OscUni_BearsPower:
               m_controls=new CUniOscControls_BearsPower();
            break;
            case OscUni_BullsPower:
               m_controls=new CUniOscControls_BullsPower();
            break;
            case OscUni_CCI:
               m_controls=new CUniOscControls_CCI();
            break;
            case OscUni_Chaikin:
               m_controls=new CUniOscControls_Chaikin();
            break;
            case OscUni_DeMarker:
               m_controls=new CUniOscControls_DeMarker();
            break;
            case OscUni_Force:
               m_controls=new CUniOscControls_Force();
            break;
            case OscUni_Momentum:
               m_controls=new CUniOscControls_Momentum();
            break;
            case OscUni_MACD:
               m_controls=new CUniOscControls_MACD();
            break;
            case OscUni_OsMA:
               m_controls=new CUniOscControls_OsMA();
            break;
            case OscUni_RSI:
               m_controls=new CUniOscControls_RSI();
            break;
            case OscUni_RVI:
               m_controls=new CUniOscControls_RVI();
            break;
            case OscUni_Stochastic:
               m_controls=new CUniOscControls_Stochastic();
            break;
            case OscUni_TriX:
               m_controls=new CUniOscControls_TriX();
            break;
            case OscUni_WPR:
               m_controls=new CUniOscControls_WPR();
            break;
         }
        
         m_controls.SetPointers(m_value1,m_value2,m_value3,m_price,m_method,m_volume,m_sto_price);
         m_controls.InitControls();
        
         m_value1.SetReadOnly(false);
         m_value2.SetReadOnly(false);
         m_value3.SetReadOnly(false);
        
         m_value1.SetMinValue(1);        
         m_value2.SetMinValue(1);
         m_value3.SetMinValue(1);
        
         m_Height=m_controls.FormHeight();        
        
      }  

Al inicio del método se ejecuta la eliminación del objeto, si existía este. A continuación, dependiendo del tipo de indicador, se ejecuta la carga de la clase correspondiente. Debajo del método se llama el método SetPointers(), y el método InitControls(). Después se ejecutan unas cuantas acciones adicionales: para los controles SpinBox se activa la entrada desde el teclado (llamada del método ReadOnly()), se establecen los valores mínimos (llamada del método SetMinValue()), y a la variable m_Height se le asigna un nuevo valor de altura del formulario.

En los métodos OnShowEvent() y OnHideEvent() los formularios se llaman con los métodos correspondientes m_controls:

void OnShowEvent(int aLeft, int aTop){
   m_cmb_main.Show(aLeft+10,aTop+10);
   m_controls.Show(aLeft+10,aTop+10+20);
}
void OnHideEvent(){
   m_cmb_main.Hide();            
   m_controls.Hide();          
}  

Queda "reavivar" los eventos del objeto m_controls. Añadimos al indicador en la función OnChartEvent() la llamada del método Event():

int ce=frm.m_controls.Event(id,lparam,dparam,sparam);

Añadimos a OnInit() del indicador la llamada del método SetType() del formulario (después de la llamada del método SetSelectedIndex()):

frm.SetType(_Type);

Después de la carga del oscilador hay que lograr que en el formulario se representen los valores de sus parámetros, para ello, añadimos el método SetValues() a la clase del formulario:

void SetValues(int period1,
               int period2,
               int period3,
               long method,
               long price,
               long volume,  
               long sto_price  
){
  
   m_value1.SetValue(period1);
   m_value2.SetValue(period2);      
   m_value3.SetValue(period3);
  
   for(int i=0;i<ArraySize(e_price);i++){
      if(price==e_price[i]){
         m_price.SetSelectedIndex(i);
         break;
      }
   }
  
   for(int i=0;i<ArraySize(e_method);i++){
      if(method==e_method[i]){
         m_method.SetSelectedIndex(i);
         break;
      }
   }            

   for(int i=0;i<ArraySize(e_volume);i++){
      if(volume==e_volume[i]){
         m_volume.SetSelectedIndex(i);
         break;
      }
   }
  
   for(int i=0;i<ArraySize(e_sto_price);i++){
      if(sto_price==e_sto_price[i]){
         m_sto_price.SetSelectedIndex(i);
         break;
      }
   }

}      

En el método SetValues(), para los elementos del tipo SpinBox los valores se establecen tal como son, y para las enumeraciones se realiza la búsqueda del índice en las matrices con los valores de las enumeraciones. Llamamos el método SetValues() tras la llamada del método SetType():

frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice);

En esta etapa, la interfaz gráfica se puede considerar totalmente lista (fig. 5), pero por el momento, el indicador no sabe reaccionar a ella.


Fig. 5. Aspecto de la ventana con los controles para el indicador ATR 

Finalizando la creación del oscilador universal

Las clases del oscilador ya están listas, las clases de la interfaz gráfica también, solo queda unirlas. 

En esta etapa, la función OnChatEvent() deberá tener el aspecto siguiente:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{

   int e=frm.Event(id,lparam,dparam,sparam);  
   if(e==1){
      int win=ChartWindowFind(0,ShortName);
      ChartIndicatorDelete(0,win,ShortName);
      ChartRedraw();
   }
  
   int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
  
   int ce=frm.m_controls.Event(id,lparam,dparam,sparam);

}

Es necesario procesar el evento de cambio del indicador (variable me) y el evento de cambio de parámetros (variable ce).

Cambio de indicador:

if(me==1){

   // reinicialización del indicador
  
   _Type=osctype[frm.m_cmb_main.SelectedIndex()]; // nuevo tipo
  
   delete(osc); // eliminación del objeto antiguo
   LoadOscillator(); // carga del objeto nuevo
  
   if(!osc.CheckHandle()){
      Alert("Error al cargar el indicador "+osc.Name());
   }
  
   SetStyles(); // configuración de estilos
  
   // configuración del nombre corto
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

   // actualización del formulario

   frm.SetType(osctype[frm.m_cmb_main.SelectedIndex()]); // configuración del tipo
   frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice); // configuración de los valores
   frm.Refresh(); // actualización del formulario
  
   // recálculo del indicador
   EventSetMillisecondTimer(100);

}  

Veamos este código con mayor detalle. Al elegir el oscilador en la lista principal, el método Event() retorna 1, en este caso, a la variable _Type se le asigna un nuevo valor de tipo, se elimina el objeto antiguo, se carga el nuevo, se configuran los estilos y el nombre corto. Tras la carga del indicador, se actualiza el aspecto del formulario: se configuran el tipo, los parámetros y se llama el método Refresh(), para que el aspecto del formulario cambie de acuerdo con los nuevos parámetros. Al final se inicia un temporizador (sobre él hablaremos un poco más tarde).

Veamos el segmento para el cambio de parámetros: 

if(ce==1){
  
   if((int)frm.m_value1.Value()>0){
      _Period1=(int)frm.m_value1.Value();
   }
   if((int)frm.m_value2.Value()>0){
      _Period2=(int)frm.m_value2.Value();
   }
   if((int)frm.m_value3.Value()>0){
      _Period3=(int)frm.m_value3.Value();
   }      
   if(frm.m_method.SelectedIndex()!=-1){
      _MaMethod=e_method[frm.m_method.SelectedIndex()];
   }
   if(frm.m_price.SelectedIndex()!=-1){
      _Price=e_price[frm.m_price.SelectedIndex()];
   }
   if(frm.m_volume.SelectedIndex()!=-1){
      _Volume=e_volume[frm.m_volume.SelectedIndex()];
   }
   if(frm.m_sto_price.SelectedIndex()!=-1){
      _StPrice=e_sto_price[frm.m_sto_price.SelectedIndex()];
   }
  
   delete(osc);
   LoadOscillator();
   if(!osc.CheckHandle()){
      Alert("Error al cargar el indicador "+osc.Name());
   }  
  
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
  
   EventSetMillisecondTimer(100);
    
}

Al cambiar los parámetros, el método Event() de la clase de control retorna 1. En este caso, a todas las variables se les asignan nuevos valores, pero son sometidos a una comprobación preliminar. Los valores de los controles de SpinBox deberán ser superiores a cero, y  los de las listas desplegables deberán ser distintos a -1. Después, todo será igual que con el cambio de indicador.

Ahora hablemos del temporizador. Para realizar el cálculo del indicador se necesitará cierto tiempo. Por eso se inicia un temporizador, y en su  función se ejecutará la comprobación periódica de la preparación del indicador con la ayuda de la función BarsCalculated(). Si el valor retornado es mayor a cero, significa que el indicador se ha calculado por completo, además, se llama el método Calculate() del objeto osc:

void OnTimer(){
   if(BarsCalculated(osc.Handle())>0){
      if(osc.Calculate(Bars(Symbol(),Period()),0,Label1Buffer,Label2Buffer)!=0){
         ChartRedraw();    
         EventKillTimer();
      }
   }
}

Como primer parámetro se transmite al método Calculate() el número de barras, y como segundo - 0, para que el indicador se recalcule por completo. Después de ello, el gráfico se actualiza (ChartRedaraw()) y se desactiva el temporizador. 

Ahora el indicador debe reaccionar a la interfaz gráfica. Eso significa que está casi listo.

Añadiremos un pequeño detalle final: posibilitaremos el trabajo con el indicador sin interfaz gráfica. Con este fin, añadiremos la variable externa UseGUI:

input bool                 UseGUI      =  true;

Una parte del código de la función OnInit(), relacionada con la creación del formulario, solo la ejecutaremos si la variable UseGUI está desactivada:

if(UseGUI){
   frm=new CUniOscForm();
   frm.Init();
   int ind=0;
  
   for(int i=0;i<ArraySize(osctype);i++){
      frm.m_cmb_main.AddItem(EnumToString(osctype[i]));
      if(osctype[i]==_Type){
         ind=i;
      }
   }
  
   frm.m_cmb_main.SetSelectedIndex(ind);      
   frm.SetType(_Type);
   frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice);
  
   frm.SetSubWindow(0);
   frm.SetPos(10,30);
   frm.Show();
}

Y otro pequeño detalle final. La biblioteca incGUI da soporte al cambio del esquema de color de los controles. Vamos a usar esta posibilidad.

Justo después de los parámetros externos, añadimos el siguiente código:

enum eColorScheme{
   DefaultScheme=0,
   YellowBrownScheme=1,
   BlueScheme=2,
   GreenScheme=3,
   YellowBlackScheme=4,
   LimeBlackScheme=5,
   AquaBlackScheme=6
};

input eColorScheme ColorScheme=DefaultScheme;

Este código añadirá a la ventana de propiedades del indicador una lista desplegable para elegir el esquema de color. Al principio de la función OnInit(), añadiremos una línea:

ClrScheme.SetScheme(ColorScheme);

Ahora el indicador iUniOscGUI está completamente finalizado, y es posible incluso cambiar el color de la interfaz gráfica (fig. 6).

 
Fig. 6. Varios esquemas de color de la interfaz gráfica del indicador iUniOscGUI 

Conclusión

El indicador obtenido ha resultado muy útil, no solo en la comparación de diferentes indicadores, sino también para observar la influencia de los parámetros externos en el aspecto de los indicadores. El indicador cambia de aspecto al instante, en caso de modificar los parámetros. Este efecto no se puede lograr usando la ventana de propiedades, y por consiguiente, no es posible conseguir la misma impresión sobre la influencia de los parámetros en la apariencia del indicador.

Archivos adjuntos

  • UniOscDefines.mqh — archivo con la lista de los tipos de osciladores.
  • CUniOsc.mqh — clases del oscilador universal.
  • iUniOsc.mq5 — oscilador universal sin interfaz gráfica.
  • UniOscGUI.mqh — clases para crear la interfaz gráfica del oscilador. 
  • iUniOscGUI.mq5 — oscilador universal con interfaz gráfica. 
  • IncGUI_v4.mqh — biblioteca para trabajar con objetos gráficos y crear una interfaz gráfica. Antes había cierta confusión con la versión de la biblioteca. Existían dos archivos de la versión 3 con el mismo nombre: en el artículo y en el CodeBase (con una clase actualizada para crear los recuadros CTable). En el archivo IncGUI_v4 no solo se han realizado las correcciones pertinentes, sino que también se ha sustituido la clase para crear recuadros por otra más actual (disponible en CodeBase).