English Русский 中文 Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
Desarrollo de un EA comercial desde cero (Parte 24): Dotando de robustez al sistema (I)

Desarrollo de un EA comercial desde cero (Parte 24): Dotando de robustez al sistema (I)

MetaTrader 5Trading | 3 octubre 2022, 10:06
425 0
Daniel Jose
Daniel Jose

1.0 - Introducción

A diferencia de lo que mucha gente piensa, algunas cosas no son tan sencillas. El sistema de órdenes es una de estas cosas. Incluso ustedes pueden crear un sistema más modesto que les sirva perfectamente, como se hizo en el artículo Desarrollando un EA desde cero en el que se creó un sistema muy básico que incluso puede llegar a ser útil para muchas personas, pero que para otras no lo es lo suficiente. Por ello, hubo un momento en el que todo empezó a cambiar, naciendo así la primera parte de esta secuencia sobre un nuevo sistema de órdenes. Esto se puede ver en el artículo Desarrollo de un EA desde cero (Parte 18). Fue allí donde empezamos a desarrollar un sistema que sería gestionado por EA pero mantenido por MetaTrader 5. En ese sistema la idea era no tener un límite de órdenes en el gráfico. Al principio el sistema parecía bastante atrevido, tengo que confesar que el hecho de crear un sistema donde los objetos no serían mantenidos por el EA sino por MetaTrader 5 me parecía algo sin sentido y poco eficiente.

Pero el sistema se estaba desarrollando, y en el artículo Desarrollando un EA desde cero (Parte 23) hemos elaborado un sistema fantasma para facilitar la gestión de órdenes, posiciones o puntos límite (Take y Stop). Esto fue bastante interesante de desarrollar, e incluso resultó bastante curioso, pero hay un problema. Si ustedes miran la cantidad de objetos usados y visibles en comparación con los objetos mantenidos por MetaTrader 5, se sorprenderán, porque la cantidad mantenida siempre será mayor.

El problema no es tan grave en muchas de las ocasiones, incluso se puede sortear y convivir con algunas cosas, pero hay dos cuestiones que, durante las pruebas, en momentos de gran volatilidad del mercado, hacían que el sistema no fuera muy estable, y en algunos casos, hacían que el operador actuara de forma equivocada. Esto se debe a que en algunos momentos el operador añadía una orden pendiente, el sistema enviaba esa orden al servidor, y el servidor a veces tardaba un poco más de lo normal en contestar, y el sistema simplemente indicaba en algunos momentos que había una orden allí y en otros momentos que no. Y cuando esto se hacía en posiciones (hay una diferencia entre órdenes y posiciones, léase la documentación para más detalles) la cosa era aún más engorrosa, al no saber si el servidor había o no ejecutado el comando como se esperaba.

Hay varias formas de solucionarlo, algunas más sencillas, otras más complejas, pero independientemente de ello, debemos confiar en el EA, de lo contrario no deberíamos utilizarlo bajo ningún concepto.


2.0 - Planificación

La gran cuestión aquí es planificar un sistema que tenga dos cualidades, velocidad y robustez. En algunos tipos de sistemas es bastante complejo conseguir ambas cosas, por no decir imposible. Así que, en muchos casos, intentamos equilibrar las cosas, pero como lo que está en juego es el dinero, NUESTRO dinero, no queremos correr el riesgo de tener un sistema que no tenga estas cualidades. Recordando que estamos tratando con un sistema que funciona en TIEMPO REAL, y este es el escenario más complicado en el que un diseñador se va a meter, ya que siempre hay que tratar de tener un sistema que sea extremadamente rápido, es decir, que responda rápidamente a los eventos, al mismo tiempo que tiene que ser lo suficientemente robusto como para que no se caiga por algún intento de mejorarlo. Así que ustedes mismos pueden ver que el reto es bastante grande.

La velocidad se puede conseguir asegurando que las rutinas se llamen y ejecuten de la forma más adecuada posible, evitando llamadas innecesarias en momentos aún más innecesarios. Con esto tendremos un sistema lo más rápido posible dentro de los límites del lenguaje, aunque si se quiere algo aún más rápido habrá que bajar al nivel del lenguaje máquina, y en este caso hablo de ASSEMBLY, pero esto muchas veces es innecesario, podemos usar el lenguaje C y obtener resultados igual de buenos.

Una forma de conseguir robustez es intentar reutilizar el código lo máximo posible, de esta forma él mismo será probado todo el tiempo y en diversas ocasiones. Pero esta es solo una de las formas, otra forma es el uso de la programación OOP (Object Oriented Programming), si esto se hace de forma correcta y adecuada, con cada clase de objeto sin manipular los datos de las clases de objeto directamente, excepto en el caso de la herencia, ya será suficiente para tener un sistema muy robusto, esto en algunos momentos reduce la velocidad de ejecución, pero esta reducción es tan pequeña que prácticamente puede ser ignorada por el incremento exponencial que genera el encapsulamiento que proporciona la clase, este encapsulamiento nos da la robustez que necesitamos.

Como vemos, no es tan sencillo como parece conseguir un sistema que sea a la vez rápido y robusto. Pero el gran detalle es que no hay que sacrificar tanto, como ustedes se pueden estar imaginando a primera vista, simplemente podemos consultar la documentación del sistema y revisar que se puede cambiar para mejorar las cosas, el simple hecho de no tratar de reinventar la rueda ya será un buen comienzo, recuerden que los programas y sistemas viven en constante mejora, por lo que siempre es bueno tratar de usar lo más posible lo que está disponible, y solo en el último caso reinventar realmente la rueda.

Antes de que a algunos les parezca innecesario presentar los cambios que se hicieron en este artículo, o que piensen que estoy cambiando demasiado el código sin que se salga de su sitio, permítanme aclarar un detalle: cuando codificamos algo no tenemos ni idea de cómo será el código final, lo único que tenemos son los objetivos a conseguir, una vez conseguido el objetivo en mente empezamos a mirar cómo lo hemos hecho para alcanzarlo y empezamos a intentar modificar las cosas, para hacerlas mejor.

En el caso de un sistema comercial, ya sea un ejecutable o una biblioteca, hacemos los cambios y los publicamos como una actualización. El usuario no necesita realmente conocer las formas en que se logra el objetivo, ya que se trata de un sistema comercial. Incluso es bueno que no lo sepa, pero como es un sistema abierto, y en este caso documentado, no quiero dar a entender que podrá desarrollar un sistema extremadamente eficiente desde el principio. Pensar así no es adecuado, es incluso un insulto, ya que, aunque un programador o desarrollador tenga los conocimientos del lenguaje a utilizar, siempre habrá cosas que se puedan mejorar con el tiempo.

Así que no contemplen esta secuencia como algo que se pueda resumir en 3 o 4 artículos, porque si fuera así sería mejor simplemente haber creado el código de la manera que me hubiera parecido más adecuada y haberlo sacado al mercado. Mi intención no es esta. Aprendí a programar viendo el código de otros programadores más experimentados, sé el valor que esto tiene. Saber cómo se desarrolla algo a lo largo del tiempo, es mucho más apropiado que simplemente tomar todo lo que ya está terminado y tratar de entender cómo funciona.

Una vez hechas estas consideraciones, vayamos al grano.


3.0 - Implementación

3.0.1 - Un nuevo modelado de los indicadores de posición

Lo primero que se debe advertir en el nuevo formato del código es el cambio de una rutina que ahora se ha convertido en una macro.

inline string MountName(ulong ticket, eIndicatorTrade it, eEventType ev, bool isGhost = false)
{
        return StringFormat("%s%c%c%c%llu%c%c%c%s", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)(isGhost ? ev + 32 : ev), def_SeparatorInfo, (isGhost ? def_IndicatorGhost : def_IndicatorReal));
}

Incluso si el compilador realmente pone este código en cada uno de los puntos donde él está siendo referenciado gracias a la palabra reservada inline, no quiero dar esto por sentado, ya que esta rutina es llamada muchas veces dentro del código, tengo que garantizar que de hecho se ejecuta tan rápido como sea posible, por lo tanto, su nuevo código se verá así:

#define macroMountName(ticket, it, ev, Ghost) 								 \
		StringFormat("%s%c%llu%c%c%c%c%c%c%c", def_NameObjectsTrade, def_SeparatorInfo,          \                                                                                                                                                                                                                                                                                                
                                                       ticket, def_SeparatorInfo,                        \                                                                                                                                                                                                                                        
                                                       (char)it, def_SeparatorInfo,                      \ 
                                                       (char)(Ghost ? ev + 32 : ev), def_SeparatorInfo,  \ 
                                                       (Ghost ? def_IndicatorGhost : def_IndicatorReal))

Observen que ha habido un cambio entre los datos de la versión antigua de la macro y los datos de esta versión. Esto tiene una razón que se verá más adelante en este artículo.

Pero debido a esta modificación también tenemos que hacer un pequeño cambio en el código de otra rutina.

inline bool GetIndicatorInfos(const string sparam, ulong &ticket, eIndicatorTrade &it, eEventType &ev)
                        {
                                string szRet[];
                                char szInfo[];
                                
                                if (StringSplit(sparam, def_SeparatorInfo, szRet) < 2) return false;
                                if (szRet[0] != def_NameObjectsTrade) return false;
                                ticket = (ulong) StringToInteger(szRet[1]);
                                StringToCharArray(szRet[2], szInfo);
                                it = (eIndicatorTrade)szInfo[0];
                                StringToCharArray(szRet[3], szInfo);
                                ev = (eEventType)szInfo[0];

                                return true;
                        }

El cambio aquí fue sólo en el índice que se utilizará para indicar quién es el ticket y quién es el indicador, nada demasiado complicado, sólo un simple detalle, pero hay que hacerlo, de lo contrario tendríamos datos inconsistentes durante el uso de esta función.

Pero entonces ustedes se preguntan: "¿Por qué estos cambios? ¿No funcionaba ya el sistema perfectamente?" SÍ lo hacía, pero hay cosas que uno no controla realmente, por ejemplo, la simple mejora por parte de los que mantienen y desarrollan la plataforma MetaTrader 5 en algunas de las funciones que no se están utilizando en el EA no hará que nuestro EA se beneficie. La regla es no tratar de reinventar la rueda, sino intentar utilizar los recursos que tenemos a mano. Por eso debemos intentar utilizar siempre las funciones que nos proporciona el lenguaje, que en este caso es MQL5, y evitar crear nuestras propias funciones, lo cual parece absurdo, pero en realidad si uno se detiene, observa y piensa, verá que cada cierto tiempo la plataforma sufre mejoras en algunas funciones, y si uno utiliza esas mismas funciones tendrá un mejor rendimiento y una mayor seguridad en sus programas y esto sin tener que hacer ningún esfuerzo extra.

De este modo, el fin justifica los medios. ¿Pero estos cambios realizados anteriormente ayudarían o EA a beneficiarse de cualquier mejora en la biblioteca MQL5? La respuesta es un rotundo NO, los cambios realizados anteriormente son necesarios para que el modelado de los nombres de los objetos sea el adecuado para que efectivamente podamos beneficiarnos de las posibles mejoras futuras que lleguen por parte de los desarrolladores de MQL5 y de la plataforma MetaTrader 5. Uno de los puntos que podría ser de provecho se muestra a continuación:

inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
                ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));
        else ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)it));
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        m_InfoSelection.bIsMovingSelect = false;
        ChartRedraw();
}

La versión anterior de este mismo código se ve justo debajo, para los que no lo recuerdan, o no lo vieron antes, el código es el siguiente:

inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL)
{
#define macroDestroy(A, B)      {                                                                               \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND, B));                            \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE, B));                              \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE, B));                             \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_EDIT, B));                              \
                if (A != IT_RESULT)     ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_MOVE, B));      \
                else ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_PROFIT, B));                       \
                                }

        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
        {
                macroDestroy(IT_RESULT, true);
                macroDestroy(IT_RESULT, false);
                macroDestroy(IT_PENDING, true);
                macroDestroy(IT_PENDING, false);
                macroDestroy(IT_TAKE, true);
                macroDestroy(IT_TAKE, false);
                macroDestroy(IT_STOP, true);
                macroDestroy(IT_STOP, false);
        } else
        {
                macroDestroy(it, true);
                macroDestroy(it, false);
        }
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
#undef macroDestroy
}

Puede parecer que el código simplemente se hizo más compacto, pero no, esto es lo que encontrará una persona menos capacitada, que sólo hubo una reducción de código, pero la verdad es mucho más profunda, lo que en realidad sucedió fue que se reemplazó el viejo código por uno nuevo que aprovecha mejor los recursos de la plataforma, sin embargo, como el modelado del nombre de los objetos que existía antes no permitía esta mejora, se rehízo el modelado y así podemos contar con la posibilidad de usar una función del propio MQL5. Así, si esta función se mejora por cualquier motivo, el EA se beneficiará de esta modificación, sin tener que hacer ningún cambio en la estructura del EA. La función en cuestión es ObjectsDeleteAll, y si la usamos de la manera correcta, el propio MetaTrader 5 hará la limpieza por nosotros, y no necesitamos dar demasiados detalles, sólo indicamos el prefijo del nombre del objeto, u objetos, y confiamos el resto a MetaTrader 5 para que lo haga. Los puntos en los que se utiliza esta función están resaltados en el nuevo código. Obsérvese cómo se ha realizado el modelado para informar del prefijo a utilizar, esto no era posible antes de la modificación del modelado del nombre del objeto.

Quiero llamar la atención sobre un detalle en el fragmento del nuevo código, que se destaca a continuación.

if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT))
        ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));

¿Por qué añado la parte resaltada? ¡¿Se imaginan por qué?!

El detalle es que, si el sistema crea un ticket que comienza con un valor igual a 1, todos los objetos serán eliminados de la pantalla tan pronto como se realice la orden pendiente. ¡¿No lo han entendido?! La entrada utilizada para colocar la orden pendiente tiene el valor 1, es decir, el indicador 0 realmente tiene el valor 1 y no 0, ya que el 0 se utiliza para realizar otras pruebas dentro del EA, por ello el valor inicial será 1. Ahora tenemos un problema, supongamos que el sistema de trading crea un ticket 1221766803, por lo tanto, el objeto que representará este ticket tendrá como prefijo el siguiente valor: SMD_OT#1221766803, así que cuando el EA ejecute la función ObjectsDeleteAll para eliminar el indicador 0, el nombre de los objetos sería SMD_OT#1, y con ello se eliminarán todos los objetos iniciados con esta información, incluido el sistema recién creado. Para solucionarlo, hago un pequeño ajuste en el nombre a informar a la función ObjectsDeleteAll, con el fin de añadir un carácter extra al final del nombre para que la función sepa si estamos eliminando el indicador 0 u otro indicador.

Así, si el indicador 0 es el que hay que eliminar, la función recibirá el siguiente valor SMD_OT#1#. Esto evitará el problema, al mismo tiempo, en el caso del ejemplo anterior, la función obtendrá el nombre SMD_OT#1221766803*. Parece ser algo simple, pero puede hacer que uno siga tratando de entender por qué el EA está eliminando el indicador de una orden recién colocada.

Ahora un detalle curioso, en la nueva función, observen que al final de la función hay una llamada a ChartRedraw, pero ¿¡por qué está esa llamada ahí!? ¡¿MetaTrader 5 no hace esta actualización por nosotros?! SÍ, lo hace, pero nunca se sabe exactamente cuándo va a ocurrir, y tenemos otro problema, todas las llamadas para colocar o quitar objetos en el gráfico son llamadas síncronas, es decir, se ejecutan en un momento determinado, pero no necesariamente en el que uno se imagina, sino que nuestro sistema de órdenes va a utilizar objetos ya sea para mostrar o gestionar las órdenes, y tenemos que estar seguros de si el objeto está en el gráfico o no cuando vayamos a analizar las cosas, no podemos permitirnos pensar que MetaTrader 5 ya ha puesto o quitado los objetos del gráfico, tenemos que estar seguros de ello, así que forzamos a la plataforma a hacer esta actualización.

De esta manera, cuando hacemos la llamada a ChartRedraw forzamos a la plataforma a actualizar la lista de objetos del gráfico, por lo que podemos estar seguros de que un determinado objeto está o no está presente en el gráfico. Si todavía no entiendes la razón de esto, veamos el siguiente tema.


3.0.2 - Menos objetos equivalen a más velocidad

La rutina que inicializaba los indicadores en la versión anterior era muy pesada. Había muchas pruebas que se repetían y algunas cosas se duplicaban. Además de tener algunos problemas menores, el sistema reutilizaba muy poco la capacidad que ya se había implementado. Así que, para aprovechar el nuevo modelado, decidí reducir la cantidad de objetos que se creaban durante la inicialización. De esta manera la rutina pasó por un corte de lo que es excesivo quedando como se muestra a continuación:

void Initilize(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_OBJECT_DESCR, false);
        ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, false);
        ChartSetInteger(Terminal.Get_ID(), CHART_DRAG_TRADE_LEVELS, false);
        for (int c0 = OrdersTotal(); c0 >= 0; c0--) IndicatorInfosAdd(OrderGetTicket(c0));
        for (int c0 = PositionsTotal(); c0 >= 0; c0--) IndicatorInfosAdd(PositionGetTicket(c0));
}

Parece que era diferente, de hecho, lo era, ahora estamos reutilizando una rutina que estaba siendo infrautilizada, es la rutina de adición de indicadores en el gráfico, vamos a echar un vistazo a esta rutina tan especial.

inline void IndicatorAdd(ulong ticket)
{
        char ret;
                                
        if (ticket == def_IndicatorTicket0) ret = -1; else
        {
                if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_PENDING, EV_LINE, false), OBJPROP_PRICE) != 0) return;
                if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_RESULT, EV_LINE, false), OBJPROP_PRICE) != 0) return;
                if ((ret = GetInfosTradeServer(ticket)) == 0) return;
        }
        switch (ret)
        {
                case  1:
                        CreateIndicatorTrade(ticket, IT_RESULT);
                        PositionAxlePrice(ticket, IT_RESULT, m_InfoSelection.pr);
                        break;
                case -1:
                        CreateIndicatorTrade(ticket, IT_PENDING);
                        PositionAxlePrice(ticket, IT_PENDING, m_InfoSelection.pr);
                        break;
        }
        ChartRedraw();
        UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
}

Fíjense bien en el código de arriba. A primera vista parece contener pruebas que no tienen mucho sentido, pero estas pruebas están ahí por una razón muy simple. Esta rutina es la única vía para que se cree realmente un indicador de orden pendiente o de posición. De este modo, las dos líneas resaltadas comprobarán si el indicador existe. Para ello se comprueba si existe algún valor almacenado en el objeto que se está utilizando como línea, aquí este valor será el valor del precio donde se encuentra el objeto. Este valor sólo será distinto de cero si el indicador está en el gráfico. En todos los demás casos será cero, ya sea porque el indicador no existe, o por cualquier otra razón, esto no importa, el indicador no existe. ¿Han visto por qué tenemos que actualizar forzosamente el gráfico? Si esta actualización forzada no se produjera, el EA estaría añadiendo objetos sin necesidad, por ello no podemos esperar a que la plataforma lo haga en algún momento desconocido, tenemos que estar seguros de que el gráfico se actualizó, de lo contrario, cuando se hagan estas pruebas, informarán de cosas incoherentes con la situación actual de los indicadores, lo que hará que el sistema sea menos fiable.

A pesar de que estas pruebas parecen reducir la velocidad del propio EA, esto es un error conceptual, ya que cuando hacemos dichas pruebas y evitamos tratar de forzar a la plataforma a crear un objeto que posiblemente ya esté en la cola de creación, le decimos a la plataforma "ACTUALIZA AHORA", y luego, cuando lo necesitemos, hacemos una prueba para verificar si el objeto ya fue creado, y en caso de que ya haya sido creado, lo utilizaremos de la manera deseada. Esto es programar de forma correcta. Porque así hacemos que la plataforma trabaje menos, sin que tenga que probar los objetos creados o no, y el EA es más fiable, porque sabemos que tenemos los datos que queremos para trabajar.

Así, como las pruebas indicarán que no hay ningún indicador en el gráfico que corresponda a la entrada informada, se creará el indicador. Pero fíjense que justo al principio hacemos otra prueba, y verificamos que estamos creando el indicador 0 o cualquier otro indicador. Esto asegura que no tenemos ningún objeto innecesario siendo mantenido por MetaTrader 5, sólo tenemos los objetos realmente utilizados actualmente en el gráfico. Por lo tanto, si estamos creando el indicador 0, no es necesario realizar más pruebas, ya que se creará en condiciones muy especiales y específicas. Ese indicador 0 se utiliza para posicionar las órdenes a través de los comandos SHIFT o CTRL más el uso del ratón. Pero no se preocupen, pronto verán cómo él es trabajado y mantenido.

Pero hay un detalle en el código anterior, ¿por qué estoy actualizando el gráfico antes de llamar a la rutina Update!!!? Esto no tiene sentido. Para entenderlo, veamos la rutina UpdateIndicators que está justo debajo.

void UpdateIndicators(ulong ticket, double tp, double sl, double vol, bool isBuy)
{
        double pr;
        bool b0 = false;
                                
        pr = macroGetLinePrice(ticket, IT_RESULT);
        pr = (pr > 0 ? pr : macroGetLinePrice(ticket, IT_PENDING));
        SetTextValue(ticket, IT_PENDING, vol);
        if (tp > 0)
        {
                if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_TAKE, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0))
                        CreateIndicatorTrade(ticket, IT_TAKE);
                PositionAxlePrice(ticket, IT_TAKE, tp);
                SetTextValue(ticket, IT_TAKE, vol, (isBuy ? tp - pr : pr - tp));
        }
        if (sl > 0)
        {
                if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_STOP, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0))
                        CreateIndicatorTrade(ticket, IT_STOP);
                PositionAxlePrice(ticket, IT_STOP, sl);
                SetTextValue(ticket, IT_STOP, vol, (isBuy ? sl - pr : pr - sl));
        }
        if (b0) ChartRedraw();
}

Esta rutina se encargará básicamente de los indicadores de los límites. Ahora fíjense en las dos líneas resaltadas: si el gráfico no se actualiza, estas líneas fallarán, devolviendo un valor de 0, y si esto ocurre todo el resto del código fallará, y los indicadores de límite no se mostrarán correctamente en la pantalla.

Pero tengan en cuenta que incluso antes de crear los indicadores de los límites, hacemos algunas pruebas, para ver si realmente deben ser creados o simplemente ajustados. Y esto se hace de la misma manera que lo hicimos al crear el indicador base. E incluso aquí cuando se crean los objetos de los indicadores, también forzaremos una actualización en el gráfico, de esta manera siempre tendremos el gráfico lo más actualizado posible.

Pero entonces uno se pregunta: "¿Por qué tantas actualizaciones forzadas? ¿¡Esto es realmente necesario!?" Y la respuesta es un GRAN y SANTO ... y la razón es la rutina justo debajo:

inline double SecureChannelPosition(void)
{
        double Res = 0, sl, profit, bid, ask;
        ulong ticket;
                                
        bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID);
        ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK);
        for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
        {
                IndicatorAdd(ticket = PositionGetInteger(POSITION_TICKET));
                SetTextValue(ticket, IT_RESULT, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_PRICE_OPEN));
                sl = PositionGetDouble(POSITION_SL);
                if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
                {
                        if (ask < sl) ClosePosition(ticket);
                }else
                {
                        if ((bid > sl) && (sl > 0)) ClosePosition(ticket);
                }
                Res += profit;
        }
        return Res;
};

Podría pensar que esta rutina no tiene nada de especial. ¡¿Cierto?! ERROR... esta rutina contiene un punto clave, y es que tenemos que asegurarnos de que el indicador está en el gráfico, de lo contrario todo el código que permite su creación será llamado varias veces, creando así una gran cola para que MetaTrader 5 la gestione, y puede ser que algunos de los datos se pierdan o queden demasiado obsoletos, lo que hace que el sistema sea inestable, menos seguro y, en consecuencia, poco fiable. Observemos que tenemos una llamada a la función que crea el indicador, resaltada. Si no estuviéramos forzando a MetaTrader 5 a actualizar el gráfico por nosotros en momentos estratégicos, podríamos tener problemas, ya que esta rutina de arriba es llamada precisamente por el evento OnTick, y en momentos de alta volatilidad el número de llamadas provenientes de OnTick es bastante alto, esto podría generar un exceso de objetos en la cola, lo cual no es nada bueno. Así, forzamos las actualizaciones a través de la llamada ChartRedraw, y probamos a través de ObjectGetDouble, de esta manera reducimos la posibilidad de que se produzca un exceso de objetos en la cola.

Y antes de nada, sin ni siquiera mirar cómo está funcionando el sistema, ya pueden pensar: "Qué bien que ahora, en caso de que el objeto TradeLine se borre accidentalmente, el EA se dé cuenta de ello, y cuando se haga la prueba a través de ObjectGetDouble y ésta falle, el indicador se volverá a crear". Esta es la idea. Pero no es aconsejable que el operador o usuario elimine los objetos presentes en la ventana de la lista de objetos sin saber realmente de qué objeto se trata, ya que, si se elimina cualquier otro objeto que no sea el TradeLine, el EA puede no llegar a echar de menos al indicador, quedando así sin medios para acceder al indicador, ya que simplemente no tiene otra forma de ser accedido más que por los botones presentes en él.

El escenario anterior sería una verdadera pesadilla si no fuera por la rutina que viene justo después, y que es la responsable de mantener todo el flujo de mensajes dentro de la clase, sin embargo, no es el único punto de entrada, y sí, estoy hablando de la rutina DispatchMessage, así que vamos a echarle un vistazo.

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong   ticket;
        double  price;
        bool    bKeyBuy,
                bKeySell,
                bEClick;
        datetime        dt;
        uint            mKeys;
        char            cRet;
        eIndicatorTrade it;
        eEventType      ev;
                                
        static bool bMounting = false, bIsDT = false;
        static double valueTp = 0, valueSl = 0, memLocal = 0;
                                
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        Mouse.GetPositionDP(dt, price);
                        mKeys   = Mouse.GetButtonStatus();
                        bEClick  = (mKeys & 0x01) == 0x01;    //Clique esquerdo
                        bKeyBuy  = (mKeys & 0x04) == 0x04;    //SHIFT Pressionada
                        bKeySell = (mKeys & 0x08) == 0x08;    //CTRL Pressionada
                        if (bKeyBuy != bKeySell)
                        {
                                if (!bMounting)
                                {
                                        Mouse.Hide();
                                        bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
                                        valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
                                        valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
                                        m_InfoSelection.it = IT_PENDING;
                                        m_InfoSelection.pr = price;
                                }
                                m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
                                m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
                                m_InfoSelection.bIsBuy = bKeyBuy;
                                if (!bMounting)
                                {
                                        IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
                                        m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
                                        m_InfoSelection.bIsMovingSelect = bMounting = true;
                                }
                                MoveSelection(price);
                                if ((bEClick) && (memLocal == 0))
                                {
                                        RemoveIndicator(def_IndicatorTicket0);
                                        CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
                                }
                        }else if (bMounting)
                        {
                                RemoveIndicator(def_IndicatorTicket0);
                                Mouse.Show();
                                memLocal = 0;
                                bMounting = false;
                        }else if ((!bMounting) && (bKeyBuy == bKeySell))
                        {
                                if (bEClick) SetPriceSelection(price); else MoveSelection(price);
                        }
                        break;
                case CHARTEVENT_OBJECT_DELETE:
                        if (GetIndicatorInfos(sparam, ticket, it, ev))
                        {
                                if (GetInfosTradeServer(ticket) == 0) break;
                                CreateIndicatorTrade(ticket, it);
                                if ((it == IT_PENDING) || (it == IT_RESULT))
                                        PositionAxlePrice(ticket, it, m_InfoSelection.pr);
                                ChartRedraw();
				m_TradeLine.SpotLight();
                                m_InfoSelection.bIsMovingSelect = false;
                                UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        ReDrawAllsIndicator();
                        break;
                case CHARTEVENT_OBJECT_CLICK:
                        if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev)
                        {
                                case EV_CLOSE:
                                        if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it)
                                        {
                                                case IT_PENDING:
                                                case IT_RESULT:
                                                        if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                                                        break;
                                                case IT_TAKE:
                                                case IT_STOP:
							m_InfoSelection.ticket = ticket;
							m_InfoSelection.it = it;
                                                        m_InfoSelection.bIsMovingSelect = true;
                                                        SetPriceSelection(0);
                                                        break;
                                        }
                                        break;
                                case EV_MOVE:
                                        if (m_InfoSelection.bIsMovingSelect)
                                        {
                                                m_TradeLine.SpotLight();
                                                m_InfoSelection.bIsMovingSelect = false;
                                        }else
                                        {
                                                m_InfoSelection.ticket = ticket;
                                                m_InfoSelection.it = it;
                                                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0))
                                                m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
                                        }
                                        break;
                        }
                        break;
        }
}

Esta rutina pasó por tantos cambios que de hecho tendré que dividirla en partes más pequeñas para explicar lo que sucede dentro de ella, porque si ya tienen experiencia en programación, no tendrán problemas para entender lo que ella hace, pero si son entusiastas o aspirantes a programadores usando el lenguaje MQL5, puede ser un poco confuso entender esta rutina al principio, así que lo explicaré con calma en el siguiente tema.


3.0.3 - Desglosemos la rutina DispatchMessage

Este tema será la explicación de lo que sucede en la rutina DispatchMessage, si ya ustedes se han hecho una idea de cómo funciona con sólo mirar el código, este tema no les aportará nada.

Lo primero que tenemos después de las variables locales, son las variables estáticas.

static bool bMounting = false, bIsDT = false;
static double valueTp = 0, valueSl = 0, memLocal = 0;

Podrían declararse simplemente como variables privadas dentro de la clase, pero como sólo se utilizarán en este punto del código, no tiene sentido que otras rutinas de la clase vean esas variables. No obstante, debido a esto, tenemos que declararlas como estáticas porque estas deberían recordar sus valores cuando la rutina sea llamada de nuevo, si no añadimos la palabra clave static en las declaradas, y perderían sus valores tan pronto como la rutina termine. Hecho esto empezaremos a manejar los eventos que MetaTrader 5 le indica al EA.

Hecho esto empezaremos a manejar los eventos que MetaTrader 5 le indica al EA. El primer evento que vamos a abordar comienza a verse a continuación:

case CHARTEVENT_MOUSE_MOVE:
        Mouse.GetPositionDP(dt, price);
        mKeys   = Mouse.GetButtonStatus();
        bEClick  = (mKeys & 0x01) == 0x01;    //Clique esquerdo
        bKeyBuy  = (mKeys & 0x04) == 0x04;    //SHIFT Pressionada
        bKeySell = (mKeys & 0x08) == 0x08;    //CTRL Pressionada

Lo que hacemos aquí es capturar y aislar los datos del ratón y de algunas teclas (presentes en el teclado) vinculadas al ratón. Una vez hecho esto, introducimos un largo conjunto de código que comienza con una prueba.

if (bKeyBuy != bKeySell)

Si ustedes presionan la tecla SHIFT o CTRL, pero no ambas al mismo tiempo, esta prueba hará que el EA imagine que ustedes quieren colocar una orden a un determinado precio. Si este es el caso, haremos una nueva prueba.

if (!bMounting)
{
        Mouse.Hide();
        bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl);
        valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
        valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol);
        m_InfoSelection.it = IT_PENDING;
        m_InfoSelection.pr = price;
}

En el caso de que no se haya ensamblado todavía el indicador 0, esta prueba será superada, y al hacerlo se ocultará el ratón, luego se capturarán los valores presentes en el Chart Trade, se hará la conversión de estos valores a puntos en función del nivel de apalancamiento que el operador informará en el Chart Trade, y se indicará un valor inicial en el que se colocará la orden. Esta secuencia sólo ocurrirá una vez, en cada ciclo de uso.

Lo siguiente es crear los niveles de take y stop, e indicar si vamos a comprar o vender.

m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp));
m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl);
m_InfoSelection.bIsBuy = bKeyBuy;

Estos se crean fuera del círculo, porque al mover el ratón a otro rango de precios tendremos que mover también el take y el stop. Pero, ¿por qué este código de arriba no está dentro de la prueba de ensamble? La razón es que si uno cambia, suelta la tecla SHIFT y pulsa la tecla CTRL, o viceversa, sin mover el ratón, esto con los indicadores en la pantalla, los valores de los indicadores de take y stop se intercambiarán, para evitar esto este fragmento tiene que quedar fuera de la prueba. Pero esto nos obliga a hacer una nueva prueba de ensamble, que se ve a continuación:

if (!bMounting)
{
        IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0);
        m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false));
        m_InfoSelection.bIsMovingSelect = bMounting = true;
}

¿Pero por qué dos pruebas? ¿No podríamos hacer la cosa con una sola prueba? Bueno, esto sería lo ideal, pero la rutina resaltada en el código anterior no nos permite hacerlo así, miren IndicatorAdd para entender este hecho. Una vez realizada la creación del indicador 0, lo ponemos como seleccionado, y mostramos que ya está iniciado y ensamblado. Así que podemos moverlo usando la siguiente línea.

MoveSelection(price);

Pero aún dentro de este mismo criterio de pulsar SHIFT o CTRL para colocar una orden pendiente, tenemos una última etapa.

if ((bEClick) && (memLocal == 0))
{
        RemoveIndicator(def_IndicatorTicket0);
        CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price,  price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT);
}

Esta hará que se añada una orden pendiente en el punto exacto al que estamos apuntando, es necesario que se respeten dos condiciones, la primera es que se pulse el botón izquierdo, y la segunda es que no lo hayamos hecho sobre el mismo precio a la vez, es decir, que para colocar dos o más órdenes al mismo precio, hay que estar posicionando esta nueva orden con otra llamada, porque en la misma llamada esto no va a ocurrir. 

Al mismo tiempo que se elimina el indicador 0 del gráfico, se envía una orden al servidor comercial, con todos los parámetros debidamente rellenados.

Ahora vamos al siguiente paso...

if (bKeyBuy != bKeySell)
{

// ... código descrito até o momento ....

}else if (bMounting)
{
        RemoveIndicator(def_IndicatorTicket0);
        Mouse.Show();
        memLocal = 0;
        bMounting = false;
}

Ahora, si se ha ensamblado el indicador 0, pero no se cumple la condición de que sólo se haya pulsado SHIFT o CTRL, se ejecutará el código resaltado, con lo que se eliminará el indicador 0 de la lista de objetos, a la vez que se reiniciará el ratón y las variables estáticas quedarán en su estado inicial. En otras palabras, el sistema estará limpio.

El siguiente y último paso dentro del evento del ratón de la rutina se muestra a continuación:

if (bKeyBuy != bKeySell)
{

// ... código já descrito ....

}else if (bMounting)
{

// ... código já descrito ...

}else if ((!bMounting) && (bKeyBuy == bKeySell))
{
        if (bEClick) SetPriceSelection(price); else MoveSelection(price);
}

El código resaltado es el último paso del trabajo con el ratón dentro del procesamiento de mensajes. En el caso de que no tengamos el indicador 0 ensamblado, ni las teclas SHIFT o CTRL en diferente estado, lo que significa que pueden ser pulsadas o soltadas al mismo tiempo, tenemos el siguiente comportamiento, si hacemos clic con el botón izquierdo del ratón, el precio será enviado al indicador, y si sólo movemos el ratón, el precio será utilizado para mover el indicador. Pero entonces uno se pregunta: ¿qué indicador? Tranquilos que pronto veremos qué indicador, pero por si tienen curiosidad el indicador 0 no utiliza este fragmento resaltado. Si no lo entienden vuelvan al principio de este tema y lean cómo funciona este tratamiento de mensajes.

El siguiente mensaje que consideraremos se ve a continuación:

case CHARTEVENT_OBJECT_DELETE:
        if (GetIndicatorInfos(sparam, ticket, it, ev))
        {
                if (GetInfosTradeServer(ticket) == 0) break;
                CreateIndicatorTrade(ticket, it);
                if ((it == IT_PENDING) || (it == IT_RESULT))
                        PositionAxlePrice(ticket, it, m_InfoSelection.pr);
                ChartRedraw();
		m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
                UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy);
        }
        break;

¿Ustedes se acuerdan cuando les dije que el EA tenía un pequeño sistema de seguridad para evitar que los indicadores fueran borrados indebidamente? Pues bien, este sistema se encuentra en el código para el tratamiento de mensajes de eventos que envía MetaTrader 5 cuando se elimina un objeto.

Cuando esto ocurre, MetaTrader 5 informa mediante el parámetro sparam cuál es el nombre del objeto eliminado, por lo que se comprueba si era un indicador, y si es así, cuál. Fíjense que no importa qué objeto fue, lo que queremos saber es qué indicador fue afectado, después de eso comprobaremos si hay alguna orden o posición relacionada con el indicador, si es así, volveremos a crear todo el indicador, en un caso extremo de que el indicador afectado fuera el indicador base, ahora lo volveremos a posicionar inmediatamente, y forzaremos a MetaTrader 5 a colocar el indicador en el gráfico inmediatamente, independientemente de qué indicador fuera, quitaremos la indicación de selección de cualquier indicador seleccionado, y haremos una petición de actualización en los datos de límite del indicador. 

El siguiente evento a procesar es extremadamente sencillo, simplemente hace una petición para redimensionar todos los indicadores de la pantalla, su código se ve a continuación.

case CHARTEVENT_CHART_CHANGE:
        ReDrawAllsIndicator();
        break;

El siguiente evento a tratar es el evento de clic en un objeto.

case CHARTEVENT_OBJECT_CLICK:
        if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev)
        {
//....
        }
        break;

Esto comienza como se ve arriba, donde MetaTrader 5 nos dice qué objeto recibió el clic, para que el EA pueda probar y verificar qué tipo de evento debe manejar. Hasta ahora tenemos 2 eventos CLOSE y MOVE, veamos primero el evento CLOSE que cerrará y determinará el final de un indicador en la pantalla.

case EV_CLOSE:
        if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it)
        {
                case IT_PENDING:
                case IT_RESULT:
                        if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket);
                        break;
                case IT_TAKE:
                case IT_STOP:
			m_InfoSelection.ticket = ticket;
			m_InfoSelection.it = it;
                        m_InfoSelection.bIsMovingSelect = true;
                        SetPriceSelection(0);
                        break;
        }
        break;

El evento de cierre hará lo siguiente: Utilizará el ticket para buscar conjuntamente con el servidor qué es lo que hay que cerrar, y si de hecho tenemos algo que cerrar, porque puede ocurrir que en el momento en que se vaya a cerrar algo, el servidor ya lo haya hecho, pero el EA aún no haya sido informado de ello. Como tenemos algo que debe cerrarse, hagámoslo de la manera correcta, así tenemos algunas pruebas y una manera correcta de informar a la clase para que cierre o elimine el indicador del gráfico.

Así llegamos al último paso de este tema, que se ve a continuación.

case EV_MOVE:
        if (m_InfoSelection.bIsMovingSelect)
        {
                m_TradeLine.SpotLight();
                m_InfoSelection.bIsMovingSelect = false;
        }else
        {
                m_InfoSelection.ticket = ticket;
                m_InfoSelection.it = it;
                if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0))
                m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false));
        }
        break;

El evento MOVE es el que realmente hará que la selección del indicador se mueva según los movimientos del ratón. La indicación se hace aquí, pero el movimiento se ejecuta durante un evento de movimiento del ratón. Recuerden que al principio del tema dije que había una condición en la que no nos ocupábamos del indicador 0 y que aun así algo se movería, pues bien, ese algo se indica en este punto, en el evento move, aquí probaremos si hay algo seleccionado para mover, si es así, el indicador que estaba seleccionado dejará de estarlo y no recibirá los eventos de movimiento del ratón, y se seleccionará el nuevo indicador. En este caso los datos del nuevo indicador que recibirá los datos del ratón serán almacenados en una estructura, y este indicador recibirá un cambio que indica que él será seleccionado, este cambio se ve en el grosor de la línea, dándole así cierto realce.


3.0.4 - Una nueva clase objeto Mouse

Además de las mejoras que hemos visto anteriormente, también tenemos otras que merecen ser destacadas.

Aunque la mayoría de los operadores no sienten la necesidad de tener el sistema de indicación que se implementa en el EA y que se apoya en el uso del ratón, otros pueden necesitar y querer tener este sistema funcionando perfectamente, pero por algún desliz puede ocurrir que en algún momento el operador acabe borrando alguno de los objetos que forman parte del indicador del ratón, lo que provoca que funcione mal o deje de funcionar del todo. Pues bien, afortunadamente podemos evitar esto utilizando el sistema de EVENTOS, y en cuanto se detecte un evento de borrado de objeto y se envíe al EA, la clase a la que pertenece el objeto podrá recrear el objeto dejando el sistema estable. Pero es bueno dejar el menor número posible de objetos en la lista y crearlos a medida que se necesiten y luego eliminarlos cuando ya no sean necesarios. Eso es lo que hemos hecho hasta ahora, pero faltaba la clase Mouse para que todo el sistema fuera lo más ligero posible.

Empezaremos creando algunas definiciones, esto reemplaza el sistema de crear nombres constantes para las variables.

#define def_MousePrefixName "MOUSE "
#define def_NameObjectLineH def_MousePrefixName + "H"
#define def_NameObjectLineV def_MousePrefixName + "TMPV"
#define def_NameObjectLineT def_MousePrefixName + "TMPT"
#define def_NameObjectBitMp def_MousePrefixName + "TMPB"
#define def_NameObjectText  def_MousePrefixName + "TMPI"

Hecho esto, la nueva rutina de inicialización es la siguiente:

void Init(color c1, color c2, color c3)
{
        m_Infos.cor01 = c1;
        m_Infos.cor02 = c2;
        m_Infos.cor03 = c3;
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_MOUSE_MOVE, true);
        ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false);
        Show();
}

Fíjense que es mucho más sencillo que la versión anterior, pero no le faltará nada. En este punto tenemos la llamada que mostrará el sistema del ratón, esto se hace en el punto resaltado en el código anterior, que llamará al código que realmente creará el sistema de indicación en el eje del precio.

inline void Show(void)
{
        if (ObjectGetDouble(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_PRICE) == 0)
        {
                ObjectCreate(Terminal.Get_ID(), def_NameObjectLineH, OBJ_HLINE, 0, 0, 0);
                ObjectSetString(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_TOOLTIP, "\n");
                ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_BACK, false);
        }
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, m_Infos.cor01);
}

Este código es muy curioso, porque lo que estamos haciendo es comprobar si el objeto indicador del ratón existe o no en el precio. Si la prueba tiene éxito significa que la línea o cualquier cosa relacionada con el ratón existe en el gráfico, de esta manera todo lo que hacemos es ajustar el color de la línea horizontal. Pero, ¿por qué hacemos esta prueba? Para entenderlo tenemos que ver la rutina responsable de ocultar, o mejor dicho, de quitar los objetos adheridos al ratón. Veamos la rutina a continuación:

inline void Hide(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        ObjectsDeleteAll(Terminal.Get_ID(), def_MousePrefixName + "T");
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
}

Aquí tenemos un estilo de funcionamiento extremadamente curioso. Todos los objetos vinculados al ratón y que poseen un nombre prefijado serán eliminados del gráfico por MetaTrader 5, de esta manera la lista de objetos será siempre muy pequeña. Pero la línea horizontal no se eliminará, sólo se le cambiará el color, por eso la rutina que muestra el ratón hace una prueba antes de crear el objeto, porque en realidad no se excluye de la lista de objetos, sólo se oculta, pero todos los demás objetos se eliminan de la lista de objetos. Pero entonces, ¿cómo vamos a utilizar estos otros objetos que se utilizan en los estudios? Dado que los estudios son momentos de corta duración en los que simplemente se quieren conocer algunos detalles, no tiene sentido mantener los objetos en la lista sólo para ser utilizados 1 o 2 veces, es mejor crearlos, hacer el estudio y luego eliminarlos de la lista, de esta manera tendremos un sistema más robusto.

Puede parecer una tontería hacer esto, pero el sistema de órdenes que os estoy enseñando a crear se basa en el uso de objetos, y cuantos más objetos tengamos en la lista mayor será el trabajo de MetaTrader 5 para buscar en la lista cuando queramos acceder a un objeto concreto, de esta forma no dejaremos objetos innecesarios en el gráfico o en la lista de objetos, mantengamos el sistema lo más ligero posible.

De esta manera tenemos nuestra atención puesta en la rutina DispatchMessage, que se inicia como se muestra a continuación:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        int     w = 0;
        uint    key;
        static int b1 = 0;
        static double memPrice = 0;

Justo después tenemos el código que comenzará a procesar el primer evento.

switch (id)
{
        case CHARTEVENT_MOUSE_MOVE:
                Position.X = (int)lparam;
                Position.Y = (int)dparam;
                ChartXYToTimePrice(Terminal.Get_ID(), Position.X, Position.Y, w, Position.dt, Position.price);
                ObjectMove(Terminal.Get_ID(), def_NameObjectLineH, 0, 0, Position.price = Terminal.AdjustPrice(Position.price));
                if (b1 > 0) ObjectMove(Terminal.Get_ID(), def_NameObjectLineV, 0, Position.dt, 0);
                key = (uint) sparam;
                if ((key & 0x10) == 0x10)    //Botão do meio....
                {
                        CreateObjectsIntern();
                        b1 = 1;
                }

Cuando pulsamos el botón central del ratón, generamos una llamada. Pero este no es el caso ahora, más adelante veremos lo que hace la rutina. Observen que estamos intentando mover un objeto que no existe, ya que no está en la lista de objetos que mantiene MetaTrader 5. Esta llamada sólo se producirá cuando se pulse el botón central del ratón, fíjense en la variable b1, ella controla en qué momento el operador está dentro del conjunto que interviene en la generación del estudio.

Una vez que el operador pulsa el botón izquierdo del ratón, y se completa el primer paso, tendremos el siguiente fragmento en ejecución:

if (((key & 0x01) == 0x01) && (b1 == 1))
{
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 0, Position.dt, memPrice = Position.price);
        b1 = 2;
}

Él posicionará una línea de tendencia y llamará al siguiente paso con el cambio del valor de la variable b1. En este punto pasaremos al siguiente fragmento.

if (((key & 0x01) == 0x01) && (b1 == 2))
{
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 1, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineT, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectMove(Terminal.Get_ID(), def_NameObjectBitMp, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectBitMp, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
        ObjectSetString(Terminal.Get_ID(), def_NameObjectText, OBJPROP_TEXT, StringFormat("%.2f ", Position.price - memPrice));
        ObjectMove(Terminal.Get_ID(), def_NameObjectText, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
}

Este fragmento de arriba es lo que realmente mostrará los estudios en la pantalla, todos estos objetos que están en este fragmento no existirán cuando el estudio termine, serán creados y destruidos dentro de esta rutina. Aunque parece que no es muy eficiente hacer esto, no noté ninguna disminución o aumento en el tiempo de procesamiento durante la fase de estudio, de hecho, noté una ligera mejora en el sistema de órdenes, algo muy sutil, que está prácticamente dentro del margen de error de una evaluación comparativa, por lo que no puedo decir que estos cambios realmente trajeron mejoras en el punto de vista del procesamiento.

Pero fíjense que el estudio se realizará mientras el ratón esté con el botón izquierdo pulsado, una vez que se suelte, ejecutaremos el siguiente fragmento.

if (((key & 0x01) != 0x01) && (b1 == 2))
{
        b1 = 0;
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
        Hide();
        Show();
}
Position.ButtonsStatus = (b1 == 0 ? key : 0);

En tal fragmento eliminaremos de la lista de objetos todos los objetos utilizados para crear el estudio, y volveremos a mostrar la línea del ratón en la pantalla. Lo genial de la idea es el código resaltado, que evita que cualquier función o rutina dentro de EA reciba una falsa indicación cuando capturamos los botones del ratón. Si se está haciendo algún estudio, el estado de los botones debe ser ignorado por el EA, y para ello, utilizamos esta línea resaltada, no es una solución perfecta, pero es mejor que nada.

Bueno, pero faltaba mostrar el código que crea los objetos para ejecutar los estudios, pero como es una rutina bastante sencilla no le daré énfasis aquí en el artículo.


4.0 - Conclusión

Aunque parezca una tontería, todos los cambios realizados aquí suponen en realidad una gran diferencia en el propio sistema. Hay que recordar una cosa: Nuestro sistema de órdenes se basa en objetos gráficos en la pantalla, por lo tanto, cuantos más objetos tenga que procesar el EA menor será su rendimiento durante alguna consulta que tenga que hacer buscando algún objeto concreto. Y para que todo sea aún más complejo, estamos ante un sistema que se ejecuta en tiempo real, es decir, cuanto más rápido sea el sistema de nuestro EA, mejor será su rendimiento. Por lo tanto, cuantas menos cosas tenga que hacer el EA de hecho, mejor, lo ideal sería que pudiera ocuparse sólo y únicamente del sistema de órdenes, y todo lo demás se pusiera en otro nivel, y MetaTrader 5 se encargara de ellas. Pero esto lo haremos, por supuesto poco a poco, ya que tendremos que hacer muchos pequeños cambios, pero nada demasiado complicado, y esto se hará en el transcurso de los próximos artículos, muy probablemente no en uno centrado exclusivamente en dar más robustez al EA.

Pero una cosa es cierta, en el futuro el EA será responsable sólo del sistema de órdenes. En el próximo artículo dejaremos el EA de una forma muy interesante, reduciremos aún más el número de objetos presentes en la lista durante el funcionamiento del EA, ya que el gran generador de objetos es el sistema de órdenes, mostraré cómo cambiar este sistema de forma que se reduzca al máximo la carga que genera a MetaTrader 5.

Debido a esto, en este artículo no dejaré ninguna parte o modificación adjunta, ya que el código en sí seguirá sufriendo cambios, sin embargo, no se preocupen, valdrá mucho la pena esperar al próximo artículo. Los cambios elevarán en gran medida el rendimiento general de nuestro EA... así que, nos vemos en el próximo artículo de esta serie.


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

Desarrollo de un EA comercial desde cero (Parte 25): Dotando de robustez al sistema (II) Desarrollo de un EA comercial desde cero (Parte 25): Dotando de robustez al sistema (II)
Aquí terminaremos de dar un empujón en el rendimiento del EA... así que prepárense para una larga lectura. Lo primero que haremos para que nuestro EA sea robusto es eliminar del código todo y absolutamente todo lo que no forme parte del sistema comercial.
Trabajamos con matrices y vectores en MQL5 Trabajamos con matrices y vectores en MQL5
Para resolver problemas matemáticos, se han añadido a MQL5 matrices y vectores. Los nuevos tipos tienen métodos incorporados para escribir un código conciso y fácilmente comprensible que se acerque a una notación matemática. Los arrays son algo bueno, pero las matrices, en muchos casos, resultan mejores.
Desarrollo de un EA comercial desde cero (Parte 26): Rumbo al futuro (I) Desarrollo de un EA comercial desde cero (Parte 26): Rumbo al futuro (I)
Llevaremos nuestro sistema de órdenes al siguiente nivel, pero primero tenemos que resolver algunas cosas. Y es que ahora tenemos cuestiones que dependen de cómo queremos operar y de qué tipo de cosas hacemos durante la jornada de tráding.
DoEasy. Elementos de control (Parte 10): Objetos WinForms: dando vida a la interfaz DoEasy. Elementos de control (Parte 10): Objetos WinForms: dando vida a la interfaz
Ha llegado el momento de revitalizar la interfaz gráfica de usuario, haciendo que los objetos interactúen con el usuario y otros objetos. Y para que los objetos más complejos funcionen correctamente, necesitaremos una funcionalidad que permita a los objetos interactuar entre sí y con el usuario.