English Русский 中文 Deutsch 日本語 Português
preview
Cómo construir un EA que opere automáticamente (Parte 05): Gatillos manuales (II)

Cómo construir un EA que opere automáticamente (Parte 05): Gatillos manuales (II)

MetaTrader 5Trading | 26 enero 2023, 09:59
585 0
Daniel Jose
Daniel Jose

Introducción

En el artículo anterior, Cómo construir un EA que opere automáticamente (Parte 04): Gatillos manuales (I). He demostrado cómo tú, con la ayuda de un poco de programación, podrías enviar órdenes de mercado y dejar órdenes en el libro, utilizando para ello el conjunto teclado-mouse.

Al final del artículo anterior, pensé que sería apropiado permitir el uso del EA de forma manual, al menos durante un tiempo. Esto resultó ser mucho más interesante de lo que se pretendía, ya que la idea inicial era hacer 3 o 4 artículos, que mostrasen cómo, de hecho, hacer para desarrollar una EA que pueda operar de forma automática. Aunque esto es bastante sencillo para los programadores, resulta muy complicado para los aficionados que quieren aprender a programar, ya que hay poco material disponible que explique de forma clara cómo programar realmente determinadas cosas. Y limitarse a un nivel de conocimientos no es algo que una persona deba hacer.

Y como muchos pueden estar utilizando estos artículos, presentes en esta comunidad, para comenzar a aprender a programar, vi una oportunidad para compartir un poco de mi experiencia, adquirida a lo largo de los años programando en C/C++ y mostrar cómo hacer algunas cosas en MQL5, que es bastante similar a C/C++. Quiero desmitificar esa idea que muchos tienen sobre lo que realmente significa programar.

Bien, para que nuestro EA funcione de manera más cómoda en modo manual, necesitamos hacer algunas cosas. Para aquellos que son programadores, esto es súper sencillo y fácil de hacer, por lo que podemos ir directamente al punto, que es crear las líneas que indican dónde estarán los límites de la orden que se enviará en el servidor de negociación.

Estos límites son más apropiados para ser visualizados cuando estás usando el mouse para posicionar las órdenes, es decir, cuando estás creando una orden pendiente. Una vez que la orden ya esté en el servidor, la indicación es manejada por la plataforma MetaTrader 5. Pero antes de que esto suceda, necesitamos mostrar al usuario dónde es probable que se coloquen y posicionen los límites de la orden. Esto es realizado por nosotros los programadores. El único soporte que MetaTrader 5 nos da es la posibilidad de usar líneas horizontales en el gráfico. Además de esto, todo el trabajo debe ser realizado a través de la programación del EA.

Para hacer esto, simplemente necesitamos programar un código que coloque esas líneas en el gráfico en las posiciones correctas. Pero no queremos hacerlo de cualquier manera. Queremos hacerlo de una forma controlada, ya que no queremos comprometer el código que ya se ha creado y no queremos tener trabajo en caso de que necesitemos y tengamos que retirar la clase C_Mouse y el manejador de eventos OnChartEvent de nuestro EA en el futuro. Esto es porque un EA automático no necesita estas cosas, pero un EA manual sí. Necesita estas cosas para ser mínimamente utilizable. 


Creación de una clase C_Terminal

Para hacer esto, generamos algún tipo de comodidad para las operaciones manuales. Necesitaremos agregar las líneas que indican los límites probables de una orden o posición que será enviada, y, aprovechando, haremos la eliminación de códigos que están siendo repetidos tanto en la clase C_Orders, como en la clase C_Mouse. Así nacerá una nueva clase: la clase C_Terminal, que nos ayudará a construir y a aislar algunas cosas, lo que nos dará todo el soporte que necesitamos para poder trabajar de la manera más cómoda posible. Al hacer uso de esta clase, podremos, en el futuro, crear tanto un EA automático como uno manual sin correr el riesgo de generar algún tipo de falla catastrófica en nuestro nuevo EA.

El mayor problema es que muchas veces, cuando se quiere crear un nuevo EA automático, se hace desde cero. Este tipo de actitud suele generar muchos errores nuevos, ya que el código a menudo no ha sido probado adecuadamente.

Es cierto que sería interesante convertir estas clases en una biblioteca particular, pero como el objetivo actual no es ese, pensaré en ello. Tal vez lo haga en el futuro. Pero ahora, veamos lo que de hecho vamos a hacer. Empezando con lo siguiente: Creamos como de costumbre un archivo de cabecera, llamado C_Terminal.mqh. Este comenzará con el código más básico de todos, y siempre presente en toda clase que se cree, este código puede verse a continuación:

class C_Terminal
{
        private :
        public  :
};

Siempre inicializa tu código de una clase de esta manera, así nunca te olvidarás de que existen puntos que deben ir en la sección privada y otros que pueden ir en la sección pública. Incluso si no tienes nada privado dentro de la clase, es siempre bueno dejar las cosas claras. Principalmente porque puedes mostrar tu código a otras personas.

Un código bien delimitado y bien escrito, es decir, fácil de leer, con toda seguridad atraerá el interés de otras personas para analizarlo, en caso de que necesites ayuda para corregir algún problema. Un código todo desordenado, sin ningún tipo de organización, sin usar tabulaciones y a menudo sin una explicación a través de comentarios, hace que el código sea poco interesante, incluso si la idea es buena, nadie realmente querrá perder tiempo organizando el código para entender lo que está haciendo.

Entonces ese es el consejo. No es que mis códigos sean la maravilla de la perfección, pero: Organiza tus códigos siempre, usa tabulaciones siempre que necesites colocar varias líneas y que estén anidadas dentro de un procedimiento único, esto ayuda mucho. No solo a otras personas, sino principalmente a ti, ya que a veces un código está tan desorganizado que ni siquiera el creador puede entenderlo, ¿qué dirá otro programador?

Bueno, pero vamos a empezar a codificar, agregando una estructura a nuestra clase. Estas primeras líneas de código se pueden ver a continuación.

class C_Terminal
{
        protected:
//+------------------------------------------------------------------+
                struct stTerminal
                {
                        ENUM_SYMBOL_CHART_MODE ChartMode;
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                };
//+------------------------------------------------------------------+
        private :
        public  :
};

Aquí tenemos una novedad, una palabra reservada llamada protected ¿y qué nos dice? Normalmente solo usamos declaraciones públicas y privadas, ¿pero qué es esta? Bueno, esta estaría en un término medio entre lo que es público y lo que es privado. Para entender esto realmente, necesitas entender algunos conceptos básicos sobre la programación orientada a objetos.

Uno de estos conceptos es la Herencia. Pero antes de entrar en el tema de la herencia, necesitas entender el tema de la clase, a nivel individual. Para que realmente puedas entenderlo, piensa que cada clase sería un individuo, un ser vivo, único y exclusivo. Ahora podemos pasar a la explicación.

Algunas informaciones son públicas, lo que permite a cualquiera, además del propio individuo que las mantiene, beneficiarse de su uso y conocimiento. Este tipo de cosas siempre se pondrá en una cláusula pública. Otras informaciones son privadas para el individuo, es decir, solo él las tiene. Cuando él deja de existir, estas informaciones desaparecen, y dicho individuo es el único que puede en realidad beneficiarse de ellas. Piensa en ellas como una habilidad personal, el individuo no puede enseñar ni pasar esto a nadie más, y nadie puede quitárselo. Este tipo de información se encuentra en la cláusula privada.

Pero existen informaciones que no encajan en ninguno de estos conceptos, y estas se encuentran en la cláusula protegida, es decir, el individuo puede o no hacer uso de ellas. Lo principal es que pueden ser transmitidas solo a los miembros de su linaje. Y para entender cómo esto sucede, es necesario entrar en el tema de la herencia.

Cuando entramos en el tema de la herencia, la manera más sencilla de entenderlo es pensando en líneas de sangre. Existen 3 tipos de herencia: la herencia pública, la herencia privada y la herencia protegida, aquí estoy hablando de herencia, no más de cuestiones individuales, de cada miembro del linaje.

En una herencia pública, toda la información, datos y contenido de los padres se pasan a los hijos y a toda su descendencia, incluso nietos y más allá, y todos fuera de la línea de sangre, en teoría, pueden acceder a estas cosas pasadas. Presta atención a esto, es en teoría, ya que existen algunos matices en esta transmisión. Pero luego veremos esto con más calma, vamos a enfocarnos primero en la herencia. Ahora, en una herencia privada, solo la primera generación tendrá acceso a la información pasada, la próxima generación no podrá acceder a esta información, incluso siendo parte de la línea de sangre.

Y en el último caso, tenemos la herencia protegida. Esta generará algo muy similar a la herencia privada. Pero tenemos un agravante, que hace que muchas personas no entiendan estos conceptos: La cláusula de los padres. Esto es porque existe una regla de transmisión, incluso en casos de herencia pública. Algunas cosas no se pueden acceder fuera de la línea de sangre. Para entender esto, ve la tabla a continuación, en la que muestro de manera resumida esta cuestión:

Definición en la clase Padre Tipo de herencia Acceso desde la clase Hijo Acceso llamando a la clase Hijo 
private public Acceso denegado No se puede acceder a datos o procedimientos de la clase base
public public Acceso permitido Acceso permitido a los datos o procedimientos de la clase base
protected public Acceso permitido No se puede acceder a datos o procedimientos de la clase base
private private Acceso denegado No se puede acceder a datos o procedimientos de la clase base
public private Acceso permitido No se puede acceder a datos o procedimientos de la clase base
protected private Acceso permitido No se puede acceder a datos o procedimientos de la clase base
private protected Acceso denegado No se puede acceder a datos o procedimientos de la clase base
public protected Acceso permitido  No se puede acceder a datos o procedimientos de la clase base
protected protected Acceso permitido No se puede acceder a datos o procedimientos de la clase base

Tabla 1) Sistema de herencia basado en la definición de la información

Ten en cuenta que, dependiendo de la cláusula utilizada en la definición de un tipo de información en el momento de la herencia, el hijo puede o no tener acceso a esos datos, pero cualquier llamada fuera de la línea de sangre no tendrá acceso, salvo en un caso único, que es cuando los datos del padre son declarados como públicos y el hijo hereda de forma igualmente pública. Aparte de esto, no es posible acceder a ninguna información fuera de la línea de sangre.

Por no entender este esquema, mostrado en la tabla 01, muchos programadores con menos experiencia simplemente desdeñan el tema de la programación orientada a objetos. Pero esto es debido a una falta de conocimiento de cómo funcionan realmente las cosas. Quienes han estado siguiendo mis artículos y han observado mis códigos, deberían haber notado que hago un uso masivo de la programación orientada a objetos.

Esto es porque nos da un nivel de seguridad en la implementación de cosas muy complicadas, que no sería posible hacer de otra manera, además de que aquí estoy hablando solo de lo que rige la herencia. Además, también tenemos el polimorfismo y el encapsulamiento, pero estos son temas para otro momento. A pesar de que el encapsulamiento es parte de la tabla 01, merece una explicación más detallada, pero esto se saldría del enfoque de este artículo.

Bien, entonces continuemos. Si prestas atención, notarás que la estructura que se puede ver en el código anterior es la misma presente en la clase C_Orders. Ten en cuenta esto, ya que la clase C_Order perderá la definición de estos datos dentro de ella y comenzará a heredar estos datos de la clase C_Terminal. Pero por ahora, sigamos dentro de la clase C_Terminal.

La próxima cosa a añadir a la clase C_Terminal son las funciones que tanto la clase C_Mouse como la clase C_Orders comparten. Estas funciones serán añadidas dentro de la cláusula protegida de la clase C_Terminal, de esta manera, cuando las clases C_Mouse y C_Orders hereden de C_Terminal, estas funciones y procedimientos seguirán la tabla 01. Los códigos que añadirás se pueden ver a continuación:

//+------------------------------------------------------------------+
inline double AdjustPrice(const double value)
                        {
                                return MathRound(value / m_TerminalInfo.PointPerTick) * m_TerminalInfo.PointPerTick;
                        }
//+------------------------------------------------------------------+
inline double FinanceToPoints(const double Finance, const uint Leverage)
                        {
                                double volume = m_TerminalInfo.VolMinimal + (m_TerminalInfo.VolStep * (Leverage - 1));
                                
                                return AdjustPrice(MathAbs(((Finance / volume) / m_TerminalInfo.AdjustToTrade)));
                        };
//+------------------------------------------------------------------+

Es decir, ahora estos mismos códigos dejarán de estar duplicados en ambas clases, quedando solo y exclusivamente dentro de la clase C_Terminal, facilitando así su mantenimiento, pruebas y posibles correcciones. De esta manera, nuestro código se volverá cada vez más robusto y atractivo al momento de ser utilizado y expandido.

Hay algunas otras cosas que debes ver dentro de la clase C_Terminal, pero primero vamos a ver el constructor de la clase. Puedes verlo en el código de abajo:

        public  :
//+------------------------------------------------------------------+
                C_Terminal()
                        {
                                m_TerminalInfo.nDigits          = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_TerminalInfo.VolMinimal       = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_TerminalInfo.VolStep          = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_TerminalInfo.PointPerTick     = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_TerminalInfo.ValuePerPoint    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_TerminalInfo.AdjustToTrade    = m_TerminalInfo.ValuePerPoint / m_TerminalInfo.PointPerTick;
                                m_TerminalInfo.ChartMode        = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE);
                        }
//+------------------------------------------------------------------+

Observa que es prácticamente idéntico al que existía en la clase C_Orders. Entonces, ahora la clase C_Orders podrá tener su código modificado para heredar lo que estamos haciendo en la clase C_Terminal. Pero hay un detalle en esta historia. Mira el código donde se declara la estructura que se está inicializando en el constructor anterior. Verás que no hay ninguna variable allí. ¿Por qué?

La razón es el encapsulamiento, no debes permitir que códigos fuera de la clase accedan y así puedan modificar el contenido de las variables internas de la clase. Esto es un grave error de programación, a pesar de que el compilador no se quejará, NUNCA debes permitirlo. Toda y cualquier variable global de la clase siempre debe ser declarada dentro de la cláusula privada. De esta manera, la declaración de la variable queda como se muestra a continuación.

//+------------------------------------------------------------------+
        private :
                stTerminal m_TerminalInfo;
        public  :
//+------------------------------------------------------------------+

Observa que la variable global de la clase está definida entre las cláusulas private y public. De esta manera, ella será inaccesible para cualquier clase que herede de C_Terminal, es decir, estamos garantizando el encapsulamiento de la información y al mismo tiempo, estamos añadiendo herencia en nuestro código. Esto hará que el nivel de robustez del mismo aumente de manera exponencial mientras su utilidad se expande.

Pero entonces, puedes pensar: ¿Cómo accederemos a los datos que necesitamos en las clases de arriba? ¡¡¡Necesitamos dar algún nivel de acceso a las variables de la clase padre, que en este caso es la clase C_Terminal!!! Sí, necesitamos eso, pero no debemos hacerlo poniendo las variables como públicas o incluso protegidas. Esto es un error de programación, debes agregar algún medio para que las clases derivadas puedan acceder a los valores de la clase padre. Pero aquí reside un peligro y esto es importante, NO DEBES PERMITIR DE NINGUNA MANERA QUE LAS CLASES DERIVADAS PUEDAN MODIFICAR LAS VARIABLES DE LA CLASE PADRE

Para ello, es necesario convertir de algún modo una variable en una constante. Es decir, la clase padre puede modificar el valor de las variables, de la forma que necesite, cuando lo necesite, y si alguna clase hijo quiere hacer un cambio en alguna variable de la clase padre, el hijo debe llamar a algún procedimiento que la clase padre proporcionará, con el fin de decir lo que debería ser el valor deseado para algún tipo de variable presente en la clase padre. Dicho procedimiento, que debe ser implementado en la clase padre, comprobará si los datos pasados por el hijo son de alguna manera válidos, y si este es el caso, el procedimiento dentro de la clase padre promoverá los cambios solicitados por el hijo.

Pero nunca, y nunca, un hijo puede cambiar los datos del padre sin que la clase padre se entere del cambio. He visto mucho código potencialmente peligroso que hace esto. He visto muchos códigos potencialmente peligrosos que hacen esto, ya que muchas veces algunos dicen que, al llamar a un procedimiento dentro de la clase padre, para validar los datos proporcionados por el hijo, hace que el código sea más lento y hace que el programa sea poco eficiente en su ejecución. Pero esto es una tontería, el costo y el riesgo de enviar valores incorrectos dentro de la clase padre no valen el ligero aumento de velocidad que se promoverá al no hacer la llamada del procedimiento para validar los datos. No caigas en esto de que esto hará que el código sea lento.

De esta manera, entramos en otro punto. Tú como programador, y deseando convertirte en un profesional, debes siempre dar preferencia a colocar cualquier procedimiento, que será heredado por otras clases, primero dentro de una cláusula protegida, y solo en último caso, pasar el procedimiento a la cláusula pública. Esto es debido a que siempre priorizamos el encapsulamiento. Solo si realmente es necesario, dejamos el encapsulamiento y permitimos el uso público de funciones y procedimientos. Pero nunca haremos esto con variables, estas siempre deben ser privadas.

Con el objetivo de crear un procedimiento o función que permita a la clase hija acceder a los datos de la clase padre, viene la función de abajo:

inline const stTerminal GetTerminalInfos(void) const
                        {
                                return m_TerminalInfo;
                        }

Ahora quiero que prestes mucha atención a lo que voy a explicar, porque esto es extremadamente importante y marca toda la diferencia entre un código bien escrito y uno meramente bien hecho.

Durante este artículo, he dicho que tenemos que, de alguna manera, permitir que el código fuera de la clase, en la que las variables están siendo declaradas y utilizas, acceda a ellas. Dije que lo ideal sería que, dentro de la clase donde se declara la variable, ella se pueda modificar siempre que sea necesario. Pero fuera de la clase, la variable debe ser tratada como una constante, es decir, no se puede modificar su valor.

Este sencillo código de arriba, a pesar de ser extremadamente simple, puede hacer precisamente eso. Es decir, puede asegurar que dentro de la clase C_Terminal tenemos una variable accesible y que puede tener su valor modificado, pero fuera de la clase, esta misma variable será vista como una constante. ¿Y cómo pude hacer esto, y por qué tenemos dos palabras reservadas const aquí?

Vayamos por partes: La primera palabra const dice al compilador que la variable m_TerminalInfo que será devuelta debe ser tratada como una constante en el llamador de la función. Es decir, si el llamador, intenta modificar el valor de alguno de los miembros de la estructura presente en la variable devuelta, el compilador debe generar un error e impedir que el código sea compilado. Mientras que la segunda palabra const le dice al compilador que si aquí en este código, por una razón u otra, algún valor es modificado, él compilador debe generar un error. Por lo tanto, no podrás modificar, aunque quieras, ningún dato dentro de esta función, esta sólo existe para devolver un valor.

Algunos programadores a veces cometen este tipo de error, modificando valores de variables dentro de funciones o procedimientos, en los cuales estas variables deben ser usadas solo para acceso externo, no para algún tipo de factorización. Utilizando este tipo de programación, que se muestra arriba, se evita este tipo de error.

Bien, ya que aún no hemos terminado nuestra clase básica C_Terminal, podemos eliminar las partes duplicadas en el código, haciendo la clase C_Mouse con el mismo tipo de código que la clase C_Orders. Pero como el cambio en la clase C_Mouse es mucho más simple, veamos cómo se verá ahora que hereda la clase C_Terminal, esto se puede ver en el siguiente código:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Terminal.mqh"
//+------------------------------------------------------------------+
#define def_MouseName "MOUSE_H"
//+------------------------------------------------------------------+
#define def_BtnLeftClick(A)     ((A & 0x01) == 0x01)
#define def_SHIFT_Press(A)      ((A & 0x04) == 0x04)
#define def_CTRL_Press(A)       ((A & 0x08) == 0x08)
//+------------------------------------------------------------------+
class C_Mouse : private C_Terminal
{
// Código interno da classe ....
};

Aquí, estamos incluyendo el archivo de cabecera de la clase C_Terminal, observe que aquí el nombre del archivo está entre comillas dobles. Esto es para decirle al compilador que el archivo C_Terminal.mqh se encuentra en el mismo directorio que el archivo C_Mouse.mqh. De esta forma, si necesitas mover ambos archivos a otra ubicación, el compilador siempre encontrará la ubicación correcta ya que para el compilador ambos estarán en el mismo directorio.

Ahora siguiendo con la idea de empezar siempre las cosas dando el mínimo acceso posible, hacemos que la clase C_Mouse herede de forma privada a la clase C_Terminal. Ahora ya puedes eliminar la función AdjustPrice de la clase C_Mouse, así como la variable PointPerTick, presente en la clase C_Mouse, porque ahora estará utilizando el procedimiento presente en la clase C_Terminal, y como la clase fue heredada privadamente y la función AdjustPrice está dentro de la cláusula protegida, en la clase C_Terminal, tendrás el resultado de la tabla 01. Así pues, no será posible llamar al procedimiento AdjustPrice fuera de la clase C_Mouse, como se hacía antes.

Pero estos cambios en la clase C_Mouse son por ahora. Haremos algunos otros para añadir las líneas de delimitación que necesitemos cuando utilicemos EA de forma manual. Pero no te preocupes por esto por ahora. Veamos cómo hacer cambios mucho más profundos en la clase C_Orders, que, por lo tanto, merece un texto hablando sólo y únicamente sobre ella. Así que pasemos al siguiente tema.


Modificación de la clase C_Orders tras heredar la clase C_Terminal

Iniciamos los cambios casi de la misma forma que se hizo en la clase C_Mouse. Pero aquí empiezan las diferencias, como puede verse en el siguiente código.

#include "C_Terminal.mqh"
//+------------------------------------------------------------------+
class C_Orders : private C_Terminal
{
        private :
//+------------------------------------------------------------------+
                MqlTradeRequest m_TradeRequest;
                ulong           m_MagicNumber;
                struct st00
                {
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                        bool    PlotLast;
                        ulong   MagicNumber;
                }m_Infos;
//+------------------------------------------------------------------+

Todo el principio es casi igual que la clase C_Mouse, pero aquí empiezan a aparecer las diferencias, primero, iremos quitando la estructura de la clase C_Orders, tal y como se muestra con las líneas resaltadas. Pero necesitamos uno de los datos desde dentro de esta estructura, así que lo haremos privado, pero como una variable normal.

Al eliminar las partes resaltadas, es posible que pienses que será mucho trabajo volver a codificar. En realidad, será muy poco trabajo, pero de inmediato vamos directamente al código del constructor de esta clase C_Orders. Y la razón es que el cambio empezará realmente ahí. A continuación, puedes ver el aspecto del nuevo código del constructor.

                C_Orders(const ulong magic)
                        :C_Terminal(), m_MagicNumber(magic)
                        {
                                m_Infos.MagicNumber     = magic;
                                m_Infos.nDigits         = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_Infos.VolMinimal      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_Infos.VolStep         = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_Infos.PointPerTick    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_Infos.ValuePerPoint   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_Infos.AdjustToTrade   = m_Infos.ValuePerPoint / m_Infos.PointPerTick;
                                m_Infos.PlotLast        = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST);
                        };

Muy bien, como puedes ver, todo el contenido interno del constructor fue eliminado, pero aquí estamos forzando la llamada del constructor de la clase C_Terminal. Esto es para asegurar que se llama antes que cualquier otra cosa. Normalmente el compilador lo hace por nosotros, pero aquí lo estamos haciendo explícitamente y, al mismo tiempo, estamos inicializando la variable que indica el número mágico en otro punto del código

Normalmente esto se hace en los constructores, porque queremos que una variable tenga su valor definido, antes de que se ejecute cualquier código, de esta forma el compilador generará un código adecuado. Pero si el valor es constante, como normalmente será, ganamos algo de tiempo en la inicialización de la clase C_Orders al hacer esto. Pero recuerda el siguiente detalle: Sólo tendrás algún beneficio si el valor es una constante, de lo contrario el compilador generará un código que no nos aportará ningún valor práctico.

Lo siguiente que hay que hacer es eliminar las funciones AdjustPrice y FinanceToPoints de la clase C_Orders, pero, como esto se puede hacer directamente, no lo mostraré aquí. A partir de ahora, estas llamadas utilizarán código dentro de la clase C_Terminal.

Ahora veamos algo de código que utilizará la variable que se está declarando en la clase C_Terminal. Y de esta forma entenderemos, a partir de ahora, cómo acceder a las variables de una clase padre. Para saber cómo se hará, puedes ver el siguiente código:

inline void CommonData(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double Desloc;
                                
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.magic            = m_Infos.MagicNumber;
                                m_TradeRequest.magic            = m_MagicNumber;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.volume           = NormalizeDouble(GetTerminalInfos().VolMinimal + (GetTerminalInfos().VolStep * (Leverage - 1)), GetTerminalInfos().nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, GetTerminalInfos().nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), GetTerminalInfos().nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), GetTerminalInfos().nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.stoplimit        = 0;
                                m_TradeRequest.expiration       = 0;
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                        }

Las partes resaltadas han sido removidas y en su lugar han venido otros códigos que están en destaque, pero lo que realmente quiero es que prestes atención a los códigos destacados en amarillo. Estos tienen algo que muchos quizás nunca hayan visto realmente. Observa que en estos códigos en amarillo tenemos la presencia de una función que está siendo tratada como si fuera una estructura. ¡Pero qué locura es esta! 😵😱

Tranquilo, amigo lector. Tranquilo. No es ninguna locura. Es solo el uso de la programación de una forma un poco más exótica de lo que se ve normalmente. Para entender por qué esto es permitido y por qué funciona, vamos a separar la función en un fragmento, como se muestra a continuación:

GetTerminalInfos().nDigits

