
Indicador de perfil de mercado — Market Profile (Parte 2): Otimização e desenho em canvas
Conteúdo
Introdução
Retomando o tema do indicador de Perfil de Mercado iniciado no artigo anterior, vale dizer que a construção do gráfico de perfil de mercado com objetos gráficos convencionais consome muitos recursos. Isso ocorre porque cada ponto de preço do Low ao High da barra diária é preenchido com objetos gráficos-retângulos na quantidade de barras que, ao longo do dia, alcançaram esse nível de preço. E isso para cada ponto, todos contendo múltiplos objetos gráficos, criados e desenhados para cada dia em que se desenha o gráfico de perfil. Quando o indicador cria milhares de objetos gráficos, isso pode causar atrasos significativos ao lidar com outros objetos gráficos e na atualização do gráfico.
Rodar o indicador no gráfico M30 e construir o Perfil de Mercado para apenas três dias:
leva à criação de 4697 objetos gráficos-retângulos:
Esse é um uso bem ineficiente de recursos. Basta aumentar nas configurações o número de dias exibidos para que a quantidade de objetos criados para desenhar os gráficos de Perfil de Mercado de cada dia mostrado cresça de forma exponencial.
Mas aqui estamos apenas desenhando gráficos com objetos gráficos-retângulos. Um curto segmento de linha do histograma do perfil é um objeto gráfico. Ou seja, é possível desenhar não diretamente no gráfico, mas em um único objeto gráfico-canvas, posicionado no gráfico nas coordenadas necessárias. Assim, teremos apenas um (!) objeto gráfico por dia. E para três dias, seriam três objetos em vez de 4697! Isso faz uma diferença enorme! E isso é possível graças à classe para criação simplificada de desenhos personalizadosCCanvas, fornecida na Biblioteca Padrão do terminal cliente.
A versão do indicador de Perfil de Mercado que desenha o histograma do perfil no canvas está disponível no terminal na pasta \MQL5\Indicators\Free Indicators\ no arquivo MarketProfile Canvas.mq5. Ao estudar o código, percebemos que, diferente da primeira versão (MarketProfile.mq5), a saída gráfica aqui é feita com objetos da classe CCanvas. Ou seja, a lógica do indicador permanece a mesma, já explorada no primeiro artigo na seção Estrutura e princípios, mas agora o desenho é feito com a ajuda da classe especial CMarketProfile, que utiliza desenho em CCanvas.
A lógica de funcionamento é extremamente simples:
- em um laço que percorre a quantidade de dias especificada,
- criamos ou obtemos o objeto da classe CMarketProfile para o dia atual no laço,
- desenhamos ou redesenhamos o perfil do dia no canvas, correspondente ao dia atual no laço.
Ou seja, o trabalho principal de desenho do gráfico de perfil é feito dentro da classe CMarketProfile. Vamos analisar a estrutura e funcionamento dessa classe.
Classe de perfil de mercado CMarketProfile
Abrimos o arquivo \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 e localizamos o código da classe CMarketProfile. Vamos ver o que há nele e discutir para que serve cada parte:
//+------------------------------------------------------------------+ //| 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); //--- проверяет, создан ли объект для указанной даты bool Check(string prefix, datetime time); //--- устанавливает максимум/минимум и массив внутридневных баров void SetHiLoBars(double high, double low, MqlRates &bars[]); //--- устанавливает размеры холста и параметры рисования void UpdateSizes(void); //--- находится ли профиль в видимой части графика? bool isVisibleOnChart(void); //--- изменился ли масштаб графика? bool isChartScaleChanged(void); //--- рассчитывает профиль по сессиям bool CalculateSessions(void); //--- рисует профиль void Draw(double multiplier=1.0); //--- protected: CCanvas m_canvas; // объект класса CCanvas для рисования профиля uchar m_alpha; // значение альфа-канала, устанавливающее прозрачность string m_prefix; // уникальный префикс объекта OBJ_BITMAP string m_name; // имя объекта OBJ_BITMAP, используемого в m_canvas double m_high; // High дня double m_low; // Low дня datetime m_time1; // время начала дня datetime m_time2; // время окончания дня int m_day_size_pt; // высота дневного бара в пунктах int m_height; // высота дневного бара в пикселях на графике int m_width; // ширина дневного бара в пикселях на графике MqlRates m_bars[]; // массив баров текущего таймфрейма между m_time1 и m_time2 vector m_asia; // массив счётчиков баров для азиатской сессии vector m_europe; // массив счётчиков баров для европейской сессии vector m_america; // массив счётчиков баров для американской сессии double m_vert_scale; // вертикальный коэффициент масштабирования double m_hor_scale; // горизонтальный коэффициент масштабирования };Métodos públicos declarados na classe:
- O método Check() é usado para verificar a existência do objeto de perfil de mercado criado para um determinado dia;
- O método SetHiLoBars() é utilizado para definir no objeto de perfil de mercado os valores de preço High e Low do dia e para repassar ao objeto o array de barras intradiárias;
- O método UpdateSizes() define no objeto de perfil de mercado os tamanhos do canvas e os coeficientes de escala para desenhar os retângulos;
- O método isVisibleOnChart() retorna um sinalizador indicando que o perfil de mercado está dentro da área visível no gráfico;
- O método isChartScaleChanged() está declarado na classe, mas não está implementado;
- O método CalculateSessions() calcula os parâmetros e preenche os arrays das sessões de negociação;
- O método Draw() desenha no canvas o histograma do perfil de mercado com base nos dados de todas as sessões de negociação.
A finalidade das variáveis declaradas na seção protegida da classe é bem clara. Vale destacar os arrays de contadores de barras das sessões.
Todos eles são declarados como variáveis-vetor, o que permite que sejam usados como arrays de dados, só que de maneira mais simples:
O uso de vetores e matrizes, mais especificamente dos métodos especiais que esses tipos de dados oferecem, permite escrever um código mais simples, mais curto e mais compreensível, mais próximo da notação matemática. Isso livra o programador da necessidade de criar laços aninhados e se preocupar com a indexação correta dos arrays envolvidos nos cálculos. Com isso, aumenta-se a confiabilidade e a velocidade no desenvolvimento de programas complexos.
Vamos examinar a implementação dos métodos declarados da classe.
Construtor:
//+------------------------------------------------------------------+ //| 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) { //--- копируем массив внутридневных баров в массив структур MqlRates, //--- создаём имя графического объекта и определяем размер дневной свечи 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)); //--- устанавливаем размеры векторов для торговых сессий m_asia=vector::Zeros(m_day_size_pt); m_europe=vector::Zeros(m_day_size_pt); m_america=vector::Zeros(m_day_size_pt); //--- устанавливаем ширину и высоту холста UpdateSizes(); //--- если это первый тик в начале дня, то размеры холста будут нулевыми - установим размеры в 1 пиксель по высоте и ширине m_height=m_height?m_height:1; m_width=m_width?m_width:1; //--- создаём графический объект 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); } }
O construtor paramétrico da classe recebe como parâmetros o prefixo do nome do objeto-canvas que será criado para desenhar o perfil do dia, o horário de início e fim do dia, os preços máximo e mínimo do dia e o array das barras intradiárias. Os valores dessas variáveis são armazenados nas variáveis correspondentes da classe na linha de inicialização. Em seguida
- o array passado por referência é copiado para o array da classe, um nome único para o objeto gráfico é criado a partir do prefixo passado nos parâmetros do construtor, da abreviação "_MP_" e do horário de abertura do dia, e o tamanho da vela diária em pontos é calculado;
- cada um dos arrays das sessões de negociação recebe um tamanho igual ao tamanho da barra diária em pontos e ao mesmo tempo é preenchido com zeros, ou seja, é inicializado;
- os tamanhos do canvas para desenho do perfil são definidos, sendo que, se for o primeiro tick do dia, o tamanho será zero, e são atribuídos os tamanhos mínimos permitidos de um pixel em ambas as dimensões para largura e altura;
- o canvas para desenho é criado com os tamanhos especificados.
Método para verificar a existência do objeto de perfil de mercado criado para determinado dia:
//+------------------------------------------------------------------+ //| 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 o nome de cada objeto-canvas para desenhar o perfil é definido no construtor da classe, e esse nome usa a representação em string do horário de início do dia, então, para verificar se o objeto foi criado para um determinado horário, o método recebe o horário de início do dia, cria-se uma string idêntica ao nome do objeto e essa string criada é comparada com o nome real do objeto. O resultado da verificação é retornado pelo método.
Método para definir no objeto de perfil de mercado os valores de preços High e Low do dia e para repassar ao objeto o array de barras intradiárias:
//+------------------------------------------------------------------+ //| Sets High/Low and a set of current-timeframe bars | //+------------------------------------------------------------------+ void CMarketProfile::SetHiLoBars(double high, double low, MqlRates &bars[]) { //--- если максимум дня изменился, переместим объект OBJ_BITMAP на новую координату Y 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; //--- дневной диапазон в пунктах m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT)); //--- переустанавливаем размеры векторов для торговых сессий m_asia=vector::Zeros(m_day_size_pt); m_europe=vector::Zeros(m_day_size_pt); m_america=vector::Zeros(m_day_size_pt); }
O método recebe os valores High e Low da vela diária e, por referência, o array de barras intradiárias no formato da estrutura MqlRates.
- o preço High é armazenado na variável do objeto e o canvas é deslocado para a nova coordenada;
- as barras intradiárias do array recebido são copiadas para o array interno;
- o preço Low do dia é atribuído à variável da classe;
- o novo tamanho da barra diária em pontos é calculado;
- os arrays das sessões de negociação são redimensionados de acordo com o valor calculado do tamanho da barra diária em pontos e são preenchidos com zeros, ou seja, inicializados.
Vale destacar que, para inicializar os vetores, é usado o método Zeros() das matrizes e vetores, que ao mesmo tempo define o tamanho do vetor e preenche todo o array com zeros.
Para um array comum, seria necessário fazer duas operações separadas: ArrayResize() e ArrayInitialize().
Método que define no objeto de perfil de mercado os tamanhos do canvas e os coeficientes de escala para desenhar os retângulos:
//+------------------------------------------------------------------+ //| Sets drawing parameters | //+------------------------------------------------------------------+ void CMarketProfile::UpdateSizes(void) { //--- преобразуем время/цену в координаты x/y int x1, y1, x2, y2; ChartTimePriceToXY(0, 0, m_time1, m_high, x1, y1); ChartTimePriceToXY(0, 0, m_time2, m_low, x2, y2); //--- рассчитываем размеры холста m_height=y2-y1; m_width =x2-x1; //--- рассчитываем коэффициенты для преобразования вертикальных уровней цен //--- и горизонтальных счетчиков баров в пиксели графика m_vert_scale=double(m_height)/(m_day_size_pt); m_hor_scale =double(m_width*PeriodSeconds(PERIOD_CURRENT))/PeriodSeconds(PERIOD_D1); //--- изменяем размер холста m_canvas.Resize(m_width, m_height); }
A lógica do método está comentada no código. Os coeficientes de escala são usados para definir os tamanhos dos retângulos desenhados no canvas, de acordo com a proporção entre o tamanho do canvas e o tamanho da janela do gráfico.
Os coeficientes calculados são adicionados ao cálculo da altura e largura dos retângulos desenhados.
Método que retorna o sinalizador de que o perfil de mercado está dentro da área visível no 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); // последний видимый бар на графике слева long first_bar=last_bar+-ChartGetInteger(0, CHART_VISIBLE_BARS); // первый видимый бар на графике справа first_bar=first_bar>0?first_bar:0; datetime left =iTime(Symbol(), Period(), (int)last_bar); // время левого видимого бара на графике datetime right=iTime(Symbol(), Period(), (int)first_bar); // время правого видимого бара на графике //--- возвращаем флаг того, что холст расположен внутри левого и правого видимых баров графика return((m_time1>= left && m_time1 <=right) || (m_time2>= left && m_time2 <=right)); }
Aqui são identificados os números dos bares visíveis à esquerda e à direita no gráfico, seus horários são obtidos e é retornado um sinalizador indicando que o horário da borda esquerda e da borda direita do canvas está dentro da área de bares visíveis no gráfico.
Método que calcula os parâmetros e preenche os arrays das sessões de negociação:
//+------------------------------------------------------------------+ //| Prepares profile arrays by sessions | //+------------------------------------------------------------------+ bool CMarketProfile::CalculateSessions(void) { double point=SymbolInfoDouble(Symbol(), SYMBOL_POINT); // значение одного пункта //--- если массив внутридневных баров не заполнен - уходим if(ArraySize(m_bars)==0) return(false); //---- перебираем все бары текущего дня и отмечаем ячейки массивов (векторов), в которые попадают перебираемые в цикле бары int size=ArraySize(m_bars); for(int i=0; i<size; i++) { //--- получаем час бара MqlDateTime bar_time; TimeToStruct(m_bars[i].time, bar_time); uint hour =bar_time.hour; //--- рассчитываем уровни цены в пунктах от Low дня, которые были достигнуты ценой на каждом баре цикла int start_box=(int)((m_bars[i].low-m_low)/point); // индекс начала ценовых уровней, которые были достигнуты ценой на баре int stop_box =(int)((m_bars[i].high-m_low)/point); // индекс конца ценовых уровней, которые были достигнуты ценой на баре //--- американская сессия if(hour>=InpAmericaStartHour) { //--- в цикле от начала до конца ценовых уровней заполняем счётчики баров, где была цена на этом уровне for(int ind=start_box; ind<stop_box; ind++) m_america[ind]++; } else { //--- европейская сессия if(hour>=InpEuropeStartHour && hour<InpAmericaStartHour) //--- в цикле от начала до конца ценовых уровней заполняем счётчики баров, где была цена на этом уровне for(int ind=start_box; ind<stop_box; ind++) m_europe[ind]++; //--- азиатская сессия else //--- в цикле от начала до конца ценовых уровней заполняем счётчики баров, где была цена на этом уровне for(int ind=start_box; ind<stop_box; ind++) m_asia[ind]++; } } //--- векторы всех сессий готовы return(true); }
No artigo anterior foi detalhada a lógica para determinar a quantidade de barras da sessão de negociação cujos preços atingiram níveis em pontos do Low ao High do dia. Se na versão anterior do indicador isso tudo era feito no laço principal do indicador, aqui todo esse cálculo foi extraído para um método separado do objeto de perfil do dia. A ideia aqui é contar e registrar nas células do array (vetor) a quantidade de barras que atravessaram cada nível de preço, calculado em pontos do Low ao High do dia. Após a execução do método, todos os vetores estarão preenchidos de acordo com o movimento do preço em cada nível. Quantas barras atravessaram cada nível, esse será o valor registrado nas células correspondentes do array (vetor).
Método que desenha no canvas o histograma do perfil de mercado com base nos dados de todas as sessões de negociação:
//+------------------------------------------------------------------+ //| Draw Market Profile on the canvas | //+------------------------------------------------------------------+ void CMarketProfile::Draw(double multiplier=1.0) { //--- суммируем все сессии для отрисовки vector total_profile=m_asia+m_europe+m_america; // профиль, объединяющий все сессии vector europe_asia=m_asia+m_europe; // профиль, объединяющий только европейскую и азиатскую сессии //--- устанавливаем полностью прозрачный фон для холста m_canvas.Erase(ColorToARGB(clrBlack, 0)); //--- переменные для рисования прямоугольников int x1=0; // X-координата левого угла прямоугольника всегда начинается с нуля int y1, x2, y2; // координаты прямоугольников int size=(int)total_profile.Size(); // размер всех сессий //--- рисуем американскую сессию закрашенными прямоугольниками for(int i=0; i<size; i++) { //--- нулевые значения векторов не рисуем - пропускаем if(total_profile[i]==0) continue; //--- рассчитываем две точки для рисования прямоугольника, x1 всегда равен 0 (X левого нижнего угла прямоугольника) y1=m_height-int(i*m_vert_scale); // координата Y нижнего левого угла прямоугольника y2=(int)(y1+m_vert_scale); // координата Y верхнего правого угла прямоугольника x2=(int)(total_profile[i]*m_hor_scale*multiplier); // координата X верхнего правого угла прямоугольника //--- рисуем прямоугольник по рассчитанным координатам с установленным для американской сессии цветом и прозрачностью m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAmericaSession, InpTransparency)); } //--- рисуем европейскую сессию закрашенными прямоугольниками for(int i=0; i<size; i++) { //--- нулевые значения векторов не рисуем - пропускаем if(total_profile[i]==0) continue; //--- рассчитываем две точки для рисования прямоугольника y1=m_height-int(i*m_vert_scale); y2=(int)(y1+m_vert_scale); x2=(int)(europe_asia[i]*m_hor_scale*multiplier); //--- поверх нарисованной американской сессии рисуем прямоугольник по рассчитанным координатам //--- с установленным для европейской сессии цветом и прозрачностью m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpEuropeSession, InpTransparency)); } //--- рисуем азиатскую сессию закрашенными прямоугольниками for(int i=0; i<size; i++) { //--- нулевые значения векторов не рисуем - пропускаем if(total_profile[i]==0) continue; //--- рассчитываем две точки для рисования прямоугольника y1=m_height-int(i*m_vert_scale); y2=(int)(y1+m_vert_scale); x2=(int)(m_asia[i]*m_hor_scale*multiplier); //--- поверх нарисованной европейской сессии рисуем прямоугольник по рассчитанным координатам //--- с установленным для азиатской сессии цветом и прозрачностью m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAsiaSession, InpTransparency)); } //--- обновляем объект OBJ_BITMAP без перерисовки графика m_canvas.Update(false); }
A lógica do método está detalhada nos comentários do código. Em resumo: temos os arrays (vetores) calculados e preenchidos de três sessões — asiática, europeia e americana. É necessário desenhar o histograma de perfil para cada uma dessas sessões. Primeiro é desenhada a sessão americana, sobre ela é desenhada a sessão europeia e, por fim, sobre as duas anteriores, a sessão asiática.
Por que desenhamos as sessões na ordem inversa ao seu horário de funcionamento?
- A sessão americana — mais precisamente, seu histograma — abrange tanto o tempo já negociado das duas sessões anteriores quanto o tempo da própria sessão americana, ou seja, é o histograma de perfil mais completo do dia inteiro. Por isso, ela é desenhada primeiro.
- Em seguida, desenha-se a sessão europeia, que inclui também o tempo já negociado da sessão asiática. Sendo assim, como aqui temos apenas duas sessões — asiática e europeia — o histograma será mais curto no eixo X do que o da sessão americana, portanto deve ser desenhado sobre o da americana.
- Por fim, é desenhado o histograma mais curto no eixo X, o da sessão asiática.
Vale destacar como é prático unir os dados dos arrays ao se utilizar vetores:
//--- суммируем все сессии для отрисовки vector total_profile=m_asia+m_europe+m_america; // профиль, объединяющий все сессии vector europe_asia=m_asia+m_europe; // профиль, объединяющий только европейскую и азиатскую сессии
Basicamente, trata-se de uma união elemento a elemento de vários arrays com o mesmo tamanho em um único resultado, que pode ser representado por um código como este:
#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 */
O código apresentado faz exatamente o que é realizado pela linha do método discutido acima:
vector total_profile=m_asia+m_europe+m_america; // профиль, объединяющий все сессии
Nem é preciso dizer o quanto o código fica mais conveniente e conciso...
No destrutor da classe, o objeto canvas criado é removido e o gráfico é redesenhado para refletir as alterações:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CMarketProfile::~CMarketProfile(void) { //--- удаляем все графические объекты после использования ObjectsDeleteAll(0, m_prefix, 0, OBJ_BITMAP); ChartRedraw(); }
Agora, em vez de desenhar com objetos gráficos dentro do laço do indicador, basta criar uma instância da classe discutida para cada barra diária, calcular os dados de todas as sessões e desenhar no canvas o histograma do perfil de mercado de cada dia. A quantidade de objetos gráficos criados será exatamente igual à quantidade de dias configurada nas opções de exibição do perfil, ao contrário da versão anterior do indicador, onde cada tracinho da linha do histograma era desenhado com um objeto gráfico próprio.
Otimizando o indicador
Vamos agora analisar como foi feito o indicador utilizando a classe de perfil de mercado. Abrimos desde o início o arquivo do indicador \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 e o estudamos.
Logo no início do arquivo do indicador são incluídos os arquivos da classe para criação simplificada de desenhos personalizados CCanvas e o arquivo da classe para criação de listas fortemente 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
Em seguida, são definidos os parâmetros de entrada do indicador, o prefixo único dos objetos gráficos, a declaração da classe de perfil de mercado e a declaração da lista de objetos dessa classe:
//--- input parameters input uint InpStartDate =0; /* day number to start calculation */ // номер дня, с которого начнём расчёт (0 - текущий, 1 - предыдущий, и т.д.) input uint InpShowDays =7; /* number of days to display */ // количество отображаемых дней, начиная и включая день в 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 */ // прозрачность профиля рынка, 0 = полностью прозрачный input uint InpEuropeStartHour =8; /* European session opening hour */ // час открытия европейской сессии input uint InpAmericaStartHour=14; /* American session opening hour */ // час открытия американской сессии //--- уникальный префикс для идентификации графических объектов, принадлежащих индикатору string ExtPrefixUniq; //--- декларируем класс CMarketProfile class CMarketProfile; //--- объявляем список указателей на объекты класса CMarketProfile CArrayList<CMarketProfile*> mp_list;
Como a classe de perfil de mercado está escrita abaixo do código principal do indicador, é necessária uma declaração antecipada (forward declaration) da classe, para que não ocorra erro de tipo desconhecido na hora da compilação.
'CMarketProfile' - unexpected token
A lista fortemente tipada conterá ponteiros para objetos do tipo da classe CMarketProfile, que está escrita mais abaixo no código.
No manipulador OnInit(), é criado um prefixo para os objetos gráficos usando os 4 últimos dígitos do número de milissegundos desde o início do sistema:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- создаём префикс для имён объектов string number=StringFormat("%I64d", GetTickCount64()); ExtPrefixUniq=StringSubstr(number, StringLen(number)-4); Print("Indicator \"Market Profile Canvas\" started, prefix=", ExtPrefixUniq); return(INIT_SUCCEEDED); }
Vamos analisar o código completo do manipulador 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[]) { //--- время открытия текущего дневного бара datetime static open_time=0; //--- номер последнего дня для расчетов //--- (при InpStartDate = 0 и InpShowDays = 3, lastday = 3) //--- (при InpStartDate = 1 и InpShowDays = 3, lastday = 4) etc ... uint lastday=InpStartDate+InpShowDays; //--- если первый расчет уже был if(prev_calculated!=0) { //--- получаем время открытия текущего дневного бара datetime current_open=iTime(Symbol(), PERIOD_D1, 0); //--- если текущий день не рассчитываем if(InpStartDate!=0) { //--- если время открытия не было получено - уходим if(open_time==current_open) return(rates_total); } //--- обновляем время открытия open_time=current_open; //--- далее будем рассчитывать только один день, так как все остальные дни уже посчитаны при первом запуске lastday=InpStartDate+1; } //--- в цикле по указанному количеству дней (либо InpStartDate+InpShowDays при первом запуске, либо InpStartDate+1 на каждом тике) for(uint day=InpStartDate; day<lastday; day++) { //--- получаем в структуру данные дня с индексом day MqlRates day_rate[]; //--- если индикатор запускается в выходные или праздничные дни, когда нет тиков, сначала нужно открыть дневной график символа //--- если не получили данные бара по индексу day дневного периода - уходим до следующего вызова OnCalculate() if(CopyRates(Symbol(), PERIOD_D1, day, 1, day_rate)==-1) return(prev_calculated); //--- получаем время начала и окончания дня datetime start_time=day_rate[0].time; datetime stop_time=start_time+PeriodSeconds(PERIOD_D1)-1; //--- получаем все внутредневные бары текущего дня MqlRates bars_in_day[]; if(CopyRates(Symbol(), PERIOD_CURRENT, start_time, stop_time, bars_in_day)==-1) return(prev_calculated); CMarketProfile *market_profile; //--- если Профиль рынка уже создавался и его рисование ранее было выполнено if(prev_calculated>0) { //--- найдём объект Профиля рынка (класса CMarketProfile) в списке по времени открытия дня с индексом day market_profile=GetMarketProfileByDate(ExtPrefixUniq, start_time); //--- если объект не найден возвращаем ноль для полного перерасчёта индикатора 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 найден в списке; устанавливаем в него значения High и Low дня и передаём массив внутридневных баров //--- при этом объект смещается на новую координату, соответствующую High дневной свечи, и все массивы (векторы) переинициализируются market_profile.SetHiLoBars(day_rate[0].high, day_rate[0].low, bars_in_day); } //--- если это первый расчёт else { //--- создаём новый объект класса CMarketProfile для хранения Профиля рынка дня с индексом day market_profile = new CMarketProfile(ExtPrefixUniq, start_time, stop_time, day_rate[0].high, day_rate[0].low, bars_in_day); //--- добавляем указатель на созданный объект CMarketProfile в список mp_list.Add(market_profile); } //--- устанавливаем размеры холста и параметры рисования линий market_profile.UpdateSizes(); //--- рассчитываем профили для каждой торговой сесии market_profile.CalculateSessions(); //--- рисуем Профиль рынка market_profile.Draw(InpMultiplier); } //--- по завершении цикла после создания и обновления всех объектов, перерисуем график ChartRedraw(0); //--- возвращаем количество баров для следующего вызова OnCalculate return(rates_total); }
A lógica do manipulador está detalhada nos comentários do código. Resumidamente, ela funciona da seguinte forma:
- No laço que percorre a quantidade de dias do perfil de mercado a serem exibidos;
- obtém-se, na estrutura, o dia correspondente ao índice do laço;
- obtém-se a quantidade de barras do período atual do gráfico que pertencem ao dia selecionado no laço;
- ou se obtém o objeto de perfil de mercado já criado para o dia selecionado, ou se cria um novo, caso ele ainda não exista na lista;
- obtém-se o tamanho da barra diária do Low ao High em pixels do gráfico e os arrays (vetores) das sessões de negociação são reinicializados;
- de acordo com o novo tamanho da barra do dia selecionado, alteramos o tamanho do canvas;
- recalculamos o perfil de mercado do dia para cada sessão;
- redesenhamos no canvas os perfis de cada sessão de negociação;
- Ao final do laço, o gráfico é redesenhado.
No manipulador OnDeinit() do indicador, todos os objetos gráficos criados são removidos:
//+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- удаляем все графические объекты Market Profile после использования Print("Indicator \"Market Profile Canvas\" stopped, delete all objects CMarketProfile with prefix=", ExtPrefixUniq); //--- в цикле по количеству объектов CMarketProfile в списке int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- получаем указатель на объект CMarketProfile из списка по индексу цикла CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- если указатель валидный и объект существует - удаляем его if(market_profile!=NULL) if(CheckPointer(market_profile)!=POINTER_INVALID) delete market_profile; } //--- перерисовываем график для немедленного отображения результата ChartRedraw(0); }
No manipulador de eventos OnChartEvent(), os tamanhos do canvas de cada dia do perfil de mercado são ajustados:
//+------------------------------------------------------------------+ //| Custom indicator chart's event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { //--- если это пользовательское событие - уходим if(id>=CHARTEVENT_CUSTOM) return; //--- если есть изменение чарта, обновляем размеры всех объектов класса CMarketProfile с перерисовкой графика if(CHARTEVENT_CHART_CHANGE==id) { //--- в цикле по количеству объектов CMarketProfile в списке int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- получаем указатель на объект CMarketProfile по индексу цикла CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- если объект получен и если он находится в видимой области графика if(market_profile) if(market_profile.isVisibleOnChart()) { //--- обновляем размеры холста и перерисовываем гистограммы профиля рынка market_profile.UpdateSizes(); market_profile.Draw(InpMultiplier); } } //--- после перерасчёта всех Профилей, обновляем график ChartRedraw(); } }
Como a escala de exibição do gráfico tanto vertical quanto horizontal pode ser alterada, é necessário que os objetos gráficos, onde os histogramas das sessões de negociação são desenhados, também mudem seus tamanhos conforme a nova escala do gráfico. Por isso, no manipulador de eventos, sempre que houver alteração no gráfico, todos os objetos da classe CMarketProfile devem ser atualizados em tamanho e redesenhados no canvas, que passou a ter um novo tamanho conforme a nova escala do gráfico.
Função que retorna o objeto de perfil de mercado criado para o horário de início de dia especificado:
//+------------------------------------------------------------------+ //| Returns CMarketProfile or NULL by the date | //+------------------------------------------------------------------+ CMarketProfile* GetMarketProfileByDate(string prefix, datetime time) { //--- в цикле по количеству объектов CMarketProfile в списке int size=mp_list.Count(); for(int i=0; i<size; i++) { //--- получаем указатель на объект CMarketProfile по индексу цикла CMarketProfile *market_profile; mp_list.TryGetValue(i, market_profile); //--- если указатель валидный и объект существует, if(market_profile!=NULL) if(CheckPointer(market_profile)!=POINTER_INVALID) { //--- если объект Профиля рынка, полученный по указателю, создан для искомого времени - возвращаем указатель if(market_profile.Check(prefix, time)) return(market_profile); } } //--- ничего не найдено - возвращаем NULL return(NULL); }
A função é usada no laço do indicador que percorre os dias de negociação e retorna um ponteiro para o objeto da classe CMarketProfile da lista, que foi criado para a barra diária com determinado horário de abertura do dia. Permite obter o objeto necessário pelo horário, para posterior atualização.
Considerações finais
Analisamos a possibilidade de otimização do código do indicador visando à redução do consumo de recursos. Eliminamos milhares de objetos gráficos, substituindo-os por apenas um objeto gráfico-desenho para cada dia, no qual é desenhado o perfil de mercado.
Como resultado da otimização realizada, cada dia de negociação, na quantidade configurada nas opções (por padrão, 7), é exibido em seu próprio canvas (objeto OBJ_BITMAP), onde são desenhadas como histogramas as três sessões de negociação (asiática, europeia e americana), cada uma com sua cor definida nas configurações. Para três dias de negociação, o perfil de mercado final terá este aspecto:
Aqui temos apenas três objetos gráficos, nos quais os histogramas das sessões foram desenhados com a ajuda da classe CCanvas. É possível notar claramente que a atualização "ao vivo" das imagens, mesmo para apenas três objetos gráficos do tipo "Desenho", causa um leve cintilar e pequenos saltos nas imagens. Isso indica que ainda há espaço para uma nova rodada de otimização do código. De qualquer forma, agora, em vez de milhares de objetos gráficos, temos apenas três. O que representa uma economia significativa no uso de recursos. E os artefatos visuais podem ser corrigidos com uma análise adicional do código para sua eliminação (como, por exemplo, o método isChartScaleChanged() ainda não implementado da classe CMarketProfile, que pode ser usado para tentar redesenhar apenas no momento de uma real mudança de escala do gráfico, e não a cada modificação).
Concluindo, podemos afirmar com segurança que qualquer código pode ser otimizado. Mesmo que, para isso, seja necessário recorrer a uma abordagem diferente para a construção do componente visual, como foi feito neste indicador.
O artigo é acompanhado pelo arquivo completo e comentado do indicador, que pode ser baixado e estudado por conta própria, permitindo, se desejado, continuar com sua otimização.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/16579





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso