Desarrollando un EA comercial desde cero (Parte 08): Un salto conceptual (I)

17 mayo 2022, 07:25
Daniel Jose
0
285

1.0 - Introducción

A veces, cuando desarrollamos algo, vemos nuevas posibilidades que pueden ser oportunas y, por tanto, aportar una gran mejora en el sistema que estemos creando, pero entonces surge una pregunta: ¿cómo implementar de la forma más sencilla posible una nueva funcionalidad?

El gran problema es que a veces nos vemos obligados a olvidar todo lo que ya se ha desarrollado y empezar de cero, y esto es completamente DESMOTIVADOR. Sin embargo, hay una línea de pensamiento que adquirí con el tiempo, después de más de 20 años programando en C++, nosotros desarrollamos algunos conceptos que nos ayudan a planificar las cosas y hacer cambios con un mínimo esfuerzo, aunque a veces las cosas pueden volverse mucho más complejas de lo que estimamos inicialmente.

Hasta ahora el EA ha sido construido de una manera que le permite al mismo recibir código sin perder su funcionalidad actual, las clases son simplemente creadas y añadidas, pero ahora tenemos que dar un paso atrás y luego dar dos pasos adelante. Y el dar un paso atrás nos permitirá incorporar una nueva funcionalidad en el EA, esta funcionalidad es una clase de ventana con algún tipo de información, basada en templates, esta aquí será la primera parte, cambiaremos radicalmente el código, pero manteniendo toda la funcionalidad que existe hasta ahora, en la segunda parte me ocuparé del IDE.


2.0 - Planificación

Nuestro EA actualmente está estructurado en una clase objeto, y ésta se puede ver en el diagrama a continuación.

Con todo esto, tenemos el funcionamiento actual y la excelente estabilidad del sistema, pero ahora el EA será reestructurado para ser como se muestra a continuación, veamos que sólo había la adición de una clase y el cambio de posición de la clase C_TemplateChart con la clase C_SubWindow.


¡¿Y por qué esta reestructuración?! El problema es que, para añadir ventanas flotantes que contienen los datos de los activos, la forma de hacerlo era el adecuado, por lo tanto, surge la necesidad de pasar por este cambio, pero este cambio no era sólo estético en términos de estructura, sino que era necesario un cambio extremo en el código, por lo que es muy diferente de los códigos vistos anteriormente.

Así que pongamos manos a la obra, o mejor, el código en la pantalla.


3.0 - Implementación en la práctica

3.1 - Cambios en el código interno del EA

El primer gran cambio comienza en el archivo de inicialización del EA, veamos el fragmento a continuación:

input group "Window Indicators"
input string                    user01 = "";                    //Indicadores de subventanas
input group "WallPaper"
input string                    user10 = "Wallpaper_01";        //BitMap a utilizar
input char                      user11 = 60;                    //Transparencia (0 a 100)
input C_WallPaper::eTypeImage   user12 = C_WallPaper::IMAGEM;   //Tipo de imagen de fondo
input group "Chart Trader"
input int                       user20   = 1;                   //Factor de apalancamiento
input int                       user21   = 100;                 //Take Profit ( FINANCIERO )
input int                       user22   = 75;                  //Stop Loss ( FINANCIERO )
input color                     user23   = clrBlue;             //Color de la línea del precio del precio
input color                     user24   = clrForestGreen;      //Color de la línea Take Profit
input color                     user25   = clrFireBrick;        //Color de la línea del Stop
input bool                      user26   = true;                //Day Trade ?
input group "Volume At Price"
input color                     user30  = clrBlack;             //Color de barra
input char                      user31  = 20;                   //Transparencia (0 a 100 )
//+------------------------------------------------------------------+
C_TemplateChart Chart;
C_WallPaper     WallPaper;
C_VolumeAtPrice VolumeAtPrice;
//+------------------------------------------------------------------+
int OnInit()
{
        static string   memSzUser01 = "";
        
        Terminal.Init();
        WallPaper.Init(user10, user12, user11);
        if (memSzUser01 != user01)
        {
                Chart.ClearTemplateChart();
                Chart.AddThese(memSzUser01 = user01);
        }
        Chart.InitilizeChartTrade(user20, user21, user22, user23, user24, user25, user26);
        VolumeAtPrice.Init(user24, user25, user30, user31);
        
   OnTrade();
        EventSetTimer(1);
   
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+

Notemos que ahora solo tenemos una única variable para decir qué templates se van a cargar, el resto del código parece ser el mismo que se ha venido viendo, excepto por la parte resaltada. Es posible que no tengamos idea de qué hace esto, o por qué se colocó este código resaltado aquí, en la inicialización del EA. Pues bien, cuando cargamos nuestro EA en el gráfico, se crean algunas cosas, y durante el uso normal se pueden modificar, antes no tenía sentido añadir el código resaltado, y la razón era que estaba todo orientado a trabajar en conjunto, siendo que los cambios no afectaban al comportamiento o apariencia del EA, pero cuando añadimos indicadores flotantes, empieza a ocurrir una cosa MOLESTA, cada vez que cambiamos el MARCO DE TIEMPO el EA se REINICIA, y las ventanas vuelven a su estado inicial, si no se destruyen, empieza a haber una acumulación de cosas inútiles en el gráfico, y si se destruyen como debe ser, se reconstruirán en sus ubicaciones originales, y esto es una gran molestia, luego si el usuario no cambia los templates deseados, el código resaltado evitará que las ventanas flotantes se destruyan indebidamente, pero si hay cambios en los templates, el EA se reinicia normalmente. Algo muy simple, pero extremadamente eficiente.

Lo siguiente que llama la atención es respecto al sistema de mensajería interna, anteriormente había una variable extra, pero fue eliminada, por lo que el código quedó como se muestra a continuación:

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

Ahora el sistema utiliza de forma más eficiente el propio sistema de mensajería presente en MQL5, y la mensajería es muy similar a la propia función OnChartEvent, pudiendo pasar los parámetros sin ningún tipo de estrés a las clases objeto para que cada clase trabaje los mensajes de eventos generados por MT5 de la forma más adecuada a la propia clase, lo que hace que la cosa coja aún más cuerpo y fuerza, aislando aún más cada clase objeto, y así el EA puede tomar una apariencia más diferente para cada tipo de usuario, y con un esfuerzo cada vez menor.


3.2 - Cambios en el código de soporte de subventanas

Hasta entonces el código de las subventanas era muy sencillo, pero tenía un problema, cada vez que por una u otra razón el EA no podía eliminar la subventana creada, al abrir de nuevo el EA, se creaba una nueva subventana, lo que provocaba un descontrol muy grande en el propio sistema, pero esto se corrigió, y sorprendentemente de una forma muy sencilla, lo primero que hay que observar es el fragmento del archivo de soporte que se puede ver a continuación:

int OnInit()
{
        IndicatorSetString(INDICATOR_SHORTNAME, "SubWinSupport");
        
        return INIT_SUCCEEDED;
}

La línea resaltada creará un alias para el archivo de soporte, y este alias es el que será visto por EA para comprobar si el sistema de subventanas está cargado o no, no importa el nombre del archivo, para EA lo que importa es el alias. Este mismo tipo de código se utilizará en otro momento para soportar otro tipo de cosas dentro del EA, aquí no entraré en mucho detalle sobre ello, pero en el futuro en otro artículo explicaré cómo aprovechar este código destacado.

Una vez hecho esto vamos a ver el código para cargar y crear subventanas, ahora se puede ver en el fragmento de abajo:

void Init(void)
{
        int i0;
        if ((i0 = ChartWindowFind(Terminal.Get_ID(), def_Indicador)) == -1)
                ChartIndicatorAdd(Terminal.Get_ID(), i0 = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WINDOWS_TOTAL), iCustom(NULL, 0, "::" + def_Resource));
        m_IdSubWinEA = i0;
}

Véase que es mucho más sencillo, pero este fragmento no es público, se accede a él a través de otro fragmento que sí es público, y que se puede ver a continuación:

inline int GetIdSubWinEA(void)
{
        if (m_IdSubWinEA < 0) Init();
        return m_IdSubWinEA;
}

¡¿Pero por qué así?! Bueno, el caso es que si te fijas en la inicialización del EA, verás que puede ocurrir que el EA no utilice ningún indicador en una subventana, y cuando el sistema se da cuenta de ello, elimina la subventana del gráfico y sólo la creará si es necesario. Pero quien toma esta decisión no es el código del EA, sino la clase C_TemplateChart.


3.3 - La nueva clase C_TemplateChart

Véase la animación a continuación:

Noten que ahora tenemos una línea vertical que indica donde estamos analizando, estas líneas son independientes entre sí, pero antes no existían, lo que dificultaba el análisis de algunos puntos del indicador en función del gráfico, esta es una de las mejoras incorporadas a la clase C_TemplateChart, pero analicemos el código dentro de la clase para entender aún más, ya que los cambios fueron grandes.

Veamos el siguiente fragmento, donde se declaran las variables:

class C_TemplateChart : public C_SubWindow
{
#define def_MaxTemplates                8
#define def_NameTemplateRAD     "IDE"
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
        enum eParameter {TEMPLATE = 0, PERIOD, SCALE, WIDTH, HEIGHT};
//+------------------------------------------------------------------+
                struct st
                {
                        string          szObjName,
                                        szSymbol,
                                        szTemplate,
                                        szVLine;
                        int             width,
                                        scale;
                        ENUM_TIMEFRAMES timeframe;
                        long            handle;
                }m_Info[def_MaxTemplates];
                int     m_Counter,
                        m_CPre,
                        m_Aggregate;
                struct st00
                {
                        int     counter;
                        string  Param[HEIGHT + 1];
                }m_Params;

Lo primero que hay que notar es que la clase C_TemplateChart extenderá la clase C_SubWindow, este fragmento no parece tan especial, pero hay que prestar atención a la parte resaltada, ésta indica un sistema interno de análisis de datos para poder construir y presentar los indicadores solicitados por el usuario de forma adecuada, es decir, ahora el sistema para describir cómo el usuario indicará las cosas ha quedado estandarizado, aunque al principio parezca confuso, con el tiempo sucederá de forma muy natural. Para explicar el nuevo formato tenemos que analizar el siguiente fragmento, que se encarga de analizar la solicitud del usuario:

int GetCommand(int iArg, const string szArg)
{
        for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = "";
        m_Params.counter = 0;
        for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg])
        {
                case ')':
                case ';':
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++);
                        return iArg + 1;
                case ' ':
                        c2 += (c1 == iArg ? 0 : 1);
                        c1 = (c1 == iArg ? iArg + 1 : c1);
                        break;
                case '(':
                case ',':
                        c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2);
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        c2 = 0;
                        c1 = iArg + 1;
                        break;
                default:
                        c2++;
                        break;
        }
        return -1;
}

Lo primero que hacemos es limpiar cualquier dato que se encuentre previamente en la estructura, después empezamos a analizar y obtener parámetro por parámetro si existen, no son obligatorios, pero indican cómo se presentarán las cosas al usuario, y este sistema es autoexpandible, es decir, si se quiere añadir más información, sólo hay que indicarlo en la enumeración eParameter. Actualmente el sistema tiene 5 parámetros, y deben ser dados en el orden indicado en el enumerador de eParámetros que fue resaltado en el fragmento de declaración de variables, a continuación vemos cada parámetro en la orden y lo que le dicen al sistema.

Parámetro Resultado
1. TEMPLATE o ACTIVO Indica qué template o activo ver
2. PERIODO Si se indica, bloqueará el indicador en un período determinado, de la misma manera que se usaba antes
3. ESCALA Si se indica un valor, bloqueará el indicador en una escala fija.
4. ANCHO Si se indica, indicará qué tan ancho será el indicador en la ventana
5. ALTURA Este es un parámetro nuevo y se verá más adelante en este artículo; sirve para indicar el uso de una ventana flotante.

Después de todo, el único marco que no se beneficiará en el momento del parámetro número 5 es el IDE, pero esto será corregido, y en el próximo artículo mostraré cómo implementar esto en el IDE, este artículo se centrará sólo en los otros sistemas.

Ahora supongamos que, por una u otra razón, queremos permitir que el usuario controle el color de la línea vertical del indicador, no necesitaremos hacer ningún cambio en el código de análisis de los parámetros, sino que simplemente haremos el cambio según el ejemplo siguiente:

class C_TemplateChart : public C_SubWindow
{
#define def_MaxTemplates        8
#define def_NameTemplateRAD     "IDE"
//+------------------------------------------------------------------+
        private :
//+------------------------------------------------------------------+
        enum eParameter {TEMPLATE = 0, COLOR_VLINE, PERIOD, SCALE, WIDTH, HEIGHT};
//+------------------------------------------------------------------+

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

                struct st00
                {
                        int     counter;
                        string  Param[HEIGHT + 1];
                }m_Params;

al hacer esto el sistema reconocerá automáticamente que pueden venir 6 parámetros en una sola llamada. Ahora tenemos un problema, aunque el código GetCommand visto anteriormente funciona bien, tiene un fallo, y este fallo muchas veces pasa desapercibido cuando nosotros mismos estamos creando un sistema que vamos a utilizar, pero cuando el sistema se dispone para que otras personas lo utilicen, el fallo se hará evidente y puede dejar a algunos programadores menos experimentados sin saber cómo resolver el problema, por eso la programación orientada a objetos está tan bien valorada y es cuando se hace correctamente el modelo más adecuado para ser utilizado en programas de gran interés, una de las premisas de la POO (Programación Orientada a Objetos) es asegurar que los datos y variables de la clase se inicializan correctamente, y para asegurar esto hay que probar las cosas, y aunque el código GetCommand parece correcto, contiene un fallo, no prueba el límite máximo de parámetros, o si el modelo inicial acepta sólo 5 parámetros, ¿qué pasa si el usuario pone 6 parámetros? Esto es lo que debemos evitar, no debemos asumir que las cosas van a funcionar, debemos garantizar que van a funcionar, por lo que para arreglar el código debe quedar como se muestra a continuación, los puntos de fijación están resaltados.

int GetCommand(int iArg, const string szArg)
{
        for (int c0 = TEMPLATE; c0 <= HEIGHT; c0++) m_Params.Param[c0] = "";
        m_Params.counter = 0;
        for (int c1 = iArg, c2 = 0; szArg[iArg] != 0x00; iArg++) switch (szArg[iArg])
        {
                case ')':
                case ';':
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        for (; (szArg[iArg] != 0x00) && (szArg[iArg] != ';'); iArg++);
                        return iArg + 1;
                case ' ':
                        c2 += (c1 == iArg ? 0 : 1);
                        c1 = (c1 == iArg ? iArg + 1 : c1);
                        break;
                case '(':
                case ',':
                        if (m_Params.counter == HEIGHT) return StringLen(szArg) + 1;
                        c2 = (m_Params.counter == SCALE ? (c2 >= 1 ? 1 : c2) : c2);
                        m_Params.Param[m_Params.counter++] = StringSubstr(szArg, c1, c2);
                        c2 = 0;
                        c1 = iArg + 1;
                        break;
                default:
                        c2++;
                        break;
        }
        return -1;
}

Veremos que simplemente añadiendo esta única línea evitamos que el sistema genere algo inesperado, ya que si el último parámetro esperado es una ALTURA, pero no es el último, lógicamente significa que algo está mal, por lo que el sistema ignora todo lo declarado después, evitando así problemas.

Así que usted puede haber notado cómo el sistema reconoce los templates y los parámetros, pero para aquellos que no entienden, la sintaxis se ve a continuación:

Parámetro_00 ( Parámetro_01 , Parámetro_02 , Parámetro_03 , Parámetro_04 )

Donde el parámetro_00 indica el template a utilizar, y los otros están separados por coma ( , ) e indican los valores que están definidos en la enumeración eParameter, si quisiéramos modificar sólo el parámetro_03, podemos dejar los otros vacíos, como se muestra en la figura siguiente, donde muestro el sistema funcionando como lo desea el usuario.

       

Nótese que tenemos una indicación estandarizada, que recuerda mucho a las llamadas a funciones, pero tal vez le parezca confuso, pero entendamos lo que realmente sucedió: Nótese que indicamos el template RSI, después no informamos ni un periodo, ni una escala, estos valores están vacíos para que el sistema entienda que debe seguir el gráfico principal, pero informamos un ancho y un alto, el sistema entiende que esto debe mostrarse en una ventana flotante, y eso es lo que sucede, el indicador RSI se muestra en una ventana flotante. En la plantilla ADX, sólo informamos el ancho para que el sistema lo muestre con el ancho definido dentro de la subventana. El indicador Stoch ocupará toda la subventana restante, compartiendo espacio con el ADX. Pero si el usuario quiere cambiar las cosas, no le será complicado, véase lo que ocurre cuando indicamos una altura para el ADX.

El sistema cambia inmediatamente la forma de presentar el ADX poniéndolo en una ventana flotante, y deja toda la subventana al Stoch. Veremos que cada ventana flotante es totalmente independiente de la otra. Pero la cosa va más allá de lo que se está viendo, véase la siguiente animación.

Véase que la subventana se ha eliminado porque ya no es necesaria. ¿Pero qué rutina es la responsable de todo esto? Pues bien, podemos verlo a continuación, y nos damos cuenta de cómo podemos hacer las cosas realmente interesantes, con unas pocas modificaciones:

void AddTemplate(void)
{
        ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT;
        string sz0 = m_Params.Param[PERIOD];
        int w, h, i;
        bool bIsSymbol;

        if (sz0 == "1M") timeframe = PERIOD_M1; else
        if (sz0 == "2M") timeframe = PERIOD_M2; else
        if (sz0 == "3M") timeframe = PERIOD_M3; else
        if (sz0 == "4M") timeframe = PERIOD_M4; else
        if (sz0 == "5M") timeframe = PERIOD_M5; else
        if (sz0 == "6M") timeframe = PERIOD_M6; else
        if (sz0 == "10M") timeframe = PERIOD_M10; else
        if (sz0 == "12M") timeframe = PERIOD_M12; else
        if (sz0 == "15M") timeframe = PERIOD_M15; else
        if (sz0 == "20M") timeframe = PERIOD_M20; else
        if (sz0 == "30M") timeframe = PERIOD_M30; else
        if (sz0 == "1H") timeframe = PERIOD_H1; else
        if (sz0 == "2H") timeframe = PERIOD_H2; else
        if (sz0 == "3H") timeframe = PERIOD_H3; else
        if (sz0 == "4H") timeframe = PERIOD_H4; else
        if (sz0 == "6H") timeframe = PERIOD_H6; else
        if (sz0 == "8H") timeframe = PERIOD_H8; else
        if (sz0 == "12H") timeframe = PERIOD_H12; else
        if (sz0 == "1D") timeframe = PERIOD_D1; else
        if (sz0 == "1S") timeframe = PERIOD_W1; else
        if (sz0 == "1MES") timeframe = PERIOD_MN1;
        if ((m_Counter >= def_MaxTemplates) || (m_Params.Param[TEMPLATE] == "")) return;
        bIsSymbol = SymbolSelect(m_Params.Param[TEMPLATE], true);
        w = (m_Params.Param[WIDTH] != "" ? (int)StringToInteger(m_Params.Param[WIDTH]) : 0);
        h = (m_Params.Param[HEIGHT] != "" ? (int)StringToInteger(m_Params.Param[HEIGHT]) : 0);
        i = (m_Params.Param[SCALE] != "" ? (int)StringToInteger(m_Params.Param[SCALE]) : -1);
        i = (i > 5 || i < 0 ? -1 : i);
        if ((w > 0) && (h > 0)) AddIndicator(m_Params.Param[TEMPLATE], 0, -1, w, h, timeframe, i); else
        {
                SetBase(m_Params.Param[TEMPLATE], (bIsSymbol ? m_Params.Param[TEMPLATE] : _Symbol), timeframe, i, w);
                if (!ChartApplyTemplate(m_Info[m_Counter - 1].handle, m_Params.Param[TEMPLATE] + ".tpl")) if (bIsSymbol) ChartApplyTemplate(m_Info[m_Counter - 1].handle, "Default.tpl");
                if (m_Params.Param[TEMPLATE] == def_NameTemplateRAD)
                {
                        C_Chart_IDE::Create(GetIdSubWinEA());
                        m_Info[m_Counter - 1].szVLine = "";
                }else
                {
                        m_Info[m_Counter - 1].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand();
                        ObjectCreate(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJ_VLINE, 0, 0, 0);
                        ObjectSetInteger(m_Info[m_Counter - 1].handle, m_Info[m_Counter - 1].szVLine, OBJPROP_COLOR, clrBlack);
                }
                ChartRedraw(m_Info[m_Counter - 1].handle);
        }
}

Las secciones que se destacan son las que hacen la selección de cómo se presentarán los datos en la pantalla, el código no es muy diferente de lo que ya existía, pero son precisamente estas pruebas las que aseguran que el sistema se comportará como lo desea el usuario. Todo esto hasta ahora no necesitaba realmente una reestructuración de código en un nuevo modelo, pero cuando miramos la rutina responsable del redimensionamiento de la subventana, las cosas cambian.

void Resize(void)
{
#define macro_SetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, A, B)
        int x0, x1, y;
        if (!ExistSubWin()) return;
        x0 = 0;
        y = (int)(ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS, GetIdSubWinEA()));
        x1 = (int)((Terminal.GetWidth() - m_Aggregate) / (m_Counter > 0 ? (m_CPre == m_Counter ? m_Counter : (m_Counter - m_CPre)) : 1));
        for (char c0 = 0; c0 < m_Counter; x0 += (m_Info[c0].width > 0 ? m_Info[c0].width : x1), c0++)
        {
                macro_SetInteger(OBJPROP_XDISTANCE, x0);
                macro_SetInteger(OBJPROP_XSIZE, (m_Info[c0].width > 0 ? m_Info[c0].width : x1));
                macro_SetInteger(OBJPROP_YSIZE, y);
                if (m_Info[c0].szTemplate == "IDE") C_Chart_IDE::Resize(x0);
        }
        ChartRedraw();
#undef macro_SetInteger
}

La línea resaltada evita que el sistema se construya sobre la base antigua, esta línea probará si existe una subventana abierta y mantenida por el EA, si no es así, retorna y la rutina no hará nada más, pero si dicha subventana existe, todas las cosas presentes en ella deberán ser redimensionadas según sea necesario, y es sólo por esta prueba que el sistema fue totalmente remodelado.

La siguiente rutina que sufrió cambios se puede ver a continuación:

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        int mx, my;
        datetime dt;
        double p;

        C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam);
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        mx = (int)lparam;
                        my = (int)dparam;
                        ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p);
                        for (int c0 = 0; c0 < m_Counter; c0++)  if (m_Info[c0].szVLine != "")
                        {
                                ObjectMove(m_Info[c0].handle, m_Info[c0].szVLine, 0, dt, 0);
                                ChartRedraw(m_Info[c0].handle);
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        Resize();
                        for (int c0 = 0; c0 < m_Counter; c0++)
                        {
                                ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_PERIOD, m_Info[c0].timeframe);
                                ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_CHART_SCALE,(m_Info[c0].scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Info[c0].scale));
                        }
                        break;
        }
}

La parte resaltada es la que realmente merece especial atención. ¿Qué hace? Es exactamente este código el que presentará la línea vertical en el lugar correcto y en el template correcto. El resto del código simplemente mantendrá y ajustará las plantillas a medida que el gráfico cambie.

Hay enormes ventajas en hacer esto aquí, en la clase objeto, que hacerlo en el EA, dentro del sistema OnChartEvent, y lo principal es que cada clase puede tratar el evento que MT5 está enviando al EA, en lugar de centralizar todo en una sola rutina, dejamos que cada clase haga su trabajo, y si no queremos usar la clase en el EA, simplemente la eliminamos no teniendo efectos secundarios en el resto del código.

¿¡La programación es o no es algo HERMOSO!? Yo AMO programar....

Antes de pasar al siguiente punto dentro de este artículo, comentaré brevemente los valores que se pueden utilizar en los parámetros 1 y 2. Al parámetro 1 se le pueden asignar los siguientes valores: 1M, 2M, 3M, 4M, 5M, 6M, 10M, 12M, 15M, 20M, 30M, 1H, 2H, 3H, 4H, 6H, 8H, 12H, 1D, 1S, 1MES estos valores no son aleatorios y no fueron elegidos al azar, se derivan de la enumeración ENUM_TIMEFRAME y reproducen exactamente lo que se haría si se utilizara el gráfico normal. El parámetro 2 puede recibir valores entre 0 y 5, donde cero es la escala más lejana y cinco la más cercana, para más detalles véase CHART_SCALE.


3.4 Compatibilidad con ventanas flotantes

Ahora entendamos cómo se crean y mantienen las ventanas flotantes, porque sin entender esto no será posible aprovechar realmente el sistema. La clase objeto responsable de esto ha sido denominada C_ChartFloating, pero se podría pensar: ¿Por qué no se utiliza la clase Control de la biblioteca estándar MQL5? La razón es simple. La clase control nos permite crear y mantener una ventana con una funcionalidad muy similar a la del sistema operativo presente en la máquina, pero para nuestro propósito es demasiado exagerado, necesitamos algo mucho más simple, usar la clase control para hacer lo que queremos sería como usar una bazuca para matar una mosca, así que por esta razón se concibió la clase C_ChartFloating, que contiene el mínimo de elementos necesarios para soportar ventanas flotantes, al mismo tiempo que nos permite controlarlas.

La clase en sí no necesita mucha explicación ya que lo único que hacemos es crear 4 objetos gráficos, pero tiene entre las funciones internas tiene 2 que merecen más énfasis, empecemos viendo la función que crea la ventana, esta tiene su código a continuación:

bool AddIndicator(string sz0, int x = 0, int y = -1, int w = 300, int h = 200, ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT, int Scale = -1)
{
        m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS);
        m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS);
        if (m_MaxCounter >= def_MaxFloating) return false;
        y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y);
        CreateBarTitle();
        CreateCaption(sz0);
        CreateBtnMaxMin();
        CreateRegion(TimeFrame, Scale);
        m_Win[m_MaxCounter].handle = ObjectGetInteger(Terminal.Get_ID(), m_Win[m_MaxCounter].szRegionChart, OBJPROP_CHART_ID);
        ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl");   
        m_Win[m_MaxCounter].szVLine = (string)ObjectsTotal(Terminal.Get_ID(), -1, -1) + (string)MathRand();
        ObjectCreate(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJ_VLINE, 0, 0, 0);
        ObjectSetInteger(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJPROP_COLOR, clrBlack);
        m_Win[m_MaxCounter].PosX = -1;
        m_Win[m_MaxCounter].PosY = -1;
        m_Win[m_MaxCounter].PosX_Minimized = m_Win[m_MaxCounter].PosX_Maximized = x;
        m_Win[m_MaxCounter].PosY_Minimized = m_Win[m_MaxCounter].PosY_Maximized = y;
        SetDimension(w, h, true, m_MaxCounter);
        SetPosition(x, y, m_MaxCounter);
        ChartRedraw(m_Win[m_MaxCounter].handle);
        m_MaxCounter++;
        return true;
}

Lo que hace este código es crear todo el soporte necesario para que podamos aplicar un template en un objeto CHART, este se aplica en el punto resaltado del código, nótese que para llamar a esta función, el único parámetro realmente necesario es el nombre del template, todos los demás valores están preinicializados, pero nada impide indicar cuales son los deseados. Por cada nueva ventana creada, la siguiente se desplazará ligeramente para que no se superponga, por defecto, a las demás, esto se consigue en la siguiente línea:

y = (y < 0 ? m_MaxCounter * def_SizeBarCaption : y);

La siguiente rutina de verdadero interés en esta clase es la encargada de manejar los mensajes, y se puede ver a continuación:

void DispatchMessage(int id, long lparam, double dparam, string sparam)
{
        int mx, my;
        datetime dt;
        double p;
        static int six = -1, siy = -1, sic = -1;
                                
        switch (id)
        {
                case CHARTEVENT_MOUSE_MOVE:
                        mx = (int)lparam;
                        my = (int)dparam;
                        if ((((int)sparam) & 1) == 1)
                        {
                                if (sic == -1)  for (int c0 = m_MaxCounter - 1; (sic < 0) && (c0 >= 0); c0--)
                                        sic = (((mx > m_Win[c0].PosX) && (mx < (m_Win[c0].PosX + m_Win[c0].Width)) && (my > m_Win[c0].PosY) && (my < (m_Win[c0].PosY + def_SizeBarCaption))) ? c0 : -1);
                                if (sic >= 0)
                                {
                                        if (six < 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
                                        six = (six < 0 ? mx - m_Win[sic].PosX : six);
                                        siy = (siy < 0 ? my - m_Win[sic].PosY : siy);
                                        SetPosition(mx - six, my - siy, sic);
                                }
                        }else
                        {
                                if (six > 0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
                                six = siy = sic = -1;
                        }
                        ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p);
                        for (int c0 = 0; c0 < m_MaxCounter; c0++)
                                ObjectMove(m_Win[c0].handle, m_Win[c0].szVLine, 0, dt, 0);
                        break;
                case CHARTEVENT_OBJECT_CLICK:
                        for (int c0 = 0; c0 < m_MaxCounter; c0++) if (sparam == m_Win[c0].szBtnMaxMin)
                        {
                                SwapMaxMin((bool)ObjectGetInteger(Terminal.Get_ID(), m_Win[c0].szBtnMaxMin, OBJPROP_STATE), c0);
                                break;
                        }
                        break;
                case CHARTEVENT_CHART_CHANGE:
                        for(int c0 = 0; c0 < m_MaxCounter; c0++)
                        {
                               ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_PERIOD, m_Win[c0].TimeFrame);
                               ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_CHART_SCALE,(m_Win[c0].Scale < 0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Win[c0].Scale));
                        }
                        m_LimitX = (int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS);
                        m_LimitY = (int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS);
                        break;
        }
        for (int c0 = 0; c0 < m_MaxCounter; c0++)
                ChartRedraw(m_Win[c0].handle);
}

Esta rutina concentra en sí misma todo el manejo de eventos que soporta la clase C_ChartFloating, no importa cuántas ventanas estén presentes, las manejará todas de la misma manera, si hiciéramos esto dentro de la función OnChartEvent en el EA, la función sería extremadamente compleja y muy propensa a fallar, pero haciéndola aquí en la clase objeto, garantizamos la integridad del código, ya que si deseamos no usar ventanas flotantes lo único que tenemos que hacer es quitar el archivo de la clase y los puntos donde se accedería, y esto se hace mucho más rápido y sencillo si hacemos la cosa como está.

Pero en el código de arriba hay una parte interesante que está resaltada y cuyo código interno se muestra a continuación:

void SwapMaxMin(const bool IsMax, const int c0)
{
        m_Win[c0].IsMaximized = IsMax;
        SetDimension((m_Win[c0].IsMaximized ? m_Win[c0].MaxWidth : 100), (m_Win[c0].IsMaximized ? m_Win[c0].MaxHeight : 0), false, c0);
        SetPosition((m_Win[c0].IsMaximized ? m_Win[c0].PosX_Maximized : m_Win[c0].PosX_Minimized), (m_Win[c0].IsMaximized ? m_Win[c0].PosY_Maximized : m_Win[c0].PosY_Minimized), c0);
}

¿Qué está haciendo el código de arriba? ¿Suena demasiado confuso? Para entender, observemos atentamente la animación a continuación.


Lo que se ha hecho es lo siguiente: Cuando se crea la ventana flotante comienza a tener un punto de anclaje inicial, este es indicado por el programador, o por el sistema de posicionamiento de la propia clase, este punto de anclaje es el mismo tanto para cuando se maximiza, como para el momento en que se minimiza. Estos valores no son fijos, es decir, el usuario puede modificar estos puntos fácilmente.

Supongamos lo siguiente, que se necesita un lugar determinado del gráfico limpio, entonces se puede mover la ventana maximizada a un lugar de fácil y rápida lectura, luego minimizar la misma ventana y moverla a un lugar diferente, por ejemplo en la esquina de la pantalla. Pronto, el sistema lo memorizará y cuando se maximice la ventana irá al último punto de anclaje en el que se encontraba antes de ser minimizada, lo mismo ocurre con la fase en la que se minimiza la ventana.


Conclusión

Bueno, eso es todo por ahora, en el próximo artículo extenderé esta funcionalidad a la clase de soporte del IDE.


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

Archivos adjuntos |
EA.zip (3616.46 KB)
Aprendiendo a diseñar un sistema de trading con Envelopes Aprendiendo a diseñar un sistema de trading con Envelopes
En este artículo, compartiré con ustedes uno de los métodos para comeciar con bandas. Esta vez analizaremos el indicador Envelopes y veremos lo fácil que resulta crear algunas estrategias basadas en él.
Desarrollando un EA comercial desde cero (Parte 07): Adición de el Volume At Price (I) Desarrollando un EA comercial desde cero (Parte 07): Adición de el Volume At Price (I)
Este es uno de los indicadores más poderosos que existen. Para aquellos que operan y tratan de tener un cierto grado de asertividad, no pueden dejar de tener este indicador en su gráfico, aunque es más utilizado por aquellos que operan observando el flujo («tape reading») también puede ser utilizado por aquellos que utilizan sólo la acción del precio.
Desarrollando un EA comercial desde cero (Parte 09): Un salto conceptual (II) Desarrollando un EA comercial desde cero (Parte 09): Un salto conceptual (II)
Colocación del Chart Trade en una ventana flotante. En el artículo anterior creamos el sistema base para utilizar templates dentro de una ventana flotante.
Qué podemos hacer con la ayuda de medias móviles Qué podemos hacer con la ayuda de medias móviles
En este artículo, hemos recopilado algunos usos del indicador de media móvil. Si se requiere un análisis de curvas, para casi todos los métodos se han hecho indicadores que permiten visualizar una idea útil. En la mayoría de los casos, las ideas se han tomado prestadas de otros autores, pero, en conjunto, suelen ayudar a ver las tendencias principales con mayor precisión y, con suerte, a tomar mejores decisiones comerciales. Nivel de conocimiento de MQL5: inicial.