Ahora quiero que regresen al código de la clase C_Terminal y veamos cómo esta función está declarada. Esto se puede ver justo debajo:

inline const stTerminal GetTerminalInfos(void) const
                        {
                                return m_TerminalInfo;
                        }

Observen que la función GetTerminalInfos está devolviendo una estructura, esta estructura se ve en el fragmento a continuación:

                struct stTerminal
                {
                        ENUM_SYMBOL_CHART_MODE ChartMode;
                        int     nDigits;
                        double  VolMinimal,
                                VolStep,
                                PointPerTick,
                                ValuePerPoint,
                                AdjustToTrade;
                };

Entonces, para el compilador, lo que estamos haciendo al usar el código GetTerminalInfos().nDigits, sería equivalente a decir que GetTerminalInfos() no es una función, sino una variable 😲. ¿Te has quedado confuso? Pues las cosas se ponen aún más interesantes, ya que, para el compilador, el código GetTerminalInfos().nDigits sería equivalente al siguiente código:

stTerminal info;
int value = info.nDigits;

value = 10;
info.nDigits = value;

Es decir, puedes tanto leer un valor como también escribir un valor. Entonces, si por casualidad escribieras el siguiente fragmento:

GetTerminalInfos().nDigits = 10;

El compilador entendería que se debe colocar el valor 10 en la variable que está siendo referenciada por la función GetTerminalInfos(). Y esto sería un problema, ya que la variable que está siendo referenciada está en la clase C_Terminal y esta variable está declarada en una cláusula privada. Es decir, no podría ser modificada por la llamada hecha anteriormente. Pero debido a que la función GetTerminalInfos() está como protegida (pero también podría estar como pública y sería lo mismo), la variable declarada como privada tiene el mismo nivel de acceso que la función que la está referenciando.

¿Vieron lo peligrosas que pueden ser las cosas? Es decir, aunque declares una variable como privada, pero no codifiques de manera adecuada las funciones o procedimientos que están referenciándola, tú o cualquier otra persona pueden modificar su valor sin querer. Y esto rompe todo el concepto de encapsulamiento.

Pero debido a que durante la declaración de la función fue iniciada con la palabra const, esto cambia las cosas, ya que ahora el compilador verá la función GetTerminalInfos() de una manera diferente. Para entenderlo, sólo necesitarás intentar usar el código debajo en cualquier punto de la clase C_Orders:

GetTerminalInfos().nDigits = 10;

Si intentas hacer esto, el compilador generará un error. Ya que, para el compilador, GetTerminalInfos().nDigits o cualquier otra cosa dentro de la estructura que GetTerminalInfos() está referenciando se considera una constante y no es posible modificar el valor de una constante. Esto se considera un error.

¿Entiendes ahora cómo referenciar datos constantes utilizando una variable? Es decir, para la clase C_Terminal, la estructura referenciada por la función GetTerminalInfos() es una variable, pero para cualquier otra parte del código, la estructura será una constante 😁.

Muy bien, ahora que ya he explicado esta parte, continuemos la conversión, ya que ahora creo que podrás entender lo que está sucediendo y de dónde vienen los datos referenciados en la clase C_Orders. La próxima función a ser modificada se ve justo debajo:

                ulong CreateOrder(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double  bid, ask;
                                
                                bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
                                bid = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : SYMBOL_BID));
                                ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                ask = (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                CommonData(type, AdjustPrice(Price), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_PENDING;
                                m_TradeRequest.type     = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                    (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));                              
                                
                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

Y la última función a ser modificada se ve a continuación:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action   = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order    = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), GetTerminalInfos().nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), GetTerminalInfos().nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), GetTerminalInfos().nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), GetTerminalInfos().nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), GetTerminalInfos().nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

Ahora finalizamos esta parte. Con esto, nuestro código continúa con el mismo nivel de estabilidad visto anteriormente, pero en términos de robustez, acabamos de mejorarlo. Ya que no tenemos más funciones duplicadas, corriendo el riesgo de que en una clase sea modificada mientras en otra clase permanezca igual. Y si ocurriera algún tipo de error, no sería tan fácil de corregirlo, ya que podríamos corregirlo en una clase, pero permanecería en la otra, dejando el código menos robusto y confiable.

Siempre piensa en esto: un poco de trabajo para mejorar tu código en términos de estabilidad y robustez nunca es trabajo, solo es un pasatiempo 😁.

Pero aún no hemos terminado lo que vinimos a hacer en este artículo. Te acuerdas de que queremos agregar las líneas de límite de precio, esos puntos de take profit y stop loss, para tener una idea durante la colocación de una orden pendiente, de manera totalmente manual, aún falta este punto para finalmente terminar este artículo y pasar a la próxima etapa. Pero para separar las cosas de lo que se ha visto hasta ahora, vamos a crear un nuevo tema aquí.


Creación de las líneas take profit y stop loss

Ahora tenemos una pregunta, necesitamos pensar un poco: ¿Dónde sería más adecuado colocar el código de estas líneas? Bueno, ya tenemos el punto de llamada. Y se puede ver justo debajo:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        uint            BtnStatus;
        double  Price;
        static double mem = 0;
        
        (*mouse).DispatchMessage(id, lparam, dparam, sparam);
        (*mouse).GetStatus(Price, BtnStatus);
        if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL))
        {
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))  (*manager).ToMarket(ORDER_TYPE_BUY, user03, user02, user01, user04);
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL, user03, user02, user01, user04);
        }
        if (def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus))
        {
// This point ...
                if (def_BtnLeftClick(BtnStatus) && (mem == 0)) (*manager).CreateOrder(def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, mem = Price, user03, user02, user01, user04);
        }else mem = 0;
}

