Desarrollando un EA comercial desde cero (Parte 21): Un nuevo sistema de órdenes (IV)

Daniel Jose | 20 julio, 2022

1.0 - Introducción

En el artículo anterior, desarrollando un EA de trading desde cero (Parte 20), mostré los principales cambios que había que hacer para empezar a tener un sistema de órdenes visual, pero los siguientes pasos ya exigían mucho en cuanto a explicación, decidí dividir el artículo en más partes, aquí terminaremos de hacer los cambios básicos, y no serán pocos, serán muchos, y todos ellos necesarios, y todo el trabajo será muy interesante, pero no lo terminaré todo aquí, ya que hay algunas cosas que deben hacerse para terminar realmente el sistema, pero ya estará con casi todas las funcionalidades.

Así que vayamos directamente a la aplicación.


2.0 - Aplicación

Lo primero que haremos es implementar el botón de cierre o cancelación, y será el indicador de orden, la clase responsable de esto se ve a continuación.

2.0.1 La clase C_Object_BtnBitMap

Esta clase se encarga de mantener los botones de mapa de bits en el gráfico, se puede ver a continuación.

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
#define def_BtnClose    "Images\\NanoEA-SIMD\\Btn_Close.bmp"
//+------------------------------------------------------------------+
#resource "\\" + def_BtnClose
//+------------------------------------------------------------------+
class C_Object_BtnBitMap : public C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
		void Create(string szObjectName, string szResource1, string szResource2 = NULL)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_BITMAP_LABEL);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_BMPFILE, 0, "::" + szResource1);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_BMPFILE, 1, "::" + (szResource2 == NULL ? szResource1 : szResource2));
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE, false);
                        };
//+------------------------------------------------------------------+
                bool GetStateButton(string szObjectName) const
                        {
                                return (bool) ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE);
                        }
//+------------------------------------------------------------------+
};

Durante la codificación de esta clase me di cuenta de que el código de posicionamiento podía ser enviado a la clase C_Object_Base, y con esto la clase C_Object_BackGround perdía este código, ya que ahora pertenecerá a una clase inferior, esto es lo que se conoce como reutilización de código, se programa menos, produce más, pero sobre todo el código es más estable, ya que se va probando y testeando las modificaciones que se van haciendo.

Para añadir el botón de CLOSE, hacemos lo siguiente:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_TradeLine.mqh"
#include "C_Object_BtnBitMap.mqh"
//+------------------------------------------------------------------+
class C_ObjectsTrade
{

// ... Código de la clase ...

}

Siguiente paso...

enum eEventType {EV_GROUND = 65, EV_LINE, EV_CLOSE};

Siguiente paso...

C_Object_BackGround     m_BackGround;
C_Object_TradeLine      m_TradeLine;
C_Object_BtnBitMap      m_BtnClose;

Siguiente paso...

inline void CreateIndicatorTrade(ulong ticket, eIndicatorTrade it)
                        {
                                color cor1, cor2;
                                string sz0;
                                

// ... Código interno de la función ...

                                switch (it)
                                {
                                        case IT_TAKE:
                                        case IT_STOP:
                                                m_BackGround.Size(sz0, 92, 22);
                                                break;
                                        case IT_PENDING:
                                                m_BackGround.Size(sz0, 110, 22);
                                                break;
                                }
                                m_BtnClose.Create(MountName(ticket, it, EV_CLOSE), def_BtnClose);
                        }

Siguiente paso...

#define macroDelete(A)  {                                                               \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND));       \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE));         \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE));        \
                        }
                                        
inline void RemoveIndicatorTrade(ulong ticket, eIndicatorTrade it = IT_NULL)
                        {
                                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
                                if ((it != NULL) && (it != IT_PENDING) && (it != IT_RESULT)) macroDelete(it)
                                else
                                {
                                        macroDelete(IT_PENDING);
                                        macroDelete(IT_RESULT);
                                        macroDelete(IT_TAKE);
                                        macroDelete(IT_STOP);
                                }
                                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
                        }
#undef macroDelete

Y finalmente, el último paso...

