Русский
preview
Análise de lacunas tempo<rais de preço em MQL5 (Parte II): Criamos um mapa de calor da distribuição de liquidez no tempo

Análise de lacunas tempo<rais de preço em MQL5 (Parte II): Criamos um mapa de calor da distribuição de liquidez no tempo

MetaTrader 5Indicadores |
47 4
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Na primeira parte do artigo, destrinchamos o conceito de lacunas temporais e sua relação com a atividade institucional. No entanto, detectar essas lacunas é apenas metade da tarefa. Para negociar de forma eficaz, o trader precisa ver o quadro completo: onde o preço passa muito tempo, onde passa pouco, e como essas zonas interagem entre si.

É exatamente aqui que entra o nosso indicador, uma ferramenta que transforma padrões temporais invisíveis em um mapa de calor visual e intuitivo. Se na primeira parte procurávamos anomalias (lacunas), agora construímos um mapa completo do comportamento normal do preço.

A ideia principal é simples: representar todo o intervalo de preços como um conjunto de microzonas e, para cada zona, calcular quanto tempo o preço permaneceu nela. Quanto maior o tempo, mais "quente" é a zona e mais importante ela se torna para o mercado. O resultado é visualizado por meio de um esquema de cores que vai do vermelho frio ao azul quente.


Base matemática: do caos à ordem

Qualquer mercado pode ser representado como uma luta contínua entre oferta e demanda. Nos locais onde essa disputa é mais intensa, o preço permanece por mais tempo. Matematicamente, isso é expresso por meio da função de densidade temporal:

T(p) = Σt_i, где цена находится в диапазоне [p-δ, p+δ]

Aqui p é o nível de preço analisado, δ é o tamanho da zona de análise, e t_i é a duração de cada período em que o preço permaneceu nessa zona.

Mas os dados brutos dizem pouco por si só. É necessária uma normalização que transforme valores absolutos de tempo em porcentagens relativas. Usamos a fórmula:

P(p) = ((T(p) - T_min) / (T_max - T_min)) × 99% + 1%

Essa fórmula garante que as zonas mais "frias" recebam 1% de presença (cor vermelha), enquanto as zonas mais "quentes" recebam 100% (cor azul). Todas as demais zonas são distribuídas entre esses extremos de forma proporcional à sua importância.


Arquitetura da solução: modularidade como base da confiabilidade

A criação desse indicador exige uma arquitetura bem planejada. Na base está a estrutura PriceLevel, que encapsula todas as informações sobre cada nível de preço:

struct PriceLevel
{
    double      price;             // Центральная цена уровня
    double      price_high;        // Верхняя граница зоны
    double      price_low;         // Нижняя граница зоны
    long        time_spent;        // Накопленное время в барах
    double      presence_percent;  // Процент присутствия
    color       level_color;       // Динамический цвет
    string      object_name;       // Уникальный идентификатор
};

Cada nível vive sua própria dinâmica: acumula tempo, recalcula porcentagens e altera sua cor. Não se trata apenas de uma estrutura de dados, mas de uma entidade viva do mercado.

A principal inovação foi o uso de uma janela deslizante de análise. Em vez de processar todo o histórico disponível (o que poderia levar segundos), analisamos apenas as últimas MaxHistory barras por meio de uma janela de tamanho AnalysisPeriod. Isso garante resultados atualizados com desempenho aceitável.


Algoritmo: a matemática encontra a realidade

O processo começa com a determinação do intervalo de preços para análise. O algoritmo encontra automaticamente o máximo e o mínimo dentro do período estudado, o que permite adaptar-se à volatilidade de qualquer instrumento.

Em seguida, esse intervalo é dividido em zonas iguais. O número de zonas é calculado dinamicamente: se o tamanho do tick estiver especificado, ele é utilizado; caso contrário, utiliza-se o Point mínimo do instrumento. Ao mesmo tempo, o sistema equilibra entre detalhamento, com pelo menos 50 níveis, e desempenho, com no máximo 1000 níveis.

A parte que mais consome recursos é o cálculo do tempo em cada nível. Uma abordagem ingênua exigiria verificar cada barra contra cada nível, o que resulta em complexidade O(n²). Otimizamos isso para O(n×k), onde k é o número médio de níveis afetados por uma única barra.

// Оптимизация: находим только релевантные уровни для каждого бара
int startLevel = MathMax(0, (int)((lowPrice - minPrice) / realTickSize));
int endLevel = MathMin(totalPriceLevels - 1, (int)((highPrice - minPrice) / realTickSize) + 1);

for(int levelIdx = startLevel; levelIdx <= endLevel; levelIdx++)
{
    if(DoesBarTouchLevel(highPrice, lowPrice, levels[levelIdx]))
    {
        levels[levelIdx].time_spent++;
    }
}

A função DoesBarTouchLevel verifica a interseção do intervalo High-Low da barra com os limites do nível de preço. A lógica é simples: se o máximo da barra estiver acima do limite inferior do nível e o mínimo da barra estiver abaixo do limite superior, então existe interseção.


Alquimia das cores: transformando números em imagens

Após o cálculo do tempo, começa a parte mais criativa, que é a transformação dos dados brutos em um esquema de cores. Utilizamos um sistema de cinco etapas: vermelho (1%), laranja (25%), amarelo (50%), azul-claro (75%), azul (100%).

Entre os pontos principais ocorre uma interpolação suave. Por exemplo, um nível com 37% de presença receberá uma cor entre laranja e amarelo. A interpolação funciona no espaço RGB:

color InterpolateColor(color color1, color color2, double factor)
{
    // Разложение на RGB компоненты
    int r1 = (color1 >> 16) & 0xFF;
    int g1 = (color1 >> 8) & 0xFF;
    int b1 = color1 & 0xFF;
    
    // Линейная интерполяция каждого канала
    int r = (int)(r1 + (r2 - r1) * factor);
    int g = (int)(g1 + (g2 - g1) * factor);
    int b = (int)(b1 + (b2 - b1) * factor);
    
    return (r << 16) | (g << 8) | b;
}

O resultado são transições de cores suaves que criam um mapa de calor natural do mercado.


Visualização: do algoritmo ao gráfico

Cada nível de preço é exibido como um retângulo no gráfico. A criação de milhares de objetos gráficos é uma tarefa tecnicamente complexa que exige otimização.

Os retângulos são posicionados desde o início do período de análise até o momento atual, cobrindo toda a área estudada. Cada objeto é configurado para funcionar em modo de fundo: ele não interfere na análise do gráfico, não é selecionado ao clicar e é redesenhado automaticamente quando a escala muda.

O sistema de gerenciamento de objetos inclui um mecanismo de limpeza dos resultados anteriores antes da renderização dos novos. Isso evita o acúmulo de "lixo" no gráfico e garante a atualização correta da visualização.

Em modo de tempo real, o desempenho é crítico. O indicador utiliza vários níveis de otimização:

  • o primeiro nível são cálculos preguiçosos. O recálculo ocorre apenas quando surge uma nova barra. O sistema monitora o momento da última atualização e executa os cálculos somente quando ocorre alteração.
  • o segundo nível é a otimização de memória. Os arrays de dados são alocados uma única vez e reutilizados. As estruturas de dados são projetadas para consumo mínimo de memória: são utilizados long em vez de double para contadores, e evitam-se variáveis de string desnecessárias.
  • o terceiro nível é a otimização algorítmica. A janela deslizante de análise limita o volume de dados processados. A grade adaptativa de níveis evita a criação de um número excessivo de zonas.


Interpretação dos resultados: o que dizem as cores

As zonas vermelhas (1–25% de presença) indicam áreas pelas quais o preço passa rapidamente. Essas são potenciais zonas de lacunas temporais discutidas na primeira parte do artigo. Nas zonas vermelhas frequentemente se formam rejeições e rompimentos falsos, portanto exigem uma abordagem cautelosa.

As zonas laranja e amarelas (25-75% de presença) representam áreas de atividade moderada. Aqui o preço permanece periodicamente, mas sem dominância clara. Essas são zonas de transição que podem tornar-se suporte ou resistência, dependendo do contexto do mercado. É exatamente nelas que se negocia melhor a favor da tendência.

As zonas azul-claro e azul (75-100%) são as protagonistas da nossa análise. Aqui o preço passa o maior tempo, o que indica alta atividade de negociação. Esses níveis possuem uma forte força magnética: o preço retorna regularmente a eles, utilizando-os como apoio para o movimento ou como barreira a ser superada.

A estratégia mais eficaz é negociar rejeições a partir das zonas azuis. Quando o preço se aproxima de uma área de presença máxima, a probabilidade de reversão torna-se significativamente maior que a média. Isso funciona especialmente bem em mercados laterais, nos quais as zonas azuis definem claramente os limites do canal.

Rompimentos através de zonas amarelas frequentemente sinalizam continuação do movimento. Se o preço atravessa facilmente uma área de presença média com bom volume, isso indica ausência de resistência significativa adiante.

Já as estratégias de rejeição funcionam melhor através das zonas vermelhas.

A combinação com análise de volume fortalece os sinais várias vezes. Quando uma zona azul no tempo coincide com alto volume no perfil de volume, forma-se uma área de importância máxima.


Configuração para diferentes mercados: universalidade por meio da adaptação

O Forex, com sua alta liquidez, exige maiores valores de AnalysisPeriod (300-500 barras) e MaxHistory (5000-8000 barras). Os movimentos aqui são mais suaves, portanto é necessária maior profundidade de análise para identificar zonas relevantes.

O mercado de ações funciona bem com configurações moderadas: AnalysisPeriod de 200-300 barras, MaxHistory de 3000-5000 barras. A estrutura de sessões cria pausas naturais que aparecem claramente no mapa de calor.

O tamanho do tick (TickSize) é criticamente importante para o funcionamento correto. Se for definido um valor muito pequeno, surgirá detalhamento excessivo sem utilidade prática. Um valor muito grande levará à perda de nuances importantes. O valor 0, modo automático, normalmente é ideal, pois o sistema seleciona o tamanho com base nas características do instrumento.

A transparência (Transparency) afeta não apenas a percepção visual, mas também o desempenho. Valores altos (70-90%) criam um mapa semitransparente que não interfere na análise das velas, mas exige mais recursos para renderização.

O limite de 1000 níveis não foi introduzido por acaso. O MetaTrader 5 possui restrições quanto ao número de objetos gráficos, e exceder limites razoáveis leva à desaceleração da interface sem melhoria significativa na qualidade da análise.


Integração com outras ferramentas: sinergia de métodos

O indicador de mapa de calor complementa perfeitamente o perfil de volume. A coincidência de zonas azuis de tempo com picos de volume cria áreas de importância excepcional. Essas zonas frequentemente se tornam pontos-chave para movimentos de preço de longo prazo.

A combinação com níveis de Fibonacci produz resultados interessantes. Quando níveis importantes de Fibonacci coincidem com zonas azuis do mapa de calor, sua relevância aumenta significativamente. Isso é natural: níveis matemáticos são reforçados pelo comportamento real do preço.

Indicadores de volatilidade, por exemplo as Bandas de Bollinger, funcionam bem em conjunto com o mapa de calor. A expansão das bandas em zonas vermelhas frequentemente antecede movimentos fortes, enquanto o estreitamento nas zonas azuis indica acumulação de energia para um impulso futuro.

Na próxima versão do indicador está planejada a inclusão de aprendizado de máquina para otimização automática dos parâmetros para um instrumento específico. O algoritmo analisará as estatísticas de reação dos níveis e ajustará as configurações para máxima eficiência.

A integração com sistemas de alertas permitirá receber notificações quando o preço se aproximar de zonas-chave. Isso é especialmente útil para swing traders que não podem monitorar os gráficos constantemente.

A exportação de dados para sistemas externos abrirá possibilidades para a criação de robôs de negociação baseados no mapa de calor. Os robôs poderão utilizar a força dos níveis como filtro adicional para entrada em posição.


Filosofia do método: o tempo como moeda do mercado

Na base do indicador está uma ideia profunda: o tempo é a moeda do mercado. Os participantes do mercado gastam tempo de forma tão consciente quanto gastam dinheiro. Quanto mais tempo é gasto em um determinado nível, mais emoções, decisões e capital estão associados a ele.

Essa ligação emocional cria memória de mercado. Mesmo quando o preço se afasta de um nível significativo, a memória sobre ele permanece no subconsciente dos participantes. Quando o preço retorna a esse nível, antigas lembranças despertam: para alguns, lembranças de lucro; para outros, lembranças de perdas. Essas lembranças influenciam novas decisões de negociação.

O mapa de calor do tempo torna essa memória invisível visível. Ele transforma a psicologia do mercado em matemática, emoções em algoritmos e intuição em dados.


Considerações finais: um novo olhar sobre verdades antigas

O indicador não revela novos princípios, ele torna os antigos mais claros. Os níveis de suporte e resistência sempre existiram, mas agora podem ser medidos e classificados por importância. O mapa de calor mostra onde o mercado passa tempo e, portanto, onde está escondida a verdadeira força.

Em combinação com o indicador de lacunas temporais, isso oferece uma visão completa do comportamento dos grandes participantes: onde eles agem rapidamente e onde permanecem por mais tempo. Juntos, os instrumentos permitem compreender melhor a estrutura do mercado e tomar decisões baseadas em lógica, e não apenas em intuição.

Na próxima parte veremos como unir tudo isso em um único sistema de negociação.

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/18661

Arquivos anexados |
Últimos Comentários | Ir para discussão (4)
Maxim Kuznetsov
Maxim Kuznetsov | 3 jul. 2025 em 09:42

Isso me faz lembrar: eu costumava fazer isso há muito tempo,

Na minha opinião, você tem apenas um pouco incompleto - resta revelar a estrutura regular (ou mostrar que ela não existe).

Esse não é um mapa de calor, é claro - apenas o extremo da parte regular de um mapa de temperatura semelhante.

Stanislav Korotky
Stanislav Korotky | 3 jul. 2025 em 14:27
Como isso difere do conhecido "perfil de mercado" (além da coloração)?
Maxim Kuznetsov
Maxim Kuznetsov | 3 jul. 2025 em 16:33

A propósito, o mapa é calculado usando o método mais trabalhoso e propenso a erros.

Tudo é mais simples - para cada vela, 2 pares {price,weight} : { price=high, weight=-1; } { price=low,weight=+1;} A coleção é classificada por preço, a soma com a acumulação por peso é o mapa de calor. Em seguida, ele é quantificado como você desejar.

Lipe Ramos
Lipe Ramos | 15 jul. 2025 em 18:44
Decidi fazer um experimento com o ChatGPT para acrescentar algumas "melhorias" e criar algo útil para mim mesmo. A parte comentada é minha tentativa de detectar quando o preço entra em uma das zonas coloridas e mostrar visualmente sua reação. Mas a ideia era captar as tonalidades dessas cores: por exemplo, quando a zona é laranja ligeiramente mais brilhante, de acordo com minhas observações, o preço fica de lado.
//+------------------------------------------------------------------+
//| Heat Map Plus - v3.00 modificada |
//| Mantém buffer/alertas e adiciona ADX, estatísticas e classificação
//+------------------------------------------------------------------+
#property copyright "Chart Coloring by Time and Volume Distribution"
#property link      "https://www.mql5.com"
#property version   "3.00"
#property indicator_chart_window
#property strict

#include <Math\Stat\Math.mqh>    // MathMean, MathStdDev
#include <Indicators\Trend.mqh>     // ADX para filtrar tendências

// buffers para EA
#property indicator_buffers 3
#property indicator_plots   0
double LevelPriceBuffer[];
double LevelStrengthBuffer[];
double LevelTypeBuffer[];

//--- Parâmetros de entrada
sinput group "=== Heat Map Settings ==="
input int      AnalysisPeriod   = 500;
input int      MaxHistory       = 10000;
input double   TickSize         = 0;
input bool     UseTickVolume    = true;
input double   VolumeWeight     = 0.5;
input int      SessStartHour    = 8;
input int      SessEndHour      = 17;
input double   MinBarRange      = 10;
input bool     EnableAlerts     = true;

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
sinput group "=== Cores (1%→100%) ==="
input color    Color1Percent    = clrRed;
input color    Color25Percent   = clrOrange;
input color    Color50Percent   = clrYellow;
input color    Color75Percent   = clrAqua;
input color    Color100Percent  = clrBlue;
input int      Transparency     = 70;

//--- globais
struct PriceLevel {
 double price, price_high, price_low;
 long   time_spent;
 long   volume;
 double presence_percent;
 color  level_color;
 string object_name; };
PriceLevel      levels[];
datetime        startTime, endTime, lastUpdate;
int             totalPriceLevels = 0;
long            maxTimeSpent = 0, minTimeSpent = LONG_MAX;
long            maxVolume = 0,    minVolume    = LONG_MAX;
bool            calculated = false;

//--- Alça ADX
CiADX   adx;

//+------------------------------------------------------------------+
//| Inicialização|
//+------------------------------------------------------------------+
int OnInit() {
// buffers para EA
 SetIndexBuffer(0, LevelPriceBuffer);
 SetIndexBuffer(1, LevelStrengthBuffer);
 SetIndexBuffer(2, LevelTypeBuffer);
 if(AnalysisPeriod < 50 || MaxHistory < AnalysisPeriod) {
  Print("Parâmetros incorretos: 50 <= AnalysisPeriod <= MaxHistory");
  return(INIT_PARAMETERS_INCORRECT); }
 if(VolumeWeight < 0 || VolumeWeight > 1) {
  Print("VolumeWeight deve estar em [0,1]");
  return(INIT_PARAMETERS_INCORRECT); }
// ADX(14)
 if(!adx.Create(_Symbol, _Period, 14)) {
  Print("Erro ao criar ADX");
  return(INIT_FAILED); }
 ArrayResize(levels, 0);
 lastUpdate = 0;
 calculated = false;
 totalPriceLevels = 0;
 return(INIT_SUCCEEDED); }

//+------------------------------------------------------------------+
//| Cálculo|
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &/*abrir*/[],
                const double &high[],
                const double &low[],
                const double &/*fechar*/.[],
                const long &tick_volume[],
                const long &volume[],
                const int &/*spread*/.[]) {
 if(rates_total < AnalysisPeriod) return(0);
// só recalcula em nova vela
 if(lastUpdate == time[rates_total - 1]) return(rates_total);
 lastUpdate = time[rates_total - 1];
 calculated = false;
 if(!calculated) {
  // atualiza ADX
  adx.Refresh(1);
  // cálculo principal
  CalculateTimeDistribution(time, high, low, tick_volume, volume, rates_total);
  ColorChart();
  ExportBuffers(rates_total);
  calculated = true; }
 return(rates_total); }

//+------------------------------------------------------------------+
//| Janela deslizante + filtros de sessão e volatilidade
//+------------------------------------------------------------------+
void CalculateTimeDistribution(const datetime &time[],
                               const double &high[],
                               const double &low[],
                               const long &tick_volume[],
                               const long &volume[],
                               int rates_total) {
 ArrayResize(levels, 0);
 int historyStart = MathMax(0, rates_total - MaxHistory);
 startTime = time[historyStart];
 endTime   = time[rates_total - 1];
// faixa de preço
 double maxP = high[ArrayMaximum(high, historyStart, MaxHistory)];
 double minP = low [ArrayMinimum(low,  historyStart, MaxHistory)];
 double priceRange = maxP - minP;
 if(priceRange <= 0) return;
// tamanho do tique
 double tk = (TickSize > 0 ? TickSize : Point());
 totalPriceLevels = (int)(priceRange / tk);
 totalPriceLevels = MathMin(1000, MathMax(50, totalPriceLevels));
 double realTick = priceRange / totalPriceLevels;
// níveis de inicialização
 ArrayResize(levels, totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++) {
  levels[i].price       = minP + i * realTick;
  levels[i].price_high  = levels[i].price + realTick / 2;
  levels[i].price_low   = levels[i].price - realTick / 2;
  levels[i].time_spent  = 0;
  levels[i].volume      = 0;
  levels[i].object_name = "HeatLevel_" + IntegerToString(i); }
// percorre barras
 for(int bar = historyStart; bar < rates_total; bar++) {
  // filtro de sessão
  int hr = TimeHour(time[bar]);
  if(hr < SessStartHour || hr > SessEndHour) continue;
  // filtro de volatilidade
  double amp = (high[bar] - low[bar]) / Point();
  if(amp < MinBarRange) continue;
  long barVol = UseTickVolume ? tick_volume[bar] : volume[bar];
  // encontrar níveis afetados
  int st = MathMax(0, (int)((low[bar]  - minP) / realTick));
  int en = MathMin(totalPriceLevels - 1,
                   (int)((high[bar] - minP) / realTick));
  for(int li = st; li <= en; li++) {
   if(DoesBarTouchLevel(high[bar], low[bar], levels[li])) {
    levels[li].time_spent++;
    levels[li].volume    += barVol; } } }
// acha min/max
 maxTimeSpent = 0; minTimeSpent = LONG_MAX;
 maxVolume    = 0; minVolume    = LONG_MAX;
 for(int i = 0; i < totalPriceLevels; i++) {
  long t = levels[i].time_spent;
  long v = levels[i].volume;
  if(t > maxTimeSpent) maxTimeSpent = t;
  if(t > 0 && t < minTimeSpent) minTimeSpent = t;
  if(v > maxVolume)    maxVolume    = v;
  if(v > 0 && v < minVolume)    minVolume    = v; }
 if(minTimeSpent == LONG_MAX) minTimeSpent = 0;
 if(minVolume   == LONG_MAX) minVolume   = 0;
// percentuais e núcleos
 CalculatePercentsAndColors(); }

//+------------------------------------------------------------------+
//| Percentuais e núcleos|
//+------------------------------------------------------------------+
void CalculatePercentsAndColors() {
 long tr = maxTimeSpent - minTimeSpent;  if(tr <= 0) tr = 1;
 long vr = maxVolume    - minVolume;     if(vr <= 0) vr = 1;
 for(int i = 0; i < totalPriceLevels; i++) {
  double tp = levels[i].time_spent > 0 ?
              ((levels[i].time_spent - minTimeSpent) / (double)tr) * 100.0 : 0;
  double vp = levels[i].volume > 0 ?
              ((levels[i].volume - minVolume) / (double)vr) * 100.0 : 0;
  double comb = (1.0 - VolumeWeight) * tp + VolumeWeight * vp;
  levels[i].presence_percent =
   MathMax(1.0, MathMin(100.0, comb));
  levels[i].level_color =
   GetPercentageColor(levels[i].presence_percent); } }

//+------------------------------------------------------------------+
//| Coloração de gráficos + alertas|
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void ColorChart() {
 ClearPreviousObjects();
 datetime nowTime = TimeCurrent();
 double lastBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Matriz de nomes de zona
 string zoneNames[5] = {"", "Reversão"., "Tendência"., "Lateralização"., "Barreira" };
// 1) Armazena ztype e preço médio
 int    types[]; ArrayResize(types, totalPriceLevels);
 double mids[];  ArrayResize(mids,  totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++) {
  types[i] = (int)LevelTypeBuffer[ArraySize(LevelTypeBuffer) - 1 - i];
  if(types[i] < 1 || types[i] > 4)
   types[i] = 4;
  mids[i] = (levels[i].price_low + levels[i].price_high) / 2.0; }
// 2) Cria objetos de zona (retângulos coloridos)
 for(int i = 0; i < totalPriceLevels; i++) {
  string nm = levels[i].object_name;
  // retângulo da zona
  if(ObjectCreate(ChartID(), nm, OBJ_RECTANGLE, 0,
                  startTime, levels[i].price_low,
                  nowTime,   levels[i].price_high)) {
   ObjectSetInteger(ChartID(), nm, OBJPROP_COLOR, levels[i].level_color);
   ObjectSetInteger(ChartID(), nm, OBJPROP_BACK, true);
   ObjectSetInteger(ChartID(), nm, OBJPROP_FILL, true);
   ENUM_LINE_STYLE style = (Transparency > 50) ? STYLE_DOT : STYLE_SOLID;
   ObjectSetInteger(ChartID(), nm, OBJPROP_STYLE, style);
   ObjectSetInteger(ChartID(), nm, OBJPROP_WIDTH, 1); }
  // alertas
  if(EnableAlerts) {
   static double lastPrice = 0;
   bool cross = ((lastPrice < levels[i].price && lastBid >= levels[i].price) ||
                 (lastPrice > levels[i].price && lastBid <= levels[i].price));
   if(cross) {
    Alert("HeatMap: preço cruzou nível",
          DoubleToString(levels[i].price, _Digits),
          " [", zoneNames[types[i]], "]"); }
   lastPrice = lastBid; } }
//// 3) Agrupa por tipo de contíguo e desenha uma linha por grupo
// struct Segment {
// tipo int;
// int start;
// int end; };
// Segmento segs[];
// ArrayResize(segs, 0);
// int currentType = types[0];
// int segStart = 0;
// for(int i = 1; i < totalPriceLevels; i++) {
// se (types[i] != currentType) {
// Segmento seg;
// seg.type = currentType;
// seg.start = segStart;
// seg.end = i - 1;
// ArrayResize(segs, ArraySize(segs) + 1);
// segs[ArraySize(segs) - 1] = seg;
// currentType = types[i];
// segStart = i; } }
//// último segmento
// Segmento lastSeg;
// lastSeg.type = currentType;
// lastSeg.start = segStart;
// lastSeg.end = totalPriceLevels - 1;
// ArrayResize(segs, ArraySize(segs) + 1);
// segs[ArraySize(segs) - 1] = lastSeg;
//// 4) Desenha linha horizontal média por segmento
// for(int j = 0; j < ArraySize(segs); j++) {
// int s = segs[j].start;
// int e = segs[j].end;
// int type = segs[j].type;
// // média dos preços médios
// double avgPrice = 0;
// for(int k = s; k <= e; k++)
// avgPrice += mids[k];
// avgPrice /= (e - s + 1);
//// Criar linha horizontal
// string lineName = StringFormat(zoneNames[types[s]] + "linha", s, e);
// if(ObjectCreate(ChartID(), lineName, OBJ_HLINE, 0, 0, avgPrice)) {
// ObjectSetInteger(ChartID(), lineName, OBJPROP_COLOR, clrWhite);
// ObjectSetInteger(ChartID(), lineName, OBJPROP_WIDTH, 4);
// ObjectSetInteger(ChartID(), lineName, OBJPROP_STYLE, STYLE_SOLID); }
//// Criar texto à direita (última vela visível)
// double textOffset = SymbolInfoDouble(_Symbol, SYMBOL_POINT) * 100; // deslocamento vertical acima da linha
// double textPrice = avgPrice + textOffset;
// datetime timeRight = iTime(_Symbol, _Period, 0); // tempo do candle mais recente
// string textName = StringFormat(zoneNames[types[s]] + "texto", s, e);
// if(ObjectCreate(ChartID(), textName, OBJ_TEXT, 0, timeRight, textPrice)) {
// ObjectSetInteger(ChartID(), textName, OBJPROP_COLOR, clrBlack);
// ObjectSetInteger(ChartID(), textName, OBJPROP_FONTSIZE, 10);
// ObjectSetInteger(ChartID(), textName, OBJPROP_CORNER, CORNER_RIGHT_LOWER); // opcional se quiser em pixel
// ObjectSetString(ChartID(), textName, OBJPROP_TEXT, zoneNames[types[s]]);
// ObjectSetInteger(ChartID(), textName, OBJPROP_ANCHOR, ANCHOR_RIGHT); // ancora à direita do ponto
// } }
}
//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
int CountZLineObjects() {
 int total = ObjectsTotal(ChartID());
 int count = 0;
 for(int i = 0; i < total; i++) {
  string name = ObjectName(ChartID(), i);
  if(StringFind(name, "ZLine_") == 0) // verifica se começa com "ZLine_"
   count++; }
 return count; }
//+------------------------------------------------------------------+
//| Exporta buffers para EA + classificação de zona |
//+------------------------------------------------------------------+
void ExportBuffers(int rates_total) {
// monta array de stats
 double stats[]; ArrayResize(stats, totalPriceLevels);
 for(int i = 0; i < totalPriceLevels; i++)
  stats[i] = levels[i].presence_percent;
 double mean = MathMean(stats);
 double sd   = MathStdDev(stats, mean);
 if(sd <= 0) sd = mean * 0.1;
 double adxValue = adx.Main(0);
 int cnt = MathMin(totalPriceLevels,
                   ArraySize(LevelPriceBuffer));
 for(int i = 0; i < cnt; i++) {
  // tipo de zona
  double p = levels[i].presence_percent;
  int ztype = 4; // BARREIRA por padrão.
  if(p < mean - sd)                                 ztype = 1; // REVERSÃO
  else if(p > mean + sd && adxValue >= 25.0)        ztype = 2; // TENDÊNCIA
  else if(adxValue < 20.0)                          ztype = 3; // LATERALIZAÇÃO
  LevelPriceBuffer   [rates_total - 1 - i] = levels[i].price;
  LevelStrengthBuffer[rates_total - 1 - i] = p;
  LevelTypeBuffer    [rates_total - 1 - i] = ztype; } }

