
Indicador de perfil de mercado — Market Profile (Parte 2): Optimización y dibujado en canvas
Contenido
Introducción
Volviendo al tema del indicador de Perfil de mercado iniciado en el artículo anterior, me gustaría decir que la implementación de la construcción de un diagrama de perfil de mercado utilizando objetos gráficos ordinarios consume bastantes recursos. Después de todo, cada punto de precio desde el mínimo hasta el máximo de la barra diaria se rellena con objetos rectangulares gráficos en el número de barras que han alcanzado ese nivel de precio a lo largo del día. Y así, para cada elemento, todos contienen muchos objetos gráficos, y todos estos objetos se crean y dibujan para cada día en el que se dibuja el diagrama de perfil. Cuando un indicador crea miles de objetos gráficos, puede provocar ralentizaciones sustanciales al trabajar con otros objetos gráficos y al redibujar el gráfico.
La ejecución del indicador en el gráfico M30 y la construcción de un Perfil de mercado para solo tres días:
conduce a la creación de 4697 objetos-rectángulos gráficos:
Es un gasto de recursos muy poco óptimo. Si aumentamos el número de días mostrados en los ajustes, el número de objetos creados que se utilizan para dibujar los diagramas del Perfil de mercado en el gráfico para cada día mostrado, aumentará como una avalancha.
Pero aquí solo se dibujan diagramas utilizando objetos gráficos rectangulares. Un segmento corto de la línea del histograma del perfil será un objeto gráfico. Por lo tanto, podemos dibujar no directamente en el gráfico, sino en un objeto-lienzo gráfico, situado a su vez en el gráfico, en las coordenadas deseadas. Entonces solo tendremos un (!) objeto gráfico para cada día. ¡Y durante tres días habrá tres objetos en lugar de 4697! Es una diferencia sustancial. Y esto es lo que podemos hacer con la clase para la creación simplificada de dibujos personalizados CCanvas, suministrada en la Biblioteca Estándar del terminal de cliente.
La versión del indicador de Perfil de mercado que dibuja el histograma del perfil en el lienzo, se presenta en el terminal en la carpeta \MQL5\Indicators\Free Indicators\ en el archivo MarketProfile Canvas.mq5. Tras estudiar el código, nos damos cuenta de que aquí, a diferencia de la primera versión (MarketProfile.mq5), la salida de los gráficos se realiza sobre objetos de la clase CCanvas. Es decir, la lógica del indicador sigue siendo la misma (ya la analizamos en el primer artículo, en la sección Diseño y principios), pero el dibujado se realiza con la ayuda de una clase especial CMarketProfile, que utiliza el dibujo en CCanvas.
La lógica del trabajo es muy sencilla:
- en un ciclo de un número determinado de días,
- creamos u obtenemos el objeto de la clase CMarketProfile para el día actual en el ciclo,
- dibujamos o redibujamos el perfil del día en el lienzo correspondiente al día actual del ciclo.
Es decir, el trabajo principal para dibujar el diagrama del perfil se realiza dentro de la clase CMarketProfile. Veamos la construcción y el funcionamiento de esta clase.
Clase de Perfil de mercado CMarketProfile
Vamos a abrir el archivo \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 y a localizar el código de la clase CMarketProfile en ella. Vamos a analizar lo que hay aquí y discutir para qué sirve:
//+------------------------------------------------------------------+ //| Class to store and draw Market Profile for the daily bar | //+------------------------------------------------------------------+ class CMarketProfile { public: CMarketProfile() {}; CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]); ~CMarketProfile(void); //--- checks if the object was created for the specified date bool Check(string prefix, datetime time); //--- set high/low and array of intraday bars void SetHiLoBars(double high, double low, MqlRates &bars[]); //--- set canvas dimensions and drawing options void UpdateSizes(void); //--- is the profile in the visible part of the chart? bool isVisibleOnChart(void); //--- has the graph scale changed? bool isChartScaleChanged(void); //--- calculates profile by sessions bool CalculateSessions(void); //--- draws a profile void Draw(double multiplier=1.0); //--- protected: CCanvas m_canvas; // CCanvas class object for drawing profile uchar m_alpha; // alpha channel value that sets transparency string m_prefix; // unique prefix of the OBJ_BITMAP object string m_name; // name of the OBJ_BITMAP object used in m_canvas double m_high; // day's High double m_low; // day's Low datetime m_time1; // start time of the day datetime m_time2; // end time of the day int m_day_size_pt; // daily bar height in points int m_height; // daily bar height in pixels on the chart int m_width; // daily bar width in pixels on the chart MqlRates m_bars[]; // array of bars of the current timeframe between m_time1 and m_time2 vector m_asia; // array of bar counters for the Asian session vector m_europe; // array of bar counters for the European session vector m_america; // array of bar counters for the American session double m_vert_scale; // vertical scaling factor double m_hor_scale; // horizontal scaling factor };Métodos públicos declarados en la clase:
- El método Check() se utiliza para comprobar la existencia de un objeto de perfil de mercado creado para un día concreto;
- El método SetHiLoBars() se utiliza para establecer en el objeto de perfil de mercado los valores de los precios High y Low del día y transmitir un array de barras intradía;
- El método UpdateSizes() establece los tamaños del lienzo y los factores de escala para dibujar rectángulos en el objeto de perfil de mercado;
- El método isVisibleOnChart() retorna una bandera que indica que el perfil de mercado está dentro del rango de visibilidad en el gráfico;
- El método isChartScaleChanged() está declarado en la clase pero no implementado;
- El método CalculateSessions() calcula los parámetros y rellena los arrays de las sesiones comerciales;
- El método Draw() dibuja en el lienzo un histograma del perfil del mercado basado en los datos de todas las sesiones comerciales.
El propósito de las variables declaradas en la sección protegida de la clase resulta bastante claro. Querría centrarme en los arrays del contador de la barra de sesiones.
Todos ellos se declaran como vectores variables, lo cual nos permitirá trabajar con ellos como si fueran arrays de datos, pero de forma un poco más sencilla:
El uso de vectores y arrays, y más concretamente, de los métodos especiales que poseen estos tipos de datos, permite escribir un código más sencillo, corto y comprensible, más cercano a la notación matemática. Esto evita al programador tener que crear ciclos anidados y acordarse de indexar correctamente los arrays que intervienen en el cálculo. De esta forma, aumenta la fiabilidad y la velocidad de desarrollo de programas complejos.
Vamos a analizar la implementación de los métodos declarados de la clase.
Constructor:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ void CMarketProfile::CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]): m_prefix(prefix), m_time1(time1), m_time2(time2), m_high(high), m_low(low), m_vert_scale(NULL), m_hor_scale(NULL) { //--- copy the array of intraday bars to the array of MqlRates structures, //--- create a name for the graphical object and define the size of the daily candle ArrayCopy(m_bars, bars); m_name=ExtPrefixUniq+"_MP_"+TimeToString(time1, TIME_DATE); m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT)); //--- set vector sizes for trading sessions m_asia=vector::Zeros(m_day_size_pt); m_europe=vector::Zeros(m_day_size_pt); m_america=vector::Zeros(m_day_size_pt); //--- set the width and height of the canvas UpdateSizes(); //--- if this is the first tick at the beginning of the day, then the canvas dimensions will be zero - set the dimensions to 1 pixel in height and width m_height=m_height?m_height:1; m_width=m_width?m_width:1; //--- create a graphical object if(m_canvas.CreateBitmap(m_name, m_time1, m_high, m_width, m_height, COLOR_FORMAT_ARGB_NORMALIZE)) ObjectSetInteger(0, m_name, OBJPROP_BACK, true); else { Print("Error creating canvas: ", GetLastError()); Print("time1=", m_time1, " high=", m_high, " width=", m_width, " height=", m_height); } }
El prefijo del nombre del objeto canvas creado, sobre el que se realizará el dibujado del perfil del día, la hora de inicio y fin del día, los precios máximo y mínimo del día y el array de barras intradiarias se transmitirán al constructor paramétrico de la clase. Los valores de estas variables se escribirán en las variables de clase correspondientes en la línea de inicialización. A continuación
- el array transmitido por referencia se copia en el array de la clase, se crea un nombre único del objeto gráfico a partir del prefijo, abreviatura "_MP_" y la hora de apertura del día pasados en los parámetros de entrada del constructor, y se calcula el tamaño de la vela diaria en puntos;
- cada uno de los arrays de las sesiones comerciales obtiene un tamaño igual al tamaño de la barra diaria en puntos y se rellena simultáneamente con ceros, a saber, se inicializa;
- luego se establecen las dimensiones del lienzo para dibujar el perfil; si se trata del primer tick del día, el tamaño será cero, y la anchura y la altura se establecerán en las dimensiones mínimas resueltas de un píxel en ambas dimensiones;
- des pués se crea un lienzo para dibujar según las dimensiones especificadas.
Método para verificar la existencia de un objeto de perfil de mercado creado para un día concreto:
//+------------------------------------------------------------------+ //| Checks if CMarketProfile object is for the specified 'time' date | //+------------------------------------------------------------------+ bool CMarketProfile::Check(string prefix, datetime time) { string calculated= prefix+"_MP_"+TimeToString(time, TIME_DATE); return (m_name==(calculated)); };
Como el nombre de cada objeto-lienzo de dibujado del perfil se establece en el constructor de la clase, y el nombre utiliza una representación de línea de la hora de inicio del día, para comprobar que el objeto se crea para una hora determinada, al método se transmite la hora de inicio del día; luego se crea una línea idéntica a la línea del nombre del objeto y se compara la línea creada con el nombre real del objeto. El método retorna el resultado de la comprobación.
Método para establecer en el objeto de perfil de mercado los valores de precio High y Low del día, y para pasar el array de barras intradía:
//+------------------------------------------------------------------+ //| Sets High/Low and a set of current-timeframe bars | //+------------------------------------------------------------------+ void CMarketProfile::SetHiLoBars(double high, double low, MqlRates &bars[]) { //--- if the maximum of the day has changed, move the OBJ_BITMAP object to the new Y coordinate if(high>m_high) { m_high=high; if(!ObjectSetDouble(0, m_name, OBJPROP_PRICE, m_high)) PrintFormat("Failed to update canvas for %s, error %d", TimeToString(m_time1, TIME_DATE), GetLastError()); } ArrayCopy(m_bars, bars); m_high=high; m_low=low; //--- daily range in points m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT)); //--- reset vector sizes for trading sessions m_asia=vector::Zeros(m_day_size_pt); m_europe=vector::Zeros(m_day_size_pt); m_america=vector::Zeros(m_day_size_pt); }
Luego transmitimos al método los valores High y Low de la vela diaria, además de transmitir por referencia un array de barras intradía en el formato de la estructura MqlRates.
- el precio High se escribe en la variable del objeto y el lienzo se desplaza a la nueva coordenada;
- las barras intradía se copian del array de barras transmitido al array interno;
- el precio mínimo del día se establece en la variable de clase;
- luego se calcula el nuevo tamaño de la barra diaria en puntos
- los arrays de sesiones comerciales se incrementan con el valor calculado del tamaño de la barra diaria en puntos y se rellenan con ceros, es decir, se inicializan.
Cabe destacar que para inicializar los vectores se utiliza el método de arrays y vectores Zeros(), que establece el tamaño del vector y rellena todo el array con ceros.
Para un array simple, tendríamos que hacer dos operaciones sobre el array: ArrayResize() y ArrayInitialize().
Método que establece las dimensiones del lienzo y los factores de escala para dibujar rectángulos en el objeto de perfil de mercado:
//+------------------------------------------------------------------+ //| Sets drawing parameters | //+------------------------------------------------------------------+ void CMarketProfile::UpdateSizes(void) { //--- convert time/price to x/y coordinates int x1, y1, x2, y2; ChartTimePriceToXY(0, 0, m_time1, m_high, x1, y1); ChartTimePriceToXY(0, 0, m_time2, m_low, x2, y2); //--- calculate canvas dimensions m_height=y2-y1; m_width =x2-x1; //--- calculate ratios for transforming vertical price levels //--- and horizontal bar counters to chart pixels m_vert_scale=double(m_height)/(m_day_size_pt); m_hor_scale =double(m_width*PeriodSeconds(PERIOD_CURRENT))/PeriodSeconds(PERIOD_D1); //--- change the canvas size m_canvas.Resize(m_width, m_height); }
La lógica del método se comenta con detalle en el código. Los factores de escala se usan para establecer el tamaño de los rectángulos dibujados en el lienzo según la relación entre el tamaño del lienzo y el tamaño de la ventana del gráfico.
A continuación, los coeficientes calculados se añaden al cálculo de la altura y la anchura de los rectángulos que se van a dibujar.
Método que retorna la bandera que indica que el perfil de mercado está dentro de la visibilidad del gráfico:
//+------------------------------------------------------------------+ //| Checks that the profile is in the visible part of the chart | //+------------------------------------------------------------------+ bool CMarketProfile::isVisibleOnChart(void) { long last_bar=ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); // last visible bar on the chart on the left long first_bar=last_bar+-ChartGetInteger(0, CHART_VISIBLE_BARS); // first visible bar on the chart on the right first_bar=first_bar>0?first_bar:0; datetime left =iTime(Symbol(), Period(), (int)last_bar); // time of the left visible bar on the chart datetime right=iTime(Symbol(), Period(), (int)first_bar); // time of the right visible bar on the chart //--- return a flag that the canvas is located inside the left and right visible bars of the chart return((m_time1>= left && m_time1 <=right) || (m_time2>= left && m_time2 <=right)); }
Aquí encontramos los números de las barras izquierda y derecha visibles del gráfico, obtenemos sus tiempos y devolvemos la bandera de que el tiempo de los bordes izquierdo y derecho del lienzo están dentro del área de barras visibles del gráfico.
Método que calcula los parámetros y rellena los arrays de sesiones comerciales:
//+------------------------------------------------------------------+ //| Prepares profile arrays by sessions | //+------------------------------------------------------------------+ bool CMarketProfile::CalculateSessions(void) { double point=SymbolInfoDouble(Symbol(), SYMBOL_POINT); // one point value //--- if the array of intraday bars is not filled, leave if(ArraySize(m_bars)==0) return(false); //---- iterate over all the bars of the current day and mark the cells of the arrays (vectors) that contain the bars being iterated over in the loop int size=ArraySize(m_bars); for(int i=0; i<size; i++) { //--- get the bar hour MqlDateTime bar_time; TimeToStruct(m_bars[i].time, bar_time); uint hour =bar_time.hour; //--- calculate price levels in points from the Low of the day reached by the price on each bar of the loop int start_box=(int)((m_bars[i].low-m_low)/point); // index of the beginning of price levels reached by the price on the bar int stop_box =(int)((m_bars[i].high-m_low)/point); // index of the end of price levels reached by the price on the bar //--- American session if(hour>=InpAmericaStartHour) { //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(int ind=start_box; ind<stop_box; ind++) m_america[ind]++; } else { //--- European session if(hour>=InpEuropeStartHour && hour<InpAmericaStartHour) //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(int ind=start_box; ind<stop_box; ind++) m_europe[ind]++; //--- Asian session else //--- in the loop from the beginning to the end of price levels, fill the counters of bars where the price was at this level for(int ind=start_box; ind<stop_box; ind++) m_asia[ind]++; } } //--- vectors of all sessions are ready return(true); }
En el artículo anterior, analizamos con detalle la lógica de definición del número de barras de la sesión comercial, cuyo precio alcanzó los niveles en puntos del Low al High del día. Si en la versión anterior del indicador todo esto se realizaba en el ciclo principal del indicador, aquí estos cálculos se colocarán en un método aparte del objeto de perfil diario. De lo que se trata aquí es de calcular y registrar en las celdas del array (vector) el número de barras que cruzan cada nivel de precios, calculado en puntos desde el Low al High del día. Una vez ejecutado el método, todos los vectores se rellenarán según los niveles de precios de acuerdo con el movimiento de los mismos. En las celdas correspondientes del array (vector), se registrará el número de barras que haya cruzado cada nivel.
Método que dibuja un histograma del perfil del mercado en un lienzo usando los datos de todas las sesiones comerciales:
//+------------------------------------------------------------------+ //| Draw Market Profile on the canvas | //+------------------------------------------------------------------+ void CMarketProfile::Draw(double multiplier=1.0) { //--- sum up all sessions for rendering vector total_profile=m_asia+m_europe+m_america; // profile that combines all sessions vector europe_asia=m_asia+m_europe; // profile that combines only the European and Asian sessions //--- set a completely transparent background for the canvas m_canvas.Erase(ColorToARGB(clrBlack, 0)); //--- variables for drawing rectangles int x1=0; // X coordinate of the left corner of the rectangle always starts at zero int y1, x2, y2; // rectangle coordinates int size=(int)total_profile.Size(); // size of all sessions //--- render the American session with filled rectangles for(int i=0; i<size; i++) { //--- skip zero vector values if(total_profile[i]==0) continue; //--- calculate two points to draw a rectangle, x1 is always 0 (X of the lower left corner of the rectangle) y1=m_height-int(i*m_vert_scale); // Y coordinate of the lower left corner of the rectangle y2=(int)(y1+m_vert_scale); // Y coordinate of the upper right corner of the rectangle x2=(int)(total_profile[i]*m_hor_scale*multiplier); // X coordinate of the upper right corner of the rectangle //--- draw a rectangle at the calculated coordinates with the color and transparency set for the American session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAmericaSession, InpTransparency)); } //--- render the European session with filled rectangles for(int i=0; i<size; i++) { //--- skip zero vector values if(total_profile[i]==0) continue; //--- calculate two points to draw a rectangle y1=m_height-int(i*m_vert_scale); y2=(int)(y1+m_vert_scale); x2=(int)(europe_asia[i]*m_hor_scale*multiplier); //--- draw a rectangle over the rendered American session using the calculated coordinates //--- with color and transparency set for the European session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpEuropeSession, InpTransparency)); } //--- draw the Asian session with filled rectangles for(int i=0; i<size; i++) { //--- skip zero vector values if(total_profile[i]==0) continue; //--- calculate two points to draw a rectangle y1=m_height-int(i*m_vert_scale); y2=(int)(y1+m_vert_scale); x2=(int)(m_asia[i]*m_hor_scale*multiplier); //--- draw a rectangle over the rendered European session using the calculated coordinates //--- with color and transparency set for the Asian session m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAsiaSession, InpTransparency)); } //--- update the OBJ_BITMAP object without redrawing the chart m_canvas.Update(false); }
La lógica del método se explica en los comentarios al código. Resumiendo: calculamos y rellenamos los arrays (vectores) de tres sesiones: asiática, europea y americana. Tenemos que dibujar un histograma de perfil diferente para cada una de las sesiones. Primero se dibuja la sesión americana, encima se dibuja la sesión europea y, al final, se dibuja la sesión asiática encima de las dos sesiones anteriores.
¿Por qué dibujamos las sesiones en orden inverso a su duración?
- La sesión americana (o más bien su histograma) incluye tanto el tiempo ya negociado de las dos sesiones anteriores como el tiempo de la sesión americana, es decir, es el histograma más completo del perfil de todo el día. Por eso se dibuja primero.
- A continuación, se dibuja la sesión europea, que incluye la el tiempo de la sesión asiática ya negociada. En consecuencia, como solo hay dos sesiones, la asiática y la europea, el histograma será más corto a lo largo del eje X de la sesión americana, lo cual significa que deberá dibujarse encima de la americana.
- Y a continuación se dibujará el histograma más corto de la sesión asiática en el eje X.
Me gustaría señalar lo cómodo que resulta combinar los datos de los arrays cuando se utilizan vectores:
//--- sum up all sessions for rendering vector total_profile=m_asia+m_europe+m_america; // profile that combines all sessions vector europe_asia=m_asia+m_europe; // profile that combines only the European and Asian sessions
En esencia, se trata de la fusión por elementos de varios arrays con el mismo tamaño en un único resultante, que puede representarse con este código:
#define SIZE 3 double array_1[SIZE]={0,1,2}; double array_2[SIZE]={3,4,5}; double array_3[SIZE]={6,7,8}; Print("Contents of three arrays:"); ArrayPrint(array_1); ArrayPrint(array_2); ArrayPrint(array_3); for(int i=0; i<SIZE; i++) { array_1[i]+=array_2[i]+=array_3[i]; } Print("\nResult of the merge:"); ArrayPrint(array_1); /* Contents of three arrays: 0.00000 1.00000 2.00000 3.00000 4.00000 5.00000 6.00000 7.00000 8.00000 Result of the merge: 9.00000 12.00000 15.00000 */
El código anterior hace lo mismo que la línea de código del método que hemos comentado antes:
vector total_profile=m_asia+m_europe+m_america; // profile that combines all sessions
Creo que no hace falta decir lo cómodo y sucinto que resulta ahora el código...
El destructor de la clase borra el objeto-lienzo creado y redibuja el gráfico para reflejar los cambios:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CMarketProfile::~CMarketProfile(void) { //--- delete all graphical objects after use ObjectsDeleteAll(0, m_prefix, 0, OBJ_BITMAP); ChartRedraw(); }
Ahora, en lugar de dibujar objetos gráficos en el ciclo del indicador, bastará con crear una instancia de la clase considerada para cada barra diaria, calcular los datos de todas las sesiones y dibujar en el lienzo un histograma del perfil del mercado para cada día. El número de días establecido en la configuración para mostrar el perfil será el número de objetos gráficos creados, a diferencia de la versión anterior del indicador, en la que cada línea del histograma se dibujaba con su propio objeto gráfico.
Optimizamos el indicador
Veamos ahora cómo se elabora el indicador usando la clase de perfil de mercado. Vamos a abrir el archivo del indicador \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 y a estudiarlo desde el principio.
En primer lugar, conectamos al archivo del indicador los archivos de clase para la creación simplificada de dibujos personalizados de CCanvas y el archivo de clase para crear listas estrictamente tipadas CArrayList<T>:
//+------------------------------------------------------------------+ //| MarketProfile Canvas.mq5 | //| Copyright 2009-2024, MetaQuotes Ltd | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window #property indicator_plots 0 #include <Canvas\Canvas.mqh> #include <Generic\ArrayList.mqh> //--- input parameters
A continuación declaramos la lista de parámetros de entrada del indicador, el prefijo único de los objetos gráficos, la clase de perfil de mercado y la lista de objetos de esta clase:
//--- input parameters input uint InpStartDate =0; /* day number to start calculation */ // 0 - current, 1 - previous, etc. input uint InpShowDays =7; /* number of days to display */ // starting with and including the day in InpStartDate input int InpMultiplier =1; /* histogram length multiplier */ input color InpAsiaSession =clrGold; /* Asian session */ input color InpEuropeSession =clrBlue; /* European session */ input color InpAmericaSession =clrViolet; /* American session */ input uchar InpTransparency =150; /* Transparency, 0 = invisible */ // market profile transparency, 0 = fully transparent input uint InpEuropeStartHour =8; /* European session opening hour */ input uint InpAmericaStartHour=14; /* American session opening hour */ //--- unique prefix to identify graphical objects belonging to the indicator string ExtPrefixUniq; //--- declare CMarketProfile class class CMarketProfile; //--- declare a list of pointers to objects of the CMarketProfile class CArrayList<CMarketProfile*> mp_list;
Como la clase del perfil de mercado se escribe debajo del código del indicador, deberemos realizar la declaración anticipada de la clase para que no se produzca un error de tipo desconocido de la variable durante la compilación
'CMarketProfile' - unexpected token
La lista estrictamente tipada contiene los punteros a los objetos con el tipo de clase CMarketProfile, que se escribe debajo del código.
En el manejador OnInit() , el prefijo de los objetos gráficos se crea como los 4 últimos dígitos del número de milisegundos que han pasado desde que se inició el sistema:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- create a prefix for object names string number=StringFormat("%I64d", GetTickCount64()); ExtPrefixUniq=StringSubstr(number, StringLen(number)-4); Print("Indicator \"Market Profile Canvas\" started, prefix=", ExtPrefixUniq); return(INIT_SUCCEEDED); }
Veamos el código completo del manejador OnCalculate():
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- opening time of the current daily bar datetime static open_time=0; //--- number of the last day for calculations //--- (if InpStartDate = 0 and InpShowDays = 3, lastday = 3) //--- (if InpStartDate = 1 and InpShowDays = 3, lastday = 4) etc ... uint lastday=InpStartDate+InpShowDays; //--- if the first calculation has already been made if(prev_calculated!=0) { //--- get the opening time of the current daily bar datetime current_open=iTime(Symbol(), PERIOD_D1, 0); //--- if we do not calculate the current day if(InpStartDate!=0) { //--- if the opening time was not received, leave if(open_time==current_open) return(rates_total); } //--- update opening time open_time=current_open; //--- we will only calculate one day from now on, since all other days have already been calculated during the first run lastday=InpStartDate+1; } //--- in a loop for the specified number of days (either InpStartDate+InpShowDays on first run, or InpStartDate+1 on each tick) for(uint day=InpStartDate; day<lastday; day++) { //--- get the data of the day with index day into the structure MqlRates day_rate[]; //--- if the indicator is launched on weekends or holidays when there are no ticks, you should first open the daily chart of the symbol //--- if we have not received bar data for the day index of the daily period, we leave until the next call to OnCalculate() if(CopyRates(Symbol(), PERIOD_D1, day, 1, day_rate)==-1) return(prev_calculated); //--- get day start and end time datetime start_time=day_rate[0].time; datetime stop_time=start_time+PeriodSeconds(PERIOD_D1)-1; //--- get all intraday bars of the current day MqlRates bars_in_day[]; if(CopyRates(Symbol(), PERIOD_CURRENT, start_time, stop_time, bars_in_day)==-1) return(prev_calculated); CMarketProfile *market_profile; //--- if the Market Profile has already been created and its drawing has been performed earlier if(prev_calculated>0) { //--- find the Market Profile object (CMarketProfile class) in the list by the opening time of the day with the 'day' index market_profile=GetMarketProfileByDate(ExtPrefixUniq, start_time); //--- if the object is not found, return zero to completely recalculate the indicator if(market_profile==NULL) { PrintFormat("Market Profile not found for %s. Indicator will be recalculated for all specified days", TimeToString(start_time, TIME_DATE)); return(0); } //--- CMarketProfile object is found in the list; set it to High and Low values of the day and pass the array of intraday bars //--- in this case, the object is shifted to a new coordinate corresponding to the High of the daily candle, and all arrays (vectors) are reinitialized market_profile.SetHiLoBars(day_rate[0].high, day_rate[0].low, bars_in_day); } //--- if this is the first calculation else { //--- create a new object of the CMarketProfile class to store the Market Profile of the day with 'day' index market_profile = new CMarketProfile(ExtPrefixUniq, start_time, stop_time, day_rate[0].high, day_rate[0].low, bars_in_day); //--- add a pointer to the created CMarketProfile object to the list mp_list.Add(market_profile); } //--- set canvas dimensions and line drawing parameters market_profile.UpdateSizes(); //--- calculate profiles for each trading session market_profile.CalculateSessions(); //--- draw the Market Profile market_profile.Draw(InpMultiplier); } //--- redraw the chart after the loop has been completed and all objects have been created and updated ChartRedraw(0); //--- return the number of bars for the next OnCalculate call return(rates_total); }
La lógica del manejador se describe por entero en los comentarios al código. En pocas palabras, sería así:
- En un ciclo por el número de días del perfil de mercado mostrados;
- obtenemos el día correspondiente al índice del ciclo en la estructura;
- obtenemos el número de barras del periodo actual del gráfico incluidas en el día seleccionado en el ciclo;
- u obtenemos el objeto previamente creado del perfil de mercado para el día seleccionado, o creamos uno nuevo, si aún no está en la lista;
- obtenemos el tamaño de la barra diaria de Low a High en píxeles del gráfico y reinicializamos los arrays (vectores) de las sesiones comerciales;
- según el nuevo tamaño de barra del día seleccionado, cambiamos el tamaño del lienzo;
- recalculamos el perfil de mercado del día para cada sesión;
- redibujamos los perfiles de cada sesión comercial en el lienzo;
- Al final del ciclo, redibujamos el gráfico.
En el manejador OnDeinit() del indicador borramos todos los objetos gráficos creados:
//+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete all Market Profile graphical objects after use Print("Indicator \"Market Profile Canvas\" stopped, delete all objects CMarketProfile with prefix=", ExtPrefixUniq); //--- in a loop by the number of CMarketProfile objects in the list int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- get the pointer to the CMarketProfile object from the list by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- if the pointer is valid and the object exists, delete it if(market_profile!=NULL) if(CheckPointer(market_profile)!=POINTER_INVALID) delete market_profile; } //--- redraw the chart to display the result immediately ChartRedraw(0); }
En el manejador del evento OnChartEvent(), redimensionamos el lienzo de cada día del perfil de mercado:
//+------------------------------------------------------------------+ //| Custom indicator chart's event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { //--- if this is a user event, leave if(id>=CHARTEVENT_CUSTOM) return; //--- if there is a chart change, update the sizes of all objects of the CMarketProfile class with redrawing the chart if(CHARTEVENT_CHART_CHANGE==id) { //--- in a loop by the number of CMarketProfile objects in the list int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- get the pointer to the CMarketProfile object by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- if the object is received and if it is in the visible area of the chart if(market_profile) if(market_profile.isVisibleOnChart()) { //--- update canvas dimensions and redraw market profile histograms market_profile.UpdateSizes(); market_profile.Draw(InpMultiplier); } } //--- update the chart after recalculating all Profiles ChartRedraw(); } }
Como la escala vertical y horizontal de la visualización del gráfico puede modificarse, será necesario que los objetos gráficos en los que se dibujan los histogramas de las sesiones comerciales, también cambien sus tamaños en relación con los nuevos tamaños del gráfico. Por lo tanto, en el manejador de eventos cuando el gráfico cambia, todos los objetos de la clase CMarketProfile deben ser actualizados en tamaño y redibujados en el lienzo que ha sido redimensionado según la nueva escala del gráfico.
Función que retorna el objeto de perfil de mercado creado para el tiempo de inicio del día especificado:
//+------------------------------------------------------------------+ //| Returns CMarketProfile or NULL by the date | //+------------------------------------------------------------------+ CMarketProfile* GetMarketProfileByDate(string prefix, datetime time) { //--- in a loop by the number of CMarketProfile objects in the list int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- get the pointer to the CMarketProfile object by the loop index CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- if the pointer is valid and the object exists, if(market_profile!=NULL) if(CheckPointer(market_profile)!=POINTER_INVALID) { //--- if the Market Profile object obtained by the pointer was created for the required time, return the pointer if(market_profile.Check(prefix, time)) return(market_profile); } } //--- nothing found - return NULL return(NULL); }
La función se usa en el ciclo de indicadores según los días comerciales y devuelve el puntero a un objeto de la clase CMarketProfile de la lista, creado para una barra diaria con algún tiempo de apertura del día. Esta permite recuperar el objeto deseado según el tiempo para su posterior actualización.
Conclusión
Hoy hemos analizado la posibilidad de optimizar el código del indicador para reducir el consumo de recursos. Asimismo, hemos eliminado miles de objetos gráficos, sustituyéndolos por un único objeto-dibujo gráfico para el día del que se dibuja el perfil de mercado.
Como resultado de esta optimización, cada día comercial, en el número especificado en los ajustes (7 por defecto) se muestra en su propio lienzo (objeto OBJ_BITMAP), donde tres sesiones comerciales -asiática, europea y americana- se dibujan como histogramas, cada uno en su propio color, especificado en los ajustes. Durante tres días comerciales, el perfil del mercado tendrá finalmente este aspecto:
Aquí solo tenemos tres objetos gráficos, en los que se dibujan los histogramas de las sesiones comerciales utilizando la clase CCanvas. Se ve perfectamente que el redibujado sobre la marcha de las imágenes, incluso para tres objetos gráficos "Dibujo", provoca parpadeos y crispaciones notables en las imágenes. Esto demuestra que aún hay margen para seguir optimizando el código. En cualquier caso, ahora, en lugar de varios miles de objetos gráficos, solo tenemos tres, y eso supone un notable ahorro en el consumo de recursos. Asimismo, los artefactos visuales pueden corregirse analizando posteriormente el código para su eliminación (por ejemplo, recordemos el método no implementado isChartScaleChanged() de la clase CMarketProfile, que puede utilizarse para intentar realizar el redibujado solo en el momento de un evento real de cambio de escala del gráfico, y no en cualquiera de sus cambios).
En resumen, podemos afirmar que cualquier código siempre puede optimizarse, aunque para ello haya que recurrir a un concepto diferente de construcción del componente visual, como se hace en este indicador.
El artículo va acompañado de un archivo con indicadores totalmente comentado. Podrá descargarlo y estudiarlo por su cuenta, y si desea seguir optimizándolo.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/16579
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.





- 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
Por qué no escribir un perfil de volumen perfecto
¿Qué se entiende por "perfecto"?
Por qué no escribir el perfil de volumen perfecto