#define macroSetAxleY(A)        {                                               \
                m_BackGround.PositionAxleY(MountName(ticket, A, EV_GROUND), y); \
                m_TradeLine.PositionAxleY(MountName(ticket, A, EV_LINE), y);    \
                m_BtnClose.PositionAxleY(MountName(ticket, A, EV_CLOSE), y);    \
                                }
                                                                        
#define macroSetAxleX(A, B)     {                                               \
                m_BackGround.PositionAxleX(MountName(ticket, A, EV_GROUND), B); \
                m_TradeLine.PositionAxleX(MountName(ticket, A, EV_LINE), B);    \
                m_BtnClose.PositionAxleX(MountName(ticket, A, EV_CLOSE), B + 3);\
                                }
inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy)
                        {

// ... Código interno ...
                                
                        }
#undef macroSetAxleX
#undef macroSetAxleY

Al ejecutar el sistema tendremos el siguiente resultado:


Sin embargo este botón sigue sin funcionar, a pesar de que MT5 genera el evento para que el EA lo maneje, aún no implementamos esto, lo dejaremos un poco más adelante, pero aún dentro de este artículo.


2.0.2 - La clase C_Object_Edit

Bueno, el sistema no sería de gran utilidad si no tuviera una forma de informar al operador acerca de los valores que se están operando. Y para ello tenemos la clase C_Object_Edit. En el futuro esta clase tendrá que sufrir algunos cambios, para aumentar su funcionalidad, pero por el momento, la voy a dejar en un nivel que nos permita informar al operador de lo que está pasando, y para ello necesitamos pocas cosas en esta clase, el primer fragmento que se ve es:

void Create(string szObjectName, color cor, int InfoValue)
{
        C_Object_Base::Create(szObjectName, OBJ_EDIT);
        ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, "Lucida Console");
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, 10);
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_ALIGN, ALIGN_CENTER);
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clrBlack);
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_COLOR, clrBlack);
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, true);
        SetTextValue(szObjectName, InfoValue, cor);
}

El código resaltado garantiza que los valores no serán modificados por el operador, pero como dije en el futuro cambiaré esto, y para tal cambio tendremos que hacer otras alteraciones que no son importantes por el momento.

La siguiente rutina es la que hace que se muestre el texto, pero presta atención a un detalle en esta rutina:

void SetTextValue(string szObjectName, int InfoValue, color cor = clrNONE)
{
        color clr;
        clr = (cor != clrNONE ? cor  : (InfoValue < 0 ? def_ColorNegative : def_ColoPositive));
        ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, IntegerToString(InfoValue < 0 ? -(InfoValue) : InfoValue));
        ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, clr);
}

El código resaltado dirá de qué color será el fondo del texto, en función del valor introducido. Es importante que esto se haga aquí, porque no queremos tener que estar tratando de adivinar si el valor es negativo o positivo, o seguir concentrándonos en tratar de ver si hay un signo negativo en el texto, lo que realmente queremos es vigilarlo y, así, saber si el valor es positivo o negativo y esto inmediatamente. Y así como se codificó, esto sucederá, por lo que será fácil saber si un valor es negativo o positivo, pero hay una condición que es el hecho de que el color no se ha definido previamente, esto será muy útil más adelante.

A continuación tenemos la última función de esta clase que se ve a continuación.

long GetTextValue(string szObjectName) const
{
        return (StringToInteger(ObjectGetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT)) * 
                                (ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR) == def_ColorNegative ? -1 : 1));
};

Si te fijas, te darás cuenta de que cuando presentamos los valores, siempre serán positivos, porque su formato les obliga a serlo. Pero cuando verificamos el contenido del objeto, debemos tener la información correcta, y para ello utilizamos el fragmento resaltado, este se basará en el color de fondo, si el color indica que el valor es negativo, se corregirá para representar la información correcta para el EA, si es positivo se mantendrá el valor.

Las definiciones de los colores están en la propia clase, y se pueden modificar si se quieren poner otros colores más adelante, pero recuerda dejarlos diferentes, para que la función anterior funcione correctamente, de lo contrario el EA recibirá valores ambiguos, o mejor dicho cuando el valor sea negativo, el EA lo verá como positivo, y esto causará problemas en todo el análisis que el EA debe hacer.


2.0.3 - La clase C_Object_Label

Esta es la última clase que necesitamos en este punto, pero de hecho en algunos momentos cogí la idea de no crearla, ya que desempeñará un papel similar a la clase C_Object_BtnBitMap, pero como también quería tener la posibilidad de poner información textual sin depender de la clase C_Object_Edit, decidí crear esta clase desde aquí.

Su código es súper mega sencillo y puede verse a continuación.

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Edit.mqh"
//+------------------------------------------------------------------+
class C_Object_Label : public C_Object_Edit
{
        public  :
//+------------------------------------------------------------------+
                void Create(string szObjectName, string Font = "Lucida Console", string szTxt = "", int FontSize = 10, color cor = clrBlack)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_LABEL);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, Font);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, szTxt);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, FontSize);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, cor);
                        };
//+------------------------------------------------------------------+
};

Y no necesitas nada más en esta clase, ya que todo el resto del trabajo ya lo hacen las clases de objetos que están por debajo de ella.

Verás lo potente que es la POO, cuanto más organicemos el código en clases, menos necesitaremos programar clases que se parezcan unas a otras.

Pero hay un pequeño cambio que debemos hacer, durante los experimentos, noté que era muy complicado interpretar los datos del indicador de resultados, entonces se modificó como se puede ver en la imagen de abajo.

    

De esta manera es más fácil presentar valores más grandes, así será la apariencia del indicador de resultados, en la parte superior tenemos el número de contratos o factor de apalancamiento de la posición abierta, y en la parte inferior, el resultado de la posición.

Debido a esto, hubo la necesidad de un cambio en la clase C_Object_Base, ya que es responsable de gran parte del posicionamiento de los objetos, y este cambio se destaca en el código de abajo.

virtual void PositionAxleY(string szObjectName, int Y, int iArrow = 0)
                        {
                                int desl = (int)ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YSIZE);
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_YDISTANCE, (iArrow == 0 ? Y - (int)(desl / 2) : (iArrow == 1 ? Y : Y - desl)));
                        };

Con esto podemos pasar al siguiente paso, modificar la clase C_ObjectsTrade.


2.0.4 - La clase C_ObjectsTrade

Ahora vamos a terminar de dibujar todos los objetos y así podremos tener realmente el resultado que queremos en el gráfico, con cada uno de los indicadores presentados, con todos sus objetos interconectados. El paso a paso aquí es muy sencillo, si entiendes cómo se hace en realidad, podrás poner cualquier otra información que desees, sólo sigue las instrucciones y todo saldrá bien. Lo primero que hay que hacer es definir los nuevos eventos a los que deben responder los objetos, estos están resaltados en el fragmento de abajo.

enum eEventType {EV_GROUND = 65, EV_LINE, EV_CLOSE, EV_EDIT, EV_VOLUME, EV_MOVE};

Ahora vamos a añadir los objetos, en este caso los nuevos objetos también están resaltados en el fragmento de abajo:

C_Object_BackGround     m_BackGround;
C_Object_TradeLine      m_TradeLine;
C_Object_BtnBitMap      m_BtnClose;
C_Object_Edit           m_EditInfo,
                        m_InfoVol;
C_Object_Label          m_BtnMove;

Después de eso, vamos a crear los objetos, y a definir como se verán en la pantalla, recuerda lo siguiente, los objetos deben ser creados en el orden en que deben aparecer, es decir, primero el objeto de fondo, luego el objeto que estará dentro de él, y así sucesivamente hasta crear todos, si haces algo fuera de orden, el objeto puede llegar a estar oculto, sólo tienes que cambiarlo de posición aquí en este código. Bueno mira como se hace esto, para lo que pretendo presentar en la pantalla del gráfico.

inline void CreateIndicatorTrade(ulong ticket, eIndicatorTrade it)
{
        color cor1, cor2, cor3;
        string sz0;
        int infoValue;
                                
        switch (it)
        {
                case IT_TAKE    :
                        infoValue = m_BaseFinance.FinanceTake;
                        cor1 = clrForestGreen;
                        cor2 = clrDarkGreen;
                        cor3 = clrNONE;
                        break;
                case IT_STOP    :
                        infoValue = - m_BaseFinance.FinanceStop;
                        cor1 = clrFireBrick;
                        cor2 = clrMaroon;
                        cor3 = clrNONE;
                        break;
                case IT_PENDING:
                        infoValue = m_BaseFinance.Leverange;
                        cor1 = clrCornflowerBlue;
                        cor2 = clrDarkGoldenrod;
                        cor3 = clrLightBlue;
                        break;
                case IT_RESULT  :
                default:
                        infoValue = m_BaseFinance.Leverange;
                        cor1 = clrDarkBlue;
                        cor2 = clrDarkBlue;
                        cor3 = clrSilver;
                        break;
                }                               
                m_TradeLine.Create(MountName(ticket, it, EV_LINE), cor2);
                if (ticket == def_IndicatorTicket0) m_TradeLine.SpotLight(MountName(ticket, IT_PENDING, EV_LINE));
                m_BackGround.Create(sz0 = MountName(ticket, it, EV_GROUND), cor1);
                switch (it)
                {
                        case IT_TAKE:
                        case IT_STOP:
                        case IT_PENDING:
                                m_BackGround.Size(sz0, 92, 22);
                                break;
                        case IT_RESULT:
                                m_BackGround.Size(sz0, 84, 34);
                                break;
                }
                m_BtnClose.Create(MountName(ticket, it, EV_CLOSE), def_BtnClose);
                m_EditInfo.Create(sz0 = MountName(ticket, it, EV_EDIT), cor3, infoValue);
                m_EditInfo.Size(sz0, 60, 14);
                if (it != IT_RESULT) m_BtnMove.Create(MountName(ticket, it, EV_MOVE), "Wingdings", "u", 17, cor2);
                else
                {
                        m_InfoVol.Create(sz0 = MountName(ticket, it, EV_VOLUME), clrNONE, infoValue);
                        m_InfoVol.Size(sz0, 60, 14);
                }
}

Todos los puntos resaltados son las nuevas incorporaciones que ha sufrido el código desde la última versión vista en el artículo anterior. Ahora podemos codificar la siguiente rutina.

#define macroDelete(A)  {                                                                       \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND));               \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE));                 \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE));                \
                ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_EDIT));                 \
                if (A != IT_RESULT)                                                             \
                        ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_MOVE));         \
                else                                                                            \
                        ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_VOLUME));       \
                        }
                                        
inline void RemoveIndicatorTrade(ulong ticket, eIndicatorTrade it = IT_NULL)
                        {
                                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
                                if ((it != NULL) && (it != IT_PENDING) && (it != IT_RESULT)) macroDelete(it)
                                else
                                {
                                        macroDelete(IT_PENDING);
                                        macroDelete(IT_RESULT);
                                        macroDelete(IT_TAKE);
                                        macroDelete(IT_STOP);
                                }
                                ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
                        }
#undef macroDelete

Mira que nos ayuda la macro, necesitaba añadir sólo las partes resaltadas para poder eliminar todos los objetos de un indicador, y ahora estamos usando 6 objetos en 4 indicadores, si lo hiciera de otra manera sería muy laborioso y muy propenso a errores. Y para terminar la rutina de posicionamiento.

#define macroSetAxleY(A)        {                                                                       \
                m_BackGround.PositionAxleY(MountName(ticket, A, EV_GROUND), y);                         \
                m_TradeLine.PositionAxleY(MountName(ticket, A, EV_LINE), y);                            \
                m_BtnClose.PositionAxleY(MountName(ticket, A, EV_CLOSE), y);                            \
                m_EditInfo.PositionAxleY(MountName(ticket, A, EV_EDIT), y, (A == IT_RESULT ? -1 : 0));  \
                if (A != IT_RESULT)                                                                     \
                        m_BtnMove.PositionAxleY(MountName(ticket, A, EV_MOVE), y);                      \
                else                                                                                    \
                        m_InfoVol.PositionAxleY(MountName(ticket, A, EV_VOLUME), y, 1);                 \
                                }
                                                                        
#define macroSetAxleX(A, B)     {                                                               \
                m_BackGround.PositionAxleX(MountName(ticket, A, EV_GROUND), B);                 \
                m_TradeLine.PositionAxleX(MountName(ticket, A, EV_LINE), B);                    \
                m_BtnClose.PositionAxleX(MountName(ticket, A, EV_CLOSE), B + 3);                \
                m_EditInfo.PositionAxleX(MountName(ticket, A, EV_EDIT), B + 21);                \
                if (A != IT_RESULT)                                                             \
                        m_BtnMove.PositionAxleX(MountName(ticket, A, EV_MOVE), B + 80);         \
                else                                                                            \
                        m_InfoVol.PositionAxleX(MountName(ticket, A, EV_VOLUME), B + 21);       \
                                }
                                                                                
inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy)
                        {
                                double ad;
                                int x, y;
                                
                                ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price, x, y);
                                macroSetAxleY(it);
                                macroSetAxleX(it, m_PositionMinimalAlxeX);
                                if (Leverange == 0) return;
                                if (it == IT_PENDING)
                                {
                                        ad = Terminal.GetAdjustToTrade() / (Leverange * Terminal.GetVolumeMinimal());
                                        ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price + Terminal.AdjustPrice(FinanceTake * (isBuy ? ad : (-ad))), x, y);
                                        macroSetAxleY(IT_TAKE);
                                        macroSetAxleX(IT_TAKE, m_PositionMinimalAlxeX + 110);
                                        ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, price + Terminal.AdjustPrice(FinanceStop * (isBuy ? (-ad) : ad)), x, y);
                                        macroSetAxleY(IT_STOP);
                                        macroSetAxleX(IT_STOP, m_PositionMinimalAlxeX + 220);
                                }
                        }
#undef macroSetAxleX
#undef macroSetAxleY

Y de nuevo ver que se añadió muy poco código, pero aún así la función puede trabajar con todos los elementos, el posicionamiento de cada uno de ellos de la manera correcta, gracias al uso de macros. Y el resultado cuando compilamos EA en esta etapa se ve a continuación:


A pesar de ser todo muy bonito y maravilloso, estos controles aún no funcionan, tenemos que implementar los eventos para cada uno de los objetos, porque sin ellos esta interfaz es casi inútil, ya que lo único que de hecho hará es reemplazar aquellas líneas que se utilizaban originalmente.


3.0 - Cómo afrontar los problemas

Si todo fuera sencillo cualquiera podría hacer las cosas, pero en verdad siempre tenemos problemas con los que lidiar y esto es parte del proceso de creación, podría simplemente resolverlos y no mostrar cómo se hizo, pero quiero con estos artículos motivarte a lidiar con los problemas y aprender a programar realmente, así que este tema será algo interesante.


3.0.1 - Ajustamos las cosas a medida que se actualiza el gráfico

Este es el primer problema que tenemos, y es causado por la falta de actualización de las posiciones de los objetos a medida que el gráfico sufre actualizaciones, para entenderlo ve la animación de abajo:

Este tipo de cosas pueden volverte loco intentando resolver el problema, pero la solución es muy sencilla, el propio MT5 genera un evento informando de que el gráfico debe ser actualizado, lo único que tenemos que hacer es capturar este evento y actualizar nuestro sistema de órdenes.

Pues bien, la captura debe hacerse cuando se llama al evento CHARTEVENT_CHART_CHANGE, y la actualización es más sencilla si se utiliza la función UpdatePosition presente en el código del EA, de esta forma lo único que tenemos que hacer es añadir una sola línea en nuestro código, y esto se hace en la clase C_OrderView como se muestra en el fragmento siguiente:

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{

// ... Código ....
                                
        switch (id)
        {
                case CHARTEVENT_CHART_CHANGE:
                        SetPositionMinimalAxleX();
                        UpdatePosition();
                        break;

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

Esta sencilla solución tiene un problema, si tienes muchas órdenes en el activo, el proceso tardará un tiempo, lo que hace que el EA se bloquee en resolver este punto antes de volver a hacer otras cosas, hay otras soluciones más complejas que hacen las cosas más rápidas, pero para este sistema esta solución ya lo resuelve, y el resultado se ve a continuación.


Parece que es correcto, ¿no? Pero ahí hay un fallo. Es difícil ver el defecto hasta que lo pruebas, pero sí hay un defecto en este sistema, incluso después de arreglarlo, el defecto está ahí.


3.0.2 - Por favor, EA, para de seleccionar cosas automáticamente

Si miras la animación de arriba te darás cuenta de que la línea de Stop está siendo seleccionada, aunque no lo hayas hecho, el EA lo hará cada vez que manipules el gráfico, y dependiendo de la configuración en el momento en que se crearon los indicadores, puede ocurrir que sea la línea de take o la de posición la que sea seleccionada por el EA, y esto cada vez que muevas el gráfico.

De nuevo puedes volverte loco intentando resolver esto, pero la solución es aún más sencilla que la anterior, simplemente añade otra línea en el mismo fragmento y el EA dejará de seleccionar automáticamente una de las líneas, el arreglo se muestra en el fragmento de abajo resaltado.

inline void CreateIndicatorTrade(ulong ticket, eIndicatorTrade it)
{
        color cor1, cor2, cor3;
        string sz0;
        double infoValue;

// ... Código interno ...

        Select(NULL);
}

Este tipo de cosas siempre van a suceder durante el proceso de desarrollo, fallas que se pueden notar fácilmente, pero también otras que son más sutiles, y que a veces no notamos al principio, pero de una u otra manera siempre suceden, debido a esto es bueno siempre aprender a programar, porque en algunos casos puedes resolver estas fallas tú mismo, y reportarlo para que todos puedan también corregir el problema y así todos tendrán un código que funcione correctamente y sin fallas. Puedes intentar esto, incluso te animo a dicha práctica, porque así es como aprendí a programar, de esta manera aprenderás a producir un programa, es parte del aprendizaje tomar un código fuente y modificarlo, con cuidado de observar los resultados, pero debes hacerlo con un código funcional, de esta manera es más sencillo entender cómo fue construido, y esto suele dar buenos frutos, ya que aprendemos mucho entendiendo cómo cada programador fue capaz de resolver un problema específico.

Bueno, pero esto de aquí era sólo algo para motivarte a estudiar, vamos a seguir en algo realmente importante...


4.0 - Tratamiento de eventos

Lo primero que vamos a hacer es construir el sistema que se encargará de informar el resultado de una posición, esto sustituirá a esa zona en el Chart Trade, pero mantendré el Chart Trade intacto, y la razón es que en el Chart Trade se indica el resultado general de las posiciones, y en el caso de que estés utilizando una cuenta HEDGING, el valor será diferente al indicado en el sistema de órdenes, ya que uno representa el valor local y el otro el valor general, pero para los otros tipos de cuentas esta diferencia no existirá, así que si lo deseas puedes eliminar el sistema de resultados del Chart Trade.

4.0.1 - Visualización del resultado de una posición

Quien observa el código por primera vez puede perderse y no saber dónde mirar y buscar la información, creando así otras operaciones para hacer algo que el código original ya hace, y esto es lo que suele traer muchos problemas, generando código extra que puede generar bugs que el código original no tenía, además de salirse de la filosofía de reutilizar siempre, programando sólo cuando es necesario. Así que sabiendo cómo funciona MT5, y sabiendo cómo está funcionando ya el EA, hay que buscar desde dónde se genera el resultado que se presenta en el Chart Trade, ya que si se está presentando el resultado de las posiciones, es ahí donde queremos y debemos utilizar, haciendo esto dirigimos nuestra atención al fragmento de abajo.

void OnTick()
{
        Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, OrderView.CheckPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]);
        TimesAndTrade.Update();
}

Vamos entonces al punto resaltado, y el código original se ve a continuación.

inline double CheckPosition(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())
                                {
                                        ticket = PositionGetInteger(POSITION_TICKET);
                                        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;
                        };

