English Русский Deutsch 日本語
preview
De novato a experto: Desmitificando los niveles ocultos de retroceso de Fibonacci

De novato a experto: Desmitificando los niveles ocultos de retroceso de Fibonacci

MetaTrader 5Sistemas comerciales |
66 0
Clemence Benjamin
Clemence Benjamin

Contenido:

  1. Introducción
  2. Estrategia de implementación
  3. Pruebas y resultados
  4. Conclusión
  5. Archivos adjuntos


Introducción

Los niveles de retroceso de Fibonacci se utilizan ampliamente, pero, en ocasiones, el precio reacciona ante ratios intermedios o repetidos que se salen de lo habitual. Nuestra pregunta es: ¿podemos utilizar métodos sistemáticos y basados en datos para descubrir dichos niveles, comprobar si se producen más de una vez y, si resultan sólidos, incorporarlos como niveles integrados/plenos en nuestras herramientas y estrategias?

 Por qué los niveles de Fibonacci tradicionales pueden resultar incompletos

Las proporciones clásicas de Fibonacci, como el 23,6 %, el 38,2 %, el 50 %, el 61,8 % y el 78,6 %, se derivan de la sucesión de Fibonacci y de la proporción áurea. Aunque estos niveles gozan de amplia aceptación, los operadores suelen observar que, en ocasiones, los mercados respetan niveles de retroceso intermedios o alternativos que no se incluyen en este conjunto tradicional. Esto sugiere que es posible que el marco estándar no refleje plenamente el comportamiento del mercado.

Observaciones sobre las reacciones ocultas del mercado

De forma anecdótica, el precio suele frenarse, revertir o acelerarse cerca de niveles situados entre el 50 % y el 61,8 %, o en torno a otros puntos no estándar. Consideremos la posibilidad de denominar a estos niveles de Fibonacci «ocultos». La dificultad radica en que estas observaciones son subjetivas, se basan en la inspección visual de los gráficos y pueden no ser válidas de forma sistemática para todos los instrumentos o en todos los horizontes temporales.

El dilema entre la evidencia anecdótica y la prueba estadística

El reconocimiento visual de patrones es propenso al sesgo de confirmación: recordamos las ocasiones en las que el precio reaccionó en un nivel que sospechábamos, pero olvidamos los casos en los que no fue así. Sin pruebas sistemáticas, estos niveles ocultos siguen siendo objeto de especulación. No obstante, la evidencia anecdótica nos ofrece un punto de partida para poner a prueba estas ideas de forma estructurada y aportar mayor precisión a la teoría en la que se basan los operadores. El reto consiste en distinguir las tendencias estructurales reales de la aleatoriedad y el ruido.

Niveles de retroceso desconocidos

Figura 1: Demostración de los niveles de retroceso de Fibonacci no estándar.

La figura 1 (arriba) es una captura de pantalla de MetaTrader 5 en la que se muestra el par EURUSD en el marco temporal M15. El movimiento A–B se midió con la herramienta de Fibonacci; los niveles de retroceso estándar aparecen marcados en azul y etiquetados. El precio no se detuvo exactamente en esos ratios clásicos; en cambio, se puede observar una actividad del precio claramente diferenciada en puntos intermedios entre los niveles marcados (38,2 y 50). He resaltado esas reacciones intermedias con líneas discontinuas rojas y las he etiquetado con un «?» para indicar que se trata de niveles aún no identificados, potencialmente significativos.

Esas reacciones intermedias son precisamente lo que nuestra investigación pretende esclarecer. Aunque podríamos calcular y trazar valores de retroceso precisos mediante programación, la inspección manual no es fiable, ya que la herramienta de Fibonacci integrada solo traza los ratios clásicos. Lo que se necesita es un proceso en dos fases: en primer lugar, recopilar y filtrar estadísticamente un amplio conjunto de observaciones de retrocesos normalizadas para identificar qué bandas intermedias se respetan de forma reiterada; en segundo lugar, implementar un algoritmo que calibre y represente esos niveles validados en la herramienta de Fibonacci de MetaTrader 5 (acompañados de puntuaciones de confianza).


Estrategia de implementación

Métodos de detección de rangos de barras y oscilaciones

En este proyecto, los máximos y mínimos de cada barra se consideran un simple rango de «oscilación». Aplicamos un filtro de rango mínimo (por ejemplo, un multiplicador ATR) para suprimir el ruido y centrarnos en los movimientos significativos. Este proxy de rango de barras es deliberadamente ligero: es sencillo de implementar, rápido de ejecutar sobre historiales extensos y genera un conjunto de datos determinista, con una observación por cada barra cerrada, ideal para el descubrimiento estadístico.

En fases futuras adoptaremos un método de detección de oscilaciones de varias barras más preciso para captar oscilaciones más largas o de importancia estructural. La elección actual —empezar con rangos de barras— es deliberada: minimiza los costes de ingeniería, lo que nos permite recopilar rápidamente muestras de gran tamaño, validar nuestros métodos estadísticos y, a continuación, iterar. La viabilidad de este enfoque se basa en una observación práctica del mercado: las velas japonesas suelen retroceder una parte del movimiento anterior, lo que genera picos cuantificables en una distribución normalizada de los retrocesos. Véase la figura 2, que ilustra el comportamiento del retroceso intrabárico que sirve de base para este método.

Rango de retroceso de la vela

Figura 2: Retroceso del rango de la vela.

 Recopilación y preparación de datos

El primer paso consiste en recopilar datos históricos de OHLCV de múltiples instrumentos (por ejemplo, EURUSD, GBPUSD, S&P500, XAUUSD) y en distintos intervalos de tiempo (M15, H1, H4, D1). Una muestra lo suficientemente amplia —idealmente, de entre tres y cinco años de datos— garantiza que queden representadas diversas situaciones del mercado, desde fases tendenciales hasta periodos de consolidación. Además, los datos deben depurarse antes de su uso, eliminando las barras que faltan, los picos anómalos y las lagunas que podrían sesgar el análisis. Esta base garantiza que el conjunto de datos de retrocesos refleje la estructura real del mercado y no anomalías aleatorias.

Partiendo de esta base limpia, cada barra cerrada se considera un rango de oscilación independiente. El máximo y el mínimo definen los límites de referencia, y se analiza la profundidad del retroceso en la barra inmediatamente siguiente. Para eliminar el ruido, se excluyen los rangos inferiores a un umbral basado en el ATR. A continuación, el porcentaje de retroceso se normaliza en una escala de 0 a 100 y se registra junto con metadatos como el símbolo, el marco temporal, la dirección, la marca de tiempo y el volumen. Es importante destacar que no se aceptan todas las secuencias: el colector rechaza los casos no válidos en los que la barra de prueba se abre fuera del rango anterior o se cierra en sentido contrario al de la barra de referencia. La lógica de ventana ampliada agrupa las barras internas consecutivas o pequeñas secuencias de consolidación, así como los patrones envolventes y los gaps, lo que garantiza que no se confundan con retrocesos normales. Al aplicar estas validaciones y recopilar metadatos, el conjunto de datos resultante es limpio y reproducible, y está preparado también para el análisis estadístico de las proporciones clásicas de Fibonacci.

Desarrollo de un script de recopilación de datos

En los siguientes pasos, prepararemos nuestro script de recopilación de datos en MQL5 y lo utilizaremos para generar un archivo CSV con los datos de retrocesos. A continuación, este archivo se analizará en Jupyter Notebook para explorar patrones y extraer conclusiones.

Inicialización: configuramos los controles y los elementos auxiliares

Al principio declaramos todos los parámetros que controlan el comportamiento: el número de barras que se van a examinar, los ajustes de ATR, los límites de búsqueda hacia delante y los banderas de salida. Cuando se inicia el script, lee estos datos de entrada y prepara dos pequeñas funciones auxiliares (un formateador y un mapeador de intervalos de tiempo a cadenas) para que el resto del código se mantenga ordenado. A continuación, el script genera un nombre de archivo de salida que incluye el símbolo y el intervalo de tiempo, y abre un archivo CSV para escribir en él. Si no se puede abrir el archivo, el proceso se detiene y muestra un mensaje de error, por lo que siempre sabemos si la ejecución se ha iniciado realmente.

//--- input parameters
input int      BarsToProcess     = 20000;   // how many candidate reference bars to process
input int      StartShift        = 1;       // skip most recent N bars
input int      ATR_Period        = 14;      // ATR period
input double   ATR_Multiplier    = 0.3;     // min ATR filter
input int      MaxLookahead      = 3;       // extended-window lookahead
input bool     UsePerfectSetup   = true;    // require perfect setups
input bool     OutputOnlySameDir = false;   // require same-dir support
input bool     IncludeInvalidRows= false;   // output invalids
input string   OutFilePrefix     = "CandleRangeData"; // file prefix

//--- output file
string OutFileName = StringFormat("%s_%s_%s.csv", OutFilePrefix, _Symbol, PeriodToString(_Period));
int fh = FileOpen(OutFileName, FILE_WRITE|FILE_CSV|FILE_ANSI);
if(fh == INVALID_HANDLE) {
   PrintFormat("Error opening file %s", OutFileName);
   return;
}

Base de volatilidad: creamos un handle del indicador ATR para que el script pueda filtrar el ruido

Antes de analizar las barras, creamos un indicador ATR. Para cada posible barra de referencia, el script leerá el ATR de esa barra; el valor del ATR sirve como indicador de volatilidad. Si el rango de una barra es inferior a ATR * ATR_Multiplier, el script considera que se trata de ruido y la omite. Al eliminar los rangos muy pequeños, se evita que las barras aleatorias de menor tamaño generen entradas de retroceso espurias. Mejora la calidad de la señal.

//--- prepare ATR handle
int atr_handle = iATR(_Symbol, _Period, ATR_Period);
if(atr_handle == INVALID_HANDLE) {
   Print("ATR handle invalid");
   FileClose(fh);
   return;
}

Bucle de análisis principal: el script recorre el historial

El script recorre las barras cerradas en sentido inverso, desde StartShift hasta el historial disponible o hasta que haya escrito BarsToProcess filas. En cada iteración (cada barra de referencia candidata), el script lee los valores de máximo, mínimo, apertura y cierre de la barra de referencia, calcula el rango y aplica inmediatamente el filtro ATR. Si la barra supera la prueba, el script pasa a analizar la(s) barra(s) de prueba que siguen a la de referencia. Este bucle es el motor que transforma los datos históricos brutos en posibles eventos de retroceso. De este modo, eliminamos los casos problemáticos en una fase temprana para mejorar las estadísticas posteriores.

int bars = iBars(_Symbol,_Period);
double atr_buf[];

for(int r = StartShift; r <= bars - 1; r++) {
   double RefTop   = iHigh(_Symbol,_Period,r);
   double RefBot   = iLow(_Symbol,_Period,r);
   double RefOpen  = iOpen(_Symbol,_Period,r);
   double RefClose = iClose(_Symbol,_Period,r);
   double Range    = RefTop - RefBot;
   
   // ATR filter
   if(CopyBuffer(atr_handle,0,r,1,atr_buf) <= 0) continue;
   if(Range < atr_buf[0] * ATR_Multiplier) continue;
   
   // process this reference...
}

Determinar la tendencia de referencia: clasificamos la dirección como alcista, bajista o neutral.

En el caso de la barra de referencia, comprobamos si ha cerrado por encima del precio de apertura (alcista), por debajo del precio de apertura (bajista) o al mismo nivel (neutral). Esa dirección determina qué extremo de la barra de prueba representa un retroceso (una referencia alcista busca mínimos; una bajista, máximos). La normalización del porcentaje de retroceso depende de si la referencia fue al alza o a la baja.

string RefDir;
if(RefClose > RefOpen)      RefDir = "Bull";
else if(RefClose < RefOpen) RefDir = "Bear";
else                        RefDir = "Neutral";

Barra de prueba inicial y validación de la configuración perfecta: primero verificamos los casos sencillos y claros

El script lee la barra inmediatamente siguiente (barra de prueba) a la de referencia; es en esta barra donde suele producirse el retroceso. Si activamos el filtro «configuración perfecta», el script comprueba dos condiciones propias del estilo de los traders: la barra de prueba debe abrirse dentro del rango de referencia y su cierre no debe ir en contra de la dirección de referencia (por ejemplo, en el caso de una referencia alcista, la barra de prueba no debería cerrar a la baja). Si la barra de prueba falla y no queremos filas de diagnóstico, el script omite escribir nada para esta referencia.

int testIndex   = r - 1;
double testOpen = iOpen(_Symbol,_Period,testIndex);
double testClose= iClose(_Symbol,_Period,testIndex);

bool ValidSetup = true;
if(UsePerfectSetup) {
   if(testOpen < RefBot || testOpen > RefTop) ValidSetup = false;
   if(RefDir=="Bull" && testClose < testOpen) ValidSetup = false;
   if(RefDir=="Bear" && testClose > testOpen) ValidSetup = false;
}

if(!ValidSetup && !IncludeInvalidRows) continue;

Gestión de ventanas ampliadas: permitimos que el script capture retrocesos realistas de varias barras

Cuando está activada, el script examina algunas barras adicionales dentro de una ventana ampliada (un número de barras configurable) para agrupar secuencias cortas que, en conjunto, dan lugar al verdadero extremo de retroceso. Mientras analiza las barras de anticipación, realiza tres acciones:

  1. Detectar huecos: si una barra se abre fuera del rango de referencia, el script señala un hueco y registra su tamaño.
  2. Agrupar barras internas: si varias barras pequeñas consecutivas se encuentran íntegramente dentro del rango de referencia, el script actualiza el valor extremo (Ext) al mínimo más bajo (en el caso alcista) o al máximo más alto (en el caso bajista) de todas esas barras e incrementa InsideCount.
  3. Detectar barras envolventes: si una barra posterior envuelve por completo a la de referencia, el script la clasifica como «Engulf» y activa el indicador HighMomentum.

Esta agrupación garantiza que la observación refleje el episodio completo de retroceso, en lugar de un contacto parcial prematuro.

double Ext = (RefDir=="Bull") ? testLow : testHigh;
string SeqType = "Single";
bool HighMomentum = false;
int InsideCount = 0;

for(int k=1; k<=MaxLookahead; k++) {
   int idx = r - k;
   if(idx < 0) break;
   double kOpen = iOpen(_Symbol,_Period,idx);
   double kHigh = iHigh(_Symbol,_Period,idx);
   double kLow  = iLow(_Symbol,_Period,idx);
   
   if(kOpen > RefTop || kOpen < RefBot) { SeqType="Gap"; break; }
   if(kHigh <= RefTop && kLow >= RefBot) {
      if(RefDir=="Bull") Ext = MathMin(Ext,kLow);
      if(RefDir=="Bear") Ext = MathMax(Ext,kHigh);
      InsideCount++;
      continue;
   }
   if(kHigh >= RefTop && kLow <= RefBot) {
      SeqType="Engulf";
      HighMomentum=true;
      break;
   }
   // if retrace detected, stop
   break;
}

Cálculo del porcentaje de retroceso: el script normaliza el resultado para su análisis

A partir del valor extremo final registrado (Ext), el script calcula el RetracePct en una escala de 0 a 100:

  • Para la referencia alcista: RetracePct = (RefTop - Ext) / Range * 100
  • Para una referencia bajista: RetracePct = (Ext - RefBot) / Range * 100

A continuación, asigna una etiqueta al evento:

  • NoRetrace si es negativo (el precio se ha alejado),
  • Retracement si está entre 0 y 100,
  • Extensión si el valor es superior a 100 (el precio ha superado el nivel de referencia).

double Rpct = EMPTY_VALUE;
string Type = "Undefined";

if(RefDir=="Bull") Rpct = (RefTop - Ext) / Range * 100.0;
if(RefDir=="Bear") Rpct = (Ext - RefBot) / Range * 100.0;

if(Rpct < 0)       Type="NoRetrace";
else if(Rpct<=100) Type="Retracement";
else               Type="Extension";

Diagnóstico práctico: calcular la distancia negociable y la proximidad a los niveles clásicos de Fibonacci

Calculamos «RetracePips», es decir, el número absoluto de pips que representa el retroceso, para poder descartar los toques insignificantes y no negociables (por ejemplo, los que son inferiores al spread). También calculamos cuál es el nivel de Fibonacci clásico más cercano y a qué distancia se encuentra (NearestFibPct, NearestFibDistPct). Por último, establecemos SameDirSupport comprobando si la barra representativa (la última barra de la secuencia comprimida) cerró en la misma dirección que la de referencia.

double RetracePips = (RefDir=="Bull") ? (RefTop-Ext)/_Point : (Ext-RefBot)/_Point;

// Same-dir support
bool SameDirSupport = (RefDir=="Bull") ? (testClose >= testOpen) : (testClose <= testOpen);

// Nearest Fibonacci comparison
double fibLevels[] = {0,23.6,38.2,50.0,61.8,78.6,100.0};
double nearest = fibLevels[0];
double minDist = fabs(Rpct - fibLevels[0]);
for(int i=1;i<ArraySize(fibLevels);i++) {
   double d = fabs(Rpct - fibLevels[i]);
   if(d < minDist) { minDist = d; nearest = fibLevels[i]; }
}

Reglas de salida

En función de los parámetros (OutputOnlySameDir, IncludeInvalidRows), el script omite o escribe la fila. Si la escribe, la fila contiene todos los metadatos (hora, símbolo, RefTop/RefBot, Ext, RetracePips, RetracePct, SeqType, SameDirSupport, coincidencia de Fibonacci más cercana, volumen y spread). El archivo es determinista: el mismo símbolo, el mismo intervalo de tiempo y los mismos parámetros siempre generan el mismo archivo CSV.

if(OutputOnlySameDir && !SameDirSupport) continue;

FileWrite(fh,
   _Symbol,
   PeriodToString(_Period),
   TimeToString(iTime(_Symbol,_Period,r),TIME_DATE|TIME_SECONDS),
   RefTop, RefBot, Range, RefDir,
   Ext, RetracePips, Rpct, Type,
   SeqType, HighMomentum, InsideCount,
   SameDirSupport, nearest, minDist
);

Limpieza y reporte: finalizamos la ejecución e informamos al equipo de lo ocurrido

Tras el bucle, el script libera el identificador ATR, cierra el archivo y muestra un breve resumen en el que se indica cuántas filas se han escrito y cuántos candidatos no válidos se han omitido. Esta información inmediata nos sirve de guía para nuestra siguiente acción (por ejemplo, aumentar las barras, relajar los filtros o cambiar el multiplicador del ATR).

IndicatorRelease(atr_handle);
FileClose(fh);
PrintFormat("CandleRangeData_v2: finished. Wrote %d rows to %s", written, OutFileName);

Análisis estadístico y visualización de datos de retrocesos de Fibonacci con Python en Jupyter Notebook

Para avanzar en nuestra investigación, utilizaremos Python para depurar los datos y generar informes estadísticos. Para ello, he elegido Jupyter Notebook, ya que ofrece un flujo de trabajo optimizado para Python y permite obtener el tipo de resultados que pretendemos conseguir.

Jupyter Notebook es un entorno web interactivo diseñado para la computación científica, el análisis de datos y la visualización. Permite que el código, los resultados visuales y la documentación coexistan en el mismo espacio de trabajo, lo que lo hace especialmente eficaz para tareas orientadas a la investigación. En nuestro caso, este entorno ofrece la flexibilidad necesaria para experimentar con datos de retroceso, probar métodos estadísticos y visualizar al instante resultados como histogramas y gráficos de densidad. A diferencia de un script estático, que debe ejecutarse de principio a fin, Jupyter nos permite ejecutar pequeñas celdas de forma independiente, lo que supone una ventaja muy útil a la hora de ajustar cálculos o volver a ejecutar pasos concretos sin tener que reiniciar todo el proceso. Este flujo de trabajo interactivo se adapta bien al carácter iterativo y exploratorio de la búsqueda de patrones en datos financieros.

En la siguiente guía se describen los pasos para configurar Jupyter Notebook en Windows.

Para ejecutar las celdas que aparecen a continuación, utiliza Jupyter (Notebook o Lab) en Windows. Los pasos son los siguientes:

1. Instala Python 3.10 o una versión posterior desde python.org (selecciona «Add Python to PATH»).
2. Abre una consola de comandos (PowerShell o CMD) y crea un entorno virtual (opcional, pero recomendable):

python -m venv venv
venv\Scripts\activate

3. Instala Jupyter y las bibliotecas necesarias:

pip install jupyter pandas numpy matplotlib scipy scikit-learn

4. Iniciar Jupyter:

Si quieres trabajar en una carpeta concreta, utiliza el comando «cd» para cambiar de directorio. Por ejemplo, para acceder a la carpeta en la que se exporta el archivo CSV —que suele ser C:\Users\NombreDeTuOrdenador\CarpetaDeDatosDelTerminal\MQL5\Files—, deberías escribir:

cd C:\Users\NombreDeTuOrdenador\CarpetaDeDatosDelTerminal\MQL5\Files

A continuación, puedes ejecutarlo utilizando:

jupyter notebook

Las bibliotecas y su finalidad:

  • pandas: cargar y manipular la tabla CSV (filas/columnas).
  • numpy: matrices numéricas y funciones matemáticas auxiliares.
  • matplotlib: representación gráfica de histogramas y curvas KDE.
  • scipy: funciones estadísticas y KDE.
  • scikit-learn (sklearn): modelos de mezcla gaussiana para la agrupación unidimensional. 

Celda 1: Configuración del entorno de Python

Al principio de nuestro cuaderno, preparamos el entorno importando las bibliotecas de Python que nos permitirán realizar nuestro análisis. Cada biblioteca desempeña una función específica en el tratamiento de los datos exportados desde el script MQL5.

  • Pandas (pd) se utiliza para el tratamiento de datos estructurados. Nos permite cargar el archivo CSV que contiene los registros de retrocesos, manipular filas y columnas y calcular fácilmente estadísticas.
  • Numpy (np) ofrece herramientas matemáticas y operaciones numéricas, como matrices y álgebra lineal, que constituyen la base de muchos de nuestros cálculos estadísticos.
  • Matplotlib.pyplot (plt) es la herramienta básica para trazar gráficos como histogramas y gráficos de densidad, lo que nos ayudará a visualizar el comportamiento de los retrocesos.
  • Seaborn (sns) se basa en matplotlib para ofrecer gráficos más elegantes y fáciles de personalizar, lo que permite visualizar con mayor claridad los patrones estadísticos.
  • Scipy.stats (stats) ofrece métodos estadísticos avanzados, como la estimación de densidad por kernel (KDE), las pruebas de hipótesis y las distribuciones de probabilidad.
  • Sklearn.mixture.GaussianMixture procede de la biblioteca de aprendizaje automático scikit-learn y nos permite ajustar modelos de mezcla gaussiana. Esto resulta útil a la hora de agrupar los niveles de retroceso para detectar dónde pueden concentrarse los niveles ocultos similares a los de Fibonacci.

Al ejecutar esta celda, cargamos de hecho todas las herramientas necesarias para nuestro trabajo.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from sklearn.mixture import GaussianMixture

Celda 2: cargar el archivo CSV y crear una columna booleana denominada «ValidSetup_bool»

En este punto del cuaderno, el script comienza cargando el conjunto de datos de retrocesos exportado desde MQL5. El archivo CSV se lee en un DataFrame de pandas y el programa muestra inmediatamente cuántas filas se han cargado y qué columnas están disponibles. Este es un punto de comprobación importante, ya que confirma que la ruta del archivo es correcta y que existen los campos esperados, como «RetracementPct», «Type» o «ValidSetup». Para ayudarnos a visualizar la estructura, el script también muestra las primeras filas, de modo que podamos comprobar rápidamente que los datos se ajustan a lo que esperamos de nuestra exportación de MetaTrader.

Una vez que el conjunto de datos está en memoria, la siguiente tarea consiste en decidir qué filas son válidas para su posterior análisis. El problema es que no todos los cálculos de retroceso del script de MQL5 son fiables: algunos pueden estar incompletos o marcados como configuraciones no válidas. Dado que los conjuntos de datos pueden utilizar nombres de columna ligeramente diferentes para indicar la validez (como «ValidSetup», «Valid» o «validsetup»), el script busca entre varias variantes habituales. Si se encuentra alguno, estandariza los resultados en una nueva columna booleana denominada «ValidSetup_bool». Los valores como «True», «1» o «Sí» se consideran válidos. Si no se encuentra ninguna columna de validez, el script considera por defecto que todas las filas son válidas, lo que garantiza que sigamos disponiendo de datos con los que trabajar. Por último, se crea un conjunto de datos filtrado denominado «df_valid», que contiene únicamente filas válidas, y se muestran unas estadísticas rápidas para indicar cuántas filas se han cargado en total y cuántas han superado el filtro de validez.

# Robust Cell 2: load CSV and create a boolean ValidSetup_bool column
import pandas as pd
from IPython.display import display

csv_file = "CandleRangeData_NZDUSD_H4.csv"   # <- set your filename here
df = pd.read_csv(csv_file, sep=None, engine="python")
print("Loaded rows:", len(df))
print("Columns found:", list(df.columns))

# Show first 5 rows
display(df.head())

# Try to detect a 'valid setup' column (several common name variants)
candidates = ["ValidSetup", "validsetup", "Valid_Setup", "Valid", "valid", 
              "ValidSetup_bool", "Valid_Setup_bool"]
found = None
for c in candidates:
    if c in df.columns:
        found = c
        break

# Case-insensitive detection if exact not found
if found is None:
    lower_map = {col.lower(): col for col in df.columns}
    for c in candidates:
        if c.lower() in lower_map:
            found = lower_map[c.lower()]
            break

# Create boolean column 'ValidSetup_bool' using detected column or fallback
if found is not None:
    print("Using column for validity:", found)
    series = df[found].astype(str).str.strip().str.lower()
    df["ValidSetup_bool"] = series.isin(["true", "1", "yes", "y", "t"])
else:
    print("No ValidSetup-like column found. Creating ValidSetup_bool=True for all rows (no filtering).")
    df["ValidSetup_bool"] = True

# Quick stats
total = len(df)
valid_count = df["ValidSetup_bool"].sum()
print(f"Total rows = {total}, ValidSetup_bool True = {valid_count}")

# Create df_valid for downstream cells (the rest of notebook expects df_valid)
df_valid = df[df["ValidSetup_bool"] == True].copy()
print("df_valid rows:", len(df_valid))

# Display preview
display(df_valid.head())

Celda 3: calcular los valores de retroceso según el esquema del recopilador

Una vez preparado un conjunto de datos válido en la celda 2, el cuaderno se centra ahora en garantizar que los valores de retroceso estén disponibles de forma sistemática para su análisis. En el conjunto de datos exportado por MetaTrader, es posible que algunos de estos campos ya estén calculados, mientras que otros deben obtenerse a partir de los niveles de precios sin procesar. Este paso armoniza el proceso de cálculo de los retrocesos.

Lo primero que hay que hacer es asegurarse de que la columna «Range» sea numérica. Esta columna, exportada desde el colector MQL5, representa la altura total de la barra de referencia (la diferencia entre su extremo superior y su extremo inferior). Convertirlo a tipo «float» garantiza que las operaciones matemáticas posteriores se realicen según lo esperado.

A continuación, el script comprueba si la exportación de MetaTrader ya incluye una columna «RetracementPct». Si se indica, este valor se interpreta como un retroceso porcentual (por ejemplo, 50 significa el 50 %) y se convierte en una fracción normalizada entre 0 y 1 dividiéndolo entre 100. Este enfoque garantiza la coherencia en todos los cálculos y reutiliza los valores precalculados cuando están disponibles. Si no se encuentra dicha columna, el script recurre al cálculo manual del retroceso utilizando la fórmula:

Cálculo del retroceso

En este caso, «Ext» representa el nivel de retroceso extremo alcanzado antes del cierre de la barra, y «RefBot» es el mínimo de la barra de referencia. Si se divide la distancia entre Ext y RefBot por el alcance total, se obtiene el retroceso proporcional.

Dado que los retrocesos siempre deben situarse entre el 0 % y el 100 %, el script aplica una operación de recorte para forzar que todos los valores se sitúen en el intervalo [0,1]. Esto evita las anomalías provocadas por irregularidades en los datos o por sobrepasos computacionales.

Por último, como paso de verificación, el cuaderno imprime las primeras filas de las columnas clave —RefTop, RefBot, Ext, Range y Retracement— para confirmar que los valores de retroceso calculados o importados parecen razonables. Esta vista previa nos confirma que el proceso está generando medidas de retroceso normalizadas y coherentes, listas para su posterior análisis estadístico.

# Cell 3: Compute retracement values based on the collector's schema

# In our MT5 output:
# - RefTop = top of the reference bar
# - RefBot = bottom of the reference bar
# - Ext    = the extreme retracement reached before close
# - RetracementPct = retracement % already computed in MT5

# 1. Use the collector's "Range" directly
df_valid["Range"] = df_valid["Range"].astype(float)

# 2. Use the already provided retracement percentage (if available)
if "RetracementPct" in df_valid.columns:
    df_valid["Retracement"] = df_valid["RetracementPct"].astype(float) / 100.0
    print("Using MT5-calculated RetracementPct column.")
else:
    # fallback: compute from RefTop/RefBot and Ext
    df_valid["Retracement"] = (df_valid["Ext"].astype(float) - df_valid["RefBot"].astype(float)) / df_valid["Range"]
    print("No RetracementPct column found — computed from RefTop/RefBot/Ext.")

# 3. Clip between 0 and 1 (0%–100%)
df_valid["Retracement"] = df_valid["Retracement"].clip(0, 1)

# Quick check
print("Preview of retracement values:")
display(df_valid[["RefTop","RefBot","Ext","Range","Retracement"]].head())

Figura 3: Tabla de valores calculados.

Celda 4: Distribución visual de los valores de retroceso

Ahora que se han validado y estandarizado los valores de retroceso, esta celda se encarga de visualizar su distribución. Pero antes de trazar el gráfico, el script añade una medida de seguridad adicional: se asegura de que exista una columna «Retracement» válida, independientemente de las columnas que se hayan incluido en la exportación de MetaTrader.

La lógica comienza comprobando si ya existe un retroceso. Si no es así, intenta crearlo. En primer lugar, busca el «RetracementPct», el retroceso basado en porcentajes exportado por MetaTrader 5, y lo convierte a una escala normalizada de 0 a 1. Si esa columna no está disponible, el script recurre a un cálculo personalizado utilizando RefTop, RefBot, Ext y RefDir. La inclusión de RefDir (dirección de la barra de referencia) hace que este cálculo sea robusto, ya que los retrocesos deben interpretarse de forma diferente según se trate de barras de referencia alcistas o bajistas:

  • En el caso de una barra alcista, el retroceso se mide desde el RefTop hasta el extremo.
  • En el caso de una barra bajista, se mide desde el RefBot hasta el extremo.

Si no está disponible ninguna de las columnas obligatorias, el código muestra un mensaje de error claro en el que se indican los campos que faltan.

Una vez recopilados los valores de retroceso, se recortan para que se mantengan dentro del intervalo válido [0,1] y se eliminan las entradas NaN. Si, tras la limpieza, no quedan valores válidos, el script se detiene con un error para evitar generar gráficos que puedan inducir a error. De lo contrario, se muestra el número de observaciones válidas en aras de la transparencia.

 Si se dispone de la biblioteca Seaborn, el script utiliza sns.histplot para crear un histograma sobre el que se superpone una curva KDE suavizada, con el fin de facilitar la comprensión. Si no está instalado, se activa una alternativa utilizando únicamente Matplotlib: un histograma junto con una estimación de densidad de kernel calculada manualmente (mediante scipy.stats.gaussian_kde). Esto garantiza que el gráfico mantenga un aspecto cuidado incluso en entornos minimalistas.

El gráfico final está claramente etiquetado con títulos en los ejes y limitado al intervalo 0–1 en el eje x. Muestra la distribución general de los ratios de retroceso en todas las configuraciones válidas, lo que permite hacerse una idea inmediata de la frecuencia con la que se producen retrocesos superficiales, medios o profundos. Si se desea, la figura se puede guardar en un archivo PNG con el tamaño exacto necesario para fines de documentación o presentación.

# Plotting cell 4 (robust): ensures 'Retracement' exists and draws a 750px-wide plot
import numpy as np
import matplotlib.pyplot as plt

# --- Build 'Retracement' column if needed ---
if "Retracement" not in df_valid.columns:
    if "RetracementPct" in df_valid.columns:
        # MT5 already computed it as percent (0-100)
        df_valid["Retracement"] = pd.to_numeric(df_valid["RetracementPct"], errors="coerce") / 100.0
        print("Using existing RetracementPct -> created Retracement (0-1).")
    else:
        # try to compute from RefTop/RefBot/Ext with direction awareness
        required = {"RefTop","RefBot","Ext","RefDir"}
        if required.issubset(set(df_valid.columns)):
            def compute_r(row):
                try:
                    rng = float(row["RefTop"]) - float(row["RefBot"])
                    if rng == 0: 
                        return np.nan
                    if str(row["RefDir"]).strip().lower().startswith("b") :  # Bull
                        return (float(row["RefTop"]) - float(row["Ext"])) / rng
                    elif str(row["RefDir"]).strip().lower().startswith("be"): # Bear
                        return (float(row["Ext"]) - float(row["RefBot"])) / rng
                    else:
                        return np.nan
                except Exception:
                    return np.nan
            df_valid["Retracement"] = df_valid.apply(compute_r, axis=1).astype(float)
            print("Computed Retracement from RefTop/RefBot/Ext/RefDir.")
        else:
            raise KeyError("No 'Retracement' or 'RetracementPct' column, and required columns for computation are missing. "
                           "Found columns: " + ", ".join(df_valid.columns))

# Clip to 0..1
df_valid["Retracement"] = df_valid["Retracement"].clip(lower=0.0, upper=1.0)

# Drop NaNs
vals = df_valid["Retracement"].dropna().values
if len(vals) == 0:
    raise ValueError("No valid retracement values to plot after preprocessing.")

print(f"Plotting {len(vals)} retracement observations (0..1 scale).")

# --- Plot size: target ~750 px width ---
# Use figsize such that width_inches * dpi = 750. We'll choose dpi=100, width=7.5in.
fig_w, fig_h = 7.5, 4.0
fig, ax = plt.subplots(figsize=(fig_w, fig_h), dpi=100)

# Prefer seaborn if available for nice KDE overlay, otherwise fallback
try:
    import seaborn as sns
    sns.histplot(vals, bins=50, stat="density", kde=True, ax=ax)
except Exception:
    # fallback to matplotlib
    ax.hist(vals, bins=50, density=True, alpha=0.6)
    # manual KDE overlay
    from scipy.stats import gaussian_kde
    kde = gaussian_kde(vals)
    xgrid = np.linspace(0,1,500)
    ax.plot(xgrid, kde(xgrid), linewidth=2)

ax.set_title("Retracement Ratio Distribution")
ax.set_xlabel("Retracement (0 = 0%, 1 = 100%)")
ax.set_ylabel("Density")
ax.set_xlim(0,1)
plt.tight_layout()

# Optionally save a sized PNG (uncomment to save)
# plt.savefig("retracement_distribution_750px.png", dpi=100)

plt.show()

Figura 4: Distribución de los ratios de retroceso.

Celda 5: Estimación de densidad por kernel (KDE)

En este paso, vamos más allá de la visualización básica y llevamos a cabo un análisis estadístico más avanzado: la estimación de densidad por kernel (KDE), combinada con la detección de picos. Este enfoque ayuda a revelar los niveles de retroceso habituales —las zonas «ocultas» en las que el precio suele estancarse o revertir su tendencia— mediante el análisis de la forma de la distribución, en lugar de limitarse a los recuentos brutos de un histograma.

El script comienza asegurándose de que la columna «Retracement» esté disponible en formato normalizado (0–1). Si falta, lo reconstruye a partir del campo «RetracementPct» o, si es necesario, a partir de «RefTop», «RefBot», «Ext» y «RefDir», utilizando la misma lógica alcista/bajista vista anteriormente. Tras recortar los valores al intervalo válido [0,1] y eliminar los valores NaN, comprueba que haya al menos 10 puntos de retroceso válidos. Esta medida de seguridad evita que KDE genere estimaciones ruidosas o sin sentido cuando el conjunto de datos es demasiado pequeño.

A continuación, se calcula el KDE sobre una malla fina de 1001 puntos que abarca el intervalo de 0 a 1. Esta alta resolución permite que la curva de densidad refleje las sutilezas de la estructura de los datos, como los múltiples máximos locales. Para identificar estos máximos, el script normaliza la curva de densidad y aplica la función `scipy.signal.find_peaks`, configurada para ignorar las fluctuaciones mínimas al exigir un nivel mínimo de prominencia y un espaciado mínimo. Los índices máximos resultantes se corresponden con los niveles de retroceso en los que la función de densidad es más intensa a nivel local; en definitiva, los niveles de retroceso «preferidos» que se esconden en los datos.

Para facilitar la visualización, la curva de KDE se representa con un sombreado debajo y cada pico detectado se resalta con un punto rojo y se acompaña de su porcentaje de retroceso (por ejemplo, 38,20 %). A diferencia de la celda de trazado anterior, esta no impone un ancho fijo en píxeles, por lo que el tamaño de la figura se adapta con flexibilidad a los distintos entornos. Se incluyen etiquetas, rangos de ejes y una cuadrícula para que el gráfico resulte claro y fácil de interpretar.

Por último, el script muestra una lista de los niveles de retroceso detectados expresados en porcentajes, junto con sus valores de prominencia relativa, lo que ofrece un resumen tanto visual como numérico de dónde podrían encontrarse los niveles de retroceso ocultos más fuertes. Esta combinación de KDE y detección de picos transforma las observaciones brutas de retrocesos en información estadística útil.

# Cell 5: KDE + peak detection (robust, flexible sizing)
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from scipy.signal import find_peaks

# --- ensure Retracement column exists (0..1 scale) ---
if "Retracement" not in df_valid.columns:
    if "RetracementPct" in df_valid.columns:
        df_valid["Retracement"] = pd.to_numeric(df_valid["RetracementPct"], errors="coerce") / 100.0
    else:
        required = {"RefTop","RefBot","Ext","RefDir"}
        if required.issubset(set(df_valid.columns)):
            def compute_r(row):
                try:
                    rng = float(row["RefTop"]) - float(row["RefBot"])
                    if rng == 0:
                        return np.nan
                    rd = str(row["RefDir"]).strip().lower()
                    if rd.startswith("b"):  # Bull
                        return (float(row["RefTop"]) - float(row["Ext"])) / rng
                    elif rd.startswith("be") or rd.startswith("bear"):  # Bear
                        return (float(row["Ext"]) - float(row["RefBot"])) / rng
                    else:
                        return np.nan
                except Exception:
                    return np.nan
            df_valid["Retracement"] = df_valid.apply(compute_r, axis=1).astype(float)
        else:
            raise KeyError("Cannot build 'Retracement' — missing required columns. Found: " + ", ".join(df_valid.columns))

# Clip and drop NaNs
df_valid["Retracement"] = df_valid["Retracement"].clip(0,1)
vals = df_valid["Retracement"].dropna().values
n = len(vals)
if n < 10:
    raise ValueError(f"Too few retracement observations to compute KDE/peaks reliably (n={n}).")

# --- KDE on a fine grid ---
grid = np.linspace(0, 1, 1001)   # 0.001 (0.1%) resolution
kde = stats.gaussian_kde(vals)
dens = kde(grid)

# --- peak detection on normalized density ---
dens_norm = dens / dens.max()
peaks_idx, props = find_peaks(dens_norm, prominence=0.02, distance=8)  # tweak params as needed
peak_levels = grid[peaks_idx]
peak_heights = dens[peaks_idx]

# --- Plot (flexible sizing, no fixed pixel restriction) ---
fig, ax = plt.subplots(figsize=(8, 4))  # default flexible size
ax.plot(grid, dens, label="KDE", linewidth=2)
ax.fill_between(grid, dens, alpha=0.2)

# annotate peaks
for lvl, h in zip(peak_levels, peak_heights):
    ax.plot(lvl, h, "o", color="red")
    ax.text(lvl, h, f" {lvl*100:.2f}%", va="bottom", ha="left", fontsize=9)

ax.set_title("Kernel Density of Retracement Ratios")
ax.set_xlabel("Retracement (0 = 0%, 1 = 100%)")
ax.set_ylabel("Density")
ax.set_xlim(0,1)
ax.grid(True, linestyle="--", alpha=0.5)
plt.tight_layout()
plt.show()

# --- print candidate levels ---
print("Candidate Hidden Retracement Levels (%):")
print(np.round(peak_levels*100, 2))
print("Peak prominences (relative):", np.round(props["prominences"], 4) if "prominences" in props else "n/a")

KDE

Figura 5: Densidad por kernel de los ratios de retroceso.



Pruebas y resultados

Todas las celdas del cuaderno de Jupyter generaron resultados gráficos que podían interpretarse estadísticamente. Debajo de cada celda, hemos incluido tanto el código como el resultado correspondiente, lo que ilustra una de las principales ventajas de trabajar en Jupyter: la capacidad de combinar el cálculo y la visualización a la perfección.

Tras ejecutar la celda de salida final, diseñada para guardar los valores ocultos resueltos, observamos que solo se detectaron dos niveles candidatos para el intervalo de tiempo H4 en el par NZDUSD. Aunque esto nos proporcionó información útil sobre la estructura de los datos, la prueba de hipótesis no confirmó nuestras expectativas.

Detected peaks at (pct): [29.7 58.3]
Bootstrap 200/1000...
Bootstrap 400/1000...
Bootstrap 600/1000...
Bootstrap 800/1000...
Bootstrap 1000/1000...
Bootstrap done in 4.5s

Peak testing results (window ±0.40%):
Level 29.700%  mass=9.909890e-03  p=0.4960   significant=False
Level 58.300%  mass=9.729858e-03  p=0.4960   significant=False

Accepted (FDR<0.05) candidate levels (pct): []

El algoritmo detectó dos picos potenciales en la distribución de los datos, situados en el 29,7 % y el 58,3 %. Estos picos representan puntos en los que el algoritmo observó inicialmente concentraciones locales de datos, lo que sugiere la posible existencia de una estructura oculta o de patrones repetidos.

Para evaluar si estos picos eran estadísticamente significativos o si se trataba simplemente de fluctuaciones aleatorias, el modelo realizó una prueba de bootstrap con 1000 remuestreos. El proceso de bootstrap estimó la frecuencia con la que aparecerían picos similares en versiones de los datos barajadas aleatoriamente, lo que proporcionó una medida de la significación estadística.

Para ambos niveles descubiertos:

  • Nivel del 29,7 % → masa = 0,0099, p = 0,4960 → No significativo
  • Nivel del 58,3 % → masa = 0,0097, p = 0,4960 → No significativo

Los valores p (≈0,50) indican que estos picos se produjeron con la misma frecuencia en las muestras aleatorias de bootstrap que en los datos reales, lo que significa que no eran estadísticamente distinguibles del ruido. Dado que ningún pico superó el umbral de la tasa de descubrimientos falsos (FDR) de 0,05, el algoritmo concluyó que no había niveles ocultos significativos en el conjunto de datos para el intervalo de tiempo elegido (H4 en NZDUSD).

 Backtesting de estrategias con los niveles descubiertos

En MetaTrader 5, hice una prueba añadiendo dos niveles de retroceso personalizados al conjunto predeterminado de Fibonacci: el 29,7 % y el 58,3 %. Según nuestro análisis en Jupyter, ambos niveles arrojaron resultados de masa = 0,0099, p = 0,4960 y masa = 0,0097, p = 0,4960, respectivamente, que no fueron estadísticamente significativos. Sin embargo, al representarlos en los gráficos, parecía que la evolución del precio había respetado uno de esos niveles en el pasado. Esto sugiere que, aunque la prueba estadística no haya confirmado la significación, podría haber una relevancia práctica que merezca la pena explorar.

Actualmente, estos niveles se han añadido manualmente, pero en el futuro se podría trabajar en la integración programática de dichos valores en MetaTrader 5 para realizar pruebas automatizadas en múltiples pares y marcos temporales. Consulte la figura 6 que aparece a continuación para ver una ilustración.

Añadir niveles calculados a la herramienta de Fibonacci.

Figura 6: Nuevos niveles identificados.


Conclusión

Este proyecto surgió de la ambición y la curiosidad: desde las observaciones, basadas en gráficos, de la evolución de los precios dentro del marco de los retrocesos de Fibonacci, hasta el reconocimiento de irregularidades en la forma en que el precio interactúa con los niveles conocidos, e incluso el cuestionamiento de si podrían existir niveles ocultos entre los puntos de retroceso tradicionales. Para explorar estas ideas, combinamos MQL5, para la recopilación automática de datos, con Python dentro del entorno web interactivo Jupyter, que nos proporcionó potentes herramientas para el análisis y la visualización de datos, así como para la integración entre varios lenguajes.

Aunque logramos obtener resultados preliminares, surgieron algunos retos. Nuestro conjunto de datos era limitado, y la aplicación manual de los valores de retroceso calculados en los gráficos resultaba prometedora, pero no se ajustaba a nuestras hipótesis iniciales. Esto sugiere que tanto nuestro proceso de recopilación de datos como nuestros métodos de detección de oscilaciones y retrocesos podrían necesitar algunas mejoras. Ampliar el análisis para abarcar múltiples pares de divisas y marcos temporales, así como rediseñar los algoritmos de detección, probablemente mejoraría la precisión y la fiabilidad.

A pesar de estos contratiempos, los cimientos que hemos sentado son muy valiosos. Muestra cómo se pueden combinar MQL5 y Python para la investigación en trading cuantitativo, lo que supone un punto de partida práctico para los principiantes que deseen combinar la automatización de las plataformas de trading con la ciencia de datos. Aunque los resultados iniciales no confirmaron del todo nuestras expectativas, los gráficos siguen revelando posibilidades interesantes que merecen un análisis más detallado. Con pruebas más sólidas y métodos más perfeccionados, esta línea de investigación aún podría aportar nuevos conocimientos sobre las dinámicas ocultas de los retrocesos.

A diferencia de recurrir a conjeturas para añadir niveles de retroceso personalizados, este enfoque aprovecha la ciencia de datos junto con las herramientas de MQL5 disponibles para aportar eficiencia y estructura al proceso. A continuación se muestra una tabla con los recursos adjuntos. Te invitamos a que revises los materiales y experimentes para compartir tus ideas y así poder seguir debatiendo el tema. No se pierdan la próxima publicación.



Archivos adjuntos

Nombre del archivo Versión  Descripción
CandleRangeData.mq5
 1.0 Script de MQL5 que recopila datos sobre el rango de las velas y los retrocesos de los gráficos de MetaTrader 5 y los exporta a formato CSV para su análisis.
HiddenFiboLevels.ipynb
N/A Notebook de Jupyter que contiene código en Python para cargar el archivo CSV exportado, limpiar los datos, comprobar posibles niveles ocultos de retroceso de Fibonacci y visualizar los resultados.
CandleRangeData_NZDUSD_H4.csv
N/A Conjunto de datos de ejemplo generado por el script MQL5 para el par de divisas NZDUSD en el marco temporal H4, utilizado como archivo de entrada para el análisis en Python.

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

Archivos adjuntos |
CandleRangeData.mq5 (13.94 KB)
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.
Características del Wizard MQL5 que debe conocer (Parte 68): Uso de patrones de TRIX y Williams Percent Range con una red de núcleo coseno Características del Wizard MQL5 que debe conocer (Parte 68): Uso de patrones de TRIX y Williams Percent Range con una red de núcleo coseno
Retomamos nuestro último artículo, donde presentamos el par de indicadores TRIX y Williams Percent Range, y ahora analizamos cómo se podría ampliar este par de indicadores con aprendizaje automático. TRIX y Williams Percent Range forman un par complementario de tendencia y soporte/resistencia. Nuestro enfoque de aprendizaje automático utiliza una red neuronal convolucional que incorpora el núcleo coseno en su arquitectura a la hora de ajustar las previsiones de este par de indicadores. Como siempre, esto se hace en un archivo de clase de señal personalizado que funciona con el asistente de MQL5 (Wizard MQL5) para crear un asesor experto.
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.
Motor de decisión Multi-IA para MQL5 (Parte 3): Darle a las IA el contexto correcto — régimen de mercado y noticias Motor de decisión Multi-IA para MQL5 (Parte 3): Darle a las IA el contexto correcto — régimen de mercado y noticias
Tercera parte de la serie: le damos al motor multi-IA el contexto donde un modelo de lenguaje sí aporta. Leemos el régimen de mercado en el código (tendencia o rango con ADX, volatilidad con el ATR contra su promedio, dirección con la pendiente de una EMA) y definimos ventanas de noticias de alto impacto configurables, sin depender del calendario del broker. Ambos entran en un prompt más rico que le pide a cada IA razonar el contexto —no el próximo tick— y devolver una bandera de riesgo. Un gating de dos capas, por horario y por consenso de las IA, mantiene al motor fuera del mercado cuando el contexto pesa más que la señal.