Русский Português
preview
Análisis de las brechas temporales de precios en MQL5 (Parte II): Creamos un mapa de calor de la distribución de liquidez a lo largo del tiempo

Análisis de las brechas temporales de precios en MQL5 (Parte II): Creamos un mapa de calor de la distribución de liquidez a lo largo del tiempo

MetaTrader 5Indicadores |
58 4
Yevgeniy Koshtenko
Yevgeniy Koshtenko

En la primera parte del artículo, examinamos el concepto de brechas temporales y su relación con la actividad institucional. Sin embargo, detectar estas brechas supone solo la mitad del trabajo. Para negociar con eficacia, los tráders necesitan tener una visión completa: dónde el precio permanece mucho tiempo, dónde permanece poco tiempo y cómo interactúan estas zonas entre sí.

Aquí es donde entra en juego nuestro indicador: una herramienta que convierte patrones temporales invisibles en un mapa de calor visual. Si en la primera parte buscábamos anomalías (brechas), ahora estamos construyendo un mapa completo del comportamiento normal de los precios.

La idea básica es sencilla: representar todo el rango de precios como un conjunto de microzonas y, para cada zona, calcular cuánto tiempo ha permanecido el precio dentro de ella. Cuanto más tiempo transcurra, más "caliente" será la zona y más importante resultará para el mercado. El resultado se visualizará mediante una gama de colores que va del rojo frío al azul cálido.


Fundamentos matemáticos: del caos al orden

Cualquier mercado puede concebirse como una lucha continua entre la oferta y la demanda. En los lugares donde esta competencia resulta más intensa, el precio permanece allí durante más tiempo. Matemáticamente, esto se expresa mediante la función de densidad temporal:

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

Aquí, p es el nivel de precios estudiado, δ es el tamaño de la zona de análisis y t_i es la duración de cada periodo en que el precio se encuentra en esta zona.

Pero los datos brutos no aportan mucha información: es necesaria una normalización que convierta los valores de tiempo absolutos en porcentajes relativos. Por ello, utilizamos la fórmula:

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

Esta fórmula garantiza que las zonas "más frías" reciban un 1% de presencia (rojo) y las zonas "más cálidas" reciban un 100% (azul). Todas las demás zonas se distribuirán entre estos extremos en proporción a su importancia.


Arquitectura de la solución: la modularidad como base de la fiabilidad

La creación de un indicador de este tipo requiere una arquitectura bien planificada. Se basa en la estructura PriceLevel, que encapsula toda la información sobre cada nivel de precio:

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

Cada nivel tiene su propia vida: acumula tiempo, recalcula porcentajes y cambia de color. No se trata solo de una estructura de datos, es la esencia viva del mercado.

La innovación clave ha sido el uso de una ventana de análisis deslizante. En lugar de procesar toda la historia disponible (lo que podría llevar segundos), analizamos solo las últimas barras de MaxHistory a través de una ventana de tamaño AnalysisPeriod. Esto garantiza que los resultados sean relevantes y que el desempeño sea aceptable.


El algoritmo: las matemáticas se encuentran con la realidad

El proceso comienza con la determinación del rango de precios para el análisis. El algoritmo encuentra automáticamente el máximo y el mínimo para el periodo estudiado, lo cual le permite adaptarse a la volatilidad de cualquier instrumento.

Este rango se divide luego en zonas iguales. El número de zonas se calcula de forma dinámica: si se especifica el tamaño del tick, se utiliza; de lo contrario, se toma el Point mínimo del instrumento. Al mismo tiempo, el sistema busca un equilibrio entre el nivel de detalle (al menos 50 niveles) y el rendimiento (no más de 1000 niveles).

La parte que requiere más recursos es el conteo del tiempo en cada nivel. Un enfoque ingenuo requeriría comprobar cada barra respecto a cada nivel (complejidad O(n²)). Optimizamos esto a O(n×k), donde k supone el número promedio de niveles afectados por una 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++;
    }
}

