![Desarrollo de un sistema de repetición (Parte 26): Proyecto Expert Advisor — Clase C_Terminal](https://c.mql5.com/2/58/replay-p26_600x314.jpg)
Desarrollo de un sistema de repetición (Parte 26): Proyecto Expert Advisor — Clase C_Terminal
Introducción
En el artículo anterior, "Desarrollo de un sistema de repetición — Simulación de mercado (Parte 25): Preparación para la próxima etapa", preparamos la repetición/simulador para un uso básico. A pesar de eso, necesitamos más que solo revisar movimientos o potenciales acciones pasadas. Así que necesitamos algo más. Necesitamos una herramienta que nos permita realizar estudios dirigidos, como si estuviéramos operando en un mercado real. Para eso, la creación de un EA es indispensable, permitiendo estudios profundos. Además, pretendemos desarrollar un EA versátil, aplicable a diferentes mercados, sea de acciones o forex, y que también sea compatible con nuestro sistema de repetición/simulador.
Dada la envergadura del proyecto, el desafío promete ser intenso. Sin embargo, la complejidad del desarrollo no es tan aterradora como parece, una vez que ya compartí cómo ejecutar gran parte del proceso en artículos anteriores. Entre ellos están "Desarrollo de un EA de trading desde cero (Parte 31): Rumbo al futuro (IV)" y "Cómo construir un EA que opere automáticamente (Parte 15): Automatización (VII)", en los cuales detallé el desarrollo de un EA totalmente automático. A pesar de estas referencias, enfrentamos aquí un desafío único y aún más estimulante: hacer que la plataforma MetaTrader 5 simule estar conectada a un servidor de trading, promoviendo una simulación realista de mercado abierto. El desafío es, sin duda, significativamente complejo.
No debemos, no obstante, ser intimidados por la complejidad inicial. Tenemos que empezar a hacer las cosas desde algún sitio. Es necesario iniciar de algún lugar, pues, de lo contrario, acabaremos conformándonos con la complejidad del desafío sin realmente intentar superarlo. La esencia de la programación es exactamente esa: enfrentar un obstáculo y buscar superarlo a través de estudio, pruebas y extensa investigación. Antes de empezar, me gustaría decir que ha sido un placer mostrar y explicar cómo nacen realmente las cosas. Creo que muchos han aprendido con esta serie de artículos, superando lo básico que es comúnmente enseñado y mostrando que podemos alcanzar mucho más allá de lo que algunos consideran posible.
Conceptos para la implementación del Expert Advisor
Como ya debes haber notado, soy un gran adepto de la programación orientada a objetos (OOP). Esto se debe a las amplias posibilidades que la OOP ofrece, además de proporcionar, desde el inicio, una manera de codificar que hace el código más robusto, seguro y confiable. Para empezar, necesitamos establecer una visión preliminar de lo que será necesario, organizando la estructura del proyecto. Con mi experiencia tanto como usuario como programador, he percibido que un EA, para ser realmente efectivo, debe utilizar los recursos siempre disponibles: el teclado y el ratón. Considerando que la plataforma MetaTrader 5 está basada en gráficos, el uso del ratón para interacción con los elementos gráficos es esencial. Pero el teclado también es crucial para asistir en diversos aspectos. No obstante, la discusión no se limita al uso del ratón y el teclado, como se abordará en la secuencia sobre automatización. En algunos casos, la automatización completa prescinde de estos medios, pero al optar por utilizarlos, es importante considerar la naturaleza de la operación realizada. Así, no todos los EAs se adaptan bien a todos los tipos de activos.
Esto ocurre porque algunos activos presentan movimientos de precio de 0,01, otros de 0,5, mientras algunos pueden variar en 5. En el caso del forex, estos valores difieren significativamente de los ejemplos mencionados. Esta variedad de valores lleva a ciertos programadores a desarrollar Expert Advisors específicamente para activos particulares. La razón para esto es clara: el servidor de trading no acepta valores arbitrarios; es necesario adherirse a las reglas establecidas por el servidor. El mismo principio se aplica al sistema de repetición/simulador. No podemos permitir que el EA ejecute órdenes con valores aleatorios.
Implementar esta restricción no es solo necesario, es imperativo. Después de todo, de nada sirve tener un repetición/simulador funcional para entrenamiento y familiarización si, al operar en una cuenta real, el sistema se comporta de manera completamente distinta. Por eso, es esencial que el sistema mantenga una cierta estandarización, pero también que se ajuste lo más fielmente posible a la realidad de una cuenta real. Por lo tanto, es preciso desarrollar un EA que opere como si estuviera interactuando directamente con el servidor de trading, independientemente de las circunstancias.
Comenzamos por la primera de las clases: La clase C_Terminal
Aunque a menudo es posible escribir todo el código sin una orientación específica, esta aproximación no es recomendada para proyectos que tienen el potencial de volverse extremadamente grandes y complejos. Aún no tenemos claridad de cómo el proyecto va a desarrollarse, pero es fundamental iniciar siempre con enfoque en las mejores prácticas de programación. Esto evita acabar con un volumen inmenso de código desorganizado y sin una modelación práctica. Por lo tanto, es esencial pensar en grande desde el inicio, incluso si el proyecto no se revela tan grandioso o complicado. Adoptar buenas prácticas nos hace más organizados, incluso en proyectos menores, y nos habitúa a seguir una metodología consistente. Así, comenzaremos desarrollando la primera clase. Para ello, crearemos un nuevo archivo de cabecera llamado C_Terminal.mqh. Es una buena práctica nombrar el archivo con el mismo nombre de la clase, facilitando la localización del archivo cuando sea necesario trabajar en él. Entonces, el código comienza de la siguiente manera:
class C_Terminal { protected: private : public : };
En el artículo "Cómo construir un EA que opere automáticamente (Parte 05): Gatillos manuales (II)" abordé algunos conceptos sobre clases y las palabras reservadas aquí presentadas. Vale la pena consultar si aún no has visto la serie sobre la creación de un EA automático, ya que allí contiene muchos de los elementos que utilizaremos aquí. Aunque aquel código esté desactualizado y obsoleto, en este proyecto abordaremos nuevas formas de interacción para satisfacer ciertas necesidades y hacer el sistema aún más robusto, confiable y eficiente. Tras este inicio de codificación, la primera cosa que de hecho aparecerá en el código de la clase es una estructura, que será detallada a continuación:
class C_Terminal { protected: //+------------------------------------------------------------------+ struct st_Terminal { long ID; string szSymbol; int Width, Height, nDigits; double PointPerTick, ValuePerPoint, VolumeMinimal, AdjustToTrade; }; //+------------------------------------------------------------------+
Cabe notar que esta estructura está siendo declarada dentro de la cláusula protegida, lo que es crucial para lo que vamos a hacer. Es interesante observar que no declaramos ninguna variable aquí. De hecho, cualquier variable global dentro de una clase debe ser declarada dentro de la cláusula privada, asegurando así el más alto nivel de seguridad y encapsulamiento de la información. Volveremos a este tema a lo largo de la implementación para un entendimiento más profundo. Como práctica recomendada de programación, es esencial evitar que variables internas a la clase sean accedidas por otras partes del código que no pertenecen a la clase.
Ahora, veamos cómo las variables de la clase están siendo declaradas, conforme ilustrado en el fragmento a continuación:
private :
st_Terminal m_Infos;
Actualmente, tenemos solo una variable global y privada en la clase C_Terminal, donde almacenaremos datos relevantes. Más adelante, exploraremos cómo acceder a esa información desde fuera de la clase. Por el momento, es vital tener en mente que ninguna información se filtrará o entrará en la clase sin su conocimiento. Seguir este concepto es crucial. Muchos programadores principiantes permiten que códigos externos a la clase alteren los valores de las variables internas, lo que es un error, incluso si el compilador no lo señala como tal. Esta práctica compromete el encapsulamiento, haciendo el código significativamente menos seguro y manejable, ya que un cambio de valor sin el conocimiento de la clase puede llevar a errores y fallos difíciles de detectar y resolver.
Después de esto, será necesario crear un nuevo archivo de cabecera para mantener la organización. Este archivo, llamado Macros.mqh, contendrá inicialmente solo una única línea.
#define macroGetDate(A) (A - (A % 86400))
Esa línea se utilizará para aislar la información referente a la fecha. Optar por una macro en lugar de una función puede parecer inusual. Sin embargo, en muchas situaciones, el uso de macros es más apropiado. Esto se debe al hecho de que la macro se insertará en el código como si fuera una función en línea, permitiendo que sea ejecutada de la forma más rápida posible. El uso de macros se justifica también por la reducción de la probabilidad de cometer errores significativos durante la programación, especialmente cuando cierta refactorización necesita ser repetida varias veces en el código.
Observación: En este sistema, intentaré, en varios momentos, utilizar un lenguaje de nivel más alto para facilitar la lectura y comprensión, especialmente para aquellos que están comenzando a aprender programación. El uso de un lenguaje de alto nivel no implica que el código será más lento, sino que será más simple de leer. Pronto mostraré cómo puedes aplicar esto en tus propios códigos.
Siempre que sea posible, intenta escribir código en un lenguaje de alto nivel, ya que esto facilita mucho el proceso de depuración y mejora. Además, recuerda que el código se escribe no solo para la máquina, sino para que otros programadores puedan comprenderlo.
Con el archivo de cabecera Macros.mqh creado, donde todas las macros globales serán definidas, procederemos con la inclusión de este archivo en la cabecera C_Terminal.mqh. La inclusión se dará de la siguiente manera:
#include "Macros.mqh"
Observen que el nombre del archivo de cabecera está entre comillas dobles. ¿Por qué está declarado así, en lugar de estar entre signos de mayor y menor (< >)? ¿¡Hay alguna razón especial para esto!? Sí, hay una razón. Al utilizar comillas dobles, estamos instruyendo al compilador que el camino para encontrar el archivo de cabecera debe comenzar en el directorio donde el archivo de cabecera actual, en este caso C_Terminal.mqh, está localizado. Como no se indica ningún camino específico, el compilador buscará el archivo Macros.mqh en el mismo directorio donde se encuentra el archivo C_Terminal.mqh. Así, si la estructura de directorios del proyecto se altera, pero mantenemos el archivo Macros.mqh en el mismo directorio que el archivo C_Terminal.mqh, no será necesario informar un nuevo camino al compilador.
Al usar el nombre entre signos de mayor y menor (< >), estamos instruyendo al compilador a iniciar la búsqueda del archivo en un directorio predefinido dentro del sistema de compilación. Para el MQL5, ese directorio es el INCLUDE. Por lo tanto, cualquier camino hasta el archivo Macros.mqh debe ser especificado a partir de este directorio INCLUDE, localizado en la carpeta MQL5. Si la estructura de directorios del proyecto se altera, será necesario redefinir todos los caminos para que el compilador logre localizar los archivos de cabecera. Aunque pueda parecer un detalle menor, la elección entre un método u otro puede hacer una gran diferencia.
Ahora que comprendemos esta diferencia, vamos a examinar el primer código presente en la clase C_Terminal. Este código es privativo de la clase y, por lo tanto, no puede ser accedido desde fuera de ella:
void CurrentSymbol(void) { MqlDateTime mdt1; string sz0, sz1; datetime dt = macroGetDate(TimeCurrent(mdt1)); enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER; sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3); for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS); switch (eTS) { case DOL: case WDO: sz1 = "FGHJKMNQUVXZ"; break; case IND: case WIN: sz1 = "GJMQVZ"; break; default : return; } for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0)) if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break; }
El código presentado arriba, aunque pueda parecer complejo y confuso a primera vista, tiene una función bastante específica: generar el nombre del activo de modo a permitir el uso en un sistema de órdenes cruzadas. Para comprender cómo se hace esto, es importante analizar cuidadosamente el proceso. Actualmente, el foco está en la generación de nombres para operaciones con índice futuro y dólar futuro, siguiendo la nomenclatura de la B3 (Bolsa de Brasil). Al entender la lógica detrás de la creación de estos nombres, es posible adaptar el código para generar nombres de cualquier contrato futuro, posibilitando la operación de estos contratos por medio del sistema de órdenes cruzadas, conforme discutido anteriormente en el artículo "Desarrollando un EA comercial desde cero (Parte 11): Sistema de órdenes cruzadas. Sin embargo, el objetivo aquí es expandir esa funcionalidad para que el Expert Advisor se ajuste a diferentes condiciones, escenarios, activos o mercados. Esto exige que el EA tenga la capacidad de identificar con qué tipo de activo estará lidiando, lo que puede llevar a la necesidad de incluir más tipos de activos en el código. Para explicarlo mejor, dividámoslo en partes más pequeñas.
MqlDateTime mdt1; string sz0, sz1; datetime dt = macroGetDate(TimeCurrent(mdt1));
Las tres líneas mencionadas son variables que serán utilizadas en el código. La principal cuestión puede estar relacionada a la manera como estas variables son inicializadas. Se utiliza la función TimeCurrent para inicializar dos variables distintas en un solo paso. La primera variable, `mdt1`, es una estructura del tipo MqlDateTime, que almacena información de fecha y hora desglosadas en la propia variable `mdt1`, mientras que TimeCurrent también devuelve el valor que se almacena en `mdt1`. La segunda variable, `dt`, utiliza una macro para aislar el valor de la fecha y almacenarlo, permitiendo así la inicialización completa de dos variables en una única línea de código.
enum eTypeSymbol {WIN, IND, WDO, DOL, OTHER} eTS = OTHER;
Efectivamente, la línea mencionada puede parecer inusual, ya que estamos creando una enumeración, declarando una variable y asignando un valor inicial a ella simultáneamente. Comprender esta línea es crucial para entender otra parte del procedimiento. Así que presta atención a los detalles: En MQL5, una enumeración no puede ser creada sin un nombre, por eso, el nombre se especifica justo al inicio. Dentro de la enumeración, tenemos elementos que, por defecto, comienzan con el valor cero. Esto puede ser modificado, lo que exploraremos más adelante. Por ahora, recuerda que, por defecto, la enumeración comienza en cero. Así, el valor de WIN es cero, IND es uno, WDO es dos, y así sucesivamente. Por una razón que será explicada posteriormente, el último elemento debe ser OTHER, independientemente del número de elementos que quieras incluir; el último siempre debe ser OTHER. Después de definir la enumeración, declaramos una variable que utilizará los datos de esta enumeración, iniciando el valor de esta variable como el del último elemento, es decir, OTHER.
Nota Importante: Observen la declaración de la enumeración. ¿Les parece familiar? Noten que los nombres también son declarados en letras mayúsculas, lo cual es relevante. Lo que sucede es que: Si deseas añadir más activos para usar en tu contrato futuro, debes hacerlo añadiendo los tres primeros caracteres del nombre del contrato antes del elemento OTHER, para que el procedimiento pueda generar el nombre del contrato actual correctamente. Por ejemplo, si quisieras añadir el contrato de BOI (ganado), debes insertar el valor BGI en la enumeración, siendo este el primer paso. Hay otro paso que se discutirá más adelante, pero, para reforzar, si quieres añadir el maíz, debes añadir el valor CCM, y así sucesivamente, siempre antes del valor OTHER. De lo contrario, la enumeración no funcionará como se espera.
Ahora, necesitamos examinar el siguiente fragmento de código. Junto con la enumeración descrita anteriormente, completará el primer ciclo de trabajo.
sz0 = StringSubstr(m_Infos.szSymbol = _Symbol, 0, 3); for (eTypeSymbol c0 = 0; (c0 < OTHER) && (eTS == OTHER); c0++) eTS = (EnumToString(c0) == sz0 ? c0 : eTS); switch (eTS) { case DOL: case WDO: sz1 = "FGHJKMNQUVXZ"; break; case IND: case WIN: sz1 = "GJMQVZ"; break; default : return; }
La primera acción realizada es almacenar el nombre del activo en la variable global privada de la clase. Para simplificar el proceso, utilizamos la función StringSubstr para capturar y almacenar en la variable sz0 las tres primeras letras del nombre del activo del gráfico donde el código de la clase está siendo ejecutado. Esta es la etapa más simple. Ahora, vamos a hacer algo bastante inusual, pero posible: usar la enumeración para determinar qué regla de nomenclatura será aplicada al contrato. Para ello, empleamos un bucle for. La expresión utilizada en este bucle puede parecer bastante extraña; sin embargo, lo que estamos haciendo es recorrer la enumeración en busca del nombre del contrato, definido inicialmente en la enumeración, como expliqué anteriormente. Como la enumeración inicia, por defecto, con el valor cero, nuestra variable local del bucle también comienza en cero. Independientemente de cuál sea el primer elemento, el bucle iniciará a partir de él y continuará hasta encontrar el elemento OTHER o hasta que la variable eTS sea diferente de OTHER. En cada iteración, incrementamos la posición dentro de la enumeración. Ahora, la parte intrigante: con el auxilio del MQL5, utilizamos la función EnumToString para, en cada iteración del bucle, convertir el valor de la enumeración en una cadena, con el fin de compararla con el valor presente en la variable sz0. Cuando estos valores coinciden, la posición se almacena en la variable eTS, haciendo que se torne diferente de OTHER. Este procedimiento es bastante interesante y muestra que la enumeración en MQL5 no debe ser vista de la misma manera que en otros lenguajes de programación. Aquí, piensa en la enumeración como si fuera un arreglo de cadenas, ofreciendo mucho más funcionalidades y practicidad de lo que sería posible en otros lenguajes.
Habiendo identificado el valor de la búsqueda en la variable eTS, el siguiente paso es determinar la regla de nomenclatura específica para cada contrato, lo que requiere la inicialización de la variable sz1 de manera apropiada. La selección de la letra subsiguiente en sz1 depende de la investigación sobre el contrato específico que desea añadir a la regla de asignación de nombres, siguiendo la metodología presentada aquí.
Si el activo no está incluido en la enumeración para una nomenclatura especial, y si la regla correspondiente no ha sido establecida, el procedimiento se completará en este punto. Esto es particularmente relevante cuando estamos utilizando el activo en un modo de repetición/simulador, ya que este tipo de activo es, por su naturaleza, personalizado y especial. El procedimiento termina aquí para estos casos.
Ahora, vamos a examinar otro bucle presente en el procedimiento, una etapa donde, como se dice popularmente, "se complica la cosa". La complejidad de este bucle puede confundir a muchos programadores, haciendo difícil la comprensión de su funcionalidad. Por eso, es importante prestar aún más atención a la explicación que sigue. Vamos entonces a observar el código de este bucle en el fragmento a continuación:
for (int i0 = 0, i1 = mdt1.year - 2000, imax = StringLen(sz1);; i0 = ((++i0) < imax ? i0 : 0), i1 += (i0 == 0 ? 1 : 0)) if (dt < macroGetDate(SymbolInfoInteger(m_Infos.szSymbol = StringFormat("%s%s%d", sz0, StringSubstr(sz1, i0, 1), i1), SYMBOL_EXPIRATION_TIME))) break;
Aunque el código pueda parecer confuso y complicado a primera vista, es, en realidad, bastante simple. Ha sido condensado de forma a hacer la codificación un poco más eficiente. Por motivos de simplificación y para evitar complejidad innecesaria, estamos utilizando un comando `IF`, aunque no sea estrictamente necesario. Todo el comando podría, teóricamente, ser incorporado dentro del bucle `FOR`, pero eso complicaría bastante la explicación. Por lo tanto, utilizamos un `IF` en este bucle para hacer la verificación entre el nombre del contrato generado y el nombre presente en el servidor de trading, con el objetivo de identificar cuál es el contrato futuro más actual. Para comprender este proceso, es esencial conocer la regla de nomenclatura usada para generar el nombre del contrato. A título de ejemplo, mostraré cómo es el caso del contrato de mini dólar futuro negociado en la B3 (Bolsa de Brasil), que sigue una regla de nomenclatura específica:
- Los 3 primeros caracteres del nombre del contrato serán WDO. Independientemente de la fecha de vencimiento o de si se trata de un contrato histórico o no;
- A continuación tenemos un carácter que nos indica el mes de vencimiento;
- Tras este carácter de vencimiento, tenemos un valor de dos dígitos que nos indica el año de vencimiento.
Así, de manera matemática, construimos el nombre del contrato, que es exactamente lo que este bucle hace. Utilizando reglas matemáticas simples y un bucle, construimos el nombre del contrato y verificamos su vigencia. Por lo tanto, es importante seguir la explicación para entender cómo se realiza esto.
Primero, iniciamos tres variables locales en el bucle, que funcionarán como unidades de contabilidad que necesitaremos. El bucle realiza su primera iteración, que, curiosamente, sucede no dentro del cuerpo del bucle, sino en el comando `if`. No obstante, el mismo código que se encuentra dentro del comando `if` podría estar posicionado entre los dos puntos y coma (;) en el bucle `for`, y el bucle funcionaría de manera idéntica. Vamos a entender lo que sucede en esta interacción. Primeramente, creamos el nombre del contrato siguiendo las reglas específicas para su formación. Utilizando la función `StringFormat`, obtenemos el nombre necesario, que será almacenado como el nombre del símbolo, al cual podremos acceder posteriormente. Con el nombre del contrato en manos, hacemos una solicitud al servidor de trading para descubrir una de las propiedades del activo: el momento en que el contrato dejará de ser vigente, utilizando la enumeración `SYMBOL_EXPIRATION_TIME`. La función `SymbolInfoInteger` retornará un valor, pero estamos interesados solo en la fecha. Para filtrar eso, utilizamos nuestra macro, permitiéndonos comparar la fecha de vencimiento con la fecha actual. Si el valor retornado representa una fecha futura, el bucle terminará, pues habremos identificado el contrato más actual en la variable. Sin embargo, es probable que esto no suceda inicialmente, ya que el año comienza en 2000, lo cual es pasado, por lo tanto, una nueva iteración será necesaria. Antes de repetir todo el proceso descrito, necesitamos incrementar la posición para construir un nuevo nombre de contrato. Aquí es necesario cautela, pues ese incremento debe ser hecho primero en el código de vencimiento. Solo si ninguno de los códigos de vencimiento es satisfactorio en ese año es que incrementaremos el año. Esto se realiza en tres etapas, pero en el código, usamos dos operadores ternarios para efectuar ese incremento.
Antes de que el bucle realice una nueva iteración, e incluso antes de la ejecución de los operadores ternarios, incrementamos el valor que indica el carácter del mes de vencimiento. Tras ese incremento, verificamos si el valor está dentro de los límites permitidos, utilizando el primer operador ternario. Así, el índice siempre indicará uno de los valores válidos para el mes de vencimiento. El paso siguiente es la verificación del mes de vencimiento con el segundo operador ternario. Si el índice del mes de vencimiento es cero, esto indica que todos los meses han sido probados, y entonces incrementamos el año actual para una nueva tentativa de localización de un contrato válido, y esa verificación ocurre nuevamente en el comando `if`. Este proceso se repite hasta que un contrato válido sea encontrado, demostrando cómo el sistema busca el nombre del contrato vigente. No se trata de magia, sino de una combinación de matemáticas con programación.
Espero que las explicaciones hayan ayudado a comprender el funcionamiento del código de este procedimiento. A pesar de la complejidad y de la extensión del texto, mi objetivo fue explicar de forma clara para que puedas aplicar este mismo concepto para implementar la funcionalidad para otros contratos futuros, permitiendo su operación vía histórico. Esto es importante porque, independientemente de si los contratos están o no vigentes, el código siempre buscará utilizar el contrato correcto.
Vamos ahora a analizar el próximo código, que se refiere al constructor de nuestra clase, presentado a continuación:
C_Terminal() { m_Infos.ID = ChartID(); CurrentSymbol(); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false); m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint; }
Este código asegura la inicialización correcta de los valores en la estructura de la variable global. Se destacan, quizás, las partes que alteran el comportamiento de la plataforma MetaTrader 5. Así, explicaremos lo que sucede. Estamos instruyendo a la plataforma MetaTrader 5 a no generar descripción de los objetos en el gráfico donde este código sea aplicado. En esta otra línea, instruimos que, siempre que un objeto sea removido del gráfico, la plataforma MetaTrader 5 debe crear un evento notificando cuál objeto fue excluido. Y en esta línea, indicamos la remoción de la escala de tiempo. En esencia, es lo que necesitamos en esta etapa inicial, mientras que las demás líneas se dedicarán a capturar datos e información sobre el activo.
El código subsecuente que examinaremos es el destructor de la clase, presentado a continuación:
~C_Terminal() { ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, true); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false); }
En este código del destructor, restablecemos las condiciones previas a la ejecución del código del constructor de la clase, haciendo que el gráfico vuelva a su estado original. Aunque puede no ser exactamente el estado original, ya que la escala de tiempo se volverá visible nuevamente en el gráfico, la esencia de la idea permanece clara. Ahora, vamos a resolver el problema de que el comportamiento del gráfico sea alterado por la clase cuando el código es removido del gráfico. Para ello, crearemos una pequeña estructura y modificaremos tanto el código del constructor como el del destructor, con el objetivo de realmente revertir el gráfico al estado en el que estaba antes de ser modificado por la clase. Esto se realiza de la siguiente manera:
private : st_Terminal m_Infos; struct mem { long Show_Descr, Show_Date; }m_Mem; //+------------------------------------------------------------------+ public : //+------------------------------------------------------------------+ C_Terminal() { m_Infos.ID = ChartID(); CurrentSymbol(); m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR); m_Mem.Show_Date = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false); m_Infos.nDigits = (int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint; } //+------------------------------------------------------------------+ ~C_Terminal() { ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, false); }
Esta variable global representará la estructura que mantendrá los datos para nosotros. Así, la clase podrá saber cómo estaba el gráfico antes de ser alterado por el código. Aquí, capturamos los datos antes de cambiarlos y, en este punto, devolvemos el gráfico a su estado original, antes de ser modificado por el código de la clase. Noten cómo un simple cambio en el código puede hacer el sistema más agradable y amigable. Vale la pena señalar que la variable global almacenará los datos durante todo el período de vida de la clase. Sin embargo, para comprender esto, no debemos ver la clase solo como un conjunto de códigos. Es esencial pensar en la clase como si fuera un objeto o una variable especial. Cuando se crea, se ejecuta el código del constructor, y cuando se elimina o se vuelve innecesaria, se llama al código del destructor. Esto ocurre automáticamente. Si aún no has comprendido completamente cómo funciona esto, no te preocupes, pues este concepto se volverá tan claro como la luz del día más adelante. Por ahora, entiende lo siguiente: una clase no es solo un montón de código; es, de hecho, algo especial y debe ser tratada como tal.
Antes de concluir, vamos a examinar brevemente otros dos procedimientos. Serán explorados detalladamente en el próximo artículo, pero por ahora, abordaremos al menos la parte concerniente al código. Están escritos a continuación:
//+------------------------------------------------------------------+ inline const st_Terminal GetInfoTerminal(void) const { return m_Infos; } //+------------------------------------------------------------------+ virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { switch (id) { case CHARTEVENT_CHART_CHANGE: m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); break; } } //+------------------------------------------------------------------+
Estos dos procedimientos son especiales en todos los aspectos. Sin embargo, proporcionaré una breve explicación sobre ellos, ya que serán mejor elucidados a medida que se utilicen. Esta función permite que cualquier código fuera del cuerpo de la clase acceda a los datos de la variable global contenida en la clase. Este aspecto será ampliamente explorado a lo largo de todo el código que desarrollaremos. Adoptando este enfoque, aseguramos que no habrá riesgo de alterar los valores de la variable sin que la clase tome conocimiento o lo perciba, ya que usaremos el compilador para ayudarnos a evitar ese tipo de problema. Sin embargo, hay una cuestión aquí, que será abordada en el futuro. Ya este procedimiento se utiliza para actualizar los datos de la clase a medida que el gráfico se modifica. Estos valores serán frecuentemente empleados en otras partes del código cuando vayamos a realizar dibujos en el gráfico. No obstante, al igual que el procedimiento anterior, este será mejor comprendido en el futuro.
Conclusión
A partir de lo que exploramos en este artículo, ya hemos constituido nuestra clase básica C_Terminal. Sin embargo, queda un procedimiento por discutir. Este será abordado en el próximo artículo, donde crearemos la clase C_Mouse. Lo que hemos revisado aquí ya nos habilita para usar la clase para producir algo útil. Aunque no hemos incluido ningún código adjunto, esto se justifica por el hecho de que nuestro trabajo apenas está comenzando. Cualquier código proporcionado ahora no tendría aplicabilidad práctica. En el próximo artículo, nuestro objetivo será crear algo verdaderamente útil, permitiéndonos comenzar el trabajo con el gráfico. Desarrollaremos indicadores y otras herramientas de soporte para operaciones en cuentas DEMO, cuentas REAL y hasta en el Simulador/Repetición. Entonces, nos vemos en el próximo artículo.
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/11328
![Desarrollo de un sistema de repetición (Parte 27): Proyecto Expert Advisor — Clase C_Mouse (I)](https://c.mql5.com/2/58/Projeto_Expert_AdvisoraClasse_C_Mous_Avatar.png)
![Marcado de datos en el análisis de series temporales (Parte 1):Creamos un conjunto de datos con marcadores de tendencia utilizando el gráfico de un asesor](https://c.mql5.com/2/57/data-label-for-time-series-mining-avatar.png)
![Desarrollo de un sistema de repetición (Parte 28): Proyecto Expert Advisor — Clase C_Mouse (I)](https://c.mql5.com/2/58/Replay-p28_II_avatar.png)
![Redes neuronales: así de sencillo (Parte 56): Utilizamos la norma nuclear para incentivar la exploración](https://c.mql5.com/2/57/nuclear_norm_utilization_avatar.png)
![MQL5 - Lenguaje de estrategias comerciales para el terminal de cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- 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