Múltiples indicadores en un gráfico (Parte 06): Convirtamos el MetaTrader 5 en un sistema RAD (II)

Daniel Jose | 12 mayo, 2022

1.0 - Introducción

En el artículo anterior mostré cómo crear un Chart Trade utilizando los objetos de MetaTrader 5, por medio de la conversión de la plataforma en un sistema RAD. El sistema funciona muy bien, y creo que muchos han pensado en crear una librería para tener cada vez más funcionalidades en el sistema propuesto, y así lograr desarrollar un EA que sea más intuitivo a la vez que tenga una interfaz más agradable y sencilla de utilizar.

La idea es tan buena, que me ha motivado a mostrar el paso a paso de cómo proceder para añadir funcionalidades, aquí implementaré dos funcionalidades nuevas y básicas, y esto nos servirá de base para que implementemos otras según lo necesitemos y queramos, la única limitación es nuestra creatividad puesto que podemos utilizar los elementos de las más diversas maneras.


2.0 - Planificación

Nuestro IDE cambiará como se muestra en las siguientes imágenes:

         

Hicimos algunos cambios menores en el diseño en sí, como se puede apreciar, pero también agregué dos nuevas regiones: una recibirá el nombre del activo y la otra, el valor acumulado del día. Veremos que son cosas de las que podemos prescindir y que no influirán en nuestras decisiones, pero puede ser interesante para uno u otro usuario a la vez que mostraré la forma más sencilla y correcta de añadir funcionalidad a nuestro IDE. Entonces, cuando abrimos la lista de objetos en la nueva interfaz, vemos la imagen a continuación:


Los dos objetos marcados no tienen eventos vinculados a ellos, lo que significa que no son funcionales por el IDE, todos los demás objetos ya están correctamente vinculados a eventos específicos y MetaTrader 5 puede hacer que los eventos se ejecuten correctamente cuando ocurran en el EA. Es decir, podemos modificar la interfaz del IDE a nuestro gusto, pero si la funcionalidad aún no está implementada, MetaTrader 5 no hará nada más que mostrar el objeto en el gráfico. Ahora bien, queremos que el objeto EDIT 00 reciba el nombre del activo con el que estamos operando, y este nombre debe aparecer en el centro del objeto, el objeto EDIT 01 recibirá el valor acumulado durante un periodo determinado, en este caso utilizaremos el periodo diario, es decir, sabremos si estamos teniendo ganancias o pérdidas en el día, si el valor es negativo estará de un color y si es positivo, de otro.

Ambos valores, obviamente, no pueden ser cambiados por el usuario, por lo que podemos dejar sus propiedades como de solo lectura, como se muestra en la imagen a continuación.


No obstante, hay que tener en cuenta que no podemos indicar cómo se va a presentar la información, es decir, no podemos justificar el texto para que se muestre en el centro del objeto, salvo que queramos hacerlo, ello se conseguirá mediante código, ya que existe una propiedad que nos permite justificar el texto, para más detalle consulten Propiedades de los objetos y fíjense en la tabla sobre ENUM_ALIGN_MODE, allí se indica en qué objetos podemos utilizar un texto justificado.

Así que sea cual sea el cambio que vayamos a crear, primero planifiquemos cómo va a ser la nueva funcionalidad, luego cómo debe presentarse y cómo el usuario va y puede interactuar con ella, de esta forma seleccionaremos el objeto correcto y lo configuraremos lo máximo posible, utilizando la propia interfaz de MetaTrader 5, de forma que al final tendremos un IDE ya planificado, sólo nos quedará ajustar las cosas después a través del código MQL5, con lo que el IDE al final será 100% funcional. Así que vamos a empezar a hacer las modificaciones. Así que vamos a empezar a hacer los cambios.


3.0 Modificaciones

Para que el código no se convierta en un verdadero Frankenstein, debemos organizarnos al máximo comprobando cuales son las funcionalidades que ya existen, y cuales son las que realmente hay que implementar, muchas veces, podemos hacer pequeños cambios en el código existente y obtener uno nuevo, que nazca testado, este nuevo código será reutilizado dentro de uno que vayamos a implementar, entonces lo único que realmente probaremos son pequeñas rutinas de control que se añadirán para generar toda una nueva funcionalidad. Los buenos programadores siempre hacen esto, intentan reutilizar de alguna manera el código existente, agregando, para tal fin, puntos de control.


3.1 Adición del nombre de activo

Para implementar esta funcionalidad no necesitaremos grandes cambios, pero hay que hacerlos en los puntos adecuados, lo primero es añadir un nuevo enumerador, el código original se ve a continuación:

enum eObjectsIDE {eRESULT, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};

A continuación se ve el nuevo código, la parte resaltada es lo que se agregó, nótese que no lo agrego ni al principio ni al final de la lista, esto para no tener que revolver otras partes del código, que ya existe y funciona.

enum eObjectsIDE {eRESULT, eLABEL_SYMBOL, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};

Si añadimos un nuevo valor, ya sea al principio o al final de la lista, tendríamos que buscar y modificar todos los puntos en los que se utilizaron estos límites, y en muchos casos podemos cometer pequeños descuidos que generarían errores difíciles de encontrar, y no tardaríamos en imaginar que fueron las nuevas incorporaciones, cuando en realidad es por olvido, así que hagamos los cambios en un punto entre los extremos.

Hecho esto necesitamos añadir inmediatamente un nuevo mensaje al sistema, de lo contrario podemos correr el riesgo de que el código genere un error RunTime, así que añadimos la siguiente línea en nuestro código original.

static const string C_Chart_IDE::szMsgIDE[] = {
                                                "MSG_RESULT",
                                                "MSG_NAME_SYMBOL",
                                                "MSG_BUY_MARKET",
                                                "MSG_SELL_MARKET",
                                                "MSG_DAY_TRADE",
                                                "MSG_CLOSE_POSITION",
                                                "MSG_LEVERAGE_VALUE",
                                                "MSG_TAKE_VALUE",
                                                "MSG_STOP_VALUE"
                                              };

Vean que tenemos que agregarla en el mismo punto, esto garantiza la organización, pero en este punto se puede agregar la constante en cualquier punto, esto no hará ninguna diferencia, ya que sólo se utiliza para verificar qué objeto recibirá el mensaje, si bien para la organización lo ponemos como el segundo mensaje.

Ahora regresemos a MetaTrader 5 y hagamos el cambio como se muestra a continuación:

         

Bien, ahora MetaTrader 5 ya reconocerá el objeto en nuestro IDE como un objeto para recibir un mensaje, falta entonces crear el procedimiento para enviar el mensaje, ya que es un mensaje al que le añadiremos un texto y esto se hará una sola vez, podremos enviarlo tan pronto como MetaTrader 5 ponga nuestro IDE en el gráfico, y esto se podría hacer simplemente añadiendo el código necesario al final de la rutina Create de nuestra clase objeto, pero de nuevo para que el código no se convierta en un Frankenstein, lleno de parches, añadiremos el nuevo procedimiento dentro de la rutina DispatchMessage. La rutina original es así:

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if (szArg == szMsgIDE[eRESULT])
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

// ... Código restante ...

        }
}

Después del cambio se verá así:

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if (szArg == szMsgIDE[eRESULT])
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                        }else if (szArg == szMsgIDE[eLABEL_SYMBOL])
                        {
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_TEXT, Terminal.GetSymbol());
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_ALIGN, ALIGN_CENTER);
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

// ... Restante do código

        }
}

Una vez creada la rutina de mensajes, podemos elegir el punto donde enviaremos el mensaje, y el mejor lugar de hecho es al final de la rutina Create de nuestra clase objeto, por lo que el código final se verá como el que se muestra a continuación:

bool Create(int nSub)
{
        m_CountObject = 0;
        if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) return false;
        FileReadInteger(m_fp, SHORT_VALUE);
                                
        for (m_CountObject = eRESULT; m_CountObject <= eEDIT_STOP; m_CountObject++) m_ArrObject[m_CountObject].szName = "";
        m_SubWindow = nSub;
        m_szLine = "";
        while (m_szLine != "</chart>")
        {
                if (!FileReadLine()) return false;
                if (m_szLine == "<object>")
                {
                        if (!FileReadLine()) return false;
                        if (m_szLine == "type")
                        {
                                if (m_szValue == "102") if (!LoopCreating(OBJ_LABEL)) return false;
                                if (m_szValue == "103") if (!LoopCreating(OBJ_BUTTON)) return false;
                                if (m_szValue == "106") if (!LoopCreating(OBJ_BITMAP_LABEL)) return false;
                                if (m_szValue == "107") if (!LoopCreating(OBJ_EDIT)) return false;
                                if (m_szValue == "110") if (!LoopCreating(OBJ_RECTANGLE_LABEL)) return false;
                        }
                }
        }
        FileClose(m_fp);
        DispatchMessage(CHARTEVENT_CHART_CHANGE, szMsgIDE[eLABEL_SYMBOL]);
        return true;
}

La parte realmente añadida está en verde, nótese que sin hacer casi ningún cambio, ya tenemos un flujo de mensajes 100% implementado, y podemos pasar al siguiente mensaje por implementar.


3.2 Adición del resultado acumulado en el día (Punto de cobertura)

Nuevamente, seguimos la misma lógica que hicimos al agregar el nombre del activo, por lo que el nuevo código se verá así:

enum eObjectsIDE {eRESULT, eLABEL_SYMBOL, eROOF_DIARY, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};

// ... Código restante

static const string C_Chart_IDE::szMsgIDE[] = {
                                                "MSG_RESULT",
                                                "MSG_NAME_SYMBOL",
                                                "MSG_ROOF_DIARY",
                                                "MSG_BUY_MARKET",
                                                "MSG_SELL_MARKET",
                                                "MSG_DAY_TRADE",
                                                "MSG_CLOSE_POSITION",
                                                "MSG_LEVERAGE_VALUE",
                                                "MSG_TAKE_VALUE",
                                                "MSG_STOP_VALUE"
                                              };

Después de eso, modifiquemos el IDE con el nuevo mensaje:

         

Con esto nuestro nuevo IDE está listo, ahora vamos a implementar el código que creará el mensaje que contendrá el valor acumulado en el día, pero tenemos una decisión que tomar: ¿En qué clase implementar esta rutina? Muchos quizás crearían esta rutina aquí, en la clase C_Chart_IDE, pero por razones de organización será mejor juntarla con las rutinas que se ocupan de las ordenes para que el código realmente se implemente en la clase C_OrderView, el código se puede ver abajo:

double UpdateRoof(void)
{
        ulong   ticket;
        int     max;
        string  szSymbol = Terminal.GetSymbol();
        double  Accumulated = 0;
                                
        HistorySelect(macroGetDate(TimeLocal()), TimeLocal());
        max = HistoryDealsTotal();
        for (int c0 = 0; c0 < max; c0++) if ((ticket = HistoryDealGetTicket(c0)) > 0)
                if (HistoryDealGetString(ticket, DEAL_SYMBOL) == szSymbol)
                        Accumulated += HistoryDealGetDouble(ticket, DEAL_PROFIT);
                                                
        return Accumulated;
}

Ahora que el código fue implementado necesitamos agregar el mensaje al sistema, pero para facilitar la vida del operador ya he puesto el código para que informe los resultados ya completados, entonces el código será así:

void DispatchMessage(int iMsg, string szArg, double dValue = 0.0)
{
        static double AccumulatedRoof = 0.0;
        bool    b0;
        double  d0;

        if (m_CountObject < eEDIT_STOP) return;
        switch (iMsg)
        {
                case CHARTEVENT_CHART_CHANGE:
                        if ((b0 = (szArg == szMsgIDE[eRESULT])) || (szArg == szMsgIDE[eROOF_DIARY]))
                        {
                                if (b0)
                                {
                                        ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen));
                                        ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2));
                                }else
                                {
                                        AccumulatedRoof = dValue;
                                        dValue = 0;
                                }
                                d0 = AccumulatedRoof + dValue;
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eROOF_DIARY].szName, OBJPROP_TEXT, DoubleToString(MathAbs(d0), 2));
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eROOF_DIARY].szName, OBJPROP_BGCOLOR, (d0 >= 0 ? clrForestGreen : clrFireBrick));
                        }else   if (szArg == szMsgIDE[eLABEL_SYMBOL])
                        {
                                ObjectSetString(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_TEXT, Terminal.GetSymbol());
                                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_ALIGN, ALIGN_CENTER);
                        }
                        break;
                case CHARTEVENT_OBJECT_CLICK:

// .... Código restantes....

        }
}

Las partes resaltadas soportan el sistema como se describió anteriormente, si la implementación no se hiciera de esta manera tendríamos que enviar 2 mensajes al sistema para actualizar la información correctamente, pero utilizando la forma en que se implementó el código podemos rastrear el resultado tanto de una posición abierta cuando del día con un solo mensaje.

Ahora el EA contará con otra modificación que está en la rutina OnTrade y esta modificación se puede ver a continuación:

void OnTrade()
{
        SubWin.DispatchMessage(CHARTEVENT_CHART_CHANGE, C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY], NanoEA.UpdateRoof());
        NanoEA.UpdatePosition();
}

A pesar de que este sistema funciona, hay que tener cuidado con el tiempo de ejecución de la rutina OnTrade que junto con OnTick puede degradar el rendimiento del EA. En el caso del código contenido en OnTick no hay mucho camino, la optimización es algo muy crítico, pero en la rutina OnTrade la cosa es más sencilla, ya que la rutina se llama de hecho cuando hay algún cambio en la posición. Sabiendo esto tenemos entonces dos alternativas, la primera es cambiar la rutina UpdateRoof para limitar su tiempo de ejecución, la otra alternativa es cambiar la propia rutina OnTrade, pero por cuestiones prácticas modificaremos la rutina UpdateRoof y así mejoraremos al menos un poco el tiempo de ejecución cuando tengamos una posición abierta. La nueva rutina será como se muestra a continuación:

double UpdateRoof(void)
{
        ulong           ticket;
        string  szSymbol = Terminal.GetSymbol();
        int             max;
        static int      memMax = 0;
        static double   Accumulated = 0;
                
        HistorySelect(macroGetDate(TimeLocal()), TimeLocal());
        max = HistoryDealsTotal();
        if (memMax == max) return Accumulated; else memMax = max;
        for (int c0 = 0; c0 < max; c0++) if ((ticket = HistoryDealGetTicket(c0)) > 0)
                if (HistoryDealGetString(ticket, DEAL_SYMBOL) == szSymbol)
                        Accumulated += HistoryDealGetDouble(ticket, DEAL_PROFIT);
                                                
        return Accumulated;
}

Las líneas resaltadas son códigos que se añadieron a la rutina original, pero a pesar de que aparentemente no hacen mucha diferencia, sí la hacen, entendamos el porqué. Cuando el código es referenciado por primera vez, tanto la variable estática memMax como Accumulated estarán en cero, si no había ningún valor en el historial de ordenes en el periodo especificado, la prueba lo reflejará y la rutina retornará, pero si hay algún dato será probado, y tanto la variable memMax como Accumulated reflejarán la nueva condición. Pues bien, el hecho de que estas variables sean estáticas hace que sus valores se mantengan entre llamadas, por lo que cuando el valor de la posición se altere por el movimiento natural del activo, MetaTrader 5 generará un evento que hará que se ejecute la función OnTrade, en este momento tenemos una nueva llamada de la función UpdateRoof y si la posición no se cerró, la función volverá en el punto de prueba, haciendo que la rutina retorne más rápido.


Conclusión

En este artículo expuse como agregar nuevas funcionalidades al sistema RAD permitiendo crear una librería que hace al sistema ideal para la creación de una interfaz IDE con mucha más facilidad y menor generación de errores a la hora de armar una interfaz de interacción y control, a partir de este punto la única limitación real será nuestra creatividad, ya que aquí exploré solo el uso exclusivo de MQL5, pero se puede integrar esta misma idea a librerías externas ampliando así enormemente las posibilidades de crear un IDE.