//+------------------------------------------------------------------+
//| Ajudantes|
//+------------------------------------------------------------------+
bool DoesBarTouchLevel(double high, double low,
                       const PriceLevel &L) {
 return(high >= L.price_low && low <= L.price_high); }

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void ClearPreviousObjects() {
 int tot = ObjectsTotal(ChartID());
 for(int i = tot - 1; i >= 0; i--) {
  string nm = ObjectName(ChartID(), i);
  if(StringFind(nm, "HeatLevel_") == 0)
   ObjectDelete(ChartID(), nm); } }

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
color GetPercentageColor(double p) {
 if(p <= 1.0)      return Color1Percent;
 else if(p <= 25.) return InterpolateColor(
                            Color1Percent,
                            Color25Percent,
                            (p - 1) / 24);
 else if(p <= 50.) return InterpolateColor(
                            Color25Percent,
                            Color50Percent,
                            (p - 25) / 25);
 else if(p <= 75.) return InterpolateColor(
                            Color50Percent,
                            Color75Percent,
                            (p - 50) / 25);
 else              return InterpolateColor(
                            Color75Percent,
                            Color100Percent,
                            (p - 75) / 25); }

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
color InterpolateColor(color c1, color c2, double f) {
 int r1 = (c1 >> 16) & 0xFF, g1 = (c1 >> 8) & 0xFF,
     b1 = c1 & 0xFF;
 int r2 = (c2 >> 16) & 0xFF, g2 = (c2 >> 8) & 0xFF,
     b2 = c2 & 0xFF;
 int r = int(r1 + (r2 - r1) * f),
     g = int(g1 + (g2 - g1) * f),
     b = int(b1 + (b2 - b1) * f);
 return (r << 16) | (g << 8) | b; }

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
int TimeHour(const datetime time) {
 MqlDateTime dt;
 TimeToStruct(time, dt);
 return dt.hour; }
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| DrawAndExport|
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Calcula a média de um array de double |
//+------------------------------------------------------------------+
double MathMean(const double &a[], const int count) {
 if(count <= 0) return 0.0;
 double sum = 0.0;
 for(int i = 0; i < count; i++)
  sum += a[i];
 return sum / count; }
//+------------------------------------------------------------------+
//| Calcula o desvio padrão amostral de um array de double |
//+------------------------------------------------------------------+
double MathStdDev(const double &a[], const int count) {
 if(count < 2) return 0.0;
 double mean = MathMean(a, count);
 double sumSq = 0.0;
 for(int i = 0; i < count; i++)
  sumSq += MathPow(a[i] - mean, 2);
 return MathSqrt(sumSq / (count - 1));  // Desvio padrão amostral.
}
//+------------------------------------------------------------------+
Caminhe em novos trilhos: Personalize indicadores no MQL5 Caminhe em novos trilhos: Personalize indicadores no MQL5
Vou agora listar todas as possibilidades novas e recursos do novo terminal e linguagem. Elas são várias, e algumas novidades valem a discussão em um artigo separado. Além disso, não há códigos aqui escritos com programação orientada ao objeto, é um tópico muito importante para ser simplesmente mencionado em um contexto como vantagens adicionais para os desenvolvedores. Neste artigo vamos considerar os indicadores, sua estrutura, desenho, tipos e seus detalhes de programação em comparação com o MQL4. Espero que este artigo seja útil tanto para desenvolvedores iniciantes quanto para experientes, talvez alguns deles encontrem algo novo.
Indicador de sazonalidade por horas, dias da semana e meses Indicador de sazonalidade por horas, dias da semana e meses
O artigo explica como desenvolver uma ferramenta para análise de padrões recorrentes de preços nos mercados financeiros, por dias do mês (1-31), dias da semana (segunda-feira-domingo) ou horas do dia (0-23). O indicador analisa dados históricos, calcula a rentabilidade média para cada período e exibe os resultados na forma de um histograma com previsão. Inclui parâmetros configuráveis: tipo de sazonalidade, quantidade de barras analisadas, exibição em porcentagens ou valores absolutos, cores dos gráficos.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Operando com o Calendário Econômico do MQL5 (Parte 6): Automatizando a Entrada de Trades com Análise de Eventos de Notícias e Temporizadores de Contagem Regressiva Operando com o Calendário Econômico do MQL5 (Parte 6): Automatizando a Entrada de Trades com Análise de Eventos de Notícias e Temporizadores de Contagem Regressiva
Neste artigo, implementamos a entrada automática de trades utilizando o Calendário Econômico do MQL5, aplicando filtros definidos pelo usuário e deslocamentos de tempo para identificar eventos de notícias qualificados. Comparamos os valores de previsão e valores anteriores para determinar se devemos abrir uma operação BUY ou SELL. Temporizadores dinâmicos de contagem regressiva exibem o tempo restante até a divulgação da notícia e são redefinidos automaticamente após a execução de um trade.