Bueno, pensarás, pero ¿cómo voy a sacar los datos de ahí, y aplicarlos al indicador, si no tengo forma de saber a cuál de los objetos debo hacer referencia? Durante la creación, los objetos parecen haber sido creados de forma suelta, sin ningún tipo de criterio o cuidado... Bueno, siento informarte, pero esto no es cierto, si piensas, o te has imaginado la cosa de esta manera, será mejor que empieces a estudiar un poco más sobre cómo funciona realmente la plataforma MT5. Es muy cierto que no creé ningún tipo de lista, array o cualquier tipo de estructura para referenciar los objetos que se iban creando, pero esto fue hecho a propósito, traté de hacerlo así, porque sé que funciona, y te mostraré que de hecho funciona, no necesitas ningún tipo de estructura para almacenar los objetos que estarán en el gráfico para poder referenciar uno en específico, lo que necesitas es modelar de manera adecuada el nombre del objeto, solo esto, modelar el nombre del objeto.

Pregunta: ¿Cómo se han modelado los nombres de los objetos?

Se han modelado de la siguiente manera:

1 - Secuencia de la cabecera Esta secuencia distinguirá un objeto utilizado en el sistema de ordenes de todos los demás
2 - Carácter limitador Sirve para informar de que algunas otras informaciones vendrán en seguida
3 - Indicador de tipo Este marca la diferencia entre cada tipo de indicador, un indicador Take es diferente de un indicador Stop
4 - Carácter limitador Cumple la misma función que el ítem 2
5 - Ticket de orden o de posición Memoriza el valor del ticket de la orden, esto vincula los indicadores de una orden OCO y diferencia una orden de otra
6 - Carácter limitador Cumple la misma función que el ítem 2
7 - Indicador de evento Este diferencia los objetos dentro del mismo indicador

Es decir, el modelado lo es todo, aunque a los que no programan les parezca que estamos creando algo repetitivo, en realidad estamos creando algo único, cada uno de los objetos es único y puede ser referenciado mediante una simple regla. Y esta regla se crea con el siguiente código:

inline string MountName(ulong ticket, eIndicatorTrade it, eEventType ev)
                        {
                                return StringFormat("%s%c%c%c%d%c%c", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)ev);
                        }

Así que si le dices al código anterior a qué ticket de pedido, a qué indicador y a qué evento queremos acceder, tendremos el nombre del objeto concreto, y de esta forma podremos manipular sus atributos. Sabiendo esto es el primer paso, ahora tenemos que tomar otra decisión: ¿cómo hacer esta manipulación de forma segura, sin provocar el caos en el código y convertirlo en un Frankenstein?

Esta es la parte que haré ahora. Para ello iremos a la clase C_ObjectsTrade y añadiremos el siguiente código.

inline void SetResult(ulong ticket, double dVolume, double dResult)
                        {
                                m_InfoVol.SetTextValue(MountName(ticket, IT_RESULT, EV_VOLUME), (dVolume / Terminal.GetVolumeMinimal()), def_ColorVolumeResult);
                                m_EditInfo.SetTextValue(MountName(ticket, IT_RESULT, EV_EDIT), dResult);
                        }

Ahora vamos a la clase C_Router y añadimos el fragmento resaltado.

inline double CheckPosition(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())
                                {
                                        ticket = PositionGetInteger(POSITION_TICKET);
                                        SetResult(ticket, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT));
                                        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;
                        };

Esto resuelve uno de los problemas. Pero tenemos otros problemas que resolver.


4.0.2 - Indicamos el volumen de una orden pendiente

Ahora vamos a resolver el problema del volumen indicado en una orden pendiente, para ello tenemos que crear un nuevo procedimiento en la clase C_ObjectsTrade, esto se ve a continuación.

inline void SetVolumePendent(ulong ticket, double dVolume)
                        {
                                m_EditInfo.SetTextValue(MountName(ticket, IT_PENDING, EV_EDIT), dVolume / Terminal.GetVolumeMinimal(), def_ColorVolumeEdit);
                        }

Hecho esto, utilizamos la rutina UpdatePosition presente en la clase C_Router y la actualización se producirá sin más problemas.

