
Gráficos en la biblioteca DoEasy (Parte 81): Integrando gráficos en los objetos de la biblioteca
Contenido
- Concepto
- Mejorando las clases de la biblioteca
- Clase de control de objetos gráficos
- Integrando los gráficos en la biblioteca
- Simulación
- ¿Qué es lo próximo?
Concepto
Esta vez no vamos a crear nuevas construcciones gráficas ni mejoras. Hoy comenzaremos a integrar las clases de elementos gráficos ya creados en los objetos de la biblioteca. Necesitaremos todo esto para poder seguir trabajando en el desarrollo y la mejora de los elementos gráficos; en el futuro, necesitaremos implementar el desplazamiento de los objetos por el gráfico para controlar que la construcción de los objetos gráficos compuestos sea correcta y fidedigna. Y para ello, ahora deberemos pensar en la integración de estos objetos en los objetos de la biblioteca, de forma que ya podamos crear una clase para gestionar los objetos gráficos y su clase de colección.
La clase de control de objetos gráficos contendrá los métodos necesarios para crear formularios y objetos gráficos posteriores, que retornarán un puntero al objeto gráfico creado para que podamos trabajar con él más tarde. Por ello, en el futuro necesitaremos una colección de clases de elementos gráficos para crear las listas de todos los objetos gráficos construidos relacionados con los diferentes objetos de la biblioteca, de modo que podamos crear los métodos para su interacción mutua, así como con el usuario del programa.
En esta ocasión, integraremos los objetos gráficos solo en uno de los objetos de la biblioteca: en el objeto de barra. Necesitaremos algo de tiempo para depurar el concepto creado. Luego, usando como base el mecanismo creado y depurado, lo añadiremos al resto de los objetos de la biblioteca. Después de ello, volveremos al posterior desarrollo de los objetos gráficos de la biblioteca.
El concepto de hoy será el siguiente:
- Tenemos un objeto de elemento gráfico con varios métodos de dibujado;
- tenemos un objeto de barra que aún no sabe nada sobre los objetos gráficos;
- Necesitamos crear una clase para gestionar los objetos gráficos, que les permita ser creados y retornar el puntero al objeto creado;
- y también necesitamos hacer de esta clase de control uno de los componentes del objeto de barra.
Estos cuatro pasos nos permitirán obtener cualquier objeto de la biblioteca previamente creado (hoy el objeto de barra servirá como este objeto "experimental") y, con la ayuda del objeto de control (incorporado en él) del objeto gráfico, crear el objeto deseado y obtener un puntero al mismo. Gracias a este, podremos seguir trabajando como con el objeto gráfico habitual de la biblioteca que hemos analizado en artículos anteriores.
Después de depurar el funcionamiento de este concepto, escribiremos para los objetos de la biblioteca todo lo que haremos con el objeto de barra para integrar en este el componente gráfico de la biblioteca. Así, todos los objetos se animarán con una nueva vida "visual", y obtendremos una nueva herramienta para la interacción activa con la biblioteca.
Mejorando las clases de la biblioteca
Cada objeto gráfico creado por alguno de los objetos de la biblioteca debe saber qué objeto lo ha creado. Obviamente, si solo tenemos un objeto que pueda crear objetos gráficos por sí mismo (hoy es un objeto de barra), el objeto gráfico creado no necesitará saber qué objeto se ha encargado de ello. Pero si cada objeto de la biblioteca es capaz de crear objetos gráficos por sí mismo, todos los objetos gráficos creados deberán saber a partir de qué objeto han sido creados, de forma que desde el objeto gráfico puedan referirse al objeto que los ha creado y recibir sus datos del mismo. Esto puede resultar útil para mostrar estos datos en un objeto gráfico, o para establecer relaciones más complejas entre diferentes objetos.
Por supuesto, hoy no podremos hacer todo esto. No obstante, comenzaremos por lo más simple: necesitamos al menos conocer la descripción del tipo de objeto a partir del cual se ha creado el objeto gráfico. Para ello, empezaremos usando el identificador de la colección de objetos (para cada objeto se escribe el identificador de su colección que corresponde a su tipo). Con la ayuda de este identificador, podremos entender a qué tipo de objetos pertenece el objeto de biblioteca a partir del cual se ha creado el objeto gráfico. Obviamente, esto no bastará para indicar con precisión un objeto específico, pero siempre comenzaremos por uno simple, avanzando gradualmente hacia los más complicados.
Además, necesitaremos crear los métodos necesarios para mostrar las descripciones de los objetos del mismo tipo para todos los objetos de la biblioteca anteriormente creados. Estamos hablando de los métodos Print() y PrintShort(), que muestran una descripción breve y completa de las propiedades del objeto. Vamos a hacer estos métodos virtuales, y a declararlos en la clase padre de todos los objetos de la biblioteca CBaseObj. Para que la virtualización funcione, deberemos hacer que los argumentos de estos métodos sean exactamente iguales en todas las clases. Pero, por el momento, tenemos diferentes parámetros para estos métodos en diferentes clases. Deberemos convertirlos al mismo formato y corregir las llamadas al método según los parámetros modificados en los argumentos del método.
En la clase CBaseObj, en el archivo \MQL5\Include\DoEasy\Objects\BaseObj.mqh , declaramos estos dos métodos virtuales con los parámetros ya requeridos:
//--- Return an object type virtual int Type(void) const { return this.m_type; } //--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes) virtual void Print(const bool full_prop=false,const bool dash=false) { return; } //--- Display a short description of the object in the journal virtual void PrintShort(const bool dash=false,const bool symbol=false){ return; } //--- Constructor
Los parámetros en los argumentos de los métodos ya han sido seleccionados para que podamos utilizarlos en todos los métodos que hemos escrito previamente en las clases derivadas.
Por ejemplo, en la clase COrder, la clase básica de todo el sistema de órdenes de la biblioteca, ya hemos realizado los siguientes cambios:
//--- Return order/position direction string DirectionDescription(void) const; //--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes) virtual void Print(const bool full_prop=false,const bool dash=false); //--- Display a short description of the object in the journal virtual void PrintShort(const bool dash=false,const bool symbol=false); //--- }; //+------------------------------------------------------------------+
Aquí, hemos añadido otro argumento al método Print() y hemos declarado el método PrintShort().
En la implementación del método, fuera del cuerpo de la clase, también hemos declarado un argumento adicional del método:
//+------------------------------------------------------------------+ //| Send order properties to the journal | //+------------------------------------------------------------------+ void COrder::Print(const bool full_prop=false,const bool dash=false) { ::Print("============= ",CMessage::Text(MSG_LIB_PARAMS_LIST_BEG),": \"",this.StatusDescription(),"\" ============="); int beg=0, end=ORDER_PROP_INTEGER_TOTAL; for(int i=beg; i<end; i++) { ENUM_ORDER_PROP_INTEGER prop=(ENUM_ORDER_PROP_INTEGER)i; if(!full_prop && !this.SupportProperty(prop)) continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); beg=end; end+=ORDER_PROP_DOUBLE_TOTAL; for(int i=beg; i<end; i++) { ENUM_ORDER_PROP_DOUBLE prop=(ENUM_ORDER_PROP_DOUBLE)i; if(!full_prop && !this.SupportProperty(prop)) continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("------"); beg=end; end+=ORDER_PROP_STRING_TOTAL; for(int i=beg; i<end; i++) { ENUM_ORDER_PROP_STRING prop=(ENUM_ORDER_PROP_STRING)i; if(!full_prop && !this.SupportProperty(prop)) continue; ::Print(this.GetPropertyDescription(prop)); } ::Print("================== ",CMessage::Text(MSG_LIB_PARAMS_LIST_END),": \"",this.StatusDescription(),"\" ==================\n"); } //+------------------------------------------------------------------+
Como ejemplo, aquí tenemos cómo hemos mejorado las llamadas de los métodos con los parámetros añadidos en los argumentos:
//+------------------------------------------------------------------+ //| Display complete collection description to the journal | //+------------------------------------------------------------------+ void CMBookSeriesCollection::Print(const bool full_prop=false,const bool dash=false) { ::Print(CMessage::Text(MSG_MB_COLLECTION_TEXT_MBCOLLECTION),":"); for(int i=0;i<this.m_list.Total();i++) { CMBookSeries *bookseries=this.m_list.At(i); if(bookseries==NULL) continue; bookseries.Print(false,true); } } //+------------------------------------------------------------------+ //| Display the short collection description in the journal | //+------------------------------------------------------------------+ void CMBookSeriesCollection::PrintShort(const bool dash=false,const bool symbol=false) { ::Print(CMessage::Text(MSG_MB_COLLECTION_TEXT_MBCOLLECTION),":"); for(int i=0;i<this.m_list.Total();i++) { CMBookSeries *bookseries=this.m_list.At(i); if(bookseries==NULL) continue; bookseries.PrintShort(true); } } //+------------------------------------------------------------------+
Anteriormente, aquí había un parámetro, y el método se llamaba como bookseries.Print(true); ahora hemos añadido un parámetro más en el método Print() de la clase CMBookSeries antes del que necesitamos, por eso, primero transmitimos false para el parámetro añadido, y ya luego transmitimos true para el parámetro imprescindible: el que antes era una llamada al método.
Cambios similares han afectado a casi todas las clases de los objetos de la biblioteca que hemos escrito anteriormente; ya los hemos introducido en todas las clases que tienen estos métodos y que se heredan del objeto básico de todos los objetos de biblioteca:
BookSeriesCollection.mqh, ChartObjCollection.mqh, MQLSignalsCollection.mqh, TickSeriesCollection.mqh, TimeSeriesCollection.mqh.
Account.mqh, MarketBookOrd.mqh, MarketBookSnapshot.mqh, MBookSeries.mqh, ChartObj.mqh, ChartWnd.mqh, MQLSignal.mqh, Order.mqh.
Buffer.mqh, BufferArrow.mqh, BufferBars.mqh, BufferCalculate.mqh, BufferCandles.mqh, BufferFilling.mqh, BufferHistogram.mqh, BufferHistogram2.mqh, BufferLine.mqh, BufferSection.mqh, BufferZigZag.mqh, DataInd.mqh, IndicatorDE.mqh.
PendReqClose.mqh, PendReqModify.mqh, PendReqOpen.mqh, PendReqPlace.mqh, PendReqRemove.mqh, PendReqSLTP.mqh, PendRequest.mqh.
Bar.mqh, SeriesDE.mqh, TimeSeriesDE.mqh, DataTick.mqh, TickSeries.mqh.
Symbol.mqh, SymbolBonds.mqh, SymbolCFD.mqh, SymbolCollateral.mqh, SymbolCommodity.mqh, SymbolCommon.mqh, SymbolCrypto.mqh, SymbolCustom.mqh, SymbolExchange.mqh, SymbolFutures.mqh, SymbolFX.mqh, SymbolFXExotic.mqh, SymbolFXMajor.mqh, SymbolFXMinor.mqh, SymbolFXRub.mqh, SymbolIndex.mqh, SymbolIndicative.mqh, SymbolMetall.mqh, SymbolOption.mqh, SymbolStocks.mqh
BaseObj.mqh.
En algunas clases de la biblioteca, la salida de los mensajes mediante la función estándar Print() ha sido reemplazada con la salida de los mensajes mediante el método ToLog() de la clase CMessage, como, por ejemplo, en este método de la clase de colección de eventos:
//+------------------------------------------------------------------+ //| Select only market pending orders from the list | //+------------------------------------------------------------------+ CArrayObj* CEventsCollection::GetListMarketPendings(CArrayObj* list) { if(list.Type()!=COLLECTION_MARKET_ID) { CMessage::ToLog(DFUN,MSG_LIB_SYS_ERROR_NOT_MARKET_LIST); return NULL; } CArrayObj* list_orders=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_PENDING,EQUAL); return list_orders; } //+------------------------------------------------------------------+
Antes, para mostrar los mensajes, se usaba esta línea:
Print(DFUN,CMessage::Text(MSG_LIB_SYS_ERROR_NOT_MARKET_LIST));
Lista de archivos en los que hemos realizado esta correcciones:
EventsCollection.mqh, HistoryCollection.mqh, TimeSeriesCollection.mqh.
Todos los cambios en estas clases se pueden encontrar en los archivos adjuntos al artículo.
Si en el gráfico se encuentra un objeto de formulario ya creado, podremos ocultarlo o mostrarlo indicando las banderas de visualización para él en los marcos temporales especificados. Utilizaremos esta posibilidad para "desplazar" un objeto al primer plano, encima de todos los demás, usando el método BringToTop().
No obstante, tenemos métodos "especificadores" para mostrar/ocultar los objetos gráficos.
Para ello, crearemos dos métodos virtuales en la clase de elemento gráfico CGCnvElement, en el archivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh:
//--- Set the object above all void BringToTop(void) { CGBaseObj::SetVisible(false); CGBaseObj::SetVisible(true); } //--- (1) Show and (2) hide the element virtual void Show(void) { CGBaseObj::SetVisible(true); } virtual void Hide(void) { CGBaseObj::SetVisible(false); }
Los métodos simplemente establecen las banderas correspondientes para mostrar el objeto en todos los marcos temporales en el objeto básico de los objetos gráficos de la biblioteca.
La clase de objeto de formulario CForm hereda del objeto de elemento gráfico, y el objeto de formulario puede ser compuesto, constando de varios objetos de elemento gráfico. Por consiguiente, deberemos registrar para él nuestra propia implementación de estos métodos.
Abrimos el archivo \MQL5\Include\DoEasy\Objects\Graph\Form.mqh y declaramos dos métodos virtuales en la sección pública:
//+------------------------------------------------------------------+ //| Visual design methods | //+------------------------------------------------------------------+ //--- (1) Show and (2) hide the form virtual void Show(void); virtual void Hide(void); //+------------------------------------------------------------------+ //| Methods of simplified access to object properties | //+------------------------------------------------------------------+
Los implementamos fuera del cuerpo de la clase:
//+------------------------------------------------------------------+ //| Show the form | //+------------------------------------------------------------------+ void CForm::Show(void) { //--- If the object has a shadow, display it if(this.m_shadow_obj!=NULL) this.m_shadow_obj.Show(); //--- Display the main form CGCnvElement::Show(); //--- In the loop by all bound graphical objects, for(int i=0;i<this.m_list_elements.Total();i++) { //--- get the next graphical element CGCnvElement *elment=this.m_list_elements.At(i); if(elment==NULL) continue; //--- and display it elment.Show(); } //--- Update the form CGCnvElement::Update(); } //+------------------------------------------------------------------+ //| Hide the form | //+------------------------------------------------------------------+ void CForm::Hide(void) { //--- If the object has a shadow, hide it if(this.m_shadow_obj!=NULL) this.m_shadow_obj.Hide(); //--- In the loop by all bound graphical objects, for(int i=0;i<this.m_list_elements.Total();i++) { //--- get the next graphical element CGCnvElement *elment=this.m_list_elements.At(i); if(elment==NULL) continue; //--- and hide it elment.Hide(); } //--- Hide the main form and update the object CGCnvElement::Hide(); CGCnvElement::Update(); } //+------------------------------------------------------------------+
Ambos métodos se comentan con detalle en la lista de códigos. Resumiendo: a la hora de ocultar objetos, no existe mucha diferencia en el orden que lo hagamos, pero, al mostrar estos, deberemos restaurar la secuencia completa de disposición de todos los objetos anclados en el formulario principal. Por consiguiente, la visualización se realiza por capas: primero, se muestra el objeto más bajo, la sombra del formulario; luego, se muestra el formulario principal sobre la sombra, y, solo después de eso, se muestran todos los elementos gráficos adjuntos al formulario principal. En esta implementación, su orden de visualización se corresponde con el orden en el que se añaden a la lista de objetos anclados.
Comprobaremos este algoritmo al crear los objetos de formulario complejos (compuestos).
Ahora, podemos comenzar a integrar objetos gráficos en los objetos de la biblioteca.
Clase de control de objetos gráficos
¿Cómo podemos dotar a cada objeto de la biblioteca de la capacidad de crear objetos gráficos para nosotros mismos?
La mayoría de los objetos de la biblioteca se heredan del objeto básico de todos los objetos de la biblioteca CBaseObj. Y si añadimos a este objeto una instancia de clase que podrá crear todos los objetos gráficos posibles (disponibles y planificados para un mayor desarrollo), y proporcionamos acceso al puntero al objeto creado, todos sus herederos serán, por consiguiente, capaces de trabajar con los objetos gráficos.
Como tendremos una gran cantidad de objetos gráficos distintos, necesitaremos una clase que "conozca" cada uno de esos objetos, y que tamién pueda crearlos y controlarlos. Llamaremos a esta clase la clase de control de objetos gráficos.
En la carpeta \MQL5\Include\DoEasy\Objects\Graph\, creamos el nuevo archivo GraphElmControl.mqh de la clase CGraphElmControl. La clase debe heredarse de la clase básica para construir la biblioteca estándar MQL5 CObject. Debemos incluir en el listado de clases tres archivos: el archivo de la clase de matriz dinámica de punteros a las instancias de la clase CObject y sus descendientes, el archivo de funciones de servicio y el archivo de la clase de objeto de formulario:
//+------------------------------------------------------------------+ //| GraphElmControl.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include <Arrays\ArrayObj.mqh> #include "..\..\Services\DELib.mqh" #include "Form.mqh" //+------------------------------------------------------------------+ //| Class for managing graphical elements | //+------------------------------------------------------------------+ class CGraphElmControl : public CObject { private: int m_type_node; // Type of the object the graphics is constructed for public: //--- Return itself CGraphElmControl *GetObject(void) { return &this; } //--- Set a type of the object the graphics is constructed for void SetTypeNode(const int type_node) { this.m_type_node=type_node; } //--- Create a form object CForm *CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h); CForm *CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h); CForm *CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h); //--- Constructors CGraphElmControl(){;} CGraphElmControl(int type_node); }; //+------------------------------------------------------------------+
La variable m_type_node guardará el tipo de objeto entre cuyos componentes se incluye el objeto de esta clase. Al crear un nuevo objeto (hoy es el objeto de barra), llamaremos en su constructor al método SetTypeNode(), al que transmitiremos el tipo de objeto de barra escrito en su variable m_type (para la barra, este será el identificador de la colección de objetos de barra). Así, el objeto de control de objetos gráficos sabrá qué clase está construyendo sus objetos. Por ahora, solo usaremos el identificador de la colección. En el futuro, pensaremos en cómo transmitir el puntero al objeto a partir del cual se construyen los gráficos.
Vamos a analizar los métodos de la clase.
En el constructor paramétrico de la clase, escribiremos en la variable m_type_node el tipo de objeto transmitido en los argumentos del método:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CGraphElmControl::CGraphElmControl(int type_node) { this.m_type_node=m_type_node; } //+------------------------------------------------------------------+
Método que crea un objeto de formulario en el gráfico indicado, en la subventana indicada:
//+----------------------------------------------------------------------+ //| Create the form object on a specified chart in a specified subwindow | //+----------------------------------------------------------------------+ CForm *CGraphElmControl::CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h) { CForm *form=new CForm(chart_id,wnd,name,x,y,w,h); if(form==NULL) return NULL; form.SetID(form_id); form.SetNumber(0); return form; } //+------------------------------------------------------------------+
Transmitimos al método el identificador único del objeto de formulario creado, el identificador del gráfico, el número de la subventana del gráfico, el nombre del objeto de formulario y las coordenadas X e Y, así como la anchura y la altura del formulario, es decir, todo lo necesario para crear el formulario.
A continuación, creamos un nuevo objeto de formulario con los parámetros transmitidos al método, y después de crearlo con éxito, asignamos al objeto el identificador de formulario y el número en el listado de objetos (aquí es cero, ya que este objeto no contiene ningún otro objeto de formulario vinculado, es el objeto de formulario principal). Retornamos el puntero al objeto recién creado.
Método que crea un objeto de formulario en una subventana determinada del gráfico actual:
//+----------------------------------------------------------------------+ //| Create the form object on the current chart in a specified subwindow | //+----------------------------------------------------------------------+ CForm *CGraphElmControl::CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h) { return this.CreateForm(form_id,::ChartID(),wnd,name,x,y,w,h); } //+------------------------------------------------------------------+
Transmitimos al método el identificador único del objeto de formulario creado, el número de la subventana del gráfico, el nombre del objeto de formulario y las coordenadas X e Y, así como la anchura y la altura necesarios para crear el formulario. El método retorna el resultado de la operación de la forma de llamar a este método anteriormente analizada, con una indicación explícita del identificador del gráfico actual.
Método que crea un objeto de formulario en el gráfico actual en la ventana principal del gráfico:
//+----------------------------------------------------------------------+ //| Create the form object on the current chart in the chart main window | //+----------------------------------------------------------------------+ CForm *CGraphElmControl::CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h) { return this.CreateForm(form_id,::ChartID(),0,name,x,y,w,h); } //+------------------------------------------------------------------+
Transmitimos al método el identificador único del objeto de formulario creado, el nombre del objeto de formulario y las coordenadas X e Y, así como la anchura y la altura necesarios para crear el formulario. El método retorna el resultado de la operación de la forma de llamar a este método anteriormente analizada, con una indicación explícita del identificador del gráfico actual, así como el número de la ventana principal del gráfico.
No necesitamos nada más aquí para crear los objetos de formulario: todo el trabajo necesario para crear varias animaciones en el objeto de formulario creado se realizará según el puntero a este objeto retornado por los métodos anteriores.
Ya tenemos todo preparado para comenzar a integrar el trabajo con gráficos en todos los objetos de la biblioteca herederos del objeto básico de todos los objetos de la biblioteca CBaseObj.
Integrando los gráficos en la biblioteca
Por consiguiente, necesitaremos que cada objeto de la biblioteca "vea" las clases de objetos gráficos y pueda crear estos objetos por sí mismo. Para hacer esto, solo necesitaremos declarar una instancia de la clase de control de objetos gráficos como parte de la clase de objeto básico de todos los objetos de la biblioteca. Todos sus herederos estarán inmediatamente dotados de la capacidad de crear gráficos trabajando a través de la instancia de la clase CGraphElmControl que acabamos de analizar.
Abramos el archivo \MQL5\Include\DoEasy\Objects\BaseObj.mqh y le añadimos el archivo de la clase de control de objetos gráficos:
//+------------------------------------------------------------------+ //| BaseObj.mqh | //| Copyright 2019, MetaQuotes Software Corp. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include <Arrays\ArrayObj.mqh> #include "..\Services\DELib.mqh" #include "..\Objects\Graph\GraphElmControl.mqh" //+------------------------------------------------------------------+
En la sección protegida de la clase CBaseObj, declaramos una instancia del objeto de clase de control de objetos gráficos:
//+------------------------------------------------------------------+ //| Base object class for all library objects | //+------------------------------------------------------------------+ class CBaseObj : public CObject { protected: CGraphElmControl m_graph_elm; // Instance of the class for managing graphical elements ENUM_LOG_LEVEL m_log_level; // Logging level ENUM_PROGRAM_TYPE m_program; // Program type bool m_first_start; // First launch flag bool m_use_sound; // Flag of playing the sound set for an object bool m_available; // Flag of using a descendant object in the program int m_global_error; // Global error code long m_chart_id_main; // Control program chart ID long m_chart_id; // Chart ID string m_name; // Object name string m_folder_name; // Name of the folder storing CBaseObj descendant objects string m_sound_name; // Object sound file name int m_type; // Object type (corresponds to the collection IDs) public:
En la sección pública de la clase, escribimos los métodos para crear el objeto de formulario:
//--- Return an object type virtual int Type(void) const { return this.m_type; } //--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes) virtual void Print(const bool full_prop=false,const bool dash=false) { return; } //--- Display a short description of the object in the journal virtual void PrintShort(const bool dash=false,const bool symbol=false){ return; } //--- Create a form object on a specified chart in a specified subwindow CForm *CreateForm(const int form_id,const long chart_id,const int wnd,const string name,const int x,const int y,const int w,const int h) { return this.m_graph_elm.CreateForm(form_id,chart_id,wnd,name,x,y,w,h); } //--- Create a form object on the current chart in a specified subwindow CForm *CreateForm(const int form_id,const int wnd,const string name,const int x,const int y,const int w,const int h) { return this.m_graph_elm.CreateForm(form_id,wnd,name,x,y,w,h); } //--- Create the form object on the current chart in the main window CForm *CreateForm(const int form_id,const string name,const int x,const int y,const int w,const int h) { return this.m_graph_elm.CreateForm(form_id,name,x,y,w,h); } //--- Constructor
Los métodos retornan el resultado del funcionamiento de los tres métodos homónimos mencionados anteriormente de la clase de control de objetos gráficos.
Ahora, cada uno de los objetos heredados de la clase CBaseObj tiene la capacidad de crear un objeto de formulario llamando a estos métodos.
Hoy comprobaremos el trabajo con los objetos gráficos utilizando la clase de objeto "Barra".
Abrimos el archivo de esta clase \MQL5\Include\DoEasy\Objects\Series\Bar.mqh y añadimos al método de especificiación de parámetros SetProperties() la transmisión del tipo de objeto de barra a la clase de control de objetos gráficos:
//+------------------------------------------------------------------+ //| Set bar object parameters | //+------------------------------------------------------------------+ void CBar::SetProperties(const MqlRates &rates) { this.SetProperty(BAR_PROP_SPREAD,rates.spread); this.SetProperty(BAR_PROP_VOLUME_TICK,rates.tick_volume); this.SetProperty(BAR_PROP_VOLUME_REAL,rates.real_volume); this.SetProperty(BAR_PROP_TIME,rates.time); this.SetProperty(BAR_PROP_TIME_YEAR,this.TimeYear()); this.SetProperty(BAR_PROP_TIME_MONTH,this.TimeMonth()); this.SetProperty(BAR_PROP_TIME_DAY_OF_YEAR,this.TimeDayOfYear()); this.SetProperty(BAR_PROP_TIME_DAY_OF_WEEK,this.TimeDayOfWeek()); this.SetProperty(BAR_PROP_TIME_DAY,this.TimeDay()); this.SetProperty(BAR_PROP_TIME_HOUR,this.TimeHour()); this.SetProperty(BAR_PROP_TIME_MINUTE,this.TimeMinute()); //--- this.SetProperty(BAR_PROP_OPEN,rates.open); this.SetProperty(BAR_PROP_HIGH,rates.high); this.SetProperty(BAR_PROP_LOW,rates.low); this.SetProperty(BAR_PROP_CLOSE,rates.close); this.SetProperty(BAR_PROP_CANDLE_SIZE,this.CandleSize()); this.SetProperty(BAR_PROP_CANDLE_SIZE_BODY,this.BodySize()); this.SetProperty(BAR_PROP_CANDLE_BODY_TOP,this.BodyHigh()); this.SetProperty(BAR_PROP_CANDLE_BODY_BOTTOM,this.BodyLow()); this.SetProperty(BAR_PROP_CANDLE_SIZE_SHADOW_UP,this.ShadowUpSize()); this.SetProperty(BAR_PROP_CANDLE_SIZE_SHADOW_DOWN,this.ShadowDownSize()); //--- this.SetProperty(BAR_PROP_TYPE,this.BodyType()); //--- Set the object type to the object of the graphical object management class this.m_graph_elm.SetTypeNode(this.m_type); } //+------------------------------------------------------------------+
Ya tenemos prácticamente todo listo para la prueba. Pero... Hay un "pero". Cuando comenzamos a trabajar con objetos gráficos, no los conectamos a la biblioteca principal, solo utilizamos las clases de elementos gráficos "tal cual". Ahora tenemos que hacerlo todo bien: todos los objetos de la biblioteca serán accesibles a través de su objeto principal, la clase CEngine, a la que estarán conectados los archivos de colección de todos los objetos. Pero para los objetos gráficos todavía no tenemos una clase para su colección; aún es demasiado pronto para hacerlo por una simple razón: no todos los objetos han sido creados. No obstante, podemos crear una colección de clases preliminar de los objetos gráficos, para que todo se ciña ya a la idea general, y solo entonces, después de crear todos los objetos, volver a ella y terminarla como debería ser.
Usando como base estas consideraciones, vamos a crear ahora una versión preliminar de la clase de colección de objetos gráficos. Para ello, necesitamos especificar el identificador de la lista de colección de objetos gráficos en el archivo \MQL5\Include\DoEasy\Defines.mqh:
//--- Parameters of the chart collection timer #define COLLECTION_CHARTS_PAUSE (500) // Chart collection timer pause in milliseconds #define COLLECTION_CHARTS_COUNTER_STEP (16) // Chart timer counter increment #define COLLECTION_CHARTS_COUNTER_ID (9) // Chart timer counter ID //--- Collection list IDs #define COLLECTION_HISTORY_ID (0x777A) // Historical collection list ID #define COLLECTION_MARKET_ID (0x777B) // Market collection list ID #define COLLECTION_EVENTS_ID (0x777C) // Event collection list ID #define COLLECTION_ACCOUNT_ID (0x777D) // Account collection list ID #define COLLECTION_SYMBOLS_ID (0x777E) // Symbol collection list ID #define COLLECTION_SERIES_ID (0x777F) // Timeseries collection list ID #define COLLECTION_BUFFERS_ID (0x7780) // Indicator buffer collection list ID #define COLLECTION_INDICATORS_ID (0x7781) // Indicator collection list ID #define COLLECTION_INDICATORS_DATA_ID (0x7782) // Indicator data collection list ID #define COLLECTION_TICKSERIES_ID (0x7783) // Tick series collection list ID #define COLLECTION_MBOOKSERIES_ID (0x7784) // DOM series collection list ID #define COLLECTION_MQL5_SIGNALS_ID (0x7785) // MQL5 signals collection list ID #define COLLECTION_CHARTS_ID (0x7786) // Chart collection list ID #define COLLECTION_CHART_WND_ID (0x7787) // Chart window list ID #define COLLECTION_GRAPH_OBJ_ID (0x7788) // Graphical object collection list ID //--- Pending request type IDs
En la carpeta de la biblioteca \MQL5\Include\DoEasy\Collections\, creamos el nuevo archivo GraphElementsCollection.mqh de la clase CGraphElementsCollection:
//+------------------------------------------------------------------+ //| GraphElementsCollection.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "ListObj.mqh" #include "..\Services\Select.mqh" #include "..\Objects\Graph\Form.mqh" //+------------------------------------------------------------------+ //| Collection of graphical objects | //+------------------------------------------------------------------+ class CGraphElementsCollection : public CBaseObj { private: CListObj m_list_all_graph_obj; // List of all graphical objects int m_delta_graph_obj; // Difference in the number of graphical objects compared to the previous check //--- Return the flag indicating the graphical element object in the list of graphical objects bool IsPresentGraphElmInList(const int id,const ENUM_GRAPH_ELEMENT_TYPE type_obj); public: //--- Return itself CGraphElementsCollection *GetObject(void) { return &this; } //--- Return the full collection list 'as is' CArrayObj *GetList(void) { return &this.m_list_all_graph_obj; } //--- Return the list by selected (1) integer, (2) real and (3) string properties meeting the compared criterion CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode); } CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode); } CArrayObj *GetList(ENUM_CANV_ELEMENT_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByGraphCanvElementProperty(this.GetList(),property,value,mode); } //--- Return the number of new graphical objects int NewObjects(void) const { return this.m_delta_graph_obj; } //--- Constructor CGraphElementsCollection(); //--- Display the description of the object properties in the journal (full_prop=true - all properties, false - supported ones only - implemented in descendant classes) virtual void Print(const bool full_prop=false,const bool dash=false); //--- Display a short description of the object in the journal virtual void PrintShort(const bool dash=false,const bool symbol=false); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CGraphElementsCollection::CGraphElementsCollection() { ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_MOVE,true); ::ChartSetInteger(::ChartID(),CHART_EVENT_MOUSE_WHEEL,true); this.m_list_all_graph_obj.Sort(SORT_BY_CANV_ELEMENT_ID); this.m_list_all_graph_obj.Clear(); this.m_list_all_graph_obj.Type(COLLECTION_GRAPH_OBJ_ID); } //+------------------------------------------------------------------+
La estructura de la clase no se distingue en absoluto de la estructura de las clases de colección de otros objetos de la biblioteca. Además, aquí solo nos interesa el constructor de la clase; todos los demás métodos simplemente se declaran igual que en las otras colecciones de objetos de la biblioteca. Los implementaremos más adelante. Ahora nos importa que esta clase incluya un archivo de la clase de objeto de formulario, a través del cual, los programas creados usando como base esta biblioteca verán los objetos gráficos. Mientras tanto, en el constructor de la clase, se habilitará para el gráfico actual el seguimiento de los eventos de movimiento del ratón y el desplazamiento de la ruleta.
Eso es todo. El resto de esta plantilla de la clase de colección de objetos gráficos no nos importa por ahora: desarrollaremos todo esto más adelante, después de crear todos los objetos gráficos de la biblioteca.
Nos queda por conectar el archivo de la clase de colección de objetos gráficos al archivo del objeto principal de la biblioteca CEngine, ubicado en \MQL5\Include\DoEasy\Engine.mqh:
//+------------------------------------------------------------------+ //| Engine.mqh | //| Copyright 2020, MetaQuotes Software Corp. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2020, MetaQuotes Software Corp." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "Services\TimerCounter.mqh" #include "Collections\HistoryCollection.mqh" #include "Collections\MarketCollection.mqh" #include "Collections\EventsCollection.mqh" #include "Collections\AccountsCollection.mqh" #include "Collections\SymbolsCollection.mqh" #include "Collections\ResourceCollection.mqh" #include "Collections\TimeSeriesCollection.mqh" #include "Collections\BuffersCollection.mqh" #include "Collections\IndicatorsCollection.mqh" #include "Collections\TickSeriesCollection.mqh" #include "Collections\BookSeriesCollection.mqh" #include "Collections\MQLSignalsCollection.mqh" #include "Collections\ChartObjCollection.mqh" #include "Collections\GraphElementsCollection.mqh" #include "TradingControl.mqh" //+------------------------------------------------------------------+ //| Library basis class | //+------------------------------------------------------------------+ class CEngine {
Ahora ya tenemos todo listo para poner a prueba los objetos gráficos integrados en la clase del objeto de barra.
Simulación
Cómo realizaremos la prueba: crearemos una lista de series temporales para el símbolo y el periodo del gráfico actuales. En esta lista, almacenaremos los objetos de barra. Hoy hemos añadido una clase de control de objetos gráficos a estos objetos, lo cual nos permitirá crear nuestro propio objeto de formulario para cada barra.
En consecuencia, haremos lo siguiente: cuando mantengamos presionada la tecla Ctrl en el teclado y movamos el ratón por el gráfico, se creará un objeto de formulario con una sombra para la barra en la que se encuentra el cursor del ratón, en la que además se mostrará un texto describiendo el tipo de barra (alcista/bajista/doji). En cuanto mantengamos presionada la tecla Ctrl, se anulará para el gráfico la posibilidad de desplazarse con el ratón y la ruleta, y el formulario con la descripción de la barra se mostrará de inmediato. Al soltar la tecla Ctrl, la lista de objetos de formulario creados se borrará con la llegada de un nuevo tick, o bien al desplazar el gráfico, simplemente porque para la prueba, resulta opcional el seguimiento de los momentos en que se mantiene/suelta la tecla Ctrl. Sí, el borrado de la lista de objetos creados aquí también se necesita solo (en principio) para "ocultar" algunos de los problemas que surgen al cambiar la escala del gráfico: los objetos de formulario creados previamente comienzan a mostrarse en sus lugares antiguos, es decir, ya no se corresponden con la posición actual de la vela en la escala modificada del gráfico. Y para realizar una prueba rápida, resulta más fácil borrar la lista que recalcular las coordenadas del objeto en el momento que cambia la escala del gráfico.
Bien. Para la prueba, tomaremos al asesor del artículo anterior y lo guardaremos en la nueva carpeta \MQL5\Experts\TestDoEasy\Part81\
con el nuevo nombre TestDoEasyPart81.mq5.
En la zona global, en lugar incluir los archivos
#include <Arrays\ArrayObj.mqh>
#include <DoEasy\Services\Select.mqh>
#include <DoEasy\Objects\Graph\Form.mqh>
incluiremos el archivo del objeto principal de la biblioteca y declararemos su instancia:
//+------------------------------------------------------------------+ //| TestDoEasyPart81.mq5 | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //--- includes #include <DoEasy\Engine.mqh> //--- defines #define FORMS_TOTAL (4) // Number of created forms #define START_X (4) // Initial X coordinate of the shape #define START_Y (4) // Initial Y coordinate of the shape //--- input parameters sinput bool InpMovable = true; // Movable forms flag sinput ENUM_INPUT_YES_NO InpUseColorBG = INPUT_YES; // Use chart background color to calculate shadow color sinput color InpColorForm3 = clrCadetBlue; // Third form shadow color (if not background color) //--- global variables CEngine engine; CArrayObj list_forms; color array_clr[]; //+------------------------------------------------------------------+
A continuación, eliminamos el código que crea los objetos de formulario del manejador OnInit() del asesor; solo necesitamos indicar el símbolo que se usará en la biblioteca y crear una serie temporal para el símbolo y el periodo actuales. Como resultado, el manejador tendrá el aspecto siguiente:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the permissions to send cursor movement and mouse scroll events ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true); ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true); //--- Set EA global variables ArrayResize(array_clr,2); // Array of gradient filling colors array_clr[0]=C'246,244,244'; // Original ≈pale gray array_clr[1]=C'249,251,250'; // Final ≈pale gray-green //--- Create the array with the current symbol and set it to be used in the library string array[1]={Symbol()}; engine.SetUsedSymbols(array); //--- Create the timeseries object for the current symbol and period, and show its description in the journal engine.SeriesCreate(Symbol(),Period()); engine.GetTimeSeriesCollection().PrintShort(false); // Short descriptions //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Vamos a elimanar las funciones FigureType() y FigureProcessing() del asesor; no las necesitamos para esta prueba, y además ocupan casi todo el volumen del código del asesor.
En su lugar, escribiremos tres funciones.
Una función que retornará una bandera para indicar la existencia de un formulario con el nombre especificado:
//+-------------------------------------------------------------------------------+ //| Return the flag that indicates the existence of a form with a specified name | //+-------------------------------------------------------------------------------+ bool IsPresentForm(const string name) { //--- In the loop by the list of form objects, for(int i=0;i<list_forms.Total();i++) { //--- get the next form object CForm *form=list_forms.At(i); if(form==NULL) continue; //--- form the desired object name as "Program name_" + the form name passed to the function string nm=MQLInfoString(MQL_PROGRAM_NAME)+"_"+name; //--- If the current form object has such a name, return 'true' if(form.NameObj()==nm) return true; } //--- Upon the loop completion, return 'false' return false; } //+------------------------------------------------------------------+
Una función que ocultará todos los formularios salvo el formulario con el nombre indicado:
//+------------------------------------------------------------------+ //| Hide all forms except the one with the specified name | //+------------------------------------------------------------------+ void HideFormAllExceptOne(const string name) { //--- In the loop by the list of form objects, for(int i=0;i<list_forms.Total();i++) { //--- get the next form object CForm *form=list_forms.At(i); if(form==NULL) continue; //--- form the desired object name as "Program name_" + the form name passed to the function string nm=MQLInfoString(MQL_PROGRAM_NAME)+"_"+name; //--- If the current form object has such a name, display it, if(form.NameObj()==nm) form.Show(); //--- otherwise - hide else form.Hide(); } } //+------------------------------------------------------------------+
Y una función que retornará la bandera de pulsación continua de la tecla "Ctrl" en el teclado:
//+------------------------------------------------------------------+ //| Return the flag of holding Ctrl | //+------------------------------------------------------------------+ bool IsCtrlKeyPressed(void) { return((TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)&0x80)!=0); } //+------------------------------------------------------------------+
Todas las funciones son bastante simples y, en nuestra opinión, no necesitarán de explicaciones.
Eliminamos del manejador OnChartEvent() el procesamiento de la pulsación de las teclas del teclado y los clics sobre los objetos; no lo necesitaremos hoy. Simplemente añadiremos el procesamiento del movimiento del ratón:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- If the mouse is moved if(id==CHARTEVENT_MOUSE_MOVE) { CForm *form=NULL; datetime time=0; double price=0; int wnd=0; //--- If Ctrl is not pressed, if(!IsCtrlKeyPressed()) { //--- clear the list of created form objects and allow scrolling a chart with the mouse list_forms.Clear(); ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,true); return; } //--- If X and Y chart coordinates are successfully converted into time and price, if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price)) { //--- get the bar index the cursor is hovered over int index=iBarShift(Symbol(),PERIOD_CURRENT,time); if(index==WRONG_VALUE) return; //--- Get the bar index by index CBar *bar=engine.SeriesGetBar(Symbol(),Period(),index); if(bar==NULL) return; //--- Convert the coordinates of a chart from the time/price representation of the bar object to the X and Y coordinates int x=(int)lparam,y=(int)dparam; if(!ChartTimePriceToXY(ChartID(),0,bar.Time(),(bar.Open()+bar.Close())/2.0,x,y)) return; //--- Disable moving a chart with the mouse ChartSetInteger(ChartID(),CHART_MOUSE_SCROLL,false); //--- Create the form object name and hide all objects except one having such a name string name="FormBar_"+(string)index; HideFormAllExceptOne(name); //--- If the form object with such a name does not exist yet, if(!IsPresentForm(name)) { //--- create a new form object form=bar.CreateForm(index,name,x,y,76,16); if(form==NULL) return; //--- Set activity and unmoveability flags for the form form.SetActive(true); form.SetMovable(false); //--- Set the opacity of 200 form.SetOpacity(200); //--- The form background color is set as the first color from the color array form.SetColorBackground(array_clr[0]); //--- Form outlining frame color form.SetColorFrame(C'47,70,59'); //--- Draw the shadow drawing flag form.SetShadow(true); //--- Calculate the shadow color as the chart background color converted to the monochrome one color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100); //--- If the settings specify the usage of the chart background color, replace the monochrome color with 20 units //--- Otherwise, use the color specified in the settings for drawing the shadow color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3); //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes //--- Set the shadow opacity to 200, while the blur radius is equal to 4 form.DrawShadow(2,2,clr,200,3); //--- Fill the form background with a vertical gradient form.Erase(array_clr,form.Opacity()); //--- Draw an outlining rectangle at the edges of the form form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity()); //--- If failed to add the form object to the list, remove the form and exit the handler if(!list_forms.Add(form)) { delete form; return; } //--- Capture the form appearance form.Done(); } //--- If the form object exists, if(form!=NULL) { //--- draw a text with the bar type description on it and show the form. The description corresponds to the mouse cursor position form.TextOnBG(0,bar.BodyTypeDescription(),form.Width()/2,form.Height()/2-1,FRAME_ANCHOR_CENTER,C'7,28,21'); form.Show(); } //--- Redraw the chart ChartRedraw(); } } } //+------------------------------------------------------------------+
El código del manejador ha sido comentado al completo directamente en el listado. Esperamos que el lector comprenda todo. En cualquier caso, siempre podrá plantear cualquier duda en los comentarios al artículo.
Compilamos el asesor y lo ejecutamos en el gráfico. Pulsamos y mantenemos la tecla Ctrl y realizamos movimientos con el ratón por el gráfico. Para cada barra se creará un objeto de formulario en el que se mostrará la descripción del tipo de barra (bajista/alcista/doji). Al soltar la tecla Ctrl, se eliminarán todos los objetos creados.
¿Qué es lo próximo?
En el próximo artículo, continuaremos integrando los objetos gráficos en los objetos de la biblioteca.
Más abajo se adjuntan todos los archivos de la versión actual de la biblioteca y el archivo del asesor de prueba para MQL5. Puede descargarlo todo y ponerlo a prueba por sí mismo.
Si tiene preguntas, observaciones o sugerencias, podrá concretarlas en los comentarios al artículo.
*Artículos de esta serie:
Gráficos en la biblioteca DoEasy (Parte 73): Objeto de formulario del elemento gráfico
Gráficos en la biblioteca DoEasy (Parte 74): Elemento gráfico básico sobre la clase CCanvas
Gráficos en la biblioteca DoEasy (Parte 75): Métodos de trabajo con primitivas y texto en el elemento gráfico básico
Gráficos en la biblioteca DoEasy (Parte 76): Objeto de formulario y temas de color predeterminados
Gráficos en la biblioteca DoEasy (Parte 77): Clase de objeto Sombra
Gráficos en la biblioteca DoEasy (Parte 78): Fundamentos de animación en la biblioteca. Cortando las imágenes
Gráficos en la biblioteca DoEasy (Parte 79): Clase de objeto "Fotograma de animación" y sus objetos herederos
Gráficos en la biblioteca DoEasy (Parte 80): Clase de objeto "Fotograma de animación geométrica"
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/9751





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso