Del básico al intermedio: Objetos (III)
Introducción
En el artículo anterior, Del básico al intermedio: Eventos de mouse, vimos cómo manejar, de manera simple, clara y bastante objetiva, los eventos relacionados con el mouse, así como la combinación del mouse con el teclado. Esto, para producir combinaciones de botones presionados y crear algún tipo de actividad en el gráfico.
Aunque, a primera vista, ese tipo de recurso tratado en el artículo anterior parezca algo relativamente simple, cuando pasamos a la práctica notamos que allí hay algo que puede ser mucho más complicado de lo que muchos podrían imaginar. Esto ocurre porque, según el tipo de recurso que vayamos a implementar y la forma en que vayamos a implementarlo, algún usuario puede no disponer del hardware adecuado. Esto ocurre porque, aunque hoy en día sea algo relativamente común, mucha gente no usa una computadora de escritorio.
E, incluso entre quienes usan una computadora de escritorio, como se la conoce, los llamados desktops, algún usuario puede no tener un mouse con más de tres botones. Y esto complica implementar ciertos tipos de funcionalidades dirigidas al público en general. Para uso personal, no hay problema. Pero, si tú pretendes aprender a programar para comercializar tus aplicaciones, notarás que no todos disponen del equipo adecuado. Entonces, o haces las cosas de manera que atiendan a ese público, o lo ignoras y corres el riesgo de perder una parte del mercado. Y todo eso por una simple decisión.
Bien, pero no estamos aquí para aprender a comercializar las aplicaciones que estamos creando, sino para aprender a poner nuestras ideas en práctica usando MQL5 como puerta de entrada. Como el artículo anterior estuvo más orientado a cuestiones técnicas y a una demostración simple, ha llegado el momento de poner en práctica lo que se mostró allí y crear, así, una alternativa a lo que sería la cruz de análisis presente en MetaTrader 5.
Aunque la idea no es crear un sustituto de esta, sino mostrarte, mi estimado lector, que es posible hacer las cosas con muy poco conocimiento, pero con los conceptos correctos.
Un indicador personalizado
Lo que deseo mostrar aquí ya fue tema de otro de mis artículos, Desarrollando un sistema de Replay (Parte 30): Proyecto Asesor Experto - Clase C_Mouse (IV). Allí mostré cómo implementar un indicador de mouse usando programación orientada a objetos. Pero aquí veremos algo parecido, aunque con una programación mucho más simple, orientada a principiantes. Como tú, mi querido y estimado lector.
¿Por qué repetir lo que ya se mostró antes? En realidad, no repetiremos nada. Solo el concepto. Pero, como hasta este momento no he hablado absolutamente nada sobre programación orientada a objetos y solo se ha mostrado programación estructural, quiero mostrar que no necesitamos ser especialistas para construir algo adecuado a nuestras necesidades. Podemos hacerlo con un mínimo de conocimiento, pero siempre buscando utilizar los conceptos vistos hasta el momento. Y, aun así, al final tendremos el resultado que buscamos. Es decir, lo simple funciona, incluso cuando parece no ser suficiente.
Para empezar, vamos a implementar un código basado en uno de los que vimos en el artículo anterior. Puede verse a continuación.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. int OnInit() 07. { 08. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 09. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 10. //+----------------+ 11. ChartSetInteger(0, CHART_MOUSE_SCROLL, false); 12. ChartSetInteger(0, CHART_CONTEXT_MENU, false); 13. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 14. //+----------------+ 15. ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false); 16. ChartSetInteger(0, CHART_QUICK_NAVIGATION, false); 17. //+----------------+ 18. 19. return INIT_SUCCEEDED; 20. }; 21. //+------------------------------------------------------------------+ 22. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 23. { 24. return rates_total; 25. }; 26. //+------------------------------------------------------------------+ 27. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 28. { 29. string sz = ""; 30. 31. switch (id) 32. { 33. case CHARTEVENT_KEYDOWN: 34. break; 35. case CHARTEVENT_MOUSE_MOVE: 36. Comment(sz); 37. sz += "Mouse position X: " + (string)(short)lparam; 38. sz += "\nMouse position Y: " + (string)(short)dparam; 39. Comment(sz); 40. if ((uchar)sparam != 0) PrintFormat("Hexadecimal mask of mouse buttons is: 0x%02X", (uchar)sparam); 41. break; 42. case CHARTEVENT_MOUSE_WHEEL: 43. break; 44. } 45. }; 46. //+------------------------------------------------------------------+ 47. void OnDeinit(const int reason) 48. { 49. Comment(""); 50. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 51. //+----------------+ 52. ChartSetInteger(0, CHART_MOUSE_SCROLL, true); 53. ChartSetInteger(0, CHART_CONTEXT_MENU, true); 54. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 55. //+----------------+ 56. ChartSetInteger(0, CHART_KEYBOARD_CONTROL, true); 57. ChartSetInteger(0, CHART_QUICK_NAVIGATION, true); 58. //+----------------+ 59. }; 60. //+------------------------------------------------------------------+
Código 01
Este código 01 es una modificación del código visto en el artículo anterior. Esto, para que podamos probar algo de manera más simple. Observa que, en la línea 40, imprimiremos algo en el terminal. Ese valor es justamente el que queremos usar. Así, al ejecutar este código 01, tú verás algo parecido a la siguiente imagen.

Imagen 01
La información resaltada en esta imagen 01 es justamente una máscara que necesitamos usar. Esto, para que podamos construir el evento que dará origen a la cruz de análisis personalizada. Presta atención a esto. El valor que vemos aquí en la imagen 01 representa el valor registrado cuando se produce un clic en el botón central. Así, dado que al presionar el botón central deberá crearse la cruz de análisis, podemos empezar a pensar en cómo hacerlo. Hay otra cosa que también necesitas entender.
Si practicaste con los códigos vistos en el artículo anterior, debiste notar que cada una de las llamadas hechas en el evento Init, es decir, en la función OnInit, que aparece en la línea seis de este código 01, activará o desactivará una propiedad predeterminada de MetaTrader 5. Sin embargo, como solo queremos crear una cruz de análisis personalizada, no necesitamos desactivar todos esos eventos, porque eso limita demasiado el uso de MetaTrader 5 para algunos usuarios. Lo mejor es desactivar única y exclusivamente el evento que no queremos que MetaTrader 5 use de forma predeterminada. En este caso, sería el evento de la línea 13 del código 01.
Bien, entonces tenemos un primer punto que realmente debemos modificar en el código 01. Pero ya podemos empezar a trabajar en la cruz antes de ver el siguiente código que vamos a implementar. Ahora, presta atención, porque esto que vamos a tratar es importante.
En el artículo Del básico al intermedio: Objetos (II), se mostró cómo podíamos manejar dos objetos en el gráfico al mismo tiempo, una línea vertical y otra horizontal.
Pues bien, tanto uno como el otro objeto mencionado NO USAN COORDENADAS DE PANTALLA,. como se conocen las coordenadas X e Y cuando se habla de gráficos en una computadora Ambos objetos, OBJ_VLINE y OBJ_HLINE, utilizan coordenadas de cotización. Estas coordenadas usan el precio y el tiempo. Y, como el sistema operativo nos proporciona coordenadas X e Y, necesitamos convertir esos valores de coordenadas de pantalla en coordenadas de cotización. Hacer esto manualmente es una tarea bastante tediosa, porque necesitamos tener en cuenta el precio mínimo y máximo, además del tiempo mínimo y máximo, para poder crear una correlación entre precio y tiempo con los valores X e Y que nos proporciona el sistema operativo.
Por suerte, esta tarea la realiza un procedimiento de la biblioteca estándar de MQL5, lo que simplifica bastante las cosas. Entonces, antes de empezar a ver la cruz trazada en el gráfico, veamos cómo transformar los valores X e Y en coordenadas de cotización.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. int OnInit() 07. { 08. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 09. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 10. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 11. 12. return INIT_SUCCEEDED; 13. }; 14. //+------------------------------------------------------------------+ 15. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 16. { 17. return rates_total; 18. }; 19. //+------------------------------------------------------------------+ 20. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 21. { 22. string sz = ""; 23. short x, y; 24. int sub; 25. datetime dt; 26. double price; 27. 28. switch (id) 29. { 30. case CHARTEVENT_KEYDOWN: 31. break; 32. case CHARTEVENT_MOUSE_MOVE: 33. x = (short)lparam; 34. y = (short)dparam; 35. ChartXYToTimePrice(0, x, y, sub, dt, price); 36. Comment(sz); 37. sz += "Mouse position X: " + (string)x; 38. sz += "\nMouse position Y: " + (string)y; 39. sz += "\nMouse position time: " + TimeToString(dt, TIME_DATE | TIME_MINUTES); 40. sz += "\nMouse position price: " + (string)price; 41. Comment(sz); 42. if ((uchar)sparam != 0) PrintFormat("Hexadecimal mask of mouse buttons is: 0x%02X", (uchar)sparam); 43. break; 44. case CHARTEVENT_MOUSE_WHEEL: 45. break; 46. } 47. }; 48. //+------------------------------------------------------------------+ 49. void OnDeinit(const int reason) 50. { 51. Comment(""); 52. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 53. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 54. }; 55. //+------------------------------------------------------------------+
Código 02
Aquí tenemos un código más simple, orientado a activar o desactivar solo lo que realmente es necesario. Todo lo demás seguirá siendo el comportamiento predeterminado de MetaTrader 5. Observa que hicimos pequeños cambios en la parte correspondiente al manejador de eventos del mouse. Y, en la línea 35, tenemos exactamente el procedimiento de la biblioteca en uso, para convertir los valores en aquellos que realmente necesitamos o queremos utilizar. Es importante que entiendas esto antes de empezar a usar directamente esos valores para controlar los objetos OBJ_VLINE y OBJ_HLINE. Para que las cosas queden bien demostradas, mira la siguiente animación.

Animación 01
En esta animación 01, puedes observar que el gráfico está en diario. Sin embargo, al mirar los valores de tiempo mostrados en la propia animación, podemos ver que tenemos datos de lo que se consideraría intradía. Y esto puede acabar causando cierta confusión en la mente de mucha gente. Porque, si estamos en un gráfico diario, ¿cómo podríamos estar viendo valores de intradía?
En realidad, esos valores se muestran precisamente porque el procedimiento ChartXYToTimePrice NO CONVIERTE valores en algo esperado por ti, sino que convierte coordenadas de pantalla en coordenadas de cotización. Y, como pequeñas variaciones en el eje producen valores fraccionarios, estos valores terminan convirtiéndose en valores presentes en el intradía. Por eso vemos valores que, en apariencia, no tiene mucho sentido ver. Por lo tanto, no se trata de un fallo, sino de un simple detalle del cambio de un tipo de coordenada a otro.
De cualquier manera, con base en lo que se vio aquí y usando un poco de imaginación y sentido común, enseguida te das cuenta de que ya tenemos lo necesario para pensar en cómo controlar los objetos OBJ_HLINE y OBJ_VLINE. Esto modifica lo que se vio en el artículo Del básico al intermedio: Objetos (II), donde el control se hacía con base en el teclado. Pero ahora pasaremos a usar el mouse para eso.
Bien, ¿y cómo haremos esto? Esta es la parte divertida, mi estimado lector. Para ello, primero vamos a crear una cruz que permanecerá todo el tiempo en el gráfico. Esto se hace usando el código que se muestra a continuación.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)ObjectsTotal(0) 07. //+------------------------------------------------------------------+ 08. string gl_Objs[2]; 09. //+------------------------------------------------------------------+ 10. int OnInit() 11. { 12. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 13. ObjectCreate(0, gl_Objs[0] = macro_NameObject, OBJ_VLINE, 0, 0, 0); 14. ObjectCreate(0, gl_Objs[1] = macro_NameObject, OBJ_HLINE, 0, 0, 0); 15. ObjectSetString(0, gl_Objs[0], OBJPROP_TOOLTIP, "\n"); 16. ObjectSetString(0, gl_Objs[1], OBJPROP_TOOLTIP, "\n"); 17. ObjectSetInteger(0, gl_Objs[0], OBJPROP_COLOR, clrBlue); 18. ObjectSetInteger(0, gl_Objs[1], OBJPROP_COLOR, clrBlue); 19. 20. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 21. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 22. 23. return INIT_SUCCEEDED; 24. }; 25. //+------------------------------------------------------------------+ 26. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 27. { 28. return rates_total; 29. }; 30. //+------------------------------------------------------------------+ 31. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 32. { 33. int sub; 34. datetime dt; 35. double price; 36. 37. switch (id) 38. { 39. case CHARTEVENT_KEYDOWN: 40. break; 41. case CHARTEVENT_MOUSE_MOVE: 42. ChartXYToTimePrice(0, (short)lparam, (short)dparam, sub, dt, price); 43. ObjectMove(0, gl_Objs[0], 0, dt, price); 44. ObjectMove(0, gl_Objs[1], 0, dt, price); 45. if ((uchar)sparam != 0) PrintFormat("Hexadecimal mask of mouse buttons is: 0x%02X", (uchar)sparam); 46. break; 47. case CHARTEVENT_MOUSE_WHEEL: 48. break; 49. } 50. ChartRedraw(); 51. }; 52. //+------------------------------------------------------------------+ 53. void OnDeinit(const int reason) 54. { 55. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 56. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 57. 58. ObjectsDeleteAll(0, def_Prefix); 59. ChartRedraw(); 60. }; 61. //+------------------------------------------------------------------+
Código 03
Este código 03, cuando se aplique a un gráfico, producirá algo parecido a lo que se ve a continuación.

Animación 02
Observa que, al comienzo de esta animación 02, el valor del tiempo cambia, aunque la línea vertical no cambie. Y esto se debe a pequeños movimientos del mouse, algo perfectamente normal y aceptable. Pero observa que, a medida que movemos el puntero del mouse, las líneas vertical y horizontal siguen el movimiento del mouse, lo que nos permite tener una cruz todo el tiempo en el gráfico.
Como lo que aquí se hace ya se estudió en parte en el artículo Del básico al intermedio: Objetos (II), nos centraremos solo en lo que aquí es nuevo. En este caso, se trata justamente de que el movimiento ahora está ligado a los valores devueltos por el procedimiento de la biblioteca al convertir coordenadas de pantalla en coordenadas de cotización.
Pero esto no era exactamente lo que queríamos hacer. Lo que queríamos era mostrar esta misma cruz, pero solo después de presionar el botón central. Entonces, necesitamos cambiar algunos puntos del código para que eso ocurra. Pero, antes de hacerlo, veamos por qué están las líneas 15, 16 y 58. Bien, las líneas 15 y 16 evitan que MetaTrader 5 muestre un pequeño mensaje cuando estemos sobre el objeto. Si quieres mostrar un mensaje que explique el propio objeto, pero que no permanezca todo el tiempo en el gráfico, utiliza este procedimiento de las líneas 15 y 16 para colocar allí algún mensaje significativo. Ese mensaje se mostrará cuando el mouse permanezca detenido durante un breve período sobre el objeto señalado por el puntero.
La línea 58, por su parte, eliminará todos los objetos presentes en el gráfico cuyo prefijo sea el indicado. Como estamos vinculando los objetos al propio nombre corto del indicador, resulta más sencillo eliminar los objetos vinculados al propio indicador. Si quieres que el objeto permanezca en el gráfico, bastará con usar un prefijo diferente del nombre del indicador. Ahora sí, podemos ver el siguiente código.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_Prefix "Demo" 005. //+------------------------------------------------------------------+ 006. #define macro_NameObject def_Prefix + (string)ObjectsTotal(0) 007. //+------------------------------------------------------------------+ 008. enum eBtnMouse { 009. MOUSE_LEFT = 0x01, 010. MOUSE_RIGHT = 0x02, 011. MOUSE_KEY_SHIFT = 0x04, 012. MOUSE_KEY_CTRL = 0x08, 013. MOUSE_MIDDLE = 0x10, 014. MOUSE_EXTRA_1 = 0x20, 015. MOUSE_EXTRA_2 = 0x40 016. }; 017. //+------------------------------------------------------------------+ 018. struct st_Cross 019. { 020. private : 021. //+----------------+ 022. bool IsView; 023. string Objs[2]; 024. //+----------------+ 025. void Create(char index, ENUM_OBJECT type) 026. { 027. ObjectCreate(0, Objs[index] = macro_NameObject, type, 0, 0, 0); 028. ObjectSetString(0, Objs[index], OBJPROP_TOOLTIP, "\n"); 029. ObjectSetInteger(0, Objs[index], OBJPROP_COLOR, clrBlue); 030. } 031. //+----------------+ 032. public : 033. //+----------------+ 034. void Show(void) 035. { 036. if (IsView) return; 037. Create(0, OBJ_VLINE); 038. Create(1, OBJ_HLINE); 039. IsView = true; 040. ChartRedraw(); 041. } 042. //+----------------+ 043. void Hide(void) 044. { 045. if (!IsView) return; 046. for(uint c = 0; c < Objs.Size(); c++) 047. ObjectDelete(0, Objs[c]); 048. ChartRedraw(); 049. IsView = false; 050. } 051. //+----------------+ 052. void Move(short x, short y) 053. { 054. int sub; 055. datetime dt; 056. double price; 057. 058. if (!IsView) return; 059. ChartXYToTimePrice(0, x, y, sub, dt, price); 060. ObjectMove(0, Objs[0], 0, dt, price); 061. ObjectMove(0, Objs[1], 0, dt, price); 062. ChartRedraw(); 063. } 064. //+----------------+ 065. }gl_Cross; 066. //+------------------------------------------------------------------+ 067. int OnInit() 068. { 069. gl_Cross.Hide(); 070. 071. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 072. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 073. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 074. 075. return INIT_SUCCEEDED; 076. }; 077. //+------------------------------------------------------------------+ 078. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 079. { 080. return rates_total; 081. }; 082. //+------------------------------------------------------------------+ 083. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 084. { 085. 086. switch (id) 087. { 088. case CHARTEVENT_KEYDOWN: 089. break; 090. case CHARTEVENT_MOUSE_MOVE: 091. if (((uchar)sparam & MOUSE_MIDDLE) != 0) gl_Cross.Show(); 092. gl_Cross.Move((ushort)lparam, (ushort)dparam); 093. break; 094. case CHARTEVENT_MOUSE_WHEEL: 095. break; 096. } 097. ChartRedraw(); 098. }; 099. //+------------------------------------------------------------------+ 100. void OnDeinit(const int reason) 101. { 102. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 103. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 104. 105. gl_Cross.Hide(); 106. }; 107. //+------------------------------------------------------------------+
Código 04
Bien, pero ¿qué hace este código 04? Pues bien, hará lo que se ve en la siguiente animación.

Animación 03
Hum, interesante. Pero veo que has pasado a utilizar un código estructurado. ¿Por qué? Bien, mi estimado lector, el motivo es justamente que un código estructurado es más simple de construir y mantener para la propia cruz. Recuerda que el objetivo de una estructura es justamente crear todo un conjunto de funciones y procedimientos que estén directamente correlacionados con un objetivo determinado.
En los artículos Del básico al intermedio: Struct (VII), donde expliqué la forma de estructurar un código, las cosas se mostraron de una manera abstracta. Pero, cuando llevamos esto a un objetivo menos abstracto, las cosas empiezan a tener un poco más de sentido.
Aquí, en este código 04, estamos en el límite entre un código estructurado y un código orientado a objetos. Esto se debe a que, en este código 04, puedes notar que hay puntos que deben inicializarse antes incluso de que podamos usar la propia estructura. Y este es uno de los motivos por los que se creó la programación orientada a objetos. Observa lo siguiente: cuando la estructura st_Cross se declara en la línea 65 para ser utilizada por la variable gl_Cross, no tenemos certeza de los valores que contienen IsView y el array Objs. Esto hace más difícil crear ciertos tipos de código, sobre todo códigos orientados a trabajar con determinados tipos de datos.
Pero, como aquí todo transcurre con cierta tranquilidad, podemos usar la línea 69 para preparar la estructura y empezar a trabajar con ella. No es la forma más adecuada de hacerlo. Sin embargo, como ya se dijo, aquí estamos trabajando con algo relativamente sencillo. Entonces, cuando en la línea 91 verificamos si el botón central fue o no presionado, podemos mostrar o no los objetos que crean la cruz de análisis simple. Esto se hace llamando o no al procedimiento Show de la estructura st_Cross.
De todos modos, en la línea 92 ajustamos las posiciones de los objetos, para que queden vinculados al puntero del mouse. Sin embargo, si los objetos no están presentes en el gráfico, IsView será falso. Y esto, en la línea 58, detendrá la ejecución y retornará de inmediato al autor de llamada, que, en este caso, es la línea 92. Y las cosas seguirán así hasta que se presione el botón central, momento en el que, por fin, se creará la cruz. A partir de ese momento, pasará a actualizarse con cada movimiento del mouse, ya que, en este caso, IsView pasará a tener un valor verdadero, lo que permitirá que la línea 92 actualice la posición, porque la condición de la línea 58 dejará de cumplirse.
Bien, pero entonces, ¿cómo hacemos que la cruz desaparezca una vez que esté visible? Esto depende de lo que pretendas hacer con la cruz y de cómo vaya a funcionar. Pero, sin mucho trabajo y con poco código, podemos añadir una tecla para quitar la cruz del gráfico, hasta que sea funcional. Para ello, cambiamos el código de manejo de eventos para implementar esta forma de quitar la cruz del gráfico, como se muestra en el fragmento de código a continuación.
. . . 082. //+------------------------------------------------------------------+ 083. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 084. { 085. 086. switch (id) 087. { 088. case CHARTEVENT_KEYDOWN: 089. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 090. break; 091. case CHARTEVENT_MOUSE_MOVE: 092. if (((uchar)sparam & MOUSE_MIDDLE) != 0) gl_Cross.Show(); 093. gl_Cross.Move((ushort)lparam, (ushort)dparam); 094. break; 095. case CHARTEVENT_MOUSE_WHEEL: 096. break; 097. } 098. ChartRedraw(); 099. }; 100. //+------------------------------------------------------------------+ . . .
Código 05
Este simple cambio, que puedes ver en la línea 89, nos permite hacer lo que se muestra en la siguiente animación.

Animación 04
Observa que ahora podemos mostrar o no la cruz de análisis. Pero aquí, en la animación 04, hay una cuestión importante: el tamaño del tick del contrato, es decir, de cuántos en cuántos puntos varía la cotización. A diferencia de muchos casos, este contrato de futuros, que en este caso es el futuro sobre dólar, no se mueve de uno en uno, sino de 0,5 en 0,5. Para comprobarlo, podemos mirar las especificaciones del contrato, como se muestra a continuación.

Imagen 02
¿Y qué nos muestra esta imagen 02? Pues bien, en esta imagen 02 podemos ver de cuánto en cuánto varía la cotización de un símbolo. Y eso es precisamente lo que estoy destacando aquí. Esta información es importante para que podamos construir un método adecuado, a fin de que la línea de precio siempre refleje una cotización real. De nada sirve que la línea de precio refleje un valor que nunca se utilizará. Esto se debe a que ese valor no puede usarse para enviar órdenes, por ejemplo.
Para resolver esto, necesitamos modificar nuevamente el código. Pero, esta vez, no modificaremos el manejador de eventos, sino la estructura st_Cross, como se muestra a continuación.
. . . 017. //+------------------------------------------------------------------+ 018. struct st_Cross 019. { 020. private : 021. //+----------------+ 022. bool IsView; 023. string Objs[2]; 024. int nDigits; 025. double PointPerTick; 026. //+----------------+ 027. void Create(char index, ENUM_OBJECT type) 028. { 029. ObjectCreate(0, Objs[index] = macro_NameObject, type, 0, 0, 0); 030. ObjectSetString(0, Objs[index], OBJPROP_TOOLTIP, "\n"); 031. ObjectSetInteger(0, Objs[index], OBJPROP_COLOR, clrBlue); 032. } 033. //+----------------+ 034. public : 035. //+----------------+ 036. void Init(void) 037. { 038. nDigits = (int) SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); 039. PointPerTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); 040. Hide(); 041. } 042. //+----------------+ . . . 060. //+----------------+ 061. void Move(short x, short y) 062. { 063. int sub; 064. datetime dt; 065. double price; 066. 067. if (!IsView) return; 068. ChartXYToTimePrice(0, x, y, sub, dt, price); 069. price = NormalizeDouble(MathRound(price / PointPerTick) * PointPerTick, nDigits); 070. ObjectMove(0, Objs[0], 0, dt, price); 071. ObjectMove(0, Objs[1], 0, dt, price); 072. ChartRedraw(); 073. } 074. //+----------------+ 075. }gl_Cross; 076. //+------------------------------------------------------------------+ 077. int OnInit() 078. { 079. gl_Cross.Init(); 080. 081. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 082. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 083. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 084. 085. return INIT_SUCCEEDED; 086. }; 087. //+------------------------------------------------------------------+ . . .
Código 06
Ahora, observa que hicimos cambios muy simples en este fragmento del código 06. Primero, añadimos nuevas variables a la estructura. Estas sirven para evitar que tengamos que estar leyendo el mismo tipo de información todo el tiempo. Justo después, añadimos un nuevo procedimiento: Init. Está en la línea 36, donde inicializamos la estructura de una manera un poco más adecuada. Debido a esto, en OnInit cambiamos la llamada que antes se hacía al procedimiento Hide y la redirigimos a Init, como puedes ver en la línea 79.
Ahora, para corregir aquella variación en la línea de precio, usamos la línea 69 para ajustar el precio de manera adecuada. Al final, podemos ver lo que se muestra en la siguiente animación.

Animación 05
Simplemente perfecto. Con un código extremadamente simple, conseguimos crear lo que sería nuestro indicador primordial y completamente personalizado. Ahora viene la cuestión: ¿qué tipo de estudio podemos hacer con este indicador? Porque en él no se ha creado ningún estudio específico. Bien, mi estimado lector, esta es una cuestión para la que no existe una manera simple de responder. Esto se debe a que podemos implementar absolutamente cualquier tipo de estudio. Todo depende de lo que realmente quieras hacer y de cómo pretendas utilizar lo que vayas creando de ahora en adelante.
Recuerda que el contenido propuesto en estos artículos tiene un objetivo didáctico. Y, como cada punto que vemos aquí no responde, en ningún caso, a un objetivo previamente definido que busquemos alcanzar, no te aferres a una u otra forma de implementar las cosas; solo procura entender los conceptos. Y, una vez comprendido el concepto, lo único que te quedará por hacer será implementar lo que deseas o necesitas hacer.
Como podemos convertir este indicador en algo realmente muy práctico, extremadamente divertido e interesante, hagamos lo siguiente: llevar la estructura st_Cross a un archivo de cabecera. Así, podrás implementar diversos otros indicadores con un mínimo de trabajo. Además, podemos prepararla para que más adelante se convierta en una clase, cuando ya estemos hablando de clases. Así, pasamos a tener dos archivos. Uno de ellos, que se muestra a continuación, es el archivo de cabecera.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. enum eBtnMouse { 05. MOUSE_LEFT = 0x01, 06. MOUSE_RIGHT = 0x02, 07. MOUSE_KEY_SHIFT = 0x04, 08. MOUSE_KEY_CTRL = 0x08, 09. MOUSE_MIDDLE = 0x10, 10. MOUSE_EXTRA_1 = 0x20, 11. MOUSE_EXTRA_2 = 0x40 12. }; 13. //+------------------------------------------------------------------+ 14. struct st_TimePrice 15. { 16. double Price; 17. datetime Time; 18. }; 19. //+------------------------------------------------------------------+ 20. struct st_Cross 21. { 22. private : 23. //+----------------+ 24. bool IsView; 25. string Objs[2]; 26. int nDigits; 27. double PointPerTick; 28. //+----------------+ 29. void Create(char index, ENUM_OBJECT type) 30. { 31. ObjectCreate(0, Objs[index] = macro_NameObject, type, 0, 0, 0); 32. ObjectSetString(0, Objs[index], OBJPROP_TOOLTIP, "\n"); 33. ObjectSetInteger(0, Objs[index], OBJPROP_COLOR, clrBlue); 34. } 35. //+----------------+ 36. public : 37. //+----------------+ 38. void Init(void) 39. { 40. nDigits = (int) SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); 41. PointPerTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); 42. Hide(); 43. } 44. //+----------------+ 45. void Show(void) 46. { 47. if (IsView) return; 48. Create(0, OBJ_VLINE); 49. Create(1, OBJ_HLINE); 50. IsView = true; 51. ChartRedraw(); 52. } 53. //+----------------+ 54. void Hide(void) 55. { 56. if (!IsView) return; 57. for(uint c = 0; c < Objs.Size(); c++) 58. ObjectDelete(0, Objs[c]); 59. ChartRedraw(); 60. IsView = false; 61. } 62. //+----------------+ 63. st_TimePrice Move(short x, short y) 64. { 65. int sub; 66. st_TimePrice tp; 67. 68. ChartXYToTimePrice(0, x, y, sub, tp.Time, tp.Price); 69. tp.Price = NormalizeDouble(MathRound(tp.Price / PointPerTick) * PointPerTick, nDigits); 70. if (IsView) 71. { 72. ObjectMove(0, Objs[0], 0, tp.Time, tp.Price); 73. ObjectMove(0, Objs[1], 0, tp.Time, tp.Price); 74. ChartRedraw(); 75. } 76. 77. return tp; 78. } 79. //+----------------+ 80. }; 81. //+------------------------------------------------------------------+
Código 07
Y este otro, que corresponde al indicador que queremos implementar.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)ObjectsTotal(0) 07. //+------------------------------------------------------------------+ 08. #include <Tutorial\File 01.mqh> 09. //+------------------------------------------------------------------+ 10. st_Cross gl_Cross; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. gl_Cross.Init(); 15. 16. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 17. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 18. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 19. 20. return INIT_SUCCEEDED; 21. }; 22. //+------------------------------------------------------------------+ 23. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 24. { 25. return rates_total; 26. }; 27. //+------------------------------------------------------------------+ 28. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 29. { 30. 31. switch (id) 32. { 33. case CHARTEVENT_KEYDOWN: 34. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 35. break; 36. case CHARTEVENT_MOUSE_MOVE: 37. if (((uchar)sparam & MOUSE_MIDDLE) != 0) gl_Cross.Show(); 38. gl_Cross.Move((ushort)lparam, (ushort)dparam); 39. break; 40. case CHARTEVENT_MOUSE_WHEEL: 41. break; 42. } 43. ChartRedraw(); 44. }; 45. //+------------------------------------------------------------------+ 46. void OnDeinit(const int reason) 47. { 48. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 49. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 50. 51. gl_Cross.Hide(); 52. }; 53. //+------------------------------------------------------------------+
Código 08
Observa que, en el código 07, añadimos una nueva estructura. Su objetivo es devolver el valor actual en función del movimiento y la posición del mouse. Y, en el código 08, debes prestar atención a que el archivo de cabecera se añade después de las definiciones. Esto es importante para que el archivo pueda compilarse. Con esto, ahora tenemos algo con lo que podemos divertirnos durante bastante tiempo.
Entonces, vamos a empezar con un indicador relativamente simple y barato. Digo que es barato porque no exige mucho esfuerzo en su implementación, solo un poco de cuidado. Pero, para separar las cosas de manera adecuada, pasemos a un nuevo tema.
Indicador de línea de tendencia
Uno de los indicadores más simples, baratos y menos costosos de crear es el de línea de tendencia. Piensa en lo siguiente: quieres crear un indicador que te permita añadir líneas de tendencia sin tener que seleccionar el objeto en el menú de MetaTrader 5. Así, decides crear un indicador para agilizar este tipo de actividad. Basándonos en el código 08, que es un código en el que podemos apoyarnos para crear todos los demás tipos de indicadores que utilizan objetos, haremos pequeños cambios en el código para crear el indicador que utilizará la línea de tendencia.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)ObjectsTotal(0) 07. //+------------------------------------------------------------------+ 08. #include <Tutorial\File 01.mqh> 09. //+------------------------------------------------------------------+ 10. st_Cross gl_Cross; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. gl_Cross.Init(); 15. 16. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 17. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 18. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 19. 20. return INIT_SUCCEEDED; 21. }; 22. //+------------------------------------------------------------------+ 23. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 24. { 25. return rates_total; 26. }; 27. //+------------------------------------------------------------------+ 28. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 29. { 30. st_TimePrice tp; 31. static string isPaint = ""; 32. 33. switch (id) 34. { 35. case CHARTEVENT_KEYDOWN: 36. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 37. break; 38. case CHARTEVENT_MOUSE_MOVE: 39. if (((uchar)sparam & MOUSE_MIDDLE) != 0) 40. { 41. gl_Cross.Show(); 42. tp = gl_Cross.Move((ushort)lparam, (ushort)dparam); 43. if (isPaint == "") 44. { 45. ObjectCreate(0, isPaint = macro_NameObject, OBJ_TREND, 0, tp.Time, tp.Price); 46. ObjectSetInteger(0, isPaint, OBJPROP_SELECTABLE, true); 47. ObjectSetInteger(0, isPaint, OBJPROP_COLOR, clrMagenta); 48. ObjectSetInteger(0, isPaint, OBJPROP_WIDTH, 3); 49. ObjectSetInteger(0, isPaint, OBJPROP_RAY_RIGHT, true); 50. } 51. ObjectMove(0, isPaint, 1, tp.Time, tp.Price); 52. }else 53. { 54. isPaint = ""; 55. gl_Cross.Hide(); 56. } 57. break; 58. case CHARTEVENT_MOUSE_WHEEL: 59. break; 60. } 61. ChartRedraw(); 62. }; 63. //+------------------------------------------------------------------+ 64. void OnDeinit(const int reason) 65. { 66. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 67. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 68. 69. gl_Cross.Hide(); 70. 71. if (reason == REASON_REMOVE) 72. ObjectsDeleteAll(0, def_Prefix); 73. }; 74. //+------------------------------------------------------------------+
Código 09
Este código 09 servirá para que pueda explicarte algo que muchos principiantes desconocen.
En programación, el orden de los factores altera el producto.
Esta simple frase, que parece no tener efecto en la mayor parte de las decisiones que tomamos en la vida de todo programador, es una verdad suprema, desconocida por muchos principiantes. Tanto es así que, si ejecutas el código 09 en un gráfico, obtendrás lo que se ve a continuación.

Animación 06
Es decir, lo que pretendíamos hacer funciona. Pero quizá estés pensando: ¿y si deseo cambiar el trazado de la línea de tendencia, por cualquier motivo? ¿Cómo podré hacerlo? Esta es la parte fácil, mi estimado lector. Normalmente, necesitamos hacer doble clic en objeto para poder seleccionarlo. Pero, si cambias la opción que se muestra en la siguiente imagen, solo necesitarás un clic para seleccionar un objeto.

Imagen 03
Recuerda que esto solo valdrá para objetos seleccionables. Pero más adelante hablaremos más sobre esto. Por ahora, todos los objetos son inicialmente seleccionables dentro de MetaTrader 5. Bien, con la opción marcada en la imagen 03, tal como se muestra, podemos hacer lo que se ve en la siguiente animación.

Animación 07
Es decir, enseguida te sientes muy orgulloso por haber conseguido crear un código a partir de otro. Pero, aun así, aquí hay un fallo. Si intentas crear una nueva línea de tendencia, como se muestra en la siguiente animación, mira lo que ocurrirá.

Animación 08
Vaya, ahora no entendí. Qué extraño. ¿Por qué se borró la línea de tendencia anterior? ¿Y por qué una de las líneas de la cruz no apareció mientras intentabas crear una nueva línea de tendencia? Este tipo de situación es muy extraña. No esperaba que esto pudiera ocurrir, ya que, en apariencia, el código habría funcionado perfectamente bien al mirar la animación 06. Bien, mi estimado lector, el problema es que los objetos se crean en un orden incorrecto. Parece extraño decirlo. Pero sí, lo que podemos ver en esta animación 08 es justamente la consecuencia de crear los objetos en el orden incorrecto.
Si observas el código 09, notarás que primero creamos la cruz. Esto se hace en la línea 41. Justo después, en la línea 45, creamos la línea de tendencia. No habría ningún problema en esta secuencia de creación, siempre que, claro, no se intentara crear un nuevo objeto. Cuando esto ocurre, como puedes observar en la animación 08, tenemos un problema. Aunque, en realidad, serían dos problemas. El primero se debe a que la macro de la línea seis del código 09 no tiene en cuenta si ya existe algún objeto en el gráfico. Por esta razón, si los objetos se enumeran con un mismo prefijo, en algún momento colisionaremos con un objeto ya presente en el gráfico.
¿Pero cómo es eso? Bien, supongamos que inicialmente no exista ningún objeto en el gráfico. En cuanto crees la cruz, tendremos dos objetos: uno será el objeto con el nombre "Demo0" y el otro será "Demo1". Hasta ahí, bien. Cuando la línea de tendencia se cree en la línea 45 del código 09, tendremos un tercer objeto, "Demo2".
Ahora llegamos al punto que corresponde a la línea que se ve en la animación 06. Cuando se intente crear una nueva línea de tendencia, la cruz pasará a crearse intentando crear los objetos "Demo1" y "Demo2". Pero espera un momento: ya existe un objeto en el gráfico llamado "Demo2", que sería la línea de tendencia anterior. Sí, mi estimado lector, por esta razón una de las líneas de la cruz no se creará. Y esto genera lo que puedes ver al comienzo de la animación 08. Sin embargo, cuando se quite la cruz, se eliminarán tanto "Demo1" como "Demo2" y, con ello, también la línea de tendencia anterior. Y, en ese punto, ya no tendremos ningún objeto en el gráfico, lo que nos devuelve a la condición inicial para crear la línea de tendencia.
Observa que el primer problema se debió al recuento de objetos presentes en el gráfico. El segundo problema, en cambio, se debe al orden en que se crean los objetos. Así, para eliminar ambos problemas, necesitamos modificar el código 09 para dejarlo como se muestra a continuación.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 07. //+------------------------------------------------------------------+ 08. #include <Tutorial\File 01.mqh> 09. //+------------------------------------------------------------------+ 10. st_Cross gl_Cross; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. gl_Cross.Init(); 15. 16. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 17. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 18. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 19. 20. return INIT_SUCCEEDED; 21. }; 22. //+------------------------------------------------------------------+ 23. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 24. { 25. return rates_total; 26. }; 27. //+------------------------------------------------------------------+ 28. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 29. { 30. st_TimePrice tp; 31. static string isPaint = ""; 32. 33. switch (id) 34. { 35. case CHARTEVENT_KEYDOWN: 36. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 37. break; 38. case CHARTEVENT_MOUSE_MOVE: 39. if (((uchar)sparam & MOUSE_MIDDLE) != 0) 40. { 41. tp = gl_Cross.Move((ushort)lparam, (ushort)dparam); 42. if (isPaint == "") 43. { 44. ObjectCreate(0, isPaint = macro_NameObject, OBJ_TREND, 0, tp.Time, tp.Price); 45. ObjectSetInteger(0, isPaint, OBJPROP_SELECTABLE, true); 46. ObjectSetInteger(0, isPaint, OBJPROP_COLOR, clrMagenta); 47. ObjectSetInteger(0, isPaint, OBJPROP_WIDTH, 3); 48. ObjectSetInteger(0, isPaint, OBJPROP_RAY_RIGHT, true); 49. } 50. ObjectMove(0, isPaint, 1, tp.Time, tp.Price); 51. gl_Cross.Show(); 52. }else 53. { 54. isPaint = ""; 55. gl_Cross.Hide(); 56. } 57. break; 58. case CHARTEVENT_MOUSE_WHEEL: 59. break; 60. } 61. ChartRedraw(); 62. }; 63. //+------------------------------------------------------------------+ 64. void OnDeinit(const int reason) 65. { 66. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 67. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 68. 69. gl_Cross.Hide(); 70. 71. if (reason == REASON_REMOVE) 72. ObjectsDeleteAll(0, def_Prefix); 73. }; 74. //+------------------------------------------------------------------+
Código 10
Al ejecutar este código 10, veremos el resultado en la siguiente animación.

Animación 09
Observa que ahora podemos añadir tantas líneas de tendencia como queramos. Y esto sin ningún problema, ya que la línea seis de este código 10 corrige el problema de colisión en los nombres. Y la cruz solo se muestra después de haber creado la línea de tendencia que se colocará en el gráfico. Esto puedes verlo en la línea 51, donde efectivamente creamos la cruz de análisis simple. Es decir, el simple hecho de haber cambiado el orden de ejecución nos permitió resolver el segundo problema, que impedía mantener el objeto deseado en el gráfico.
Consideraciones finales
En este artículo vimos cómo podíamos poner en práctica algo que muchos consideran difícil o, al menos, no consiguen idear de una manera viable: trabajar con objetos directamente en un indicador, colocarlos en un gráfico y después utilizarlos.
Es cierto que aquí se mostró cómo trabajar con un objeto relativamente simple, que es una línea de tendencia. Pero no te imagines que, a pesar de eso, esta aplicación que se mostró aquí es algo tonto y sin mucho propósito. Puedes usar lo que se vio aquí para entrenar y practicar diversas cosas, al mismo tiempo que intentas desarrollar una aplicación que realmente te sea útil, mi estimado lector. Procura estudiar y ver qué puede mejorarse en lo que se mostró en este artículo.
Recuerda que aquí todavía no estamos trabajando con clases, lo que hace que el desafío sea aún más interesante. Esto se debe a que, según cómo pienses implementar las cosas, empezarás a ver que hay puntos que pueden mejorarse en la forma de implementarlas. Y esos puntos nos remiten a un nuevo estilo de programación, que todavía no se vio ni se mostró aquí en esta pequeña secuencia de artículos, pero que en breve formará parte de nuestro repertorio.
Entonces, diviértete con los códigos presentes en el anexo y nos vemos en el próximo artículo, donde veremos cómo hacer otros tipos de manipulación en objetos presentes en el gráfico.
| Archivo MQ5 | Descripción |
|---|---|
| Code 01 | Demostración de objetos |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/16021
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Simulación de mercado: Iniciando SQL en MQL5 (III)
Simulación de mercado (Parte 23): Iniciando SQL (VI)
Particularidades del trabajo con números del tipo double en MQL4
Del básico al intermedio: Eventos de mouse
- 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