void UpdatePosition(int iAdjust = -1)
{

// ... Código interno ....

        for (int i0 = o; i0 >= 0; i0--) if ((ul = OrderGetTicket(i0)) > 0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
        {
                price = OrderGetDouble(ORDER_PRICE_OPEN);
                take = OrderGetDouble(ORDER_TP);
                stop = OrderGetDouble(ORDER_SL);
                bTest = CheckLimits(price);
                vol = OrderGetDouble(ORDER_VOLUME_CURRENT);

// ... Código interno ...

                CreateIndicatorTrade(ul, price, IT_PENDING);
                SetVolumePendent(ul, vol);
                CreateIndicatorTrade(ul, take, IT_TAKE);
                CreateIndicatorTrade(ul, stop, IT_STOP);
        }
};

Con esto solucionamos este problema, ahora tenemos que solucionar el problema de los valores indicados como Take y Stop, ya que estos valores no están coincidiendo con la verdad, después de poner la orden en el gráfico.


4.0.3 - Evento de pulsación del botón Close del indicador

La única forma segura de eliminar una orden o uno de sus límites es el botón close que se encuentra en la esquina de cada uno de los indicadores. Pero aquí no tenemos el evento implementado correctamente, ahora vamos a arreglar esto.

Bien en la clase C_OrderView, es que se debe implementar el evento de clic de hecho, vamos a sustituir el sistema antiguo, por el código que destaca.

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong   	ticket;
        double  	price, pp, pt, ps;
        eIndicatorTrade it;
        eEventType 	ev;
                                
        switch (id)
        {

// ... Código interno ...
                case CHARTEVENT_OBJECT_CLICK:
                        if (GetInfosOrder(sparam, ticket, price, it, ev))
                        {
                                switch (ev)
                                {
                                        case EV_CLOSE:
                                                if (OrderSelect(ticket)) switch (it)
                                                {
                                                        case IT_PENDING:
                                                                RemoveOrderPendent(ticket);
                                                                break;
                                                        case IT_TAKE:
                                                                ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), 0, OrderGetDouble(ORDER_SL));
                                                                break;
                                                        case IT_STOP:
                                                                ModifyOrderPendent(ticket, OrderGetDouble(ORDER_PRICE_OPEN), OrderGetDouble(ORDER_TP), 0);
                                                                break;
                                                }
                                                if (PositionSelectByTicket(ticket)) switch (it)
                                                {
                                                        case IT_RESULT:
                                                                ClosePosition(ticket);
                                                                break;
                                                        case IT_TAKE:
                                                                ModifyPosition(ticket, 0, PositionGetDouble(POSITION_SL));
                                                                break;
                                                        case IT_STOP:
                                                                ModifyPosition(ticket, PositionGetDouble(POSITION_TP), 0);
                                                                break;
                                                }
                                                break;

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

Bueno pero todavía dentro de esta misma clase tenemos que añadir una cosa. ¿Qué pasaría si el operario borrara accidentalmente un objeto que informa de los datos de posición? Bueno, no quieres saberlo... y para evitarlo añadimos el siguiente código al sistema.

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        ulong           ticket;
        double          price, pp, pt, ps;
        eIndicatorTrade it;
        eEventType      ev;
                                
        switch (id)
        {
                case CHART_EVENT_OBJECT_DELETE:
                case CHARTEVENT_CHART_CHANGE:
                        SetPositionMinimalAxleX();
                        UpdatePosition();
                        break;

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

De esta manera, si el operador borra algo que no debería, EA restaurará rápidamente el indicador u objeto borrado.

Pues bien, en el siguiente vídeo se puede ver cómo funciona actualmente el sistema, hubo algunos otros cambios que no destaqué aquí en el artículo por ser cosas de menor orden, o mejor dicho, no aportaría mucho a la explicación en su conjunto.




5.0 - Conclusión

Bueno a pesar de que el sistema parece completo y puedes estar ansioso en operar usándolo, debo advertirte que aún no está terminado, este artículo fue para mostrarte cómo puedes agregar y modificar cosas de manera de tener un sistema de órdenes mucho más práctico y sencillo de trabajar, pero aún le falta el sistema responsable de mover las posiciones y es justamente esto lo que hará que el EA se vuelva muy didáctico, práctico e intuitivo en su uso, pero esto lo dejaremos para el próximo artículo.

En el anexo les dejo el sistema en la fase actual de desarrollo.