English Русский 中文 Deutsch 日本語 Português
preview
Tablero de cotizaciones: Versión básica

Tablero de cotizaciones: Versión básica

MetaTrader 5Trading | 9 enero 2023, 16:25
511 0
Daniel Jose
Daniel Jose

Introducción

A mucha gente le parecen bastante geniales esos tableros que vienen en algunas plataformas y que muestran las cotizaciones de algunos activos. Si no sabe de qué se trata, vea la siguiente animación:

Este tipo de cosas puede ser muy útil en algunos casos, así que aquí le mostraré cómo puede lograr implementar este tipo de elemento directamente en la plataforma MetaTrader 5 mediante programación 100% MQL5. Sé que muchos pueden considerar que lo que se va a poner en práctica es bastante básico, pero les garantizo que, si entienden los conceptos que aquí se manejan, serán capaces de producir algo mucho más elaborado.

Pero independientemente de esto, tengo la intención de hacer otros artículos implementando este tablero aún más, para que pueda convertirse en una herramienta extremadamente útil, para usted que desea operar siguiendo otra información en tiempo real.

Debo confesar que la idea de este artículo fue de un miembro de la comunidad, y este elemento es bastante interesante de ser realizado y desarrollado, pero, al margen de esto, puede ser un recurso muy útil para muchos, así que decidí mostrar cómo construir el código de este tablero.


Planificación

La realización de este tablero no es algo demasiado complicado, de hecho es incluso muy sencillo de realizar si lo comparamos con otros tipos de códigos, sin embargo, antes de empezar realmente, tenemos que planificar algunas cosas que influirán totalmente en la dirección que vamos a tomar para poder implementar la función. Como la idea es simplemente tener un tablero que muestre el activo con su cotización, no tendremos grandes problemas al principio, pero recuerde que le mostraré cómo hacer un sistema muy básico, mas que le servirá como punto de partida para algo más complejo, sofisticado y aún más elaborado.

Lo primero que hay que pensar es: ¡¿cómo se va a añadir la lista de activos que se van a colocar en el tablero?! ¡¿Por casualidad será una lista fija en que todos los activos a mostrar estarán previamente seleccionados?! ¡¿O los inseriremos uno a uno en una entrada cuando apliquemos el sistema?

Pues bien, esta es quizás la parte más complicada de todas, ya que en algunos momentos puede que usted quiera tener activos de mayor interés y en otros momentos simplemente tener los activos que tiene en cartera. De este modo, tal vez lo mejor sea utilizar un archivo que contenga todos los activos que desee utilizar en el tablero. Así que "empaquemos y vámonos": Utilizaremos un ARCHIVO, que contendrá los activos a mostrar.

Ahora surge otro problema: ¿cómo presentaremos el recurso? Parece una tontería, pero esto es algo muy importante que hay que pensar, se puede usar un EA, un script, un indicador o un servicio, si bien el uso del servicio no parece tan prometedor, aunque a mí personalmente me gusta usar el servicio más que el tablero, como servicio éste tiene sus problemas, y estos detalles y complicaciones hacen que crear el tablero sea laborioso, algo demasiado complicado, entonces estamos limitados a otras 2 opciones, porque solo tenemos 2 formas prácticas de hacer el tablero: Y son ponerlo en un EA o en un Indicador. Pero, ¿por qué no utilizarlo en un script?

La razón es simple: Si el usuario va a hacer cualquier cambio en el marco temporal, él terminará por finalizar la secuencia de comandos, por lo que a cada cambio en el gráfico el operador se verá obligado a ejecutar la secuencia de comandos de nuevo. Recordando que estoy proponiendo una solución 100% en MQL5, incluso se puede sortear este inconveniente mediante programación externa, pero este no es mi objetivo aquí.

Aun teniendo como punto de partida poder usar un EA o un Indicador para desarrollar el tablero, no me gusta mucho la idea de usar un EA, porque me gusta usar el EA enfocado solo y únicamente para lo que ha sido desarrollado, que es encargarse del sistema de órdenes y ayudarnos en el envío y control de las mismas, lo que nos deja como única solución el uso de un indicador.

Aún nos quedan otros temas por desarrollar y planificar, pero partiendo de esta planificación previa ya podemos empezar. Pues manos a la obra.


Principios básicos

Empecemos por crear el archivo del indicador, a continuación, podemos ver el aspecto que tendrá en este primer momento:

#property copyright "Daniel Jose"
#property description "Gadget para cotações em letreiro."
#property description "Este cria uma faixa que mostra o preço dos ativos."
#property description "Para detalhes de como usá-lo visite:\n"
#property description "https://www.mql5.com/pt/articles/10941"
#property link "https://www.mql5.com/pt/articles/10941"
#property indicator_separate_window
#property indicator_plots 0
//+------------------------------------------------------------------+
int OnInit()
{
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnTimer()
{
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
//+------------------------------------------------------------------+

A pesar de que el código del indicador está totalmente limpio, lo que significa que no hará nada especial, ya tenemos una pequeña noción de lo que nos espera, utilizaremos una ventana independiente, necesitaremos manejar un mayor número de eventos de los que normalmente tenemos en un indicador clásico, como el OnTime que normalmente no aparece en ningún indicador. Pero tampoco olvide lo siguiente: No graficaremos absolutamente nada, todo lo que el indicador creará y mostrará lo hará él mismo.

Normalmente tendríamos un código ya empezado, que mostraría las cosas ya en él, pero en este artículo en particular voy a mostrar las cosas con un nivel de detalle un poco diferente, para que usted pueda utilizarlo como fuente de investigación y estudio.

Así que usted ya debe haber pensado que necesitaremos muchas más cosas para que todo funcione de verdad. En cierto modo, eso es cierto, pero no serán tantas cosas. Lo primero que debe empezar a pensar es como gestionar el gráfico, y para ello tenemos una clase, que, aunque muchos la han visto en otros artículos publicados por mí, aquí será un poco diferente, ya que necesitaremos usar muchas menos cosas, así que les presento, para los que no la conozcan, la clase C_Terminal. Ella se encuentra en el archivo de cabecera C_Terminal.mqh, su código es bastante sencillo, así que veámoslo completo a continuación:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
class C_Terminal
{
//+------------------------------------------------------------------+
        private :
                struct st00
                {
                        long    ID;
                        int     Width,
                                Height,
                                SubWin;
                }m_Infos;
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+          
                void Init(const int WhatSub)
                        {
                                ChartSetInteger(m_Infos.ID = ChartID(), CHART_EVENT_OBJECT_DELETE, m_Infos.SubWin = WhatSub, true);
                                Resize();
                        }
//+------------------------------------------------------------------+
inline long Get_ID(void)   const { return m_Infos.ID; }
inline int GetSubWin(void) const { return m_Infos.SubWin; }
inline int GetWidth(void)  const { return m_Infos.Width; }
inline int GetHeight(void) const { return m_Infos.Height; }
//+------------------------------------------------------------------+
                void Resize(void)
                        {
                                m_Infos.Width = (int) ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS);
                                m_Infos.Height = (int) ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS);
                        }
//+------------------------------------------------------------------+
inline string ViewDouble(double Value)
                        {
                                Value = NormalizeDouble(Value, 8);
                                return DoubleToString(Value, ((Value - MathFloor(Value)) * 100) > 0 ? 2 : 0);
                        }
//+------------------------------------------------------------------+
                void Close(void)
                        {
                                ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, m_Infos.SubWin, false);
                        }
//+------------------------------------------------------------------+          
};
//+------------------------------------------------------------------+

Y sí, este es todo el código que necesitamos, aunque esta clase es mucho más grande, aquí tenemos sólo las partes realmente necesarias, ya que no quiero contaminar el artículo con cosas innecesarias.

Así que para aquellos que no sepan lo que hace esta clase, vamos a hacer un rápido repaso de sus partes. Dado que queremos que MetaTrader 5 nos informe de cualquier intento de eliminar un objeto, tenemos que declararlo aquí, y luego vamos a capturar las dimensiones de la ventana que estamos utilizando, así que aquí estamos creando realmente un nivel extra de abstracción para ayudarnos con la programación.

Pero esto no es realmente necesario, si usted quiere hacer las cosas de otra manera, pero debido a este nivel de abstracción en el que ocultamos lo que en realidad no se está ensamblando y ya que las cosas pueden no ser necesariamente lo que parecen, tenemos algunas llamadas para acceder a los datos de la clase. Una vez finalizada la clase, tenemos que evitar que se generen eventos, cuando el indicador empiece a eliminar objetos, y para ello, utilizamos esta función. Ahora como ya tenemos un punto en el código en el que necesitamos dar un formato visual, lo hacemos aquí, de esta manera centralizamos todo lo relacionado con la terminal en esta clase.

Fíjate que aquí las cosas son bastante simples y bastante sencillas, pero vamos a empezar a complicarlas, y vamos a hacerlo a partir de ahora.


Implementación de objetos básicos

Por extraño que parezca, vamos a utilizar sólo y únicamente dos objetos en este modelo más básico del tablero. Además, como estoy utilizando un modelo, que ustedes verán mucho en la serie de artículos Desarrollo de un EA comercial desde cero, de mi autoría, que se publicó aquí en la comunidad MQL5, voy a tomar prestado el sistema de allí. Y a pesar de que, en la serie sobre el EA, la descripción y el uso son considerablemente más intensos que aquí, voy a dar una explicación rápida sobre cómo funciona el sistema, para que usted no se pierda demasiado si no tiene todos los conocimientos sobre cómo MetaTrader 5 trabaja con los objetos.

Así que vamos a empezar con la clase base de los objetos, podemos ver su código justo debajo:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "..\Auxiliar\C_Terminal.mqh"
//+------------------------------------------------------------------+
class C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
virtual void Create(string szObjectName, ENUM_OBJECT typeObj)
                        {
                                ObjectCreate(Terminal.Get_ID(), szObjectName, typeObj, Terminal.GetSubWin(), 0, 0);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTABLE, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_SELECTED, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, true);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BACK, false);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
                        };
//+------------------------------------------------------------------+
                void PositionAxleX(string szObjectName, int X)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XDISTANCE, X);
                        };
//+------------------------------------------------------------------+
                void PositionAxleY(string szObjectName, int Y, int iArrow = 0)
                        {
                                int desl = (int)ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, (iArrow == 0 ? Y - (int)(desl / 2) : (iArrow == 1 ? Y : Y - desl)));
                        };
//+------------------------------------------------------------------+
virtual void SetColor(string szObjectName, color cor)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                        }
//+------------------------------------------------------------------+
                void Size(string szObjectName, int Width, int Height)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_XSIZE, Width);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE, Height);
                        };
//+------------------------------------------------------------------+
};

Vean que el código es sencillo y muy compacto, lo que nos permite elevar el nivel de abstracción a un nivel un poco más alto, por eso necesitaremos mucho menos código después. Si observan, verán que tenemos una función, con el detalle de que esta función es virtual, ella se encarga de crear, de la manera más genérica posible, cualquiera de los objetos, pero como vamos a utilizar solo uno en este modelo más básico, ustedes pueden pensar que esta función es una especie de pérdida de tiempo, mas en realidad no lo es, si ustedes ven este código en el sistema de órdenes del EA, entenderán de lo que estoy hablando.

De la misma manera tenemos otras dos funciones para posicionar el objeto en el gráfico adecuadamente. También tenemos una función que se utiliza para modificar el color del objeto, y al igual que la función de creación, esta también es una función virtual, ya que algunos objetos tienen un patrón de color más complejo, y por último tenemos una función que ajusta las dimensiones del objeto.

Aunque pueda parecer una tontería, hacer este tipo de abstracción nos ayuda mucho después, ya que todos los objetos serán tratados de forma única, sea cual sea el objeto. Y esto nos aporta algunas ventajas, pero las ventajas que conlleva las dejaremos para otra ocasión, así que veamos qué objeto será el elegido para crear el tablero. Y el afortunado objeto elegido es OBJ_EDIT. Y tenemos su código completo justo debajo:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
#define def_ColorNegative       clrCoral
#define def_ColoPositive        clrPaleGreen
//+------------------------------------------------------------------+
class C_Object_Edit : public C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
                template < typename T >
                void Create(string szObjectName, color corTxt, color corBack, T InfoValue)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_EDIT);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, "Lucida Console");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, 10);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_ALIGN, ALIGN_LEFT);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, corTxt);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, corBack);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_COLOR, corBack);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, true);
                                if (typename(T) == "string") ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, (string)InfoValue); else SetTextValue(szObjectName, (double)InfoValue);
                        };
//+------------------------------------------------------------------+
                void SetTextValue(string szObjectName, double InfoValue, color cor = clrNONE)
                        {
                                color clr;
                                clr = (cor != clrNONE ? cor : (InfoValue < 0.0 ? def_ColorNegative : def_ColoPositive));                                
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, Terminal.ViewDouble(InfoValue < 0.0 ? -(InfoValue) : InfoValue));
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clr);
                        };
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+
#undef def_ColoPositive
#undef def_ColorNegative
//+------------------------------------------------------------------+

¿¡Pero eso es todo!? SÍ, aunque es un poco diferente del código que ustedes encuentran en el sistema de órdenes del EA, sí, es sólo eso. Aquí tenemos todo, una función para poner valores de tipo double, y este es el tipo que realmente usamos mucho dentro de MQL5, tenemos aquí la creación del objeto de edición del tipo Obj_Edit, pero hay una cosa que los puede dejar muy confundidos en caso de que estén iniciando en la programación, observen con calma la función de creación del objeto, veámosla mejor en el fragmento de abajo:

template < typename T >
void Create(string szObjectName, color corTxt, color corBack, T InfoValue)

Estas dos líneas en realidad son consideradas por el compilador como una sola línea, pero ¿saben lo que realmente está pasando aquí o están completamente perdidos? ¿¡Piensan que me invento cosas para hacerles la vida más difícil!?

Pues bien, cuando usamos 'template < typename T > ', T puede sustituirse por cualquier otra cosa, y siempre que cumpla las reglas de nomenclatura será válido. En realidad estamos mostrando una forma de sobrecarga, lo que es bastante común, muchas veces tenemos que crear funciones que son iguales pero que recibirán diferentes argumentos o tipos de datos, esto es sumamente común, para facilitarnos la vida en estas horas utilizamos esta sintaxis, un poco extraña para algunos, pero bastante común cuando no queremos reescribir una función completa solo porque, por ejemplo, uno de nuestros datos sea diferente pero todo el cuerpo interno de la función sea el mismo.

Si usted presta atención, verá que al final del procedimiento solo hay una línea, y esta contiene un código un tanto curioso:

if (typename(T) == "string") ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, (string)InfoValue); else SetTextValue(szObjectName, (double)InfoValue);

Lo que este código está haciendo es comprobar de qué tipo de datos se está informando en la variable InfoValue, y OJO a que digo TIPO y no, VALOR, no confundan las dos cosas.

Si el tipo es una string, tendremos la ejecución de un código, si es otro, tendremos la ejecución de otro código, pero esto en realidad no lo hace el compilador o el enlazador, este tipo de análisis se suele hacer en RunTime, por esto tenemos que informar de forma explícita qué tipo de datos deben ser procesados, para que el enlazador pueda ajustar las cosas de forma adecuada, y esto se hace utilizando las siguientes medidas resaltadas.

Así que, en lugar de crear dos funciones prácticamente idénticas, con una y sólo una diferencia, la sobrecargamos y ajustamos las cosas donde haga falta, de modo que al final tendremos mucho menos trabajo.

Es un hecho que este tipo de planteamiento no era necesario en el código del EA, porque allí esta función trabajará sólo y únicamente con un tipo básico que es double, pero aquí además del double, seguiremos trabajando con strings y no quería tener que crear todo el código igual sólo para separar estos dos tipos.

Si quieren más detalles sobre esto, pueden echar un vistazo a plantillas de funciones, allí les será más sencillo entender cómo y por qué la sobrecarga de funciones es tan ampliamente utilizada, y cómo evitar tener que reescribir todo tu código, sólo a causa de tener tipos diferentes.

Pero antes de terminar este tema sobre los objetos, no puedo dejar de dar un paseo rápido por el objeto que estará en el fondo del tablero, porque eso es todo, tenemos que crear un fondo, o ¿esperan que todo funcione de una manera linda sin un fondo? Pero no se preocupen, el código para esto es extra simple, y se puede ver a continuación:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
class C_Object_BackGround : public C_Object_Base
{
        public:
//+------------------------------------------------------------------+
                void Create(string szObjectName, color cor)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_RECTANGLE_LABEL);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
                                this.SetColor(szObjectName, cor);
                        }
//+------------------------------------------------------------------+
virtual void SetColor(string szObjectName, color cor)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, cor);
                        }
//+------------------------------------------------------------------+
};
//+------------------------------------------------------------------+

Este código es tan sencillo y directo que prácticamente no necesitaremos entrar en mucho detalle, ya que su única utilidad es crear un fondo para el tablero, pero lo muestro aquí para que quede constancia, por si alguien se pregunta cómo es el código responsable de crear el fondo.

Muy bien, con esto podemos cerrar este tema y ahora que hemos implementado los objetos que necesitamos y ya tenemos la estructura de soporte, podemos pasar al siguiente paso.


Implementación de la clase principal

Bien, hasta ahora hemos preparado el terreno para esta etapa, que por cierto es la más emocionante de todas, ya que aquí haremos funcionar realmente el sistema. El código para esto se encuentra en el archivo de cabecera C_Widget.mqh, pero vamos a echarle un vistazo, empezando por las declaraciones iniciales que puedes ver justo debajo:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "Elements\C_Object_Edit.mqh"
#include "Elements\C_Object_BackGround.mqh"
//+------------------------------------------------------------------+
C_Terminal Terminal;
//+------------------------------------------------------------------+
#define def_PrefixName          "WidgetPrice"
#define def_NameObjBackGround	def_PrefixName + "BackGround"
#define def_MaxWidth            80
//+------------------------------------------------------------------+
#define def_CharSymbol          "S"
#define def_CharPrice           "P"
//+------------------------------------------------------------------+
#define macro_MaxPosition (Terminal.GetWidth() >= (m_Infos.nSymbols * def_MaxWidth) ? Terminal.GetWidth() : m_Infos.nSymbols * def_MaxWidth)
#define macro_ObjectName(A, B) (def_PrefixName + (string)Terminal.GetSubWin() + A + "#" + B)
//+------------------------------------------------------------------+

Aquí declaramos los ficheros de cabecera que realmente necesitamos, aunque hay otros, no necesitamos declararlos todos, con estos será suficiente, ya que «absorben» todos los demás.

También declaramos la clase terminal, para poder usarla aquí, y para hacernos la vida mucho más fácil durante el proceso de programación, tenemos varias definiciones y macros para ser usadas en este archivo de cabecera C_Widget.mqh, pero necesitamos de hecho tener el máximo cuidado con las macros, ya que tienen que ser usadas de la manera correcta, pero no hay grandes problemas, siempre y cuando las usemos correctamente, nos ayudarán mucho.

Una vez hecho esto, pasamos a declarar la clase con sus variables iniciales.

class C_Widget
{
        protected:
                enum EventCustom {Ev_RollingTo};
        private :
                struct st00
                {
                        color   CorBackGround,
                                CorSymbol,
                                CorPrice;
                        int     nSymbols,
                                MaxPositionX;
                        struct st01
                        {
                                string szCode;
                        }Symbols[];
                }m_Infos;

Este enumerador será muy útil más adelante, aunque no es del todo exigido, es bueno tenerlo para ayudarnos a dejar las cosas más abstractas, y así tener código más fácil de leer y entender. Poco después declaramos una estructura que nos ayudará a controlar varias cosas, pero de momento no se preocupe por ella, simplemente entienda que está ahí y es una parte totalmente privada de la clase, es decir, ningún código externo podrá acceder a ella.

Ahora vamos a entrar en los procedimientos reales, y el primero se ve justo debajo:

void CreateBackGround(void)
{
        C_Object_BackGround backGround;
                        
        backGround.Create(def_NameObjBackGround, m_Infos.CorBackGround);
        backGround.Size(def_NameObjBackGround, Terminal.GetWidth(), Terminal.GetHeight());
}

Aquí estamos de hecho creando el fondo del tablero, fíjese que usaremos toda el área de la subventana, y pondremos allí un objeto rellenando todo con un solo color. De esta forma obtendremos un fondo uniforme. Como dije en el tema anterior, estamos creando un nivel de abstracción que nos permite programar mucho menos y obtener resultados más rápido, pero ahora vamos a algo un poco más complicado.

void AddSymbolInfo(const string szArg, const bool bRestore = false)
        {
#define macro_Create(A, B, C)   {                                               \
                edit.Create(A, m_Infos.CorSymbol, m_Infos.CorBackGround, B);    \
                edit.PositionAxleX(A, def_MaxWidth * m_Infos.nSymbols);         \
                edit.PositionAxleY(A, C);                                       \
                edit.Size(A, def_MaxWidth - 1, 22);                             \
                                }
                        
                C_Object_Edit edit;

                macro_Create(macro_ObjectName(def_CharSymbol, szArg), szArg, 10);
                macro_Create(macro_ObjectName(def_CharPrice, szArg), 0.0, 32);
                if (!bRestore)
                {
                        ArrayResize(m_Infos.Symbols, m_Infos.nSymbols + 1, 10);
                        m_Infos.Symbols[m_Infos.nSymbols].szCode = szArg;
                        m_Infos.nSymbols++;
                }
#undef macro_Create
        }

Este procedimiento ya empieza a ser ignorante e inmediatamente declaro una macro para usar dentro de él, fíjense que antes de concluir el procedimiento destruiré la macro porque no tiene uso fuera del procedimiento.

Lo que estamos haciendo aquí es crear un objeto del tipo C_Object_Edit, hacer un posicionamiento temporal del mismo e informar el tamaño que debe tener. Todo ello dentro de la macro. Ya en estos puntos hacemos un pedido con la macro para tener un código más fácil de leer, ya que todo el proceso es prácticamente igual, claro que está el tema de los valores, pero el procedimiento es el mismo, así que usamos la macro para facilitar al máximo todo, recuerden: «menos tecleo, más producción».

Ahora hay un detalle, este procedimiento también es llamado cuando el usuario elimina un objeto que no debería ser eliminado, por lo que, si este es el caso, no tendremos la ejecución de las siguientes líneas, pero durante el proceso de creación normal tendremos la ejecución, en el que primero asignamos espacio en la memoria, luego colocamos el nombre del símbolo en la posición asignada e incrementamos hasta la siguiente llamada. Así podremos pasar al siguiente código.

Continuando en el orden que aparecen los códigos, la siguiente rutina es bastante interesante, veámosla:

inline void UpdateSymbolInfo(const int x, const string szArg)
{
        C_Object_Edit edit;
        string sz0 = macro_ObjectName(def_CharPrice, szArg);
        MqlRates Rate[1];
                                
        CopyRates(szArg, PERIOD_M1, 0, 1, Rate);                                
        edit.PositionAxleX(macro_ObjectName(def_CharSymbol, szArg), x);
        edit.SetTextValue(sz0, Rate[0].close, m_Infos.CorPrice);
        edit.PositionAxleX(sz0, x);
}

Mucha gente piensa que necesitamos objetos a nivel global, pero en realidad dentro de MT5 y utilizando MQL5, esto no es cierto en absoluto, ya que todos los objetos creados están disponibles para ser manipulados según sea necesario. Para saber cuál es el nombre del objeto, basta con mirar la ventana que lista todos los objetos presentes en el gráfico del activo, de esta forma podemos utilizar un acceso local y manipular los objetos contenidos en el gráfico del activo, siempre y cuando por supuesto conozcamos su nombre.

Luego creamos el nombre del objeto para poder manipularlo, para hacer este trabajo más fácil utilizaremos una macro. Hecho esto, ahora viene una cosa interesante, normalmente tendríamos que tener el activo del que queremos obtener información en la ventana observación del mercado, pero, para nosotros, aquí en el tablero de cotizaciones, esto haría que el usuario se desmotive a utilizar nuestro sistema en caso de que tuviera que abrir docenas o cientos de activos y estos quedaran allí en la ventana observación del mercado, así que para no hacer esto, utilizamos otro método, pero esto tiene un costo, nada es realmente gratis, el costo es que en cada llamada a este procedimiento haremos una copia de la última barra, así sabremos lo que pasó.

Hecho esto posicionamos los objetos correctos en la posición adecuada, e informamos el valor para que sea graficado, pero recuerde que cada llamada tendrá un pequeño retraso en la ejecución, pero esto lo mejoraremos más adelante en este mismo artículo.

La siguiente rutina de la secuencia puede verse a continuación:

bool LoadConfig(const string szFileConfig)
{
        int file;
        string sz0;
        bool ret;
                                
        if ((file = FileOpen("Widget\\" + szFileConfig, FILE_CSV | FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
        {
                PrintFormat("Arquivo de configuração %s não encotrado.", szFileConfig);
                return false;
        }
        m_Infos.nSymbols = 0;
        ArrayResize(m_Infos.Symbols, 30, 30);
        for (int c0 = 1; (!FileIsEnding(file)) && (!_StopFlag); c0++)
        {
                if ((sz0 = FileReadString(file)) == "") continue;
                if (SymbolExist(sz0, ret)) AddSymbolInfo(sz0); else
                {
                        FileClose(file);
                        PrintFormat("Ativo na linha %d não foi reconhecido.", c0);
                        return false;
                }
        }
        FileClose(file);
        m_Infos.MaxPositionX = macro_MaxPosition;
                
        return !_StopFlag;
}

Aquí vamos a leer el archivo que contiene todos los activos que se utilizarán en el tablero, observen que no estoy forzando ningún tipo de extensión, sólo una ubicación para el archivo que sea encontrado, de esta manera usted será libre de dar cualquier nombre al archivo, por lo que puede tener archivos distintos para cosas distintas.

Debemos tener cuidado de apuntar a un archivo que contenga datos correctos, de lo contrario podemos experimentar algunos problemas, pero en el anexo donde estará el código completo del sistema, voy a poner un archivo para mostrar una forma de formato interno, en este archivo se pueden encontrar todos los activos actualmente presentes en el Índice Ibovespa (IBOV), utilice este archivo como base para crear todos los demás, porque cuando las mejoras se implementan en este sistema, voy a utilizar el mismo formato que está en el archivo anexo.

Si se ha encontrado el archivo y se puede abrir, ejecutamos una llamada para asignar espacio de memoria para almacenar los datos a medida que llegan. A continuación, empezamos a leer línea por línea, hasta el final del archivo, o hasta que el usuario interrumpa el sistema. Si alguna línea está en blanco o sin ninguna información, hacemos una nueva llamada de lectura. Ahora algo importante, el activo solo será adicionado si existe, si no existe se informará un error indicando en qué línea ocurrió, este mensaje se puede ver en la ventana de herramientas, y no se leerá ninguna otra línea, se devolverá un error. Al final ajustamos una información crucial para nuestro futuro, esto evitará cálculos innecesarios más adelante.

~C_Widget()
{
        Terminal.Close();
        ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName);
        ArrayFree(m_Infos.Symbols);
}

La función anterior es un destructor de la clase, ella es llamada automáticamente cuando la clase se cierra, en el caso de que esto ocurra, todo el sistema se cerrará con ella al mismo tiempo todos los objetos creados dentro de la clase serán destruidos, y la memoria asignada será liberada.

En el siguiente código tenemos el sistema de inicialización de la clase:

bool Initilize(const string szFileConfig, const string szNameShort, color corText, color corPrice, color corBack)
{
        IndicatorSetString(INDICATOR_SHORTNAME, szNameShort);
        Terminal.Init(ChartWindowFind());
        Terminal.Resize();
        m_Infos.CorBackGround = corBack;
        m_Infos.CorPrice = corPrice;
        m_Infos.CorSymbol = corText;
        CreateBackGround();

        return LoadConfig(szFileConfig);
}

No hay mucho que decir sobre él, ya que en el transcurso del tiempo fui explicando cada cosa que aquí se utiliza, excepto los siguientes puntos, aquí definiremos un nombre corto para nuestro indicador, este nombre es informado como un parámetro, así que estén atentos a esto, ahora este código aquí es para capturar el índice de la subventana que el indicador estará utilizando, esto es importante debido a los objetos, necesitamos saber qué subventana se está utilizando, de lo contrario corremos el riesgo de colocar los objetos en el lugar equivocado.

Y como última rutina dentro de este archivo de cabecera, tenemos el sistema de gestión de mensajes.

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        static int tx = 0;
        string szRet[];
                                                        
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_RollingTo):
                        tx = (int) (tx + lparam);
                        tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx));
                        for (int c0 = 0, px = tx; (c0 < m_Infos.nSymbols); c0++)
                        {
                                if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode);
                                px += def_MaxWidth;
                                px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px);
                        }
                        ChartRedraw();
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        Terminal.Resize();
                        m_Infos.MaxPositionX = macro_MaxPosition;
                        ChartRedraw();
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (StringSubstr(sparam, 0, StringLen(def_PrefixName)) == def_PrefixName) if (StringSplit(sparam, '#', szRet) == 2)
                        {
                                AddSymbolInfo(szRet[1], true);
                                ChartRedraw();
                        }else if (sparam == def_NameObjBackGround)
                        {
                                ObjectsDeleteAll(Terminal.Get_ID(), def_PrefixName);
                                CreateBackGround();
                                for (int c0 = 0; c0 < m_Infos.nSymbols; c0++) AddSymbolInfo(m_Infos.Symbols[c0].szCode, true);
                                ChartRedraw();
                        }
                        break;
        }
}

La mayor parte de este código es bastante sencillo de entender, tenemos 2 eventos que son generados por la plataforma y son pasados al indicador para que los procese, pero también tenemos un tipo de evento que para muchos no tiene sentido ya que es un evento personalizado, este tipo de evento es bastante común en algunos tipos de proyectos, pero aquí sirve más para que podamos centralizar el tratamiento de los mensajes o eventos que puedan estar sucediendo. Aunque muchos no lo entiendan, la plataforma MetaTrader 5 así como el lenguaje MQL5 están encaminados a eventos, lo que significa que no trabajamos de forma procedimental, sino que trabajamos con eventos y los tratamos según se producen.

Para entender cómo se genera este evento personalizado, tenemos que ver el código del indicador, así que antes de explicarlo, porque creo que es precisamente en este evento donde muchos tendrán dificultades para entender lo que está pasando, vamos a ver el código del indicador, que ahora ya quedará con una forma funcional diferente a lo mostrado al principio del artículo.

