English Русский 中文 Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Múltiples indicadores en un gráfico (Parte 02): primeros experimentos

Múltiples indicadores en un gráfico (Parte 02): primeros experimentos

MetaTrader 5Ejemplos | 5 abril 2022, 11:03
774 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, múltiples indicadores en un gráfico, presenté los conceptos y bases para utilizar múltiples indicadores en un gráfico sin contaminar la pantalla con información variada, pero como ese artículo tenía como único objetivo presentar el sistema, mostrar cómo crear la base de datos a emplear y cómo aprovecharlo, decidí dejar la presentación del código fuente del sistema en este artículo, aquí comenzamos la implementación del código, y en futuros artículos ampliaremos la funcionalidad del sistema propuesto, creando uno aún más completo y versátil, ya que se trata de un sistema que promete y tiene un gran margen de mejora.


Planificación

Para facilitar su comprensión, pero sobre todo, para permitir la expansión del sistema, éste ya está dividido en 2 archivos separados y el código principal se hizo utilizando el modelado OOP (programación orientada a objetos), todo esto asegurará que el sistema pueda crecer de manera sostenible, segura y estable.

En esta primera fase utilizaremos un indicador, por lo que crearemos uno para esta ventana:

Pero, ¡¿por qué utilizar un indicador y no otro tipo de archivo?!? La razón es que con el indicador no necesitamos añadir lógica interna para crear una subventana, podemos decirle al indicador que lo haga por nosotros, lo que nos ahorra tiempo y agiliza la creación del sistema. Así, la cabecera de nuestro indicador se vería como el siguiente código:

#property indicator_plots 0
#property indicator_separate_window

Sólo con estas dos líneas podemos crear una subventana en un gráfico de activos, para los que no sepan cómo funcionan, vean el gráfico a continuación:

Código Descripción
indicador_plots 0 Esta línea le dirá al compilador que no trazaremos ningún tipo de datos y evitará que el compilador muestre mensajes de advertencia.
indicador_separar_janela Esta línea indica al compilador que añada la lógica necesaria para crear una subventana para nosotros.

Bueno, la cosa en sí debe mantenerse sencilla, por lo que para los que no están familiarizados con la programación algunas cosas que se encuentran en un código fuente pueden parecer extrañas, pero deben seguir un protocolo ampliamente difundido y aceptado por toda la comunidad de programadores, y como MT5 utiliza MQL5 que es un lenguaje casi idéntico a C++, con pocas diferencias, podemos utilizar la misma forma de programar que utilizamos en C++, y esto facilita mucho las cosas, así que aprovechando este hecho, utilizamos una directiva del lenguaje C y tendremos la siguiente línea:

 #include <Auxiliar\C_TemplateChart.mqh>

Esta directiva indica al compilador que añada (incluya) un archivo de cabecera, que está presente en la ubicación definida. ¡¿Pero un momento, ¿¡no debería ser la ruta completa Includes \ Auxiliary \ C_TemplateChart.mqh !? Sí, esa es la ruta completa, pero, como ya está estructurada, MQL5 sabe que cualquier archivo de cabecera debe estar en la carpeta includes, así que podemos suprimir este punto y limitarnos a indicar el resto de la ruta, si la ruta está entre paréntesis angulares, quiere decir que es una ruta absoluta, si estuviera entre comillas la ruta sería relativa, es decir, <Auxiliary \ C_TemplateChart.mqh> es diferente de "Auxiliary \ C_TemplateChart.mqh", un simple detalle y lo cambia todo.

Continuando con el código, tendremos las siguientes líneas:

input string user01 = "" ;       //Indicadores a usar
input string user02 = "" ;       //Ativos a acompanhar

Estas permiten la entrada de strings, si ya tenemos algo en mente para usar como comando cada vez que abrimos el indicador, ya podemos dejar este comando como predeterminado, por ejemplo, supongamos que siempre queremos usar el RSI con un grosor de línea de 3 y el MACD con uno de 2, podemos dejar esto predefinido en nuestro sistema, así el código de arriba quedaría así:

input string user01 = "RSI:3;MACD:2" ;   //Indicadores a usar
input string user02 = "" ;       //Ativos a acompanhar

Esto no nos impide cambiar el comando más tarde, pero nos facilitará la apertura del indicador, ya que estará preconfigurado para utilizar este comando. La siguiente línea creará un alias para que podamos acceder a la clase objeto que contiene todo el código "pesado", lo que nos permitirá acceder a sus funciones públicas.

C_TemplateChart SubWin;

Prácticamente nuestro código dentro del archivo del indicador personalizado está casi hecho, sólo hay que añadir 3 líneas más para que todo esté listo y sea funcional, ya que está claro que nuestra clase de objeto no contiene errores, pero en este artículo también lo veremos desde dentro. Así que para finalizar el archivo del indicador tendremos que añadir las líneas en verde, como se indica a continuación:

 //+------------------------------------------------------------------+
int OnInit ()
{
         SubWin.AddThese(C_TemplateChart::INDICATOR, user01);
         SubWin.AddThese(C_TemplateChart::SYMBOL, user02);

         return INIT_SUCCEEDED ;
}
//+------------------------------------------------------------------+

//...... demais linhas sem interesse para nos ......

//+------------------------------------------------------------------+
void OnChartEvent ( const int id,
                   const long &lparam,
                   const double &dparam,
                   const string &sparam)
{
         if (id == CHARTEVENT_CHART_CHANGE ) SubWin.Resize();
}
//+------------------------------------------------------------------+

Y eso es exactamente lo que tendrá el archivo del indicador personalizado. Ahora echemos un buen vistazo a la caja negra del archivo que contiene nuestra clase de objeto. A partir de ahora, la atención debe ser doble, pero para facilitarlo, vamos a empezar por ver qué funciones están presentes en nuestra clase de objeto y para qué sirve cada una de ellas.

Ocupación Funcionalidad
SetBase Creará el objeto necesario para mostrar los datos del indicador
decodificar Decodifica el comando que se le pasa
AddTemplate Ajusta las cosas en función del tipo de datos presentados
C_Template Constructor de clase por defecto
~ C_Template destructor de clase
redimensionar Redimensiona la subventana
AddThese Función responsable del acceso y la construcción de objetos internos

Y eso es todo, ya habrán notado que en nuestro indicador personalizado utilizamos las funciones RESIZE y ADDTHESE, y son las únicas funciones públicas por el momento, lo que significa que tenemos poco de qué preocuparnos, ya que todo lo demás está oculto dentro de nuestro objeto, asegurando que no serán modificadas innecesariamente, lo que garantiza un alto grado de fiabilidad a nuestro código final. Pero profundicemos en el código que comienza con la siguiente definición:

 #define def_MaxTemplates         6

Esta línea es muy importante para nuestra clase de objeto, ella determina el número máximo de punteros que podemos crear, si queremos agregar más o menos, sólo hay que cambiar ese número en este punto, así las cosas se vuelven mucho más fáciles ya que ganamos estar usando una asignación de memoria dinámica y no queremos demasiados punteros en nuestra pantalla. Este es quizás el único punto que realmente tendrá que cambiar si lo deseamos, pero creo que 6 es un número adecuado para la mayoría de la gente y de los monitores utilizados.

La siguiente línea es una enumeración que facilita el control de los datos en algunos puntos del programa:

 enum eTypeChart {INDICATOR, SYMBOL};

El hecho de que esta línea esté dentro de nuestra clase hará que pueda tener el mismo nombre en otra clase diferente, pero los datos indicados por ella sólo pertenecen a esta clase de objetos, por lo que para acceder correctamente a esta enumeración debemos utilizar la forma mostrada en la función OnInit de nuestro archivo de indicadores personalizados, si se omite el nombre de la clase se considerará un error de sintaxis y el código no compilará. La siguiente línea es una palabra reservada.

 private :

Esta línea indica que todo a partir de este punto será privado para esta clase de objetos, no siendo visible fuera de la clase, es decir, si se intenta acceder a cualquier cosa a partir de este punto no será posible si no se está dentro de la clase, esto asegura que el código sea aún más seguro, ya que no es posible acceder a nada que se considere privado para la clase, las siguientes líneas declararán algunas variables internas y privadas, hasta llegar a la primera función real de nuestra clase.

 void SetBase( const string szSymbol, int scale)
{
#define macro_SetInteger(A, B) ObjectSetInteger (m_Id, m_szObjName[m_Counter], A, B)

...

         ObjectCreate (m_Id, m_szObjName[m_Counter], OBJ_CHART , m_IdSubWin, 0 , 0 );
         ObjectSetString (m_Id, m_szObjName[m_Counter], OBJPROP_SYMBOL , szSymbol);
        macro_SetInteger( OBJPROP_CHART_SCALE , scale);
...
        macro_SetInteger( OBJPROP_PERIOD , _Period );
        m_handle = ObjectGetInteger (m_Id, m_szObjName[m_Counter], OBJPROP_CHART_ID );
        m_Counter++;
#undef macro_SetInteger
};

Entendamos este fragmento de código SetBase, comenzamos declarando una macro, ésta indica al compilador cómo debe interpretarse un código simplificado con el nombre de la macro, es decir, si tenemos que repetir algo varias veces, podemos utilizar esta característica del lenguaje C para producir algo más simple, y si por casualidad tenemos que cambiar algo, lo haremos sólo en la macro, esto agiliza mucho y reduce la posibilidad de errores en códigos donde sólo se modificará uno u otro argumento.

Hecho esto, creamos un objeto de tipo CHART, puede que estés pensando: ¡¿Pero qué?! ¿Vamos a utilizar algo que no se puede modificar para cambiar las cosas? Sí, así es. El siguiente paso es declarar el activo a utilizar, y aquí está el primer punto, si en el momento de guardar la configuración del gráfico no hay ningún activo presente, el activo al que está vinculado este objeto será el activo a utilizar, si hay activo presente cuando se crea la configuración, se utilizará este activo, ahora un detalle, se puede indicar un activo diferente, y con ello utilizar una configuración genérica, pero esto lo explicaré con más detalle en el próximo artículo, porque vamos a implementar algunas mejoras en este código para poder hacer algunas cosas que no son posibles aquí. A continuación tenemos el nivel de densificación de la información, esto se indica en la propiedad OBJPROP_CHART_SCALE, por lo que utilizamos valores entre 0 y 5, aunque podemos utilizar valores fuera de ese rango, es bueno mantener un estándar.

Lo siguiente que hay que mirar es la propiedad OBJPROP_PERIOD, y ten en cuenta que estamos usando el marco temporal del gráfico actual, lo que significa que si lo modificamos también cambiará, en el futuro haremos algunas modificaciones que nos permitan bloquear esto, pero si quisiéramos experimentar, podríamos usar un marco temporal definido por MT5, como por ejemplo: PERIOD_M10 que indicaría mostrar los datos en un periodo fijo de 10 minutos, pero esto lo mejoraremos en el futuro, por ahora no nos preocupemos por ello. Después de eso, incrementamos el número de subindicadores en una unidad y destruimos la macro, lo que significa que ya no tendrá ninguna representación después de eso y deberá ser redefinida para ser utilizada en otro lugar. Pero esperen un minuto, ¡¿no me olvidé de nada?! Sí, la línea que es quizás lo más importante de esta rutina.

m_handle = ObjectGetInteger (m_Id, m_szObjName[m_Counter], OBJPROP_CHART_ID );

Esta línea capturará algo que podría entenderse como un puntero, en realidad no es un puntero en sí, sino que nos permite hacer algunas manipulaciones extra sobre el objeto OBJ_CHART que creamos, necesitamos este valor para poder aplicar algunos ajustes dentro de este objeto, y estos ajustes están en el archivo de configuración que creamos anteriormente. Siguiendo con el código llegamos a la siguiente rutina que se puede ver completa a continuación:

 void AddTemplate( const eTypeChart type, const string szTemplate, int scale)
{
	if (m_Counter >= def_MaxTemplates) return ;
	if (type == SYMBOL) SymbolSelect (szTemplate, true );
	SetBase((type == INDICATOR ? _Symbol : szTemplate), scale);
	ChartApplyTemplate (m_handle, szTemplate + ".tpl" );
	ChartRedraw (m_handle);
}