La región marcada en amarillo es el punto donde deberemos colocar la llamada para mostrar las líneas de take profit y stop loss. Pero el detalle es, ¿dónde debemos codificar estas líneas?

La mejor alternativa, y con toda certeza creo que todos estarán de acuerdo, es colocar el código en la clase C_Mouse. Así cuando eliminemos el mouse, las líneas también se irán. Entonces, esto es lo que haremos. Vamos a la clase C_Mouse, para crear las líneas que representarán el take profit y el stop loss.

Pero haré algo un poco diferente a lo imaginado antes. No agregaré las líneas en el evento OnChartEvent, sino en el manejador de eventos dentro de la clase C_Mouse. De esta manera quedará mejor, a pesar de tener que hacer algunos otros cambios en el código del EA, pero eso lo dejaremos para después. Vamos entonces al archivo de cabecera C_Mouse.mqh e implementemos lo que necesitamos.

La primera cosa que haremos es agregar algunas nuevas definiciones, según se muestra a continuación:

#define def_PrefixNameObject    "MOUSE_"
#define def_MouseLineName       def_PrefixNameObject + "H"
#define def_MouseLineTake       def_PrefixNameObject + "T"
#define def_MouseLineStop       def_PrefixNameObject + "S"
#define def_MouseName           "MOUSE_H"

Observen que la antigua definición ha sido eliminada. Así podremos hacer el trabajo de una manera un poco diferente, pero que sea agradable de programar. Y para reducir el trabajo de programación, vamos a modificar el procedimiento de creación como vemos más abajo:

                void CreateLineH(void)
                void CreateLineH(const string szName, const color cor)
                        {
                                ObjectCreate(m_Infos.Id, def_MouseName, OBJ_HLINE, 0, 0, 0);
                                ObjectSetString(m_Infos.Id, def_MouseName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_BACK, false);
                                ObjectSetInteger(m_Infos.Id, def_MouseName, OBJPROP_COLOR, m_Infos.Cor);
                                ObjectCreate(m_Infos.Id, szName, OBJ_HLINE, 0, 0, 0);
                                ObjectSetString(m_Infos.Id, szName, OBJPROP_TOOLTIP, "\n");
                                ObjectSetInteger(m_Infos.Id, szName, OBJPROP_BACK, false);
                                ObjectSetInteger(m_Infos.Id, szName, OBJPROP_COLOR, cor);
                        }

Ahora todas las líneas serán creadas de manera única, solo necesitando informar el nombre y el color de las mismas. Tuve que crear 2 variables más para almacenar los colores, pero creo que no es necesario mostrarlo aquí. Entonces pasemos al constructor, ya que ahora necesitará recibir muchos más datos de lo que antes, como puedes ver abajo:

                C_Mouse(const color corPrice, const color corTake, const color corStop, const double FinanceStop, const double FinanceTake, const uint Leverage)
                        {
                                m_Infos.Id        = ChartID();
                                m_Infos.CorPrice  = corPrice;
                                m_Infos.CorTake   = corTake;
                                m_Infos.CorStop   = corStop;
                                m_Infos.PointsTake= FinanceToPoints(FinanceTake, Leverage);
                                m_Infos.PointsStop= FinanceToPoints(FinanceStop, Leverage);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_MOUSE_MOVE, true);
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, true);
                                CreateLineH(def_MouseLineName, m_Infos.CorPrice);
                        }

Como informé, fue necesario crear algunas variables más, pero el costo de hacer esto es bajo en comparación con lo que podremos ganar en términos de posibilidades. Observa un hecho aquí: No voy a esperar a que el EA llame para convertir los valores financieros en puntos, ya lo haremos aquí y ahora. De esta manera, ahorraremos tiempo después, ya que es más rápido acceder a una variable que llamar a una función. Pero, ¿qué pasa con el destructor? En realidad, no es más complicado. Todo lo que tuve que hacer en él fue cambiar el tipo de función responsable de eliminar los objetos, como se puede ver en el siguiente código:

                ~C_Mouse()
                        {
                                ChartSetInteger(m_Infos.Id, CHART_EVENT_OBJECT_DELETE, false);
                                ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject);
                                ObjectDelete(m_Infos.Id, def_MouseName);
                        }