#property copyright "Daniel Jose"
#property description "Gadget para cotações em letreiro."
#property description "Este cria uma faixa que mostra o preço dos ativos."
#property description "Para detalhes de como usá-lo visite:\n"
#property description "https://www.mql5.com/pt/articles/10941"
#property link "https://www.mql5.com/pt/articles/10941"
#property indicator_separate_window
#property indicator_plots 0
#property indicator_height 45
//+------------------------------------------------------------------+
#include <Widget\Rolling Price\C_Widget.mqh>
//+------------------------------------------------------------------+
input string    user00 = "Config.cfg";  //Arquivo de configuração
input int       user01 = -1;            //Deslocamento
input int       user02 = 60;            //Pausa em milissegundos
input color     user03 = clrWhiteSmoke; //Cor do Ativo
input color     user04 = clrYellow;     //Cor do Preço
input color     user05 = clrBlack;      //Cor de Fundo
//+------------------------------------------------------------------+
C_Widget Widget;
//+------------------------------------------------------------------+
int OnInit()
{
        if (!Widget.Initilize(user00, "Widget Price", user03, user04, user05))
                return INIT_FAILED;
        EventSetMillisecondTimer(user02);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
{
        return rates_total;
}
//+------------------------------------------------------------------+
void OnTimer()
{
        EventChartCustom(Terminal.Get_ID(), C_Widget::Ev_RollingTo, user01, 0.0, "");
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Widget.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        EventKillTimer();
}
//+------------------------------------------------------------------+

Aquí tenemos algo muy poco visto en la mayoría de códigos de indicadores, una indicación de la altura que tendrá la ventana del indicador, pero el tema aquí no es ese y sí otro, observe el siguiente detalle:

Cuando el usuario defina un valor para este parámetro, tendremos un dato para ser utilizado como temporizador, es cierto que deberíamos evitar en la medida de lo posible poner eventos del tipo OnTime en un indicador, pero aquí desgraciadamente no tenemos otra forma, necesitamos un evento de este tipo, ahora presten atención, cuando un evento OnTime es disparado por la plataforma MT5, se generará una llamada a la función OnTime, dentro de esta función sólo tenemos una línea de código, y esta línea disparará un evento asíncrono, lo que significa que no sabemos exactamente cuando el código será llamado, este evento es un evento personalizado.

Fíjense en los parámetros dentro de este evento personalizado, no son parámetros casuales, están ahí por una razón muy fuerte, cada uno de ellos indica una cosa, pero al final tendremos como resultado una llamada a la función OnChartEvent, y esta llamará a la función presente dentro de la clase C_Widget, que manejará los mensajes generados por los eventos.

Ahora preste atención al siguiente detalle: Cuando se utiliza la función EventChartCustom, hacemos que el valor que se utilizará sea como el ID de la función OnChartEvent, un valor que se identificará allí en la función de manejo de mensajes, si se llamara a la función de manejo de mensajes directamente, el código sería síncrono, es decir, pondríamos todo el resto del código en modo espera aguardando a que vuelva la función de manejo de mensajes, pero como la cosa se hizo así, esto es, usando una llamada a EventChartCustom, el código no necesita quedar en espera, de esta forma evitamos bloquear todos los demás indicadores con algo que no sabremos cuanto tardará en resolverse.

Y el hecho de que hagamos la llamada a través de EventChartCustom tiene otra ventaja, esta llamada puede venir de cualquier parte del código, es decir, no importa desde donde hagamos la llamada, ella siempre disparará un evento ChartEvent, y este consecuentemente llamará a OnChartEvent, por lo que la cosa se desarrolla de una forma mucho más natural.

Este tipo de enfoque se verá en otro futuro artículo que ya está listo, sobre otro tema, igualmente interesante, pero no hablaré de él, los dejaré con la duda y la ansiedad hasta que salga a la luz pública.

Creo que se ha entendido esta parte, es decir, cómo se genera el evento personalizado y por qué uso un evento personalizado en lugar de hacer una llamada directamente al código que moverá el tablero, entonces volvamos al código donde tenemos el tratamiento de este evento personalizado para mover el tablero, recordando que hay un parámetro informado por el usuario que es muy importante en el desplazamiento, por lo que observen este valor contenido en el indicador.

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        static int tx = 0;
        string szRet[];
                                                        
        switch (id)
        {
                case (CHARTEVENT_CUSTOM + Ev_RollingTo):
                        tx = (int) (tx + lparam);
                        tx = (tx < -def_MaxWidth ? m_Infos.MaxPositionX : (tx > m_Infos.MaxPositionX ? -def_MaxWidth : tx));
                        for (int c0 = 0, px = tx; (c0 < m_Infos.nSymbols); c0++)
                        {
                                if (px < Terminal.GetWidth()) UpdateSymbolInfo(px, m_Infos.Symbols[c0].szCode);
                                px += def_MaxWidth;
                                px = (px > m_Infos.MaxPositionX ? -def_MaxWidth + (px - m_Infos.MaxPositionX) : px);
                        }
                        ChartRedraw();
                        break;

Las matemáticas involucradas en el fragmento anterior pueden parecer confusas para muchos, pero lo que estoy haciendo aquí es utilizar ese valor proporcionado por el usuario, para mover los objetos una cierta distancia, es decir, si el valor es positivo, el objeto se moverá de izquierda a derecha; si es negativo, de derecha a izquierda; y si es cero, el objeto permanecerá inmóvil. La idea es sencilla, pero ¡¿dónde está el cálculo que no se ve?! Por eso se dijo que el código anterior parece confuso, ya que quien hace esto, el cálculo, es precisamente estas dos líneas.

Pero puede que en realidad no estén entendiendo cómo es esto posible, cómo un cálculo tan simple logra hacer esto, pero si prestan suficiente atención, verán que el cálculo involucra límites, y cuando el límite es alcanzado inmediatamente, la posición es recalculada al límite inmediatamente adjunto, es decir, cerramos el loop, lo que provoca que cuando se alcance el valor en un punto determinado, se ajuste para empezar en el punto que estará en el límite opuesto; para entenderlo mejor, sería como si tuviéramos que contar de 0 a 99 y no supiéramos contar más allá de esos valores, ¿qué pasaría si intentáramos adicionar 1 a 99? Siguiendo la lógica, obtendríamos 100.

Correcto... pero no en este caso, ya que volveríamos inmediatamente a 0, pero si intentas adicionar 3 a 98, no obtendremos un valor mayor que 99, obtendremos el valor 1; parece extraño pero es así, lo mismo si estamos restando, al intentar quitar 3 a 2 obtendríamos como resultado 99... No es una locura 😵 😵... pero esta es la base del sistema de conteo de un ordenador, si estudian con calma, verán que el ordenador no puede contar cantidades infinitas, todo lo contrario, hay un límite para el valor máximo obtenido, y esto se aplica a otro ámbito que es la criptografía, pero esto es otra historia. 

Volvamos al código. Si no entienden lo que expliqué arriba, traten de entender esto primero, porque, cuando entremos en el loop FOR, se pondrá aún más raro.

Ahora dentro del loop FOR, haremos lo siguiente: No sabemos dónde y cuánto debemos terminar, ya que el hecho de hacer el conteo anterior no nos dice exactamente lo que debe o no debe mostrarse en la pantalla. Para ello, necesitamos crear una ventana, o mejor dicho, utilizaremos los límites de la ventana del gráfico, para saber lo que debe o no debe mostrarse.

Esta parte puede ser extremadamente confusa si no entiendes el concepto explicado anteriormente, pero las únicas dos piezas de información que tenemos son cuántos elementos debemos mostrar y cuál es el valor que se utiliza actualmente, y en función de estos datos tenemos que hacer todo lo demás. Luego iremos de elemento en elemento, empezando siempre por el elemento cero, y a medida que avancemos sumaremos la anchura de cada elemento a la posición inicial del contador de límites. En un momento dado, reventaremos este límite, ya sea en la banda superior, ya sea en la banda inferior; cuando él sea superado, el valor que estamos utilizando para indicar donde se trazaría el elemento actual deberá ajustarse en consecuencia, cuando esto ocurra, tendremos una desviación de la posición, de forma que la información desaparecerá mágicamente de un lado de la pantalla para empezar a aparecer en el otro lado de la pantalla.

Y luego el ciclo se repite hasta que se cierra el indicador, de este modo en la pantalla aparecerán todas las informaciones, no importa cuántas, aparecerán todas.

En el caso del texto en bruto, este tipo de cosas es considerablemente más simple de hacer y planificar, pero el hecho es que, aunque la técnica es bastante similar, por lo general la mayoría de la gente utiliza un código que hace uso de una matriz, donde los elementos se mueven dentro de la matriz y cada una de las celdas de la matriz ya tiene una posición bien definida para ser mostrada, pero aquí este tipo de cosas no traería buenos resultados, así que tuve que utilizar un método ligeramente diferente en el que utilizamos cálculos matemáticos puros, con el fin de tener un movimiento suave y adecuado.

Otro detalle es el hecho de que debemos evitar usar valores superiores a -1 o 1, porque el movimiento quedaría medio pulsante, y daría una impresión extraña, pero nada impide que usemos otros valores, además los resultados son muy extraños en mi opinión.

En el vídeo a continuación se puede ver el sistema en funcionamiento, con datos de los activos IBOV (Índice Ibovespa), pero es sólo para mostrar cómo funcionará el sistema...




Conclusión

Aunque parece un sistema completo y totalmente acabado, todavía puede recibir mejoras, por lo que en el próximo artículo, mostraré cómo hacer que estas mejoras se implementen y se añadan al sistema. Estén atentos a más noticias sobre este sistema. En el anexo encontrarán todo el código de este artículo, para usar y abusar.


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

Archivos adjuntos |
Tablero de cotizaciones: Versión mejorada Tablero de cotizaciones: Versión mejorada
¿Qué tal si animamos la versión básica del tablero? Lo primero que vamos a hacer es modificar el tablero para añadir una imagen, ya sea el logotipo del activo o cualquier otra imagen, para facilitar una rápida identificación del activo que estamos viendo.
DoEasy. Elementos de control (Parte 19): Scrolling de pestañas en el elemento TabControl, eventos de objetos WinForms DoEasy. Elementos de control (Parte 19): Scrolling de pestañas en el elemento TabControl, eventos de objetos WinForms
En este artículo, crearemos la funcionalidad necesaria para el scrolling de los encabezados de las pestañas en TabControl usando los botones de control de scrolling. La funcionalidad servirá para organizar los encabezados de las pestañas en una sola línea a cualquier lado del control.
Cómo construir un EA que opere automáticamente (Parte 01): Conceptos y estructuras Cómo construir un EA que opere automáticamente (Parte 01): Conceptos y estructuras
Aprenda a crear un EA que opere automáticamente de forma sencilla y segura.
Aprendiendo a diseñar un sistema de trading con Awesome Oscillator Aprendiendo a diseñar un sistema de trading con Awesome Oscillator
En este nuevo artículo de la serie, nos familiarizaremos con otra herramienta técnica útil para el trading: el indicador Awesome Oscillator (AO). Asimismo, aprenderemos a desarrollar sistemas comerciales basados en las lecturas de este indicador.