English Русский Deutsch 日本語
preview
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 41): Creación de un EA estadístico de niveles de precio en MQL5

Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 41): Creación de un EA estadístico de niveles de precio en MQL5

MetaTrader 5 — Sistemas comerciales |
35 0
Christian Benjamin
Christian Benjamin

Índice



Introducción

La estadística siempre ha sido fundamental para el análisis financiero, pues transforma los datos del mercado, a menudo confusos, en magnitudes medibles y comparables. En esta entrega del kit de herramientas para el análisis de la acción del precio, aplicamos esas mismas herramientas estadísticas directamente a las velas japonesas: en lugar de considerar cada barra como un dato aislado, comprimimos muchas barras en niveles de precio reproducibles y características de distribución que permiten interpretar el comportamiento reciente del mercado.

Cada vela se puede resumir mediante un precio típico, que aquí se define como la media aritmética de sus tres componentes principales:

Precio típico

Por lo tanto, resulta esencial utilizar el TP (precio típico) para obtener la media, la mediana, la moda y los niveles de percentiles: la moda destaca el grupo de precios en el que el mercado pasa la mayor parte del tiempo y, a menudo, se corresponde con niveles prácticos de soporte o resistencia; la mediana identifica el centro robusto de la distribución y revela cambios de dirección cuando el precio la cruza; la media (y la puntuación z derivada) ofrece un punto de equilibrio sensible a los movimientos importantes y resulta útil para las señales normalizadas por la volatilidad; por último, los percentiles (P25/P75) enmarcan el 50 % central de la acción del precio (IQR) y ayudan a distinguir entre una consolidación ajustada y una amplia dispersión. En resumen, las estadísticas basadas en el TP (precio típico) ofrecen niveles de referencia que son a la vez estadísticamente significativos y directamente relevantes para la evolución intradía de los precios.

En este artículo mostramos cómo esas métricas se traducen en señales prácticas y fáciles de interpretar en gráficos: se convierten en líneas de referencia horizontales (media, mediana, P25/P75 y niveles modales), en datos de entrada para umbrales escalados según el ATR que distinguen las rupturas de los giros, y en la base de un motor de señales de puntuación z que señala una acción del precio inusualmente extrema. Es fundamental destacar que la implementación que presentamos (el EA «KDE Level Sentinel») hace hincapié en la reproducibilidad y la facilidad de uso: las instantáneas fijan los niveles de referencia para la supervisión futura, las etiquetas se mantienen estables y no se solapan, y las señales se representan usando flechas precisas en el gráfico que marcan el precio exacto de activación.

Siga leyendo para conocer los cálculos matemáticos que hay detrás de cada métrica, los detalles de implementación en MQL5 y cómo interpretar los resultados del EA, de modo que pueda pasar de las velas japonesas sin procesar a hipótesis de trading claras y comprobables.


Lógica de la estrategia

Como ya he mencionado, estamos intentando aplicar métodos estadísticos a la acción del precio, por lo que utilizamos el precio típico para todos los cálculos estadísticos. El precio típico (TP) se calcula sumando el máximo, el mínimo y el cierre de una barra y dividiendo la suma entre tres; equilibra el rango de cotización con el nivel de cierre, suaviza los picos aislados y genera una serie más estable que el cierre por sí solo. Incorporando los extremos dentro de la barra sin necesidad de usar el precio de apertura, el TP (precio típico) ofrece datos más completos para las estadísticas de distribución —media, mediana y estimaciones de densidad por kernel—, lo que mejora la estabilidad y la calidad de la señal para los modelos posteriores. En comparación con alternativas como Close, HL/2 u OHLC4, el TP ofrece un término medio pragmático: capta tanto el rango como la dirección en un formato compacto y robusto, ideal para el análisis estadístico de la acción del precio.

A continuación se muestran las métricas estadísticas que obtenemos a partir del precio típico (TP). Cada métrica destaca un aspecto diferente del comportamiento del precio a lo largo del tiempo, desde la tendencia central y la dispersión hasta la frecuencia y la estructura.

Métricas derivadas del TP (precio típico):
  • Media (promedio)
  • Mediana
  • Moda
  • Desviación estándar
  • Varianza
  • Rango (diferencial entre el máximo y el mínimo)
  • Asimetría y curtosis (avanzado, opcional)

Analicémoslas una por una para ver cómo los datos brutos de las velas japonesas se transforman en niveles de precios significativos y señales que orientan el análisis de la acción del precio.

1. Media (promedio)

La media representa el valor central de todos los precios típicos de una muestra. Aunque es sensible a los picos extremos, ofrece una visión general fiable de los niveles en los que suelen situarse los precios de media. Por ejemplo, si una persona gana 1.000, 1.200 y 1.100 dólares durante tres meses, el salario medio es (1.000 + 1.200 + 1.100) / 3 = 1.100, lo que refleja el nivel de ingresos global. Del mismo modo, en el trading, la media del TP (precio típico) destaca el precio medio en torno al cual fluctúa el mercado.

Media

Implementación en MQL5:

double Mean(const double &values[])
{
   double sum = 0.0;
   for(int i=0; i<ArraySize(values); i++)
      sum += values[i];
   return sum / ArraySize(values);
}

2. Mediana

La mediana es el valor central de un conjunto ordenado de precios típicos. A diferencia de la media, no se ve afectada por los valores extremadamente altos o bajos, lo que la convierte en una medida robusta de tendencia central. Por ejemplo, si las puntuaciones obtenidas en un examen son 50, 55, 60, 95 y 100, la mediana es 60, lo que representa el verdadero punto medio del rendimiento. En el trading, la mediana del TP (precio típico) muestra el centro equilibrado de la acción del precio, sin distorsiones provocadas por picos inusuales.

Mediana

Implementación en MQL5:

double Median(double &values[])
{
   ArraySort(values, WHOLE_ARRAY, 0, MODE_ASCEND);
   int size = ArraySize(values);
   if(size % 2 == 0)
      return (values[size/2 - 1] + values[size/2]) / 2.0;
   else
      return values[size/2];
}

3. Moda

La moda identifica el valor que aparece con mayor frecuencia en un conjunto de datos, lo que permite comprender mejor la agrupación natural. Por ejemplo, en un grupo en el que las tallas de calzado son 7, 8, 8, 9, 8, 7, 10, 8, 9 y 7, la talla más frecuente es la 8: la moda. Del mismo modo, en el trading, la moda del TP (precio típico) señala los niveles de precio en los que el mercado pasa más tiempo, lo que a menudo coincide con zonas sólidas de soporte o resistencia.

Moda

Implementación en MQL5:

double Mode(const double &values[])
{
   double mode = values[0];
   int maxCount = 0;

   for(int i=0; i<ArraySize(values); i++)
   {
      int count = 0;
      for(int j=0; j<ArraySize(values); j++)
      {
         if(values[j] == values[i]) count++;
      }
      if(count > maxCount)
      {
         maxCount = count;
         mode = values[i];
      }
   }
   return mode;
}

4. Desviación típica

La desviación típica mide en qué medida los valores se desvían de la media, capturando el grado de variabilidad. Imaginemos a dos personas con el mismo número medio de pasos diarios: una registra 7.900, 8.000 y 8.100 pasos, mientras que la otra registra 2.000, 15.000 y 5.000. Ambas rondan los 8.000, pero la segunda presenta una variación mucho mayor. Aplicada al trading, la desviación típica del TP (precio típico) permite distinguir los mercados tranquilos y estables de los volátiles e inestables.

Desviación estándar

Implementación en MQL5:

double StandardDeviation(const double &values[])
{
   double mean = Mean(values);
   double sum = 0.0;
   for(int i=0; i<ArraySize(values); i++)
      sum += MathPow(values[i] - mean, 2);
   return MathSqrt(sum / ArraySize(values));
}

5. Varianza

La varianza es el cuadrado de la desviación típica y cuantifica la dispersión de los valores en torno a la media en unidades al cuadrado. Aunque es menos intuitiva que la desviación típica, magnifica las grandes desviaciones y ofrece una base coherente para comparar la volatilidad entre distintos instrumentos. En el contexto del TP (precio típico), la varianza pone de relieve el grado de dispersión de los niveles de precios, lo que ofrece otra perspectiva sobre la estabilidad del mercado.

Varianza

Implementación en MQL5:

double Variance(const double &values[])
{
   double mean = Mean(values);
   double sum = 0.0;
   for(int i=0; i<ArraySize(values); i++)
      sum += MathPow(values[i] - mean, 2);
   return sum / ArraySize(values);
}

6. Rango

El rango capta la diferencia entre el valor más alto y el más bajo de un conjunto de datos. Por ejemplo, si las temperaturas semanales oscilan entre los 20 °C y los 35 °C, el rango es de 15 °C. En el trading, el rango del TP (precio típico) muestra la amplitud del movimiento del mercado, lo que ayuda a los traders a distinguir rápidamente entre consolidaciones estrechas y oscilaciones amplias.

Rango

Implementación en MQL5:

double Range(const double &values[])
{
   double minVal = values[ArrayMinimum(values)];
   double maxVal = values[ArrayMaximum(values)];
   return maxVal - minVal;
}

7. Asimetría o curtosis

La asimetría mide la falta de simetría de una distribución.

double Skewness(const double &values[])
{
   int n = ArraySize(values);
   double mean = Mean(values);
   double sd   = StandardDeviation(values);

   double sum = 0.0;
   for(int i=0; i<n; i++)
      sum += MathPow((values[i] - mean)/sd, 3);

   return (double)n / ((n-1)*(n-2)) * sum;
}

En una empresa en la que la mayoría de los empleados ganan 3.000 dólares, pero el director general gana 50.000 dólares, el salario medio se ve desplazado al alza, lo que muestra una asimetría positiva. Del mismo modo, la asimetría del TP (precio típico) revela si los precios se han inclinado más hacia extremos alcistas o bajistas, lo que indica desequilibrios direccionales en la estructura del mercado.

Asimetría

La curtosis evalúa el peso de las colas de una distribución, es decir, la probabilidad de que se produzcan resultados extremos. En una autopista, si la mayoría de los coches circulan a una velocidad de entre 60 y 70 km/h, la curtosis es baja. Si los coches suelen circular en ese rango, pero en ocasiones avanzan lentamente a 20 o aceleran hasta 150, la curtosis es elevada.

double Kurtosis(const double &values[])
{
   int n = ArraySize(values);
   double mean = Mean(values);
   double sd   = StandardDeviation(values);

   double sum = 0.0;
   for(int i=0; i<n; i++)
      sum += MathPow((values[i] - mean)/sd, 4);

   return ((double)n*(n+1) / ((n-1)*(n-2)*(n-3))) * sum
          - (3.0*MathPow(n-1,2) / ((n-2)*(n-3)));
}

En el trading, una curtosis alta del TP (precio típico) indica mercados tranquilos la mayor parte del tiempo, pero propensos a movimientos repentinos y drásticos.

Curtosis

8. Percentiles (P25 y P75)

Los percentiles dividen el conjunto de datos en posiciones ordenadas, lo que nos permite comprender dónde se sitúan los valores dentro de la distribución. El percentil 25 (P25) marca el punto por debajo del cual se sitúa el 25 % de los precios típicos, mientras que el percentil 75 (P75) marca el nivel por debajo del cual se sitúa el 75 % de los precios. Juntos, estos dos valores forman el rango intercuartílico (IQR), que representa el 50 % central de todas las observaciones.

double p25 = Percentile(values, 0.25);
double p75 = Percentile(values, 0.75);

Print("P25 = ", DoubleToString(p25, _Digits), 
      " | P75 = ", DoubleToString(p75, _Digits));

En términos de trading, el P25 destaca el «clúster inferior» de la actividad del precio, mostrando con frecuencia dónde entran de forma constante los compradores, mientras que el P75 destaca el «clúster superior», donde suelen dominar los vendedores. En conjunto, estos límites nos permiten comprender la concentración del mercado con mayor precisión que si nos basáramos únicamente en la media o la mediana. Un IQR estrecho indica consolidación, mientras que un IQR amplio sugiere una mayor dispersión del mercado.

La lógica de generación de señales se basa en la puntuación z del precio típico más reciente, calculada como (TP – media) / desviación típica utilizando la ventana estadística actual. Cuando esta desviación estandarizada supera el umbral de entrada configurado (ZScoreSignalEnter), el EA genera una señal larga si el precio se encuentra suficientemente por debajo de la media (puntuación z negativa) o una señal corta si se encuentra suficientemente por encima (puntuación z positiva). Las señales solo se confirman si están activadas las opciones correspondientes AllowLongSignals o AllowShortSignals . Una vez que se encuentra en un estado de señal, el EA espera a que la puntuación z vuelva a situarse dentro de la banda de salida definida (ZScoreSignalExit) antes de anular la señal y, posiblemente, avisar de una reversión. Cada transición de señal activa EmitAlertWithArrow, que dibuja una flecha direccional en el gráfico y, según la configuración establecida por el usuario, también puede activar una alerta emergente, un sonido o una notificación push.


Desglose del código

En esta sección se describen los detalles de implementación de KDE Level Sentinel.mq5, centrándose en la arquitectura, el flujo de datos, los algoritmos principales y las principales rutinas auxiliares que dan soporte a los elementos visuales del gráfico y a la supervisión de niveles. La presentación está estructurada para ayudar a los lectores a relacionar el diseño conceptual con los apartados correspondientes del código y las opciones de configuración.

Configuración e inicialización

Todas las opciones configurables por el usuario se declaran al principio del código fuente como parámetros de entrada. Entre ellas se incluyen la ventana de análisis (Lookback), la opción de excluir la barra que se está formando actualmente; los ajustes de KDE y del histograma (ModeBins, KDEGridPoints, KDEBandwidthFactor); los umbrales de puntuación z para las señales; los controles de instantáneas y supervisión (AutoSnapshotLevels, MonitorBars, TouchTolerancePips, BreakoutPips, ReversalPips, UseATRforThresholds), y la temporización de la interfaz de usuario y la limpieza (TimerIntervalSeconds, CleanupIntervalSeconds). Esta única sección actúa como panel de control del EA; modificando un parámetro de entrada, se altera el enfoque estadístico y el comportamiento de supervisión del EA sin necesidad de modificar el código.

// ---------- user inputs (control panel) ----------
input int    Lookback               = 1000;
input bool   ExcludeCurrent         = true;
input bool   UseWeightedByVol       = true;
input int    ModeBins               = 30;
input int    KDEGridPoints          = 100;
input double KDEBandwidthFactor     = 1.0;
input bool   DrawHistogramOnChart   = false;
input int    RefreshEveryXTicks     = 1;
input double ZScoreSignalEnter      = 2.0;
input double ZScoreSignalExit       = 0.8;
input bool   AutoSnapshotLevels     = true;
input int    MonitorBars            = 20;
input double TouchTolerancePips     = 3.0;
input bool   UseATRforThresholds    = true;
input double ATRMultiplier          = 0.5;
input int    ATRperiod              = 14;
input int    TimerIntervalSeconds   = 60;
input int    CleanupIntervalSeconds = 3600;

// ---------- OnInit (build names, cleanup, placeholders, start timer) ----------
int OnInit()
  {
   S_base = StringFormat("CSTATS_%s_%d", _Symbol, (int)TF);
   S_mean = S_base + "_MEAN";
   S_p25  = S_base + "_P25";
   // remove leftovers from previous runs
   RemoveExistingEAObjects();
   // create panel + placeholder HLINEs
   CreatePanel();
   CreateHLine(S_mean, 0.0, clrBlack, 2);
   CreateHLine(S_p25,  0.0, clrTeal,  1);
   // optionally clear previous snapshot
   if(ClearSnapshotOnStart) ClearSnapshot();
   // start periodic timer for housekeeping
   EventSetTimer(TimerIntervalSeconds);
   return(INIT_SUCCEEDED);
  }

Durante OnInit, el EA crea prefijos canónicos para los nombres de objetos y variables globales utilizando S_base = StringFormat("CSTATS_%s_%d", _Symbol, (int)TF). Esta nomenclatura determinista evita colisiones accidentales entre varias instancias de gráfico y centraliza la limpieza. La inicialización continúa eliminando los objetos restantes de instancias anteriores, creando un panel compacto en la esquina (etiquetas de resumen), colocando líneas horizontales provisionales para cada estadística principal (media, ±DT, P25/P75, mediana y ambas modas), borrando opcionalmente las instantáneas anteriores e iniciando un temporizador periódico para las tareas de mantenimiento.

Adquisición de datos y bucle principal

El cálculo principal se realiza en OnTick, limitado por RefreshEveryXTicks para evitar un uso excesivo de la CPU en ticks de alta frecuencia. La rutina copia las barras de Lookback del marco temporal configurado en rates[] mediante CopyRates, usando start = ExcludeCurrent ? 1 : 0 para omitir opcionalmente la vela en formación. A partir de rates[], calcula arrays de precios típicos vals[] = (high + low + close) / 3.0 y de volúmenes de ticks vols[] (cuando está activada la ponderación por volumen).

void OnTick()
  {
   tick_count++;
   if(tick_count < RefreshEveryXTicks) return;
   tick_count = 0;

   int start = ExcludeCurrent ? 1 : 0;
   int needed = Lookback;
   if(Bars(_Symbol, TF) - start < needed) return;

   MqlRates rates[];
   int copied = CopyRates(_Symbol, TF, start, needed, rates);
   if(copied <= 0) { Print("CopyRates failed: ", GetLastError()); return; }

   double vals[], vols[];
   ArrayResize(vals, copied);
   ArrayResize(vols, copied);
   for(int i = 0; i < copied; i++)
     {
      vals[i] = (rates[i].high + rates[i].low + rates[i].close) / 3.0; // typical price
      vols[i] = (double)rates[i].tick_volume;
     }

   // pass vals/vols into statistics routines...
  }

A continuación se realizan los cálculos estadísticos: media aritmética, media ponderada por volumen opcional, varianza muestral y desviación típica, mediana, percentiles 25 y 75, una moda agrupada en intervalos (ModeBinned) y una moda basada en KDE (ModeKDE). El ancho de banda de KDE usa una regla similar a la de Silverman, h = 1.06 * sd * n^-0.2, escalada mediante KDEBandwidthFactor; a continuación, evalúa la densidad en una cuadrícula uniforme de KDEGridPoints y devuelve el punto de la cuadrícula con la mayor densidad estimada. Se utiliza el precio típico más reciente (latest) para calcular una puntuación z [(latest - mean) / stddev], lo que genera señales sencillas de entrada y salida basadas en la puntuación z.
Las estadísticas calculadas se publican en el gráfico como líneas horizontales y etiquetas de texto, y se exportan como variables globales con claves prefijadas por S_base para permitir lecturas interoperables por parte de otros scripts o indicadores.

Creación de instantáneas y supervisión de niveles de referencia

Cuando AutoSnapshotLevels está activado, el EA bloquea una única instantánea, conservando las estimaciones de niveles actuales (media, media±desviación típica, P25, P75, mediana, modas) y creando un array refLevels[] compuesto por estructuras RefLevel. Cada RefLevel contiene name, price, touched, touchTime, monitorLeft, highest, lowest, result (0: desconocido; 1: ruptura; -1: reversión; 2: sin continuación) y resolvedTime. Las HLINEs de instantánea se denominan S_base + "_REF_" + name y actualizan los objetos TXT canónicos, si existe una etiqueta canónica, o bien utilizan etiquetas específicas de REF.

// take snapshot (single-shot)
void SnapshotReferenceLevels(double mean_val, double p25, double p75, double median_val, double mode_b, double mode_k)
  {
   snapshot_mean = mean_val;
   snapshot_p25  = p25;
   snapshot_p75  = p75;
   snapshot_median= median_val;
   // build refLevels
   ArrayResize(refLevels, 6);
   refLevels[0].name = "MEAN"; refLevels[0].price = snapshot_mean; refLevels[0].touched=false; refLevels[0].result=0;
   // ... fill others ...
   refSnapshotTaken = true;
   snapshotTakenTime = TimeCurrent();
  }

// monitor reference levels (called from OnTick)
void MonitorReferenceLevels(const MqlRates &rates[], int copied)
  {
   if(!refSnapshotTaken || copied <= 0) return;
   double barHigh = rates[0].high;
   double barLow  = rates[0].low;
   double barClose= rates[0].close;
   double pipPoints = pipToPointMultiplier();
   double touchTol = TouchTolerancePips * pipPoints;
   // compute thresholds (fixed or ATR-scaled)
   double breakoutThreshold = BreakoutPips * pipPoints;
   if(UseATRforThresholds)
     {
      int hATR = iATR(_Symbol, TF, ATRperiod);
      double atrBuf[];
      CopyBuffer(hATR,0,0,1,atrBuf);
      IndicatorRelease(hATR);
      breakoutThreshold = atrBuf[0] * ATRMultiplier;
     }

   for(int i=0;i<ArraySize(refLevels);i++)
     {
      RefLevel L = refLevels[i];
      if(L.result != 0) continue;
      if(!L.touched)
        {
         if(barHigh >= L.price - touchTol && barLow <= L.price + touchTol)
           {
            L.touched = true;
            L.touchTime = rates[0].time;
            L.monitorLeft = MonitorBars;
            L.highest = barHigh; L.lowest = barLow;
            refLevels[i] = L;
           }
        }
      else
        {
         // update highest/lowest and evaluate breakout/reversal
         if(barHigh > L.highest) L.highest = barHigh;
         if(barLow  < L.lowest)  L.lowest  = barLow;
         bool breakout = (L.highest >= L.price + breakoutThreshold);
         bool reversal = (L.lowest  <= L.price - breakoutThreshold);
         if(breakout && !reversal) { L.result = 1; L.resolvedTime = rates[0].time; DrawOutcome(L, true); }
         else if(reversal && !breakout) { L.result = -1; L.resolvedTime = rates[0].time; DrawOutcome(L, false); }
         else { if(--L.monitorLeft <= 0) { L.result = 2; L.resolvedTime = rates[0].time; DrawOutcome(L, false); } }
         refLevels[i] = L;
        }
     }
  }

La supervisión se implementa en MonitorReferenceLevels. Para cada referencia sin resolver, se comparan el máximo, el mínimo y el cierre de la última barra con una tolerancia de toque (TouchTolerancePips, convertida a unidades de precio mediante pipToPointMultiplier()). Un toque establece monitorLeft = MonitorBars y registra el máximo y el mínimo iniciales. Durante la ventana de supervisión, el EA hace seguimiento de los precios máximo y mínimo y evalúa las condiciones de ruptura o reversión. Los umbrales son valores fijos en pips o se derivan del ATR cuando UseATRforThresholds es true; el ATR se obtiene mediante iATR + CopyBuffer y se escala mediante ATRMultiplier para generar umbrales adaptativos. Se admite una confirmación opcional al cierre de la barra (UseCloseForConfirm). Los resultados se determinan de forma definitiva, bien inmediatamente tras la confirmación, bien al final de la ventana de supervisión, comparando los extremos observados con los umbrales; a continuación, se registran y se dibujan mediante DrawOutcome.

Elementos visuales y ubicación de las etiquetas

El EA da prioridad a anotaciones del gráfico estables y sin solapamientos. Las líneas horizontales para estadísticas y referencias de instantáneas utilizan la función CreateHLine, que actualiza los objetos existentes, si los hay, y establece una marca de tiempo en los metadatos. Las etiquetas de texto se gestionan usando CreateOrUpdateLineText, una rutina de colocación que:

void CreateOrUpdateLineText(string name, datetime t, double price, string text)
  {
   long chart_id = ChartID();
   int x0 = 0, y0 = 0;
   bool ok = ChartTimePriceToXY(chart_id, 0, t, price, x0, y0);
   int fontSize = 10;
   int pixelThresh = MathMax(18, fontSize * 2);

   // collect used Y positions from existing OBJ_TEXT objects
   int usedYPositions[];
   ArrayResize(usedYPositions,0);
   int total = ObjectsTotal(0);
   for(int oi=0; oi<total; oi++)
     {
      string oname = ObjectName(0, oi);
      if(oname == name) continue;
      if(ObjectGetInteger(0, oname, OBJPROP_TYPE) != OBJ_TEXT) continue;
      long ot = (long)ObjectGetInteger(0, oname, OBJPROP_TIME);
      double op = ObjectGetDouble(0, oname, OBJPROP_PRICE);
      int xp=0, yp=0;
      if(ChartTimePriceToXY(chart_id,0,(datetime)ot,op,xp,yp))
        {
         ArrayResize(usedYPositions, ArraySize(usedYPositions)+1);
         usedYPositions[ArraySize(usedYPositions)-1] = yp;
        }
     }

   // attempt to find free Y slot
   int chosenY = y0;
   if(!IsYFree(chosenY, usedYPositions, pixelThresh))
     {
      int step = pixelThresh;
      bool found = false;
      for(int s=1; s<=20 && !found; s++)
        {
         int yUp = y0 - s*step;
         if(yUp >= 0 && IsYFree(yUp, usedYPositions, pixelThresh)) { chosenY = yUp; found = true; break; }
         int yDn = y0 + s*step;
         if(IsYFree(yDn, usedYPositions, pixelThresh)) { chosenY = yDn; found = true; break; }
        }
     }

   // convert chosen XY back to time/price; fallback if needed
   datetime tt = t; double pp = price;
   if(!ChartXYToTimePrice(chart_id, 0, x0, chosenY, tt, pp))
     { // fallback: nudge price slightly
      double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
      int slotDelta = (chosenY - y0) / pixelThresh;
      pp = price + slotDelta * pixelThresh * point;
     }

   // create or update the OBJ_TEXT
   KeepSingleTextLabel(name);
   if(ObjectFind(0, name) >= 0)
     {
      ObjectSetInteger(0, name, OBJPROP_TIME, (long)tt);
      ObjectSetDouble(0, name, OBJPROP_PRICE, pp);
      ObjectSetString(0, name, OBJPROP_TEXT, text);
     }
   else
     {
      ObjectCreate(0, name, OBJ_TEXT, 0, tt, pp);
      ObjectSetString(0, name, OBJPROP_TEXT, text);
     }
   SetObjTimestamp(name);
  }

Intenta mapear el tiempo+precio objetivo a coordenadas de pantalla mediante ChartTimePriceToXY.

Recopila las posiciones Y en píxeles de los objetos OBJ_TEXT existentes (y de los pares TXT estadísticos canónicos) para que las nuevas etiquetas eviten colisiones.

Busca hacia arriba o hacia abajo en pasos de píxeles (derivados del tamaño de la fuente) para localizar un hueco libre (IsYFree) y convierte la posición XY elegida de nuevo a tiempo+precio con ChartXYToTimePrice. Si las conversiones fallan, un mecanismo de respaldo robusto ajusta la posición de forma aproximada para evitar bloqueos.

Garantiza un único objeto _TXT por cada HLINE mediante KeepSingleTextLabel.

Este método genera etiquetas legibles con un mínimo de saltos al desplazar o cambiar el tamaño del gráfico. Las flechas de señal y de resultado se dibujan mediante la función DrawArrowAt, que además crea una HLINE fina y exacta en el precio de la flecha para conservar una alineación precisa.

Histograma y KDE

DrawHistogram calcula un histograma de frecuencias de los precios típicos en ModeBins, traza HLINEs en los centros de los intervalos con un ancho proporcional al recuento y crea etiquetas de recuento compactas. ModeBinned calcula una estimación rápida de la moda devolviendo el centro del intervalo más poblado. ModeKDE proporciona una estimación más suave de la moda realizando una estimación ingenua de densidad por kernel en una cuadrícula especificada por el usuario; su complejidad computacional es O(n × gridPts) y, por tanto, debe ajustarse en función del Lookback y de la tasa intrabarra elegidos.

// histogram drawing (bin counts => HLINE widths)
void DrawHistogram(const double &arr[], int n, int bins, int maxWidth)
  {
   double minv = ArrayMin(arr, n);
   double maxv = ArrayMax(arr, n);
   double binw = (maxv - minv) / bins;
   int counts[]; ArrayResize(counts, bins); ArrayInitialize(counts,0);
   for(int i=0;i<n;i++)
     {
      int b = (int)MathFloor((arr[i]-minv)/binw);
      if(b < 0) b=0; if(b >= bins) b=bins-1;
      counts[b]++;
     }
   // draw HLINE per bin with width proportional to counts[b]
  }

// KDE-based modal estimate
double ModeKDE(const double &a[], int n, int gridPts, double bwFactor)
  {
   double mn = ArrayMin(a,n), mx = ArrayMax(a,n);
   double sd = MathSqrt(Variance(a, n, false));
   double h = 1.06 * sd * MathPow((double)n, -0.2);
   if(h <= 0) h = (mx - mn) / 20.0;
   h *= bwFactor;
   double bestX = mn, bestD = -1.0;
   const double SQRT2PI = 2.5066282746310002;
   for(int g=0; g<gridPts; g++)
     {
      double x = mn + (double)g/(gridPts-1) * (mx - mn);
      double s = 0.0;
      for(int i=0;i<n;i++)
        {
         double u = (x - a[i]) / h;
         s += MathExp(-0.5 * u * u);
        }
      double dens = s / (n * h * SQRT2PI);
      if(dens > bestD) { bestD = dens; bestX = x; }
     }
   return(bestX);
  }

Gestión de metadatos y del ciclo de vida

Cada objeto de gráfico creado se asocia a una marca temporal de metadatos almacenada en una variable global con la clave S_base + "_META_" + objectName mediante SetObjTimestamp. Esto permite que la función RemoveOldObjects (invocada en OnTimer) analice una lista de objetos candidatos y elimine los que tengan una antigüedad superior a CleanupIntervalSeconds, evitando así el desorden visual en gráficos de larga duración. RemoveExistingEAObjects y CleanupAllMetaGlobals permiten una inicialización y una finalización controladas eliminando los objetos anteriores y sus metadatos.

// store timestamp meta
void SetObjTimestamp(string name)
  {
   string g = S_base + "_META_" + name;
   GlobalVariableSet(g, (double)TimeCurrent());
  }

// read meta timestamp
datetime GetObjTimestamp(string name)
  {
   string g = S_base + "_META_" + name;
   if(GlobalVariableCheck(g)) return (datetime)GlobalVariableGet(g);
   return 0;
  }

// remove objects older than ageSec
void RemoveOldObjects(int ageSec)
  {
   datetime now = TimeCurrent();
   string candidates[] = { S_mean, S_mean + "_TXT", S_p25, S_p25 + "_TXT", S_panel /* ... */ };
   for(int j=0;j<ArraySize(candidates);j++)
     {
      string nm = candidates[j];
      datetime ts = GetObjTimestamp(nm);
      if(ts == 0) continue;
      if((int)(now - ts) >= ageSec)
        {
         if(ObjectFind(0, nm) >= 0) ObjectDelete(0, nm);
         string g = S_base + "_META_" + nm;
         if(GlobalVariableCheck(g)) GlobalVariableDel(g);
        }
     }
  }

Alertas y gestión de señales

Las señales de puntuación z se evalúan en cada tick y están sujetas a AllowLongSignals / AllowShortSignals. Los umbrales de entrada son ZScoreSignalEnter y los umbrales de salida, ZScoreSignalExit. Cuando se produce un cambio en el estado de una señal, la función EmitAlertWithArrow dibuja la flecha en el gráfico y, opcionalmente, activa alertas de la plataforma (Alert()), sonidos (PlaySound()) y notificaciones push (SendNotification()), al tiempo que elimina las flechas de señales opuestas para limitar el ruido en el gráfico.

// z-score signal logic (called from OnTick after stats computed)
int newSig = currentSignal;
if(zscore >= ZScoreSignalEnter && AllowLongSignals) newSig = 1;
else if(zscore <= -ZScoreSignalEnter && AllowShortSignals) newSig = -1;
else if(currentSignal == 1 && zscore < ZScoreSignalExit) newSig = 0;
else if(currentSignal == -1 && zscore > -ZScoreSignalExit) newSig = 0;

if(newSig != currentSignal)
  {
   if(newSig == 1)
     EmitAlertWithArrow("CSTATS LONG " + _Symbol + " z=" + DoubleToString(zscore,3), t_now, latest, true, S_arrow_long);
   else if(newSig == -1)
     EmitAlertWithArrow("CSTATS SHORT " + _Symbol + " z=" + DoubleToString(zscore,3), t_now, latest, false, S_arrow_short);
   else // clear arrows on exit
     {
      if(ObjectFind(0, S_arrow_long) >= 0) ObjectDelete(0, S_arrow_long);
      if(ObjectFind(0, S_arrow_short) >= 0) ObjectDelete(0, S_arrow_short);
     }
   currentSignal = newSig;
  }

// emit alert helper
void EmitAlertWithArrow(string message, datetime when, double price, bool isBuy, string arrowName)
  {
   DrawArrowAt(arrowName, when, price, isBuy);
   if(SendAlertOnSignal) Alert(message);
   if(PlaySoundOnSignal) PlaySound(SoundFileOnSignal);
   if(SendPushOnSignal) SendNotification(message);
  }

Funciones auxiliares estadísticas y consideraciones numéricas

El código implementa estadísticas estándar basadas en arrays: media, media ponderada, varianza muestral, mediana (incluida la promediación cuando n es par) y percentil con interpolación lineal, adaptadas a la semántica de arrays de MQL5. Las rutinas auxiliares calculan la asimetría y la curtosis si se necesitan para posibles ampliaciones. En la práctica, la resolución de KDE (KDEGridPoints) y el ancho de banda (KDEBandwidthFactor) deben seleccionarse de forma que se logre un equilibrio entre suavidad, precisión y coste de CPU. La creación y liberación de identificadores de ATR se realiza en cada evaluación; para una ejecución de alta frecuencia, se recomienda reutilizar los identificadores de indicadores o calcular el ATR solo al formarse una nueva barra para reducir la sobrecarga.

double Mean(const double &a[], int n)
  {
   if(n<=0) return 0.0;
   double s=0.0;
   for(int i=0;i<n;i++) s += a[i];
   return s / n;
  }

double WeightedMean(const double &a[], const double &w[], int n)
  {
   if(n<=0) return 0.0;
   double sw=0.0, s=0.0;
   for(int i=0;i<n;i++) { s += a[i] * w[i]; sw += w[i]; }
   if(sw == 0.0) return Mean(a, n);
   return s / sw;
  }

double Variance(const double &a[], int n, bool sample)
  {
   if(n <= 1) return 0.0;
   double mu = Mean(a,n), s = 0.0;
   for(int i=0;i<n;i++) { double d = a[i] - mu; s += d * d; }
   return sample ? s / (n-1) : s / n;
  }

double Median(const double &a[], int n)
  {
   if(n <= 0) return 0.0;
   double tmp[]; ArrayResize(tmp, n); ArrayCopy(tmp, a); ArraySort(tmp);
   if((n % 2) == 1) return tmp[n/2];
   return (tmp[n/2 - 1] + tmp[n/2]) / 2.0;
  }

double Percentile(const double &a[], int n, double q)
  {
   if(n <= 0) return 0.0;
   double tmp[]; ArrayResize(tmp, n); ArrayCopy(tmp, a); ArraySort(tmp);
   if(q <= 0.0) return tmp[0];
   if(q >= 1.0) return tmp[n-1];
   double idx = q * (n - 1);
   int i0 = (int)MathFloor(idx); double frac = idx - i0;
   if(i0 + 1 < n) return tmp[i0] * (1.0 - frac) + tmp[i0+1] * frac;
   return tmp[i0];
  }


Resultados

En esta sección analizamos el rendimiento del EA en el gráfico. Es fundamental probar el sistema a fondo, tanto mediante backtesting como en una cuenta demo, antes de desplegarlo con capital real, ya que las señales en vivo pueden llevar incluso a los traders más disciplinados a realizar operaciones prematuras. El EA presenta un panel de métricas compacto y traza líneas de referencia horizontales para cada estadística calculada; cada línea incluye una etiqueta de texto que describe su significado (media, mediana, modas, P25/P75, etc.). Estas señales visuales facilitan la evaluación de cómo interactúa el precio con niveles estadísticamente significativos y permiten validar el comportamiento del EA en pruebas históricas y prospectivas.

La figura siguiente muestra el panel estadístico del EA y los niveles horizontales correspondientes representados en el Step Index (M5). La media y la media ponderada convergen en 8114,8, lo que establece un punto de equilibrio central estable. La desviación típica (15,6) indica una volatilidad moderada en la muestra actual, mientras que la mediana (8113,3) se mantiene cercana a la media, lo cual refleja una distribución simétrica de los precios. Tanto la moda discreta (8112,4) como la moda estimada mediante KDE (8113,2) se concentran justo por debajo de la media, lo que marca una zona de negociación densa que actúa como una región natural de soporte y resistencia. Los percentiles (P25 = 8105,7; P75 = 8121,6) reflejan el rango intercuartílico de 16 puntos, lo que delimita de forma efectiva el 50 % central de la actividad y pone de relieve la banda de consolidación en la que el precio oscila con mayor frecuencia. Por último, la puntuación z (2,676) indica que el precio se ha desviado más de dos desviaciones típicas por encima de la media, lo que sugiere una sobreextensión temporal y aumenta la probabilidad de que se produzca una reversión a la media.

En conjunto, estos resultados demuestran cómo los niveles estadísticos derivados del precio típico pueden servir como puntos de referencia útiles. La visualización en el gráfico del EA permite a los traders confirmar visualmente si el comportamiento del mercado respeta, rechaza o sobrepasa estas zonas, lo que facilita alinear las decisiones de negociación con una estructura medida de forma objetiva, en lugar de basarse en un juicio subjetivo.


Conclusión

El EA calcula niveles de referencia de la distribución a partir del precio típico (TP = (H + L + C) / 3) y los publica directamente en el gráfico: líneas horizontales etiquetadas (media, media ponderada, mediana, modas por intervalos y por KDE, P25/P75), un panel compacto de métricas y señales visuales (flechas, marcadores de contacto/resultado). Además, exporta variables globales para uso programático e incluye una lógica de supervisión que registra los contactos y clasifica los resultados como ruptura, reversión o sin continuación.

Estos niveles convierten la agrupación, la tendencia central y la dispersión en puntos de referencia objetivos en el gráfico que usted puede leer de un vistazo; considérelos como un mapa estadístico del comportamiento reciente de los precios. Úselos como apoyo para la toma de decisiones: compruebe cómo interactúa el precio con estas zonas antes de actuar y considere las salidas del EA como contexto, no como una ejecución automática de operaciones. En futuros proyectos, exploraremos métodos estadísticos más avanzados y técnicas de conjunto para mejorar la estabilidad de los niveles y la capacidad predictiva.

Lea mis otros artículos.

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/19589

Archivos adjuntos |
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.
Creación de un sistema de trading profesional con Heikin Ashi (Parte 2): Desarrollo de un EA Creación de un sistema de trading profesional con Heikin Ashi (Parte 2): Desarrollo de un EA
En este artículo se explica cómo desarrollar un Asesor Experto (EA) profesional basado en Heikin Ashi en MQL5. Así, aprenderá a configurar parámetros de entrada, enumeraciones, indicadores y variables globales, además de a implementar la lógica principal de trading. También podrá ejecutar un backtest en oro para validar su trabajo.
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.
Creación de sistemas de trading basados en IA en MQL5 (Parte 1): Implementación del manejo de JSON para APIs de IA Creación de sistemas de trading basados en IA en MQL5 (Parte 1): Implementación del manejo de JSON para APIs de IA
En este artículo, desarrollaremos un framework de análisis de JSON en MQL5 para gestionar el intercambio de datos en la integración con APIs de IA, centrándonos en una clase JSON para procesar estructuras JSON. Asimismo, implementaremos métodos para serializar y deserializar datos JSON, compatibles con diversos tipos de datos, como cadenas, números y objetos. Esto resulta esencial para comunicarse con servicios de IA como ChatGPT, y permite desarrollar futuros sistemas de trading basados en IA al garantizar un tratamiento y una manipulación precisos de los datos.