Esta función tiene la capacidad de eliminar todos los objetos cuyo nombre comienza de una manera determinada. Esto es muy útil y extremadamente versátil en varias situaciones, lo que nos ahorra mucho tiempo. Así llegamos a la última rutina que necesita ser modificada, entonces veamos cómo implementé las líneas de límite de precio, y esto se puede ver en el código de abajo:

                void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
                        {
                                int w;
                                datetime dt;
                                static bool bView = false;
                                
                                switch (id)
                                        {
                                                case CHARTEVENT_OBJECT_DELETE:
                                                        if (sparam == def_MouseName) CreateLineH();
                                                        if (sparam == def_MouseLineName) CreateLineH(def_MouseLineName, m_Infos.CorPrice);
                                                        break;
                                                case CHARTEVENT_MOUSE_MOVE:
                                                        ChartXYToTimePrice(m_Infos.Id, (int)lparam, (int)dparam, w, dt, m_Infos.Price);
                                                        ObjectMove(m_Infos.Id, def_MouseName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price));
                                                        ObjectMove(m_Infos.Id, def_MouseLineName, 0, 0, m_Infos.Price = AdjustPrice(m_Infos.Price));
                                                        m_Infos.BtnStatus = (uint)sparam;
                                                        if (def_CTRL_Press(m_Infos.BtnStatus) != def_SHIFT_Press(m_Infos.BtnStatus))
                                                        {
								if (!bView)
								{
									if (m_Infos.PointsTake > 0) CreateLineH(def_MouseLineTake, m_Infos.CorTake);
									if (m_Infos.PointsStop > 0) CreateLineH(def_MouseLineStop, m_Infos.CorStop);
									bView = true;
								}
								if (m_Infos.PointsTake > 0) ObjectMove(m_Infos.Id, def_MouseLineTake, 0, 0, m_Infos.Price + (m_Infos.PointsTake * (def_SHIFT_Press(m_Infos.BtnStatus) ? 1 : -1)));
								if (m_Infos.PointsStop > 0) ObjectMove(m_Infos.Id, def_MouseLineStop, 0, 0, m_Infos.Price + (m_Infos.PointsStop * (def_SHIFT_Press(m_Infos.BtnStatus) ? -1 : 1)));
                                                        }else if (bView)
                                                        {
                                                                ObjectsDeleteAll(m_Infos.Id, def_PrefixNameObject);
                                                                bView = false;
                                                        }
                                                        ChartRedraw();
                                                        break;
                                        }
                        }

Primero, tuve que eliminar dos líneas de código antiguo, pero esto ya estaba previsto, y en su lugar vinieron otras dos con el código actualizado. Pero el gran detalle comienza en el momento en que trataremos el evento de movimiento del mouse, en el que agregamos algunas nuevas líneas, lo primero que hacemos es una prueba para verificar si las teclas SHIFT o CTRL están presionadas, pero no al mismo tiempo, si esto es verdad, pasamos a la próxima etapa.

Ahora, si es falso, verificamos si las líneas límite están siendo representadas en el gráfico. Si es así, eliminamos todas las líneas del mouse, pero no es un problema ya que inmediatamente el MetaTrader 5 genera un evento que alerta de que se han eliminado los objetos de la pantalla. Al llamar al manejador de eventos de la pantalla, serás llevado a volver a colocar la línea de precio en el gráfico.

Sin embargo, vamos a volver al punto en el que las líneas límite serán representadas si estás presionando la tecla SHIFT o CTRL. En este caso, verificaremos si las líneas ya están en la pantalla y, si no es así, las crearemos, siempre y cuando el valor sea mayor que cero, ya que no queremos un elemento extraño en el gráficoMarcamos esto como hecho para no intentar recrear estos objetos en cada llamada, y luego las posicionaremos en su lugar correspondiente, dependiendo de dónde se encuentre la línea de precio.


Conclusión

De esta manera, creamos un sistema de asesor experto (EA) para ser operado de forma totalmente manual, y estaremos listos para la próxima etapa, que veremos en el próximo artículo. Allí, añadiremos un gatillo para que el sistema pueda hacer algo de forma automática. Una vez hecho esto, te mostraré lo que necesitarás para transformar este EA, que actualmente opera de forma manual, en uno que opere de forma totalmente automática. En el próximo artículo, se abordará cómo automatizar el EA, eliminando las decisiones humanas de la operación.


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

Archivos adjuntos |
Cómo construir un EA que opere automáticamente (Parte 06): Tipos de cuentas (I) Cómo construir un EA que opere automáticamente (Parte 06): Tipos de cuentas (I)
Aprenda a crear un EA que opere automáticamente de forma sencilla y segura. Hasta ahora nuestro EA puede funcionar en cualquier tipo de situación, pero aún no está listo para ser automatizado, por lo que tenemos que hacer algunas cosas.
Cómo construir un EA que opere automáticamente (Parte 04): Gatillos manuales (I) Cómo construir un EA que opere automáticamente (Parte 04): Gatillos manuales (I)
Aprenda a crear un EA que opere automáticamente de forma sencilla y segura.
Algoritmos de optimización de la población: Enjambre de partículas (PSO) Algoritmos de optimización de la población: Enjambre de partículas (PSO)
En este artículo, analizaremos el popular algoritmo de optimización de la población «Enjambre de partículas» (PSO — particle swarm optimisation). Con anterioridad, ya discutimos características tan importantes de los algoritmos de optimización como la convergencia, la tasa de convergencia, la estabilidad, la escalabilidad, y también desarrollamos un banco de pruebas y analizamos el algoritmo RNG más simple.
Cómo construir un EA que opere automáticamente (Parte 03): Nuevas funciones Cómo construir un EA que opere automáticamente (Parte 03): Nuevas funciones
Aprenda a crear un EA que opere automáticamente de forma sencilla y segura. En el artículo anterior, comenzamos a desarrollar el sistema de órdenes que se va a utilizar en el EA automático. Sin embargo, solo construimos una de las funciones o procedimientos necesarios.