![Desarrollando un EA comercial desde cero (Parte 07): Adición de el Volume At Price (I)](https://c.mql5.com/2/49/Developing_a_trading_Expert_Advisor_from_scratch_012_600x314.jpg)
Desarrollando un EA comercial desde cero (Parte 07): Adición de el Volume At Price (I)
Introducción
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. Y al ser un indicador de volumen horizontal, con el que podemos analizar cuál fue el volumen de operaciones que salieron durante un determinado precio, es sumamente útil, a pesar de que muchos no puedan hacer la lectura correcta del mismo, al final del artículo dejaré una referencia para su estudio.
Aquí no mostraré cómo hacer esta lectura, porque estaría saliendo del margen de este artículo, la idea aquí es mostrar cómo desarrollar y crear este indicador para que no genere pérdida de desempeño de la plataforma MetaTrader 5. Un dato interesante es que a pesar de que muchos imaginan que este indicador tiene que actualizarse en tiempo real (Real Time), en realidad es aceptable un poco de retraso, siempre y cuando sea muy bajo, desde mi experiencia no he visto grandes problemas en tener un retraso de alrededor de 1 segundo en la actualización de la información, pero si quieres usarlo en Real Time, tendrás que hacer pequeños cambios, no en el indicador en sí, sino en los puntos en los que es accedido por el EA, para tener el comportamiento en Real Time, no obstante la influencia en la lectura será mínima, por no decir insignificante.
Interfaz
La interfaz para controlar la clase Volume At Price es muy sencilla, pero para tener un control total sobre ella, es necesario que el gráfico, donde se aplicará el indicador, tenga las propiedades correctas que se muestran en la figura siguiente, en la que se destaca el elemento de control principal.
Si la cuadrícula no está visible, no será posible cambiar el tamaño del indicador como se muestra en las siguientes animaciones. Observen que la interfaz es muy sencilla e intuitiva, con sólo dos controles, uno que indica la dimensión y otro que señala cuál es el punto de partida para analizar el volumen.
A pesar de todo, este indicador es bastante eficiente y muy interesante de implementar y construir, pero aquí está en su nivel más básico y sufrirá futuras mejoras en otro artículo.
Como no tengo mucho más que decir sobre la interfaz, pasemos a la implementación del código.
Implementación
Para tener el menor trabajo posible a la hora de crear este indicador, nuestro código original sufrió algunos desmembramientos y varias pequeñas modificaciones además de algunas adiciones, pero empecemos por los desmembramientos ya que mucho de lo que necesitamos ya está escrito en otra parte, y lo principal está en la clase C_Wallpaper. ¿Pero cómo? ¿Vamos a crear un indicador basado en un mapa de bits? SÍ, cualquier imagen en la pantalla de un ordenador debe ser pensada como un BITMAP, pero construido de una manera específica. Así, la nueva clase de objeto C_Wallpaper tendrá el aspecto que se muestra a continuación:
class C_WallPaper : public C_Canvas { protected: enum eTypeImage {IMAGEM, LOGO, COR}; //+------------------------------------------------------------------+ private : public : //+------------------------------------------------------------------+ ~C_WallPaper() { Destroy(); } //+------------------------------------------------------------------+ bool Init(const string szName, const eTypeImage etype, const char cView = 100) { if (etype == C_WallPaper::COR) return true; if (!Create(szName, 0, 0, Terminal.GetWidth(), Terminal.GetHeight())) return false; if(!LoadBitmap(etype == C_WallPaper::IMAGEM ? "WallPapers\\" + szName : "WallPapers\\Logos\\" + _Symbol, cView)) return false; ObjectSetInteger(Terminal.Get_ID(), szName, OBJPROP_BACK, true); return true; } //+------------------------------------------------------------------+ void Resize(void) { ResizeBitMap(Terminal.GetWidth(), Terminal.GetHeight()); } //+------------------------------------------------------------------+ };
Vean cómo el código se hizo mucho más compacto, pero al mismo tiempo eliminamos las partes que son comunes entre las clases C_Wallpaper y C_VolumeAtPrice y pusimos todo en otra clase, especificamente en la clase C_Canvas.
Pero, ¿por qué no uso la clase C_Canvas de MetaTrader 5? La cuestión es más personal que práctica, me gusta tener más control sobre cada cosa que se diseña y programa, es más un mal hábito del programador de C que algo realmente necesario, de ahí el hecho de crear una clase para dibujar en la pantalla, pero nada impide utilizar la clase ya presente en MetaTrader 5. Ahora centrémonos en la clase C_VolumeAtPrice que es el tema principal de este artículo. Esta clase tiene 7 funciones que se pueden ver en la siguiente tabla.
Función | Descripción | Tipo de acceso |
---|---|---|
Init | Inicializa la clase con valores definidos por el usuario | Público |
Update | Actualiza datos de Volume At Price dentro de límites de horario definidos | Público |
Resize | Cambia el tamaño de la imagen Volume At Price en el gráfico, esto permite analizar algunos detalles más fácilmente | Público |
DispatchMessage | Se utiliza para enviar mensajes a la clase objeto. | Público |
FromNowOn | Inicializa las variables del sistema | Privado |
SetMatrix | Crea y mantiene una matriz con datos de volumen | Privado |
Redraw | Genera la imagen del volumen | Privado |
Comencemos a implementar el sistema, comenzando con la declaración de variables, estas se pueden ver en el fragmento de código a continuación:
#define def_SizeMaxBuff 4096 //+------------------------------------------------------------------+ #define def_MsgLineLimit "Punto de partida del Volume At Price" //+------------------------------------------------------------------+ class C_VolumeAtPrice : private C_Canvas { #ifdef macroSetInteger ERROR ... #endif #define macroSetInteger(A, B) ObjectSetInteger(Terminal.Get_ID(), m_Infos.szObjEvent, A, B) private : uint m_WidthMax, m_WidthPos; bool m_bChartShift, m_bUsing; double m_dChartShift; struct st00 { ulong nVolBuy, nVolSell, nVolTotal; long nVolDif; }m_InfoAllVaP[def_SizeMaxBuff]; struct st01 { ulong memTimeTick; datetime StartTime, CurrentTime; int CountInfos; ulong MaxVolume; color ColorSell, ColorBuy, ColorBars; int Transparency; string szObjEvent; double FirstPrice; }m_Infos;
La parte resaltada en este fragmento es algo a lo que debemos prestar atención, ya que garantiza que la definición no proviene de otro archivo de manera que se contradiga con la definición que utilizaremos en este archivo. Es muy cierto que el compilador de MQL5 generará una alerta cuando una definición existente sufra un intento de redefinición, y en algunos casos es difícil saber cómo resolver esto, así que para hacernos la vida un poco más fácil, utilizamos un test como el resaltado. Las demás cosas de este fragmento no llaman mucho la atención, pero hay algo a lo que debes prestar atención, es la definición def_SizeMaxBuff, ésta nos indica cuál será el tamaño de nuestro array con la información del volumen, en caso de necesitarlo, cambiamos este valor por otro, pero con las pruebas que se hicieron, este valor es más que adecuado para la gran mayoría de los casos, ya que representa el número de variaciones, en ticks, entre el precio mínimo y su valor máximo, lo que significa que el valor actual puede manejar un rango enorme de casos.
Función Init: Donde empiezan las cosas
Esta función es la que realmente inicializa todas las variables correctamente, en el EA se llama como se muestra en el siguiente fragmento:
//.... Datos iniciales .... input color user10 = clrForestGreen; //Color de la línea Take Profit input color user11 = clrFireBrick; //Color de la línea Stop input bool user12 = true; //Day Trade? input group "Volume At Price" input color user15 = clrBlack; //Color de las barras input char user16 = 20; //Transparencia (0 a 100 ) //+------------------------------------------------------------------+ C_SubWindow SubWin; C_WallPaper WallPaper; C_VolumeAtPrice VolumeAtPrice; //+------------------------------------------------------------------+ int OnInit() { Terminal.Init(); WallPaper.Init(user03, user05, user04); if ((user01 == "") && (user02 == "")) SubWin.Close(); else if (SubWin.Init()) { SubWin.ClearTemplateChart(); SubWin.AddThese(C_TemplateChart::SYMBOL, user02); SubWin.AddThese(C_TemplateChart::INDICATOR, user01); } SubWin.InitilizeChartTrade(user06, user07, user08, user09, user10, user11, user12); VolumeAtPrice.Init(user10, user11, user15, user16); // ... Resto del código
Veamos que necesita pocos parámetros y que básicamente son información sobre los colores que va a utilizar el indicador, a continuación veamos el código interno de esta función, el fragmento de abajo muestra como se inicializan las cosas:
void Init(color CorBuy, color CorSell, color CorBar, char cView) { m_Infos.FirstPrice = Terminal.GetRatesLastDay().open; FromNowOn(macroSetHours(macroGetHour(Terminal.GetRatesLastDay().time), TimeLocal())); m_Infos.Transparency = (int)(255 * macroTransparency(cView)); m_Infos.ColorBars = CorBar; m_Infos.ColorBuy = CorBuy; m_Infos.ColorSell = CorSell; if (m_bUsing) return; m_Infos.szObjEvent = "Event" + (string)ObjectsTotal(Terminal.Get_ID(), -1, OBJ_EVENT); CreateObjEvent(); m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT); m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1); Create("VaP" + (string)MathRand(), 0, 0, 1, 1); Resize(); m_bUsing = true; };
Veamos que es algo muy sencillo, pero tiene algunas peculiaridades que hacen que el código sea interesante e intrigante para algunos, una de ellas está resaltada: Terminal.GetRatesLastDay().open esto puede ser extraño para algunos, pero es algo muy común cuando la programación sigue los principios de un código orientado a objetos (OOP), uno de estos principios dice que ningún código fuera de la clase debe tener acceso a las variables internas de la clase. Pero entonces, ¿cómo obtenemos los valores de las variables dentro de la clase? La forma correcta es utilizando un formulario que sólo aparece en una OOP, así que vamos a ver cómo se declara la rutina GetRatesLastDay dentro de la clase C_Terminal, esto se puede ver en el fragmento de abajo:
inline MqlRates GetRatesLastDay(void) const { return m_Infos.Rates; }
Entendamos cómo funciona esto realmente. Comenzando con la palabra reservada inline, esto le dirá al compilador que el código debe ser colocado en todos las posiciones donde aparezca, en lugar de que el compilador genere una llamada a la función, en realidad copiará todo el código de la rutina al punto donde la rutina está siendo referenciada. Esto hace que el código se ejecute más rápido a costa del consumo de memoria. Pero en el caso concreto lo que realmente ocurre es que la variable m_Infos.Rates será referenciada, esta variable es de tipo MqlRates, es decir podemos acceder a los valores de la estructura MqlRates, en el caso de que no estemos pasando la dirección de referencia de la variable, pero en algunos casos para hacer el código más rápido, lo que hacemos en realidad es pasar la dirección de referencia, y si esto ocurre podemos cambiar el valor de la variable al interior de la clase, lo que en principio debería estar prohibido, para asegurar que esto no ocurra utilizamos la palabra reservada const que garantiza que la variable nunca será modificada sin que sea por el procedimiento propio de la clase. Aunque muchas de las palabras reservadas presentes en C++ también están presentes en MQL5, en forma documentada, algunas no han sido documentadas aún, pero forman parte de MQL5 porque es muy cercano al código nativo de C++. Al final del artículo dejaré referencias para aquellos que deseen aprender un poco más sobre C++ y utilizar estos mismos conocimientos en la programación de MQL5.
Ahora bien, dentro del código de la función Init tenemos un fragmento muy curioso e interesante, lo he resaltado aquí abajo, para explicar lo que está pasando:
m_bChartShift = ChartGetInteger(Terminal.Get_ID(), CHART_SHIFT); m_dChartShift = ChartGetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE); ChartSetInteger(Terminal.Get_ID(), CHART_SHIFT, true); ChartSetDouble(Terminal.Get_ID(), CHART_SHIFT_SIZE, 0.1);
Cuando el EA se pone en marcha modificará el gráfico, pero es una buena práctica, devolver las cosas al original cuando el sistema es desactivado por el usuario, por lo que guardamos la configuración del desplazamiento del gráfico, y luego creamos un desplazamiento mínimo, y esto se hace por los puntos resaltados, por lo que necesitamos que la cuadrícula sea visible en el gráfico, para poder ajustar esta dimensión, y esto se hace de forma interactiva como se vio al principio del artículo, para más detalles ver CHART_SHIFT.
Fijación de objetos en la pantalla
Aunque las funciones internas de la clase son muy sencillas, hay algunos puntos que merecen cierto énfasis, el primero es el sistema de seguridad que impide al usuario eliminar el punto que indica el inicio del análisis del volumen:
El punto es muy pequeño por lo que hay que tener atención para notarlo realmente. OBSERVACIÓN IMPORTANTE: En caso de querer cambiar el punto de análisis, se debe prestar atención al marco temporal del gráfico, si se desea pasar el análisis de las 9:00 a por ejemplo las 9:02 se debe utilizar un MARCO TEMPORAL de 1 minuto o 2 minutos, si se utiliza el gráfico en 5 minutos por ejemplo, no se podrá hacer. Entendido esto, debemos cuidar que este elemento no sea borrado accidentalmente por el usuario, esto se logra con el siguiente fragmento de código:
void DispatchMessage(int iMsg, string sparam) { switch (iMsg) { // ... Partes internas del código case CHARTEVENT_OBJECT_DELETE: if ((sparam == m_Infos.szObjEvent) && (m_bUsing)) { m_bUsing = false; CreateObjEvent(); Resize(); m_bUsing = true; } break; } };
Cuando la clase se dé cuenta de que el objeto fue borrado, inmediatamente volverá a crear este objeto, esto evita que el usuario se quede sin un objeto útil para la clase, y por lo tanto se vea obligado a reiniciar el EA. Utilice este modelo mostrado en el fragmento siempre que quiera asegurarse de que el usuario no elimina un objeto sensible. Pero para asegurar que el evento es visto por el EA, se debe añadir algo de código extra:
ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, 0, true);
Esta simple línea asegura que MetaTrader 5 le informará de que un objeto ha sido eliminado. Para más detalles, véase CHART_EVENT_OBJECT_DELETE
Ensamblaje del gráfico Volume At Price
Este es el corazón de la clase, tiene 3 funciones, 1 pública y 2 privadas, empecemos por la función pública que se puede ver a continuación:
inline virtual void Update(void) { MqlTick Tick[]; int i1, p1; if (m_bUsing == false) return; if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > 0) { if (m_Infos.CountInfos == 0) { macroSetInteger(OBJPROP_TIME, m_Infos.StartTime = macroRemoveSec(Tick[0].time)); m_Infos.FirstPrice = Tick[0].last; } for (p1 = 0; (p1 < i1) && (Tick[p1].time_msc == m_Infos.memTimeTick); p1++); for (int c0 = p1; c0 < i1; c0++) SetMatrix(Tick[c0]); if (p1 == i1) return; m_Infos.memTimeTick = Tick[i1 - 1].time_msc; m_Infos.CurrentTime = macroRemoveSec(Tick[i1 - 1].time); Redraw(); }; };
Las líneas resaltadas son muy importantes para el sistema, cuando el sistema ingresa, no sabe exactamente donde comenzar, y estas líneas hacen la actualización de estos puntos, le indican al usuario donde comenzó el análisis y cual fue el precio inicial para que el sistema pueda crear la tabla interna, el sistema siempre esperará a que ingrese un nuevo tick, en cuanto esto suceda tendremos datos para ser analizados y ensamblados de manera de ser presentados en la pantalla y esto nos lleva a la siguiente función:
inline void SetMatrix(MqlTick &tick) { int pos; if ((tick.last == 0) || ((tick.flags & (TICK_FLAG_BUY | TICK_FLAG_SELL)) == (TICK_FLAG_BUY | TICK_FLAG_SELL))) return; pos = (int) ((tick.last - m_Infos.FirstPrice) / Terminal.GetPointPerTick()) * 2; pos = (pos >= 0 ? pos : (pos * -1) - 1); if ((tick.flags & TICK_FLAG_BUY) == TICK_FLAG_BUY) m_InfoAllVaP[pos].nVolBuy += tick.volume; else if ((tick.flags & TICK_FLAG_SELL) == TICK_FLAG_SELL) m_InfoAllVaP[pos].nVolSell += tick.volume; m_InfoAllVaP[pos].nVolDif = (long)(m_InfoAllVaP[pos].nVolBuy - m_InfoAllVaP[pos].nVolSell); m_InfoAllVaP[pos].nVolTotal = m_InfoAllVaP[pos].nVolBuy + m_InfoAllVaP[pos].nVolSell; m_Infos.MaxVolume = (m_Infos.MaxVolume > m_InfoAllVaP[pos].nVolTotal ? m_Infos.MaxVolume : m_InfoAllVaP[pos].nVolTotal); m_Infos.CountInfos = (m_Infos.CountInfos == 0 ? 1 : (m_Infos.CountInfos > pos ? m_Infos.CountInfos : pos)); }
Esta función puede no ser gran cosa, ya que sólo almacena y mantiene los valores de volumen en el precio, pero las líneas resaltadas en ella son el alma del sistema. Para entender realmente lo que ocurre en estas dos líneas tenemos que pensar un poco. Imaginemos lo siguiente: ¿Qué es más rápido, guardar cada uno de los precios y anotar los volúmenes en cada uno de ellos, o guardar sólo los volúmenes imaginando cuál es el precio? La segunda opción es más rápida, así que mantengamos los volúmenes e imaginemos dónde está el precio. Pero ¿cuál sería el primer precio del sistema? Pues sí, necesitamos un valor de partida, sin él la cosa se desmoronaría. ¿Qué tal si usamos el precio del primer tick negociado? Sí, esto es genial. Perfecto. Pero tenemos un problema, si el precio está subiendo, estupendo, todos los datos se pueden almacenar en un array fácilmente, pero ¿qué pasa si bajan? En ese caso tendremos valores negativos, y no podemos acceder a un array con un índice negativo. Así que podríamos utilizar 2 matrices en lugar de 1, pero esto nos traería una carga de trabajo innecesaria, pero hay una solución sencilla, miremos la tabla de abajo:
Si el index es positivo estamos tranquilos, pero si es negativo tendremos problemas, pues estamos utilizando un array con dos direcciones, siendo el valor Zero el que representa el precio del primer tick, los valores negativos son los que vinieron abajo y los positivos, los que vinieron arriba. Ahora sigue el razonamiento: Si tenemos 2 direcciones, al multiplicar el index por 2 tendremos la columna del medio, esto no parece ayudarnos, pero si convertimos los valores negativos en positivos y restamos 1 tendremos la columna de la derecha, y si observamos detenidamente veremos que, en esta columna de la derecha, los valores se intercalan dándonos un índice perfecto con el que acceder a un array que sabemos que va a crecer, pero no sabemos cuánto va a crecer. Y eso es exactamente lo que hacen las dos líneas resaltadas, crear un índice para nuestro array, intercalando los valores que están por encima con los que están por debajo del precio inicial. Pero a pesar de ser una muy buena solución, no servirá de nada si no podemos representar los datos en la pantalla, y eso es exactamente lo que hace la siguiente función.
void Redraw(void) { uint x, y, y1, p; double reason = (double) (m_Infos.MaxVolume > m_WidthMax ? (m_WidthMax / (m_Infos.MaxVolume * 1.0)) : 1.0); double desl = Terminal.GetPointPerTick() / 2.0; Erase(); p = m_WidthMax - 8; for (int c0 = 0; c0 <= m_Infos.CountInfos; c0++) { if (m_InfoAllVaP[c0].nVolTotal == 0) continue; ChartTimePriceToXY(Terminal.Get_ID(), 0, 0, m_Infos.FirstPrice + (Terminal.GetPointPerTick() * (((c0 & 1) == 1 ? -(c0 + 1) : c0) / 2)) + desl, x, y); y1 = y + Terminal.GetHeightBar(); FillRectangle(p + 2, y, p + 8, y1, macroColorRGBA(m_InfoAllVaP[c0].nVolDif > 0 ? m_Infos.ColorBuy : m_Infos.ColorSell, m_Infos.Transparency)); FillRectangle((int)(p - (m_InfoAllVaP[c0].nVolTotal * reason)), y, p, y1, macroColorRGBA(m_Infos.ColorBars, m_Infos.Transparency)); } C_Canvas::Update(); };
Esta función trazará el volumen en el gráfico, y la parte resaltada se encarga de invertir el cálculo realizado durante la captura de los volúmenes, y para que la presentación sea en el punto adecuado el precio sufre un pequeño desplazamiento, por lo que las barras se posicionan correctamente, el resto de la rutina son sólo rutinas de pintura. Vale la pena explicarlo aquí. Observe que hay dos llamadas a FillRectangle, ¿y por qué esto? La primera llamada pondrá una indicación de qué volumen fue mayor, si el de los vendedores o el de los compradores, y la segunda llamada trazará realmente el volumen. ¿Pero por qué no los trazó juntos, dividiendo la barra de volumen entre compradores y vendedores? La razón es que a medida que el volumen sube en un rango de precios, empezará a entorpecer el análisis en otros rangos de precios más pequeños, con lo que será difícil identificar cuál fue el mayor volumen, si fue de venta o de compra, y colocándolo de esta manera este problema desaparece, por lo que la lectura es más sencilla y directa, al final el gráfico quedará como se muestra en la figura siguiente:
Todas las demás rutinas de la clase sirven de apoyo a las explicadas, por lo que no tienen gran relevancia al punto de necesitar entrar en detalles sobre ellas.
Conclusión
Aquí les presento un modelo muy simple de Volume as Price, pero sumamente efectivo, si estamos comenzando a aprender programación, y queremos enfocarnos en la programación orientada a objetos (OOP), debemos estudiar este código con calma, porque en él hay varios conceptos que son muy buenos, ya que todo el código está enfocado 100% orientado a objetos.
Adjunto tenemos el EA hasta la etapa actual de desarrollo.
Referencias
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/10302
![Desarrollando un EA comercial desde cero (Parte 08): Un salto conceptual (I)](https://c.mql5.com/2/45/development__1.png)
![Qué podemos hacer con la ayuda de medias móviles](https://c.mql5.com/2/45/moving_averages.png)
![Aprendiendo a diseñar un sistema de trading con Envelopes](https://c.mql5.com/2/45/why-and-how__3.png)
![Múltiples indicadores en un gráfico (Parte 06): Convirtamos el MetaTrader 5 en un sistema RAD (II)](https://c.mql5.com/2/45/variety_of_indicators__4.png)
![MQL5 - Lenguaje de estrategias comerciales para el terminal de cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso