Русский
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 |
230 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.
}
//+------------------------------------------------------------------+
Rede neural na prática: Funções de ativação Rede neural na prática: Funções de ativação
Este com toda a certeza, foi o artigo do qual mais me agradou ter escrito até o momento sobre este tema. Visto que nele de fato mostrei que não precisamos de grandes coisas, ou de algo específico para atingir um dado objetivo. Este pode ser alcançado de maneiras diferentes, desde é claro você tenha o conhecimento adequado e a boa vontade de estudar e se dedicar a algo. Agradeço de coração a todos que me ajudaram na parte sobre quais funções escolher. O conteúdo exposto aqui, visa e tem como objetivo, pura e simplesmente a didática. De modo algum deve ser encarado como sendo, uma aplicação cuja finalidade não venha a ser o aprendizado e estudo dos conceitos mostrados.
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.
Redes neurais em trading: Segmentação periódica adaptativa (Conclusão) Redes neurais em trading: Segmentação periódica adaptativa (Conclusão)
Propomos mergulhar no fascinante mundo do LightGTS, um framework leve, porém poderoso, para previsão de séries temporais, no qual a convolução adaptativa e a codificação RoPE se combinam com métodos inovadores de atenção. Em nosso artigo você encontrará uma descrição detalhada de todos os componentes, desde a criação de patches até a complexa mistura de especialistas no decodificador, prontos para integração em projetos MQL5. Descubra como o LightGTS leva o trading automatizado a um novo nível.
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.