La función DoesBarTouchLevel comprueba si el rango máximo-mínimo de la barra cruza los límites del nivel de precio. La lógica es simple: si el máximo de la barra está por encima del límite inferior del nivel y el mínimo de la barra está por debajo del límite superior, se produce un cruce.


La alquimia del color: transformando los números en imágenes

Una vez calculado el tiempo, comienza la parte más creativa: convertir los datos brutos en una paleta de colores. Nosotros usamos un sistema de cinco pasos: rojo (1%), naranja (25%), amarillo (50%), celeste (75%), azul (100%).

Se produce una interpolación suave entre los puntos clave. Por ejemplo, un nivel con un 37% de presencia recibirá un color entre naranja y amarillo. La interpolación funciona en el espacio 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;
}

El resultado son transiciones de color suaves que crean un mapa de calor natural del mercado.


La visualización: del algoritmo al gráfico

Cada nivel de precio se muestra como un rectángulo en el gráfico. La creación de miles de objetos gráficos es una tarea técnicamente compleja que requiere optimización.

Los rectángulos se colocan desde el momento de inicio del análisis hasta el momento actual, abarcando toda el área de interés. Cada objeto está configurado para funcionar en segundo plano: no interfiere con el análisis de gráficos, no se resalta al hacer clic y se vuelve a dibujar automáticamente cuando cambia el nivel de zoom.

El sistema de gestión de objetos incluye un mecanismo para borrar los resultados anteriores antes de renderizar los nuevos. Esto evita la acumulación de "basura" en el gráfico y garantiza la correcta actualización de la visualización.

En tiempo real, el rendimiento resulta fundamental. El indicador usa varios niveles de optimización:

  • El primer nivel es la evaluación perezosa. El recálculo solo se produce cuando aparece una nueva barra. El sistema registra la hora de la última actualización e inicia los cálculos solo cuando se produce un cambio.
  • El segundo nivel es la optimización de la memoria. Los conjuntos de datos se asignan una sola vez y se reutilizan. Las estructuras de datos están diseñadas para un consumo mínimo de memoria: se usan números largos en lugar de números de doble precisión para los contadores, y se evitan las variables de cadena innecesarias.
  • El tercer nivel es la optimización algorítmica. La ventana de análisis deslizante limita la cantidad de datos procesados. La cuadrícula de niveles adaptativa previene la creación de un número excesivo de zonas.


La interpretación de los resultados: qué significan los colores

Las zonas rojas (con una presencia del 1 al 25 %) indican áreas por las que el precio pasa rápidamente. Estas son las posibles zonas de brechas temporales del anterior artículo. Las zonas rojas suelen experimentar rebotes y falsas rupturas, por lo que requieren un enfoque cauteloso.

Las zonas naranjas y amarillas (25-75%) representan áreas de actividad moderada. Aquí el precio se mantiene de forma periódica, pero sin mostrar un dominio evidente. Se trata de zonas de transición que, dependiendo del contexto del mercado, pueden convertirse en niveles de soporte o resistencia. Es precisamente en estas zonas donde mejor funciona la operativa tendencial.

Las zonas azul y celeste (75-100%) son las principales características de nuestro análisis. Es en este punto donde el precio permanece la mayor parte del tiempo, lo cual indica una alta actividad comercial. Estos niveles poseen una fuerte fuerza magnética: el precio regresa a ellos con regularidad, usándolos como soporte para el movimiento o como barrera que superar.

La estrategia más efectiva consiste en negociar con rebotes desde las zonas azules. Cuando el precio se acerca a la zona de máxima presencia, la probabilidad de un cambio de tendencia es sustancialmente mayor que la media. Esto funciona especialmente bien en mercados laterales, donde las zonas azules definen claramente los límites del canal.

Las rupturas que atraviesan las zonas amarillas suelen indicar que el movimiento continúa. Si el precio atraviesa fácilmente la zona de presencia media con un buen volumen, indica que no hay una resistencia importante por delante.

Pues bien, las estrategias de rebote funcionan mejor en las zonas rojas.

La combinación con el análisis de volumen mejora mucho las señales. Cuando la zona azul coincide temporalmente con un volumen elevado en el perfil de volumen, se crea una zona de máxima importancia.


Personalización para diferentes mercados: versatilidad a través de la adaptación.

El mercado Forex, con su alta liquidez, requiere valores elevados de AnalysisPeriod (300-500 barras) y MaxHistory (5 000-8 000 barras). Los movimientos en esta zona son más suaves, por lo que necesitamos un análisis más profundo para identificar las áreas significativas.

El mercado de valores funciona bien con ajustes moderados: AnalysisPeriod 200-300 barras, MaxHistory 3 000-5 000 barras. La estructura de la sesión crea pausas naturales que se reflejan claramente en el mapa de calor.

El tamaño del tick (TickSize) resulta fundamental para un funcionamiento correcto. Si establecemos este valor demasiado bajo, obtendremos un nivel de detalle excesivo sin ningún beneficio práctico. Un valor demasiado alto provocará que se pasen por alto matices importantes. El valor 0 (modo automático) suele ser el óptimo: el sistema seleccionará automáticamente el tamaño en función de las características del instrumento.

La transparencia (Transparency) afecta no solo a la percepción visual, sino también al rendimiento. Los valores altos (70-90%) crean un mapa semitransparente que no interfiere con el análisis de velas japonesas, pero requiere más recursos para su renderizado.

El límite de 1 000 niveles se ha introducido por una razón. MetaTrader 5 tiene limitaciones en cuanto al número de objetos gráficos, y superar los límites razonables provoca una ralentización de la interfaz sin mejorar significativamente la calidad del análisis.


Integración con otras herramientas: sinergia de métodos

El indicador de mapa de calor complementa a la perfección el perfil volumétrico. La coincidencia de las zonas de tiempo azules con los picos de volumen crea áreas de excepcional importancia. Dichas zonas suelen ser clave para el movimiento del precio a largo plazo.

La combinación con los niveles de Fibonacci ofrece resultados interesantes. Cuando los niveles importantes de Fibonacci caen en las zonas azules del mapa de calor, su importancia aumenta sustancialmente. Esto es natural: los niveles matemáticos están respaldados por el comportamiento real de los precios.

Los indicadores de volatilidad (como las bandas de Bollinger) funcionan bien en combinación con un mapa de calor. El ensanchamiento de las bandas en las zonas rojas suele preceder a movimientos fuertes, mientras que el estrechamiento en las zonas azules indica una acumulación de energía para un próximo salto.

Está previsto que la próxima versión del indicador incluya aprendizaje automático para optimizar automáticamente los parámetros de un instrumento específico. El algoritmo analizará las estadísticas de rendimiento del nivel y ajustará la configuración para lograr la máxima eficiencia.

La integración con sistemas de alerta permitirá recibir notificaciones cuando los precios se acerquen a zonas clave. Esto resulta especialmente útil para los tráders de swing trading que no pueden supervisar constantemente los gráficos.

La exportación de datos a sistemas externos abrirá posibilidades para la creación de robots de negociación basados en mapas de calor. Los robots podrán usar la fuerza de los niveles como un filtro adicional para entrar en una posición.


La filosofía del método: el tiempo como moneda de mercado

Este indicador se basa en una idea profunda: el tiempo es la moneda del mercado. Los participantes del mercado emplean el tiempo con la misma consciencia con la que emplean el dinero. Cuanto más tiempo se pasa en un nivel determinado, más emociones, decisiones y capital se asocian a él.

Este vínculo emocional crea la memoria de mercado. Incluso cuando el precio se aleja de un nivel significativo, el recuerdo de este permanece en el subconsciente de los participantes. Al regresar a este nivel, afloran viejos recuerdos: para algunos, son ganancias; para otros, pérdidas. Estos recuerdos influyen en las nuevas decisiones de inversión.

El mapa de calor del tiempo hace visible este recuerdo invisible. Convierte la psicología del mercado en matemáticas, las emociones en algoritmos y la intuición en datos.


Conclusión: una nueva perspectiva sobre viejas verdades

Este indicador no revela principios nuevos; simplemente aclara los ya existentes. Los niveles de soporte y resistencia siempre han existido, pero ahora se pueden medir y clasificar según su importancia. El mapa de calor muestra dónde pasa el tiempo el mercado y, por lo tanto, dónde reside su verdadera fortaleza.

En combinación con el indicador de brechas temporales, esto proporciona una visión completa del comportamiento de los principales participantes: dónde actúan con rapidez y dónde se retienen. En conjunto, estas herramientas permiten comprender mejor la estructura del mercado y tomar decisiones basadas en la lógica en lugar de la intuición.

En la siguiente parte, hablaremos sobre cómo combinar todo esto en un único sistema de trading.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/18661

Archivos adjuntos |
Maxim Kuznetsov
Maxim Kuznetsov | 3 jul 2025 en 09:42

Me recuerda: yo solía hacer esto hace mucho tiempo,

En mi opinión, usted tiene sólo un poco incompleta - que queda por revelar la estructura regular (o mostrar que no existe).

Esto no es un mapa de calor, por supuesto - sólo los extremos de la parte regular de un mapa de temperatura similar.

Stanislav Korotky
Stanislav Korotky | 3 jul 2025 en 14:27
¿En qué se diferencia del conocido "perfil de mercado" (aparte del color)?
Maxim Kuznetsov
Maxim Kuznetsov | 3 jul 2025 en 16:33

Por cierto, el mapa se calcula utilizando el método más laborioso y propenso a errores.

Todo es más simple - para cada vela 2 pares {precio,peso} : { precio=alto, peso=-1; } { precio=bajo,peso=+1;} La colección se ordena por precio, la suma con acumulación por peso es el mapa de calor. Luego se cuantifica como se quiera.

Lipe Ramos
Lipe Ramos | 15 jul 2025 en 18:44
Decidí experimentar con ChatGPT para atornillar un par de "mejoras" y crear algo útil para mí - esto es lo que salió de ello. La parte comentada es mi intento de detectar cuando el precio entra en una de las zonas coloreadas y mostrar visualmente su reacción. Pero la idea era captar los matices de estos colores: por ejemplo, cuando la zona es naranja ligeramente más brillante, según mis observaciones, el precio se desplaza lateralmente.
//+------------------------------------------------------------------+
//| Mapa de calor Plus - v3.00 modificada
//| Mantén el buffer/alertas y añade ADX, estadísticas y clasificación.
//+------------------------------------------------------------------+
#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 tendencias

// 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 "=== Colores (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;

//--- globales
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;

//--- Asa ADX
CiADX   adx;

//+------------------------------------------------------------------+
//| Inicialización|
//+------------------------------------------------------------------+
int OnInit() {
// buffers para EA
 SetIndexBuffer(0, LevelPriceBuffer);
 SetIndexBuffer(1, LevelStrengthBuffer);
 SetIndexBuffer(2, LevelTypeBuffer);
 if(AnalysisPeriod < 50 || MaxHistory < AnalysisPeriod) {
  Print("Parámetros incorrectos: 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 &/*cerrar*/.[],
                const long &tick_volume[],
                const long &volume[],
                const int &/*spread*/.[]) {
 if(rates_total < AnalysisPeriod) return(0);
// só recalcula em novo 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); }

//+------------------------------------------------------------------+
//| Ventana corredera + filtros de caudal y volatilidad.
//+------------------------------------------------------------------+
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];
// gama de precios
 double maxP = high[ArrayMaximum(high, historyStart, MaxHistory)];
 double minP = low [ArrayMinimum(low,  historyStart, MaxHistory)];
 double priceRange = maxP - minP;
 if(priceRange <= 0) return;
// tamaño del tick
 double tk = (TickSize > 0 ? TickSize : Point());
 totalPriceLevels = (int)(priceRange / tk);
 totalPriceLevels = MathMin(1000, MathMax(50, totalPriceLevels));
 double realTick = priceRange / totalPriceLevels;
// niveles init
 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 sesión
  int hr = TimeHour(time[bar]);
  if(hr < SessStartHour || hr > SessEndHour) continue;
  // filtro de volatilidad
  double amp = (high[bar] - low[bar]) / Point();
  if(amp < MinBarRange) continue;
  long barVol = UseTickVolume ? tick_volume[bar] : volume[bar];
  // encontrar los niveles afectados
  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;
// porcentaje de núcleos
 CalculatePercentsAndColors(); }

//+------------------------------------------------------------------+
//| Porcentajes y 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); } }

//+------------------------------------------------------------------+
//| Gráfico Colorear + Alertas|
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//||
//+------------------------------------------------------------------+
void ColorChart() {
 ClearPreviousObjects();
 datetime nowTime = TimeCurrent();
 double lastBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
// Array 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 y dibuja una línea por grupo
// struct Segmento {
// tipo int;
// int inicio;
// int fin; };
// Segmento segs[];
// ArrayResize(segs, 0);
// int tipoactual = tipos[0];
// int segStart = 0;
// for(int i = 1; i < totalNivelesPrecios; i++) {
// if(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 = tipos[i];
// segStart = i; } }
//// último segmento
// Segmento lastSeg;
// lastSeg.type = currentType;
// lastSeg.start = segStart;
// lastSeg.end = totalPreciosNiveles - 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 tipo = segs[j].tipo;
// // 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 a direita (última vela visible)
// double textOffset = SymbolInfoDouble(_Symbol, SYMBOL_POINT) * 100; // desplazamiento vertical sobre la línea
// double textPrice = avgPrice + textOffset;
// datetime timeRight = iTime(_Symbol, _Period, 0); // tempo do candle mais recente
// cadena 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 si se quiere en 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 si 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; // LATERALIZACIÓN
  LevelPriceBuffer   [rates_total - 1 - i] = levels[i].price;
  LevelStrengthBuffer[rates_total - 1 - i] = p;
  LevelTypeBuffer    [rates_total - 1 - i] = ztype; } }

//+------------------------------------------------------------------+
//| Ayudantes|
//+------------------------------------------------------------------+
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 la media de un array de dobles ||
//+------------------------------------------------------------------+
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 el desvío padrón amostral de un 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.
}
//+------------------------------------------------------------------+
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Indicador de estacionalidad por horas, días de la semana y meses Indicador de estacionalidad por horas, días de la semana y meses
Este artículo explica cómo desarrollar una herramienta para analizar patrones de precios recurrentes en los mercados financieros, ya sea por el día del mes (1-31), el día de la semana (lunes a domingo) o la hora del día (0-23). El indicador analiza datos históricos, calcula la rentabilidad media de cada periodo y muestra los resultados en forma de histograma con una previsión. Incluye parámetros personalizables: tipo de estacionalidad, número de barras analizadas, visualización como porcentajes o valores absolutos, colores del gráfico.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Redes neuronales en el trading: Segmentación periódica adaptativa (Generación de tokens) Redes neuronales en el trading: Segmentación periódica adaptativa (Generación de tokens)
Le invitamos a embarcarse en un apasionante viaje por el mundo del análisis adaptativo de series temporales financieras y a aprender cómo transformar el análisis espectral complejo y la convolución flexible en señales de trading reales. Hoy verá cómo LightGTS escucha el ritmo del mercado, adaptándose a sus cambios con un paso de ventana variable, y cómo la aceleración OpenCL convierte la computación en la vía más rápida para tomar decisiones rentables.