Veremos que primero probamos la posibilidad o no de añadir un nuevo indicador, si es posible comprobaremos si es un SÍMBOLO, y si es así el símbolo debe estar presente en la ventana Market Watch, esto lo garantiza la rutina, a partir de esto creamos el objeto que recibirá la información. Hecho esto, la plantilla se aplica a OBJ_CHART, es exactamente en este punto que la magia sucede, entonces le pedimos al objeto que se vuelva a presentar, pero ahora contendrá los datos de acuerdo a las definiciones contenidas en el archivo de configuración que se utilizó para definir OBJ_CHART, simple, hermoso y puro, se vuelve divino por su simplicidad.

Con sólo esas 2 rutinas sería posible hacer cosas, pero necesitamos al menos una rutina más, que se muestra completa a continuación:

 void Resize( void )
{
         int x0 = 0 , x1 = ( int )( ChartGetInteger (m_Id, CHART_WIDTH_IN_PIXELS , m_IdSubWin) / (m_Counter > 0 ? m_Counter : 1 ));
         for ( char c0 = 0 ; c0 < m_Counter; c0++, x0 += x1)
        {
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_XDISTANCE , x0);
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_XSIZE , x1);
                 ObjectSetInteger (m_Id, m_szObjName[c0], OBJPROP_YSIZE , ChartGetInteger (m_Id, CHART_HEIGHT_IN_PIXELS , m_IdSubWin));
        }
         ChartRedraw ();
}

Lo que hace la rutina mencionada es poner todo en su lugar, manteniendo los datos siempre dentro del área de la subventana, no hay mucho que hablar de eso y con eso terminamos todo el código necesario para que todo funcione perfectamente bien. Pero quizá piensen: ¡¿y las otras rutinas?! Tranquilos, las otras rutinas no son necesarias en absoluto, sólo apoyan la interpretación de las líneas de comando, en fin, vamos, pero antes veamos algo que también es importante para quienes quieran modificar este código en el futuro, que es la siguiente palabra reservada que aparece en el código de nuestra clase objeto:

 public   :

Esta palabra asegurará que a partir de este punto todos los datos y funciones puedan ser accedidos y vistos por otras partes del código, aunque no formen parte de la clase de objeto, así que aquí declaramos lo que realmente puede ser manipulado o accedido por otros objetos. En realidad, la conducta de un buen código orientado a objetos es no permitir nunca el acceso directo a los datos de un objeto, en un código bien diseñado sólo tendremos acceso a funciones o rutinas, y la razón es sencilla, la seguridad, cuando permitimos que código externo modifique los datos dentro de una clase, corremos el riesgo de que estos datos no sean coherentes con lo que el objeto espera, y esto provoca muchos problemas y dolores de cabeza al intentar resolver inconsistencias o defectos cuando todo parece correcto. Así que si puedo dar el consejo de alguien que lleva años programando en C++, NUNCA permitas que objetos externos modifiquen o accedan directamente a los datos de la clase que has creado, proporciona funciones o procedimientos para que se pueda acceder a los datos, pero nunca dejes que se acceda a los datos directamente y asegúrate de que las funciones y procedimientos mantengan los datos tal y como espera la clase que has creado. Dado el mensaje, pasemos a las dos últimas rutinas de nuestra lección, una es pública (AddThese) y la otra privada (Decode) se pueden ver completas a continuación:

void Decode( string &szArg, int &iScale)
{
#define def_ScaleDefault 4
         StringToUpper (szArg);
        iScale = def_ScaleDefault;
         for ( int c0 = 0 , c1 = 0 , max = StringLen (szArg); c0 < max; c0++) switch (szArg[c0])
        {
                 case ':' :
                         for (; (c0 < max) && ((szArg[c0] < '0' ) || (szArg[c0] > '9' )); c0++);
                        iScale = ( int )(szArg[c0] - '0' );
                        iScale = ((iScale > 5 ) || (iScale < 0 ) ? def_ScaleDefault : iScale);
                        szArg = StringSubstr (szArg, 0 , c1 + 1 );
                         return ;
                 case ' ' :
                         break ;
                 default :
                        c1 = c0;
                         break ;
        }
#undef def_ScaleDefault
}
//+------------------------------------------------------------------+
// ... Códigos que não interessam a esta parte ...
//+------------------------------------------------------------------+
void AddThese( const eTypeChart type, string szArg)
{
         string szLoc;
         int i0;
         StringToUpper (szArg);
         StringAdd (szArg, ";" );
         for ( int c0 = 0 , c1 = 0 , c2 = 0 , max = StringLen (szArg); c0 < max; c0++) switch (szArg[c0])
        {
                 case ';' :
                         if (c1 != c2)
                        {
                                szLoc = StringSubstr (szArg, c1, c2 - c1 + 1 );
                                Decode(szLoc, i0);
                                AddTemplate(type, szLoc, i0);
                        }
                        c1 = c2 = (c0 + 1 );
                         break ;
                 case ' ' :
                        c1 = (c1 >= c2 ? c0 + 1 : c1);
                         break ;
                 default :
                        c2 = c0;
                         break ;
        }
}

Lo que hacen estas dos rutinas es exactamente lo que expliqué arriba, aseguran la integridad de los datos dentro de la clase objeto, evitando que datos inconsistentes pasen a formar parte de los datos internos de la clase, recibirán una línea de comando y decodificarán esa línea siguiendo una sintaxis predefinida, sin embargo, no hablan de que haya un error en el comando recibido, ese no es su propósito, su propósito es solo asegurar que no entren datos inconsistentes al objeto y causen efectos secundarios que pueden ser difíciles de localizar y corregir.

El resultado final será el que se ve a continuación:



Conclusión

Bueno, espero que este código os motive, quién sabe, yo me interesé por la programación, porque la cosa es bonita y muy emocionante, aunque a veces nos da mucho dolor de cabeza para poder producir algunos tipos de resultados específicos, pero la mayoría de las veces merece la pena. En el próximo artículo les mostraré cómo hacer las cosas aún más interesantes. En este artículo se adjunta el código completo del indicador, que ya se puede utilizar como se describe en este artículo y en el anterior.


Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/10230

Gráficos en la biblioteca DoEasy (Parte 96): Trabajando con los eventos del ratón y los gráficos en los objetos de formulario Gráficos en la biblioteca DoEasy (Parte 96): Trabajando con los eventos del ratón y los gráficos en los objetos de formulario
En este artículo, comenzaremos a desarrollar las funciones necesarias para trabajar con los eventos del ratón en los objetos de formulario y añadiremos nuevas propiedades y la monitorización de las mismas al objeto de símbolo. Además, hoy finalizaremos la clase de objeto símbolo, ya que, desde el momento en que la escribimos, los símbolos gráficos han adquirido nuevas propiedades que debemos considerar, y cuyos cambios tenemos que monitorear.
Múltiples indicadores en un gráfico (Parte 01): Entendiendo los conceptos Múltiples indicadores en un gráfico (Parte 01): Entendiendo los conceptos
Entienda cómo se puede agregar varios indicadores al mismo tiempo sin ocupar un área diferente de su gráfico. A mucha gente le gusta y se siente más segura operando cuando observa varios indicadores al mismo tiempo, por ejemplo, RSI, ESTOCÁSTICO, MACD, ADX, entre otros, y en algunos casos incluso diferentes activos que componen un índice determinado.
Cómo hacer el gráfico más interesante: Adicionando un fondo de pantalla Cómo hacer el gráfico más interesante: Adicionando un fondo de pantalla
Muchos terminales de trabajo contienen alguna imagen representativa que muestra algo sobre el usuario, estas imágenes hacen que el escritorio sea más bonito y alegre. Descubra cómo hacer el gráfico más interesante poniendo un fondo de pantalla.
Gráficos en la biblioteca DoEasy (Parte 95): Elementos de control de los objetos gráficos compuestos Gráficos en la biblioteca DoEasy (Parte 95): Elementos de control de los objetos gráficos compuestos
En este artículo, analizaremos el instrumental usado para gestionar los objetos gráficos compuestos, a saber, los elementos de gestión del objeto gráfico estándar extendido. Hoy nos desviaremos un poco del tema del desplazamiento de objetos gráficos compuestos y crearemos un manejador de eventos de cambio del gráfico en el que se encuentra el objeto gráfico compuesto; también trabajaremos con los objetos de gestión de objetos gráficos compuestos.