English Русский 中文 Deutsch 日本語
preview
Guía de aprendizaje automático para MetaTrader 5 (Parte 1): Correcciones relacionadas con la fuga de datos y las marcas de tiempo

Guía de aprendizaje automático para MetaTrader 5 (Parte 1): Correcciones relacionadas con la fuga de datos y las marcas de tiempo

MetaTrader 5Sistemas comerciales |
39 7
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Introducción

Bienvenidos a la primera entrega de nuestra serie Guía de aprendizaje automático para MetaTrader 5. Este artículo aborda un problema fundamental, aunque a menudo pasado por alto, a la hora de crear modelos sólidos de aprendizaje automático para los mercados financieros utilizando datos de MetaTrader 5: la «trampa de las marcas de tiempo». Analizaremos cómo un manejo incorrecto de las marcas de tiempo puede provocar fugas de datos insidiosas, lo que pone en peligro la integridad de tus modelos y genera señales de trading poco fiables. Y lo que es más importante, ofreceremos soluciones concretas y buenas prácticas, basadas en estudios consolidados del sector, para garantizar que tus datos sean limpios, estén libres de sesgos y estén listos para un análisis cuantitativo avanzado.

Destinatarios y requisitos previos


Este artículo está dirigido específicamente a operadores cuantitativos, científicos de datos y desarrolladores que cuenten con conocimientos básicos de Python, Pandas y la API de MetaTrader 5. También resultará útil estar familiarizado con los conceptos básicos del aprendizaje automático. Nuestro objetivo es proporcionarte los conocimientos prácticos y las herramientas necesarias para crear conjuntos de datos de alta integridad con los que desarrollar modelos de aprendizaje automático fiables en el ámbito del trading algorítmico.

Hoja de ruta de la serie


Este artículo marca el inicio de una serie exhaustiva dedicada a la creación de un modelo completo de aprendizaje automático para MetaTrader 5. En esta parte, sentamos las bases esenciales garantizando la integridad de los datos. Los temas futuros profundizarán en etapas más avanzadas del proceso de aprendizaje automático, incluyendo:

  • Parte 2 - Ingeniería de características avanzadas y etiquetado: Técnicas para definir variables objetivo que capturen la verdadera dinámica del mercado.
  • Parte 3 - Entrenamiento y validación de modelos: Mejores prácticas para entrenar, validar y seleccionar modelos de aprendizaje automático adaptados a series temporales financieras.
  • Parte 4 - Backtesting riguroso y despliegue: Metodologías para evaluar el rendimiento de los modelos en entornos de negociación realistas y estrategias para desplegar modelos en entornos reales.


La trampa de la marca de tiempo en MetaTrader 5: comprensión y prevención

El data snooping o la fuga de datos pueden parecer sutiles, pero su impacto en los modelos de aprendizaje automático puede ser monumental y devastador. Imagina que estás estudiando para un examen y, sin darte cuenta, echas un vistazo a las respuestas antes de tiempo. Tu puntuación perfecta te da la sensación de haberla ganado, pero en realidad estás haciendo trampa. Esto es precisamente lo que ocurre cuando utilizamos las marcas de tiempo predeterminadas de MetaTrader 5 en el aprendizaje automático: la fuga de datos corrompe inesperadamente la integridad de tu modelo.

Cómo te engañan las marcas de tiempo de MetaTrader 5

EURUSD M5 - MetaTrader5

MetaTrader 5 etiqueta la barra de 5 minutos que comienza a las 18:55, es decir, la penúltima barra de arriba, como:
Time Open High Low Close

2 Apr 18:55

  1.08718

  1.08724

  1.08668

  1.08670

Al indicar la hora en el inicio, MetaTrader 5 da a entender que los datos de esta barra estaban disponibles a las 18:55:00, ¡nada menos que 5 minutos antes de que cerrara realmente! Si tu modelo utiliza esto durante el entrenamiento, es como darle a un estudiante las respuestas del examen 5 minutos antes de que empiece la prueba. Para contrarrestar esto, deberíamos evitar usar las barras de tiempo precompiladas de MetaTrader 5 y, en su lugar, usar datos de ticks para crear las barras que usamos en nuestros modelos.

Por qué importa la fuga de datos


La fuga de datos puede arruinar silenciosamente todo tu proyecto de aprendizaje automático. Esto ocurre cuando tu modelo aprende accidentalmente de información que no debería tener durante el entrenamiento, como si intentara vislumbrar el futuro. Como resultado, el modelo parece increíblemente preciso durante el entrenamiento, pero en realidad, simplemente se le han proporcionado respuestas que nunca obtendría en el mundo real.

En lugar de aprender patrones genuinos, el modelo comienza a memorizar ruido, convirtiéndose en un estudiante que memoriza las respuestas sin comprender el material. Esto conlleva un rendimiento deficiente a la hora de realizar predicciones reales sobre datos nuevos.

Lo que es peor, un modelo entrenado con datos filtrados puede parecer fiable, pero no dará los resultados esperados una vez implementado. Puede generar una falsa sensación de seguridad y llevar a tomar malas decisiones, algo especialmente peligroso en entornos de alto riesgo como el trading, donde incluso los pequeños errores pueden resultar costosos.

Corregir una fuga de datos después de que haya ocurrido es frustrante. Por lo general, implica volver atrás y rehacer gran parte del proceso, lo que supone una pérdida de tiempo, recursos computacionales y, a veces, incluso dinero. Por eso es tan importante detectar y prevenir las fugas de datos desde el principio.

Por qué importan las barras de ticks: una perspectiva cuantitativa


Los datos financieros suelen llegar a intervalos irregulares, y para poder utilizar el aprendizaje automático (ML) con ellos, debemos regularizarlos, ya que la mayoría de los algoritmos de ML esperan datos en formato tabular. Las filas de esas tablas se conocen comúnmente como "barras". Los gráficos que vemos en MetaTrader 5, y prácticamente en todas las demás plataformas de gráficos, representan barras de tiempo, que convierten los datos de ticks en columnas de Apertura, Máximo, Mínimo, Cierre y Volumen mediante el muestreo de ticks durante un horizonte temporal fijo, como por ejemplo una vez cada minuto.

Si bien las barras de tiempo son quizás las más populares entre profesionales y académicos, deben evitarse por dos razones. En primer lugar, los mercados no procesan la información a intervalos de tiempo constantes. La hora posterior a la apertura es mucho más activa que la hora cercana al mediodía (o la hora cercana a la medianoche en el caso de los futuros). Como seres biológicos, es lógico que los humanos organicemos nuestro día según el ciclo de la luz solar.

Pero los mercados actuales funcionan mediante algoritmos que operan con una supervisión humana mínima, para los cuales los ciclos de procesamiento de la CPU son mucho más relevantes que los intervalos cronológicos. Esto significa que las barras de tiempo recogen un exceso de información durante los periodos de baja actividad y una cantidad insuficiente de información durante los periodos de alta actividad. En segundo lugar, las series muestreadas en el tiempo suelen presentar propiedades estadísticas deficientes, como autocorrelación, heterocedasticidad y falta de normalidad en los rendimientos.

(López de Prado, 2018, p.26)

Al considerar la construcción de barras de mercado para el aprendizaje automático, suele surgir un punto crucial de debate en relación con la elección entre las barras tradicionales basadas en el tiempo y las barras basadas en la actividad (por ejemplo, barras de ticks, de volumen o de dólares). Aunque los profesionales suelen ser muy meticulosos a la hora de evitar el sesgo de anticipación utilizando únicamente la información disponible antes de tomar una decisión, sigue pudiendo producirse una forma sutil de fuga de datos con las barras temporales. Analicemos por qué, incluso con un registro de fecha y hora minucioso, esto puede suponer un problema, y cómo las barras basadas en la actividad ofrecen una solución sólida:
  • Comprender la intención del profesional: Los profesionales con experiencia marcan correctamente la hora en sus barras de tiempo al final del intervalo (por ejemplo, 09:01:00 para el periodo comprendido entre las 09:00:00 y las 09:00:59.999). Este paso fundamental garantiza que toda la información relativa a una barra completada se conozca realmente en el momento en que se registró, evitando así el clásico sesgo de anticipación derivado de las barras futuras.
  • La sutil fuga dentro de la barra: Sin embargo, puede producirse una forma más sutil de fuga de datos dentro de la propia barra temporal. Si se produce un evento significativo a mitad de una barra de 1 minuto (por ejemplo, a las 09:00:35), cualquier dato derivado de esa barra (como su precio máximo o un indicador del evento) incorporará inevitablemente esta información al final de la barra.
  • El dilema de la predicción: Por consiguiente, si un modelo de aprendizaje automático realizara una predicción o generara una señal en el momento inicial en que comenzó la barra (por ejemplo, 09:00:00), utilizando estas características que reflejan acontecimientos posteriores dentro de ese mismo minuto, obtendría implícitamente una ventaja desleal. En el mercado en tiempo real, a las 09:00:00, lo que sucederá a las 09:00:35 es realmente una incógnita.
  • Las barras basadas en la actividad como solución: Las barras basadas en la actividad, como las barras de ticks, eluden este problema de forma radical, ya que solo se completan tras alcanzarse un volumen predeterminado de actividad en el mercado (por ejemplo, un número concreto de operaciones o un valor específico de volumen o de dólares negociados). Esta estructura inherente garantiza que todas las características de dicha barra se construyan a partir de información que estaba totalmente disponible en el momento exacto en que concluyó la formación de la barra, lo que se alinea de forma natural con el flujo de información en tiempo real y evita el sesgo de anticipación dentro de la barra.

Por las razones expuestas anteriormente, se debe evitar el uso de barras de tiempo al entrenar modelos de aprendizaje automático. En su lugar, deberíamos utilizar barras cuya formación dependa de la actividad del mercado, como las barras de ticks, de volumen o en dólares. Se generan al recopilar información una vez que se ha alcanzado un determinado número de ticks, se ha negociado un volumen concreto o se ha intercambiado un importe determinado en dólares. Estas barras generan rendimientos más próximos a una distribución normal i.i.d. (independiente e idénticamente distribuida), lo que las hace más adecuadas para los modelos de aprendizaje automático, muchos de los cuales asumen que las observaciones se extraen de un proceso gaussiano I.I.D.

A continuación se muestran comparaciones de las distribuciones de los rendimientos logarítmicos para las barras de tiempo y de ticks de M5, M15 y M30. El tamaño de las barras de ticks se calcula utilizando la mediana del número de ticks en el intervalo de tiempo correspondiente al periodo de muestra; así, para el EURUSD entre 2023 y 2024, obtenemos barras de 200, 500 y 1000 ticks para los intervalos de tiempo M5, M15 y M30, respectivamente. Esto se realiza utilizando la función calculate_ticks_per_period, que se muestra en la siguiente sección. 

Comparación de la distribución de rendimientos para barras de tiempo y barras de ticks

Si bien ninguna de las distribuciones de los rendimientos logarítmicos es normal, lo cual es de esperar, las creadas por las barras de ticks son más normales que las creadas por las barras de tiempo en todos los marcos temporales.

Analicemos con mayor detalle las propiedades estadísticas de las barras de tiempo y de ticks utilizando los gráficos que aparecen a continuación.

 Análisis de volatilidad del EURUSD en M5 (2023-2024)

 Análisis de volatilidad del EURUSD en ticks de 200 (2023-2024)

Al examinar los gráficos anteriores, podemos ver que aproximadamente el 20% de las barras de tiempo explican aproximadamente el 51% del cambio total del precio, mientras que el 20% de las barras de ticks explican aproximadamente el 46% del cambio total del precio. Cabe destacar que prácticamente todas las proporciones de barras de ticks explican menos del cambio total de precio que la misma proporción de barras de tiempo, lo que indica que las barras de ticks son mejores para muestrear información que las barras de tiempo. Un vistazo al histograma corrobora esto, ya que nos muestra que el cambio absoluto del precio de las barras de ticks sigue una distribución estadísticamente mucho más satisfactoria (monótonamente decreciente) que la de las barras de tiempo, que presenta variaciones erráticas.

En este artículo, y en los siguientes, nos centraremos en la aplicación del aprendizaje automático a los instrumentos de divisas. Dado que estos instrumentos no se negocian en una bolsa centralizada, no se dispone de información sobre el volumen, por lo que limitaré el alcance de esta serie a las barras de tiempo y de ticks. El lector debe tener en cuenta que anteriormente solo he descrito formaciones de barras estándar. Para obtener más información sobre tipos avanzados de barras, recomiendo este artículo, ya que analiza en profundidad el trabajo de Marcos López de Prado publicado en Advances in Financial Machine Learning, del que se pueden consultar las notas del seminario en línea.


La solución: reescribiendo la realidad temporal mediante la creación de barras a partir de datos de ticks.

Implementación del código

Comencemos por obtener los datos de nuestro terminal y limpiarlos para evitar que se utilicen ticks erróneos al crear los datos de nuestras barras. Mostraremos cómo crear barras de tiempo y barras de ticks. Utilizaremos Python debido a la conveniencia de las manipulaciones basadas en el tiempo que ofrece pandas y a su facilidad de uso para el aprendizaje automático. 

Paso 0: Importaciones 


Estas son las importaciones que utilizaremos para los fragmentos de código en este artículo.

import numpy as np
import pandas as pd
import MetaTrader5 as mt5
import logging
from datetime import datetime as dt

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')



Paso 1: Extracción de datos

def get_ticks(symbol, start_date, end_date):
    """
    Downloads tick data from the MT5 terminal.

    Args:
        symbol (str): Financial instrument (e.g., currency pair or stock).
        start_date, end_date (str or datetime): Time range for data (YYYY-MM-DD).

    Returns:
        pd.DataFrame: Tick data with a datetime index.
    """
    if not mt5.initialize():
        logging.error("MT5 connection not established.")
        raise RuntimeError("MT5 connection error.")

    start_date = pd.Timestamp(start_date, tz='UTC') if isinstance(start_date, str) else (
        start_date if start_date.tzinfo is not None else pd.Timestamp(start_date, tz='UTC')
    )
    end_date = pd.Timestamp(end_date, tz='UTC') if isinstance(end_date, str) else (
        end_date if end_date.tzinfo is not None else pd.Timestamp(end_date, tz='UTC')
    )

    try:
        ticks = mt5.copy_ticks_range(symbol, start_date, end_date, mt5.COPY_TICKS_ALL)
        df = pd.DataFrame(ticks)
        df['time'] = pd.to_datetime(df['time_msc'], unit='ms')
        df.set_index('time', inplace=True)
        df.drop('time_msc', axis=1, inplace=True)
        df = df[df.columns[df.any()]]
        df.info()
    except Exception as e:
        logging.error(f"Error while downloading ticks: {e}")
        return None

    return df



Paso 2: Limpieza de datos

def clean_tick_data(df: pd.DataFrame,
                    n_digits: int,
                    timezone: str = 'UTC'
                    ) -> Optional[pd.DataFrame]:
    """
    Clean and validate Forex tick data with comprehensive quality checks.

    Args:
        df: DataFrame containing tick data with bid/ask prices and timestamp index
        n_digits: Number of decimal places in instrument price.
        timezone: Timezone to localize/convert timestamps to (default: UTC)

    Returns:
        Cleaned DataFrame or None if empty after cleaning
    """
    if df.empty:
        return None

    df = df.copy(deep=False)  # Work on a copy to avoid modifying the original DataFrame 
    n_initial = df.shape[0] # Store initial row count for reporting

    # 1. Ensure proper datetime index
    # Use errors='coerce' to turn unparseable dates into NaT and then drop them.
    if not isinstance(df.index, pd.DatetimeIndex):
        original_index_name = df.index.name
        df.index = pd.to_datetime(df.index, errors='coerce')
        nan_idx_count = df.index.isnull().sum()
        if nan_idx_count > 0:
            logging.info(f"Dropped {nan_idx_count:,} rows with unparseable timestamps.")
            df = df[~df.index.isnull()]
        if original_index_name:
            df.index.name = original_index_name
    
    if df.empty: # Check if empty after index cleaning
        logging.warning("Warning: DataFrame empty after initial index cleaning")
        return None

    # 2. Timezone handling
    if df.index.tz is None:
        df = df.tz_localize(timezone)
    elif str(df.index.tz) != timezone.upper():
        df = df.tz_convert(timezone)
    
    # 3. Price validity checks
    # Apply rounding and then filtering
    df['bid'] = df['bid'].round(n_digits)
    df['ask'] = df['ask'].round(n_digits)

    # Validate prices
    price_filter = (
        (df['bid'] > 0) &
        (df['ask'] > 0) &
        (df['ask'] > df['bid'])
    )
    
    n_before_price_filter = df.shape[0]
    df = df[price_filter]
    n_filtered_prices = n_before_price_filter - df.shape[0]
    if n_filtered_prices > 0:
        logging.info(f"Filtered {n_filtered_prices:,} ({n_filtered_prices / n_before_price_filter:.2%}) invalid prices.")

    if df.empty: # Check if empty after price cleaning
        logging.warning("Warning: DataFrame empty after price cleaning")
        return None
    
    # Dropping NA values
    initial_rows_before_na = df.shape[0]
    if df.isna().any().any(): # Use .any().any() to check if any NA exists in the whole DF
        na_counts = df.isna().sum()
        na_cols = na_counts[na_counts > 0]
        if not na_cols.empty:
            logging.info(f'Dropped NA values from columns: \n{na_cols}')
            df.dropna(inplace=True)

    n_dropped_na = initial_rows_before_na - df.shape[0]
    if n_dropped_na > 0:
        logging.info(f"Dropped {n_dropped_na:,} ({n_dropped_na / n_before_price_filter:.2%}) rows due to NA values.")

    if df.empty: # Check if empty after NA cleaning
        logging.warning("Warning: DataFrame empty after NA cleaning")
        return None
    
    # 4. Microsecond handling
    if not df.index.microsecond.any():
        logging.warning("Warning: No timestamps with microsecond precision found")
    
    # 5. Duplicate handling
    duplicate_mask = df.index.duplicated(keep='last')
    dup_count = duplicate_mask.sum()
    if dup_count > 0:
        logging.info(f"Removed {dup_count:,} ({dup_count / n_before_price_filter:.2%}) duplicate timestamps.")
        df = df[~duplicate_mask]

    if df.empty: # Check if empty after duplicate cleaning
        logging.warning("Warning: DataFrame empty after duplicate cleaning")
        return None

    # 6. Chronological order
    if not df.index.is_monotonic_increasing:
        logging.info("Sorting DataFrame by index to ensure chronological order.")
        df.sort_index(inplace=True)

    # 7. Final validation and reporting
    if df.empty:
        logging.warning("Warning: DataFrame empty after all cleaning steps.")
        return None
    
    n_final = df.shape[0]
    n_cleaned = n_initial - n_final
    percentage_cleaned = (n_cleaned / n_initial) if n_initial > 0 else 0
    logging.info(f"Cleaned {n_cleaned:,} of {n_initial:,} ({percentage_cleaned:.2%}) datapoints.")

    return df


Paso 3: Crear barras y convertir a «End-Time»

Primero, crearemos algunas funciones auxiliares.

set_resampling_freq
def set_resampling_freq(timeframe: str) -> str:
    """
    Converts an MT5 timeframe to a pandas resampling frequency.

    Args:
        timeframe (str): MT5 timeframe (e.g., 'M1', 'H1', 'D1', 'W1').

    Returns:
        str: Pandas frequency string.
    """
    timeframe = timeframe.upper()
    nums = [x for x in timeframe if x.isnumeric()]
    if not nums:
        raise ValueError("Timeframe must include numeric values (e.g., 'M1').")
    
    x = int(''.join(nums))
    if timeframe == 'W1':
        freq = 'W-FRI'
    elif timeframe == 'D1':
        freq = 'B'
    elif timeframe.startswith('H'):
        freq = f'{x}H'
    elif timeframe.startswith('M'):
        freq = f'{x}min'
    elif timeframe.startswith('S'):
        freq = f'{x}S'
    else:
        raise ValueError("Valid timeframes include W1, D1, Hx, Mx, Sx.")
    
    return freq

calculate_ticks_per_period
def calculate_ticks_per_period(df: pd.DataFrame, timeframe: str = "M1", method: str = 'median', verbose: bool = True) -> int:
    """
    Dynamically calculates the average number of ticks per given timeframe.

    Args:
        df (pd.DataFrame): Tick data.
        timeframe (str): MT5 timeframe.
        method (str): 'median' or 'mean' for the calculation.
        verbose (bool): Whether to print the result.

    Returns:
        int: Rounded average ticks per period.
    """
    freq = set_resampling_freq(timeframe)
    resampled = df.resample(freq).size()
    fn = getattr(np, method)
    num_ticks = fn(resampled.values)
    num_rounded = int(np.round(num_ticks))
    num_digits = len(str(num_rounded)) - 1
    rounded_ticks = int(round(num_rounded, -num_digits))
    rounded_ticks = max(1, rounded_ticks)
    
    if verbose:
        t0 = df.index[0].date()
        t1 = df.index[-1].date()
        logging.info(f"From {t0} to {t1}, {method} ticks per {timeframe}: {num_ticks:,} rounded to {rounded_ticks:,}")
    
    return rounded_ticks
flatten_column_names
def flatten_column_names(df):
    '''
    Joins tuples created by dataframe aggregation 
    with a list of functions into a unified name.
    '''
    return ["_".join(col).strip() for col in df.columns.values]

Ahora bien, las principales funciones utilizadas para crear barras.

make_bar_type_grouper
def make_bar_type_grouper(
        df: pd.DataFrame,
        bar_type: str = 'tick',
        bar_size: int = 100,
        timeframe: str = 'M1'
) -> tuple[pd.core.groupby.generic.DataFrameGroupBy, int]:
    """
    Create a grouped object for aggregating tick data into time/tick/dollar/volume bars.

    Args:
        df: DataFrame with tick data (index should be datetime for time bars).
        bar_type: Type of bar ('time', 'tick', 'dollar', 'volume').
        bar_size: Number of ticks/dollars/volume per bar (ignored for time bars).
        timeframe: Timeframe for resampling (e.g., 'H1', 'D1', 'W1').

    Returns:
        - GroupBy object for aggregation
        - Calculated bar_size (for tick/dollar/volume bars)
    """
    # Create working copy (shallow is sufficient)
    df = df.copy(deep=False)  # OPTIMIZATION: Shallow copy here only once
    
    # Ensure DatetimeIndex
    if not isinstance(df.index, pd.DatetimeIndex):
        try:
            df = df.set_index('time')
        except KeyError:
            raise TypeError("Could not set 'time' as index")

    # Sort if needed
    if not df.index.is_monotonic_increasing:
        df = df.sort_index()

    # Time bars
    if bar_type == 'time':
        freq = set_resampling_freq(timeframe)
        bar_group = (df.resample(freq, closed='left', label='right') # includes data upto, but not including, the end of the period
                    if not freq.startswith(('B', 'W')) 
                    else df.resample(freq))
        return bar_group, 0  # bar_size not used

    # Dynamic bar sizing
    if bar_size == 0:
        if bar_type == 'tick':
            bar_size = calculate_ticks_per_period(df, timeframe)
        else:
            raise NotImplementedError(f"{bar_type} bars require non-zero bar_size")

    # Non-time bars
    df['time'] = df.index  # Add without copying
    
    if bar_type == 'tick':
        bar_id = np.arange(len(df)) // bar_size
    elif bar_type in ('volume', 'dollar'):
        if 'volume' not in df.columns:
            raise KeyError(f"'volume' column required for {bar_type} bars")
        
        # Optimized cumulative sum
        cum_metric = (df['volume'] * df['bid'] if bar_type == 'dollar' 
                      else df['volume'])
        cumsum = cum_metric.cumsum()
        bar_id = (cumsum // bar_size).astype(int)
    else:
        raise NotImplementedError(f"{bar_type} bars not implemented")

    return df.groupby(bar_id), bar_size
make_bars
def make_bars(tick_df: pd.DataFrame,
              bar_type: str = 'tick',
              bar_size: int = 0,
              timeframe: str = 'M1',
              price: str = 'midprice',
              verbose=True):
    '''
    Create OHLC data by sampling ticks using timeframe or a threshold.

    Parameters
    ----------
    tick_df: pd.DataFrame
        tick data
    bar_type: str
        type of bars to create from ['tick', 'time', 'volume', 'dollar']
    bar_size: int 
        default 0. bar_size when bar_type != 'time'
    timeframe: str
        MT5 timeframe (e.g., 'M5', 'H1', 'D1', 'W1').
        Used for time bars, or for tick bars if bar_size = 0.
    price: str
        default midprice. If 'bid_ask', columns (bid_open, ..., bid_close), 
        (ask_open, ..., ask_close) are included.
    verbose: bool
        print information about the data

    Returns
    -------
    pd.DataFrame with columns [open, high, low, close, median_price, tick_volume, volume]
    '''    
    if 'midprice' not in tick_df:
        tick_df['midprice'] = (tick_df['bid'] + tick_df['ask']) / 2

    bar_group, bar_size_ = make_bar_type_grouper(tick_df, bar_type, bar_size, timeframe)
    ohlc_df = bar_group['midprice'].ohlc().astype('float64')
    ohlc_df['tick_volume'] = bar_group['bid'].count() if bar_type != 'tick' else bar_size_
    
    if price == 'bid_ask':
        # Aggregate OHLC data for every bar_size rows
        bid_ask_df = bar_group.agg({k: 'ohlc' for k in ('bid', 'ask')})
        # Flatten MultiIndex columns
        col_names = flatten_column_names(bid_ask_df)
        bid_ask_df.columns = col_names
        ohlc_df = ohlc_df.join(bid_ask_df)

    if 'volume' in tick_df:
        ohlc_df['volume'] = bar_group['volume'].sum()

    if bar_type == 'time':
        ohlc_df.ffill(inplace=True)
    else:
        end_time =  bar_group['time'].last()
        ohlc_df.index = end_time + pd.Timedelta(microseconds=1) # ensure end time is after event
	df.drop('time', axis=1, inplace=True) # Remove 'time' column


        # drop last bar due to insufficient ticks
        if len(tick_df) % bar_size_ > 0: 
            ohlc_df = ohlc_df.iloc[:-1]

    if verbose:
        if bar_type != 'time':
            tm = f'{bar_size_:,}'
            if bar_type == 'tick' and bar_size == 0:
                tm = f'{timeframe} - {bar_size_:,} ticks'
            timeframe = tm
        print(f'\nTick data - {tick_df.shape[0]:,} rows')
        print(f'{bar_type}_bar {timeframe}')
        ohlc_df.info()
    
    # Remove timezone info from DatetimeIndex
    try:
	ohlc_df = ohlc_df.tz_convert(None)
    except:
	pass
    
    return ohlc_df


Los gráficos de análisis de volatilidad que hemos utilizado anteriormente se crearon con el siguiente código:

import plotly.graph_objects as go
import numpy as np
import pandas as pd

def plot_volatility_analysis_of_bars(df, symbol, start, end, freq, thres=.01, bins=100):
    """
    Plot the volatility analysis of bars using Plotly.
    df: DataFrame containing the data with 'open' and 'close' columns.
    symbol: Symbol of the asset.    
    start: Start date of the data.
    end: End date of the data.
    freq: Frequency of the data.
    thres: Threshold for filtering large values, e.g., 1-.01 for 99th quantile.
    bins: Number of bins for the histogram.
    """
    abs_price_changes = (df['close'] / df['open'] - 1).mul(100).abs()
    thres = abs_price_changes.quantile(1 - thres)
    abs_price_changes = abs_price_changes[abs_price_changes < thres] # filter out large values for visualization

    # Calculate Histogram
    counts, bins = np.histogram(abs_price_changes, bins=bins)
    bins = bins[:-1] # remove the last bin edge

    # Calculate Proportions
    total_counts = len(abs_price_changes)
    proportion_candles_right = []
    proportion_price_change_right = []

    for i in range(len(bins)):
        candles_right = abs_price_changes[abs_price_changes >= bins[i]]
        count_right = len(candles_right)
        proportion_candles_right.append(count_right / total_counts)
        proportion_price_change_right.append(np.sum(candles_right) / np.sum(abs_price_changes))

    fig = go.Figure()

    # Histogram with Hover Template
    fig.add_trace(
        go.Bar(x=bins, y=counts, 
               name='Histogram absolute price change (%)',
               marker=dict(color='#1f77b4'), 
               hovertemplate='<b>Bin: %{x:.2f}</b><br>Frequency: %{y}',  # Custom hover text
               yaxis='y1',
               opacity=.65))

    ms = 3 # marker size
    lw = .5 # line width
    
    # Proportion of Candles at the Right with Hover Text
    fig.add_trace(
        go.Scatter(x=bins, y=proportion_candles_right, 
                   name='Proportion of candles at the right',
                   mode='lines+markers', 
                   marker=dict(color='red', size=ms), 
                   line=dict(width=lw),
                   hovertext=[f"Bin: {x:.2f}, Proportion: {y:.4f}" 
                              for x, y in zip(bins, proportion_candles_right)],  # Hover text list
                   hoverinfo='text',  # Show only the 'text' from hovertext
                   yaxis='y2'))
    

    # Proportion Price Change Produced by Candles at the Right with Hover Text
    fig.add_trace(
        go.Scatter(x=bins, y=proportion_price_change_right, 
                   name='Proportion price change produced by candles at the right',
                   mode='lines+markers', 
                   marker=dict(color='green', size=ms), 
                   line=dict(width=lw),
                   hovertext=[f"Bin: {x:.2f}, Proportion: {y:.4f}" 
                              for x, y in zip(bins, proportion_price_change_right)], # Hover text list
                   hoverinfo='text',  # Show only the 'text' from hovertext
                   yaxis='y2'))
    
    # Indices of proportion_price_change_right at 10% intervals
    search_idx = [.01, .05] + np.linspace(.1, 1., 10).tolist()
    price_idxs = np.searchsorted(sorted(proportion_candles_right), search_idx, side='right')
    for ix in price_idxs:  # Add annotations for every step-th data point as an example
        x = bins[-ix]
        y = proportion_candles_right[-ix]
        fig.add_annotation(
            x=x,
            y=y,
            text=f"{y:.4f}",  # Display the proportion value with 4 decimal points
            showarrow=True,
            arrowhead=1,
            ax=0,
            ay=-15,  # Offset for the annotation text
            font=dict(color="salmon"),
            arrowcolor="red",
            yref='y2'
        )

        y = proportion_price_change_right[-ix]
        fig.add_annotation(
            x=x,
            y=y,
            text=f"{y:.4f}",  # Display the proportion value with 4 decimal points
            showarrow=True,
            arrowhead=1,
            ax=0,
            ay=-25,  # Offset for the annotation text
            font=dict(color="lightgreen"),
            arrowcolor="green",
            yref='y2'
        )

    # Layout Configuration with Legend Inside
    fig.update_layout(
        title=f'Volatility Analysis of {symbol} {freq} from {start} to {end}',
        xaxis_title='Absolute price change (%)',
        yaxis_title='Frequency',
        yaxis2=dict(
            title='Proportion',
            overlaying='y',
            side='right',
            gridcolor='#444'  # Set grid color for the secondary y-axis
        ),
        plot_bgcolor='#222',  # Dark gray background
        paper_bgcolor='#222',
        font=dict(color='white'),
        xaxis=dict(gridcolor='#444'),  # Set grid color for the primary x-axis
        yaxis=dict(gridcolor='#444'),   # Set grid color for the primary y-axis
        legend=dict(
            x=0.3,  # Adjust x coordinate (0 to 1)
            y=0.95,  # Adjust y coordinate (0 to 1)
            traceorder="normal",  # Optional: maintain trace order
            font=dict(color="white")  # Optional: set legend text color
        ),
        # width=750,  # Set width of the figure
        # height=480,  # Set height of the figure
    )

    return fig

Ahora que hemos visto cómo crear datos estructurados a partir de nuestros datos de ticks en forma de barras de tiempo o de ticks, veamos si se cumplen algunas de las afirmaciones que hicimos sobre la superioridad de las propiedades estadísticas de las barras de ticks con respecto a las de las barras de tiempo. Utilizaremos los datos de ticks del EURUSD de 2023, que se adjuntan en los archivos siguientes.


EURUSD M5 frente a Tick-200 15-08-2023

Si observa detenidamente las áreas donde hay un alto volumen de ticks en el gráfico M5, como entre las 14:00 y las 16:00, notará que las barras formadas en el gráfico Tick-200 se superponen debido al mayor muestreo en este período de alta actividad. Por el contrario, las barras de ticks entre las 06:00 y las 08:00 son escasas y presentan grandes espacios entre ellas. Esto ilustra por qué las barras de ticks se denominan barras basadas en la actividad, a diferencia de las barras temporales, que recogen datos de forma uniforme a lo largo de un horizonte temporal fijo.


Escalabilidad y recomendaciones de hardware

Trabajar con datos de ticks de alta frecuencia y construir barras especializadas puede requerir una gran capacidad de cálculo, especialmente con grandes conjuntos de datos históricos. Para un rendimiento óptimo y un procesamiento eficiente, recomendamos la siguiente configuración informática:
  • RAM: Se recomienda un mínimo de 16 GB de RAM, aunque es preferible disponer de 32 GB o más para realizar backtesting exhaustivo o procesar datos de ticks de varios años.
  • CPU: Se recomienda encarecidamente una CPU multinúcleo (por ejemplo, Intel i7/i9 o AMD Ryzen 7/9). La capacidad de paralelizar las tareas de procesamiento de datos en varios núcleos reducirá considerablemente el tiempo de cálculo.
  • Estrategias de paralelización: Plantéate implementar técnicas de procesamiento paralelo en tu código Python. Bibliotecas como Dask para la computación distribuida o el módulo integrado multiprocessing de Python pueden resultar de gran ayuda para acelerar la preparación de datos, la ingeniería de características y las simulaciones de backtesting en conjuntos de datos de gran tamaño.



Próximos pasos

Para aplicar de forma eficaz los conceptos tratados en este artículo y prepararse para las siguientes partes de esta serie, recomendamos seguir los siguientes pasos prácticos:

  1. Aplicar la corrección de la marca de tiempo: Integra los fragmentos de código proporcionados en tu canalización de ingesta de datos para garantizar que todos los datos de MetaTrader 5 tengan una marca de tiempo correcta y estén libres de sesgos de anticipación.
  2. Experimenta con diferentes tipos de barras: Además de las barras de ticks, explora otros tipos de barras especializadas, como las barras de volumen o las barras en dólares. Observa cómo estos distintos métodos de muestreo influyen en las características de tu conjunto de datos y cuáles son sus posibles ventajas para tus estrategias de trading específicas.
  3. Prepara tu conjunto de datos: Ahora que ya dispones de datos limpios y sin sesgos, empieza a organizar y preparar tu conjunto de datos para las siguientes etapas del proceso de aprendizaje automático. En la segunda parte, profundizaremos en técnicas avanzadas de ingeniería de características y etiquetado.
En la próxima entrega de nuestra serie, nos adentraremos en uno de los pasos más importantes a la hora de crear potentes modelos de aprendizaje automático supervisado para el sector financiero: la creación de etiquetas. Aunque los métodos tradicionales con un horizonte temporal fijo predominan en gran parte de la bibliografía, a menudo no logran reflejar la verdadera dinámica de los mercados financieros.

Por eso, en el próximo artículo analizaremos dos de las innovadoras alternativas del Dr. Marcos López de Prado: el método de la triple barrera y el método de escaneo de tendencias. Estas técnicas no solo replantean el etiquetado, sino que lo redefinen.

Si alguna vez te has preguntado si tus etiquetas se ajustan realmente al comportamiento de los mercados, esta es la información que estabas esperando.



Conclusión

En este artículo introductorio, hemos analizado minuciosamente y hemos ofrecido soluciones para el grave problema conocido como «la trampa de la marca de tiempo de MetaTrader 5». Hemos demostrado cómo un manejo inadecuado de las marcas de tiempo puede provocar graves fugas de datos, lo que da lugar a modelos defectuosos y señales de trading poco fiables. Mediante la implementación de sólidos mecanismos de corrección de marcas de tiempo y el aprovechamiento de las ventajas de la construcción de barras de ticks, hemos sentado con éxito las bases para crear conjuntos de datos de alta integridad. Esta corrección es esencial para garantizar la validez de tu investigación, la precisión de tus backtests y, en última instancia, la fiabilidad de tus modelos de aprendizaje automático en el trading algorítmico. Este primer paso fundamental es imprescindible para cualquier profesional del análisis cuantitativo que se tome en serio su trabajo y aspire a desarrollar sistemas de trading verdaderamente eficaces y fiables.

En los documentos adjuntos encontrarás el código utilizado anteriormente, así como algunas funciones de utilidad para iniciar sesión en el terminal MetaTrader 5 mediante la API de Python.

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

Archivos adjuntos |
mt5_login.py (6.6 KB)
bars.py (13.44 KB)
Patrick Murimi Njoroge
Patrick Murimi Njoroge | 20 sept 2025 en 00:05
Stanislav Korotky #:

Las barras basadas en la actividad no resuelven todos los problemas que mencionas para las barras de tiempo. Por ejemplo, escribiste:

Si creas barras del mismo volumen, rango u otras barras personalizadas basadas en ticks, de todas formas estarás marcando dicha barra con una única etiqueta, y la información sobre el precio máximo se filtrará (o más exactamente, se difuminará) a lo largo de toda la barra.

La única forma de resolver este problema es crear "barras" teniendo en cuenta las características específicas (que vaya a utilizar). Por ejemplo, si las características principales son máximos o mínimos, debería intentar crear una "barra en zigzag" con extermos marcados exactamente en el tiempo.

El enfoque de marco temporal constante, y en particular la limitación a M1, es problemático en el contexto de la fuga de datos de MT5. Marcar barras M1 con la hora de fin no es, en mi opinión, mucho mejor que con la hora de inicio.


Para aquellos interesados en crear barras personalizadas (gráficos) de forma nativa en MT5, está el artículo con la implementación en MQL5 de las barras Equal Volume, Equal Range y Renko. Por supuesto, puedes marcar las barras con la hora de finalización en el código fuente abierto.

¿Qué quieres decir cuando afirmas "Si creas barras del mismo volumen, rango u otras barras personalizadas basadas en ticks, de todas formas estarás marcando dicha barra con una única etiqueta, y la información sobre el precio máximo se filtrará (o más exactamente, se difuminará) a lo largo de toda la barra"?

Stanislav Korotky
Stanislav Korotky | 20 sept 2025 en 16:00
Patrick Murimi Njoroge #:

¿Qué quieres decir cuando afirmas "Si creas barras del mismo volumen, rango u otras barras personalizadas basadas en ticks, estarás marcando dicha barra con una única etiqueta de todos modos, y la información sobre el precio máximo se filtrará (o más exactamente, se difuminará) a lo largo de toda la barra"?

No entiendo lo que no está claro. Mi frase era una respuesta directa a tu frase, citada en mi mensaje anterior, para que veas el contexto. No importa cómo se forman las barras, cada propiedad de la barra se atribuye por una sola marca de tiempo, y el "evento" real de la propiedad no coincide con ese tiempo.
Patrick Murimi Njoroge
Patrick Murimi Njoroge | 22 sept 2025 en 21:15
Stanislav Korotky # :
No entiendo lo que no está claro. mi frase es una respuesta directa a su frase que he citado en el post anterior. No importa cómo se forme la barra, todas las propiedades de la barra se atribuyen por una sola marca de tiempo, y los "eventos" reales de las propiedades no coinciden con ese tiempo.

Ahora entiendo el significado de desenfoque.

Patrick Murimi Njoroge
Patrick Murimi Njoroge | 7 mar 2026 en 23:06
Patrick Murimi Njoroge #:

Ahora entiendo el significado del desenfoque.

Mi arreglo tiene en cuenta que el usuario va a entrenar modelos con etiquetado de triple barrera. En mi opinión, la complejidad de evitar el mencionado desenfoque es mayor que la información que se obtiene en esta configuración. No obstante, voy a realizar algunos experimentos teniendo esto en cuenta. Si ya has realizado alguno, te agradecería que compartieras los resultados y la configuración del experimento.
Patrick Murimi Njoroge
Patrick Murimi Njoroge | 20 abr 2026 en 03:23
Patrick Murimi Njoroge #:

Ahora entiendo el significado del desenfoque.

Tiene razón en que el desenfoque intra-barra no se soluciona con barras basadas en la actividad. Cuando se cierra una barra en dólares, su característica alta refleja un precio que llegó en algún tick anterior dentro de esa barra. La marca de tiempo de la barra es correcta, pero el origen temporal de la característica se pierde. Ningún tipo de barra -tiempo, tick, volumen, dólar o desequilibrio- elimina esto, porque la agregación es el propio mecanismo de construcción de la barra. Una barra en zigzag lo resuelve específicamente para el máximo/mínimo, por construcción: el extremo está siempre al cierre. Eso es una ventaja genuina para las estrategias cuya característica principal es el extremo del precio.

En lo que me opondría es en la fusión de desenfoque con sesgo de anticipación. Son dos problemas distintos. El sesgo de anticipación significa que la etiqueta se construye utilizando información posterior a la marca de tiempo asociada a la observación: el modelo se entrena en el futuro. Las barras basadas en la actividad solucionan este problema: la marca de tiempo es el momento en el que el conjunto de información de la barra está completo, por lo que ninguna etiqueta ve más allá. El desenfoque dentro de la barra significa que una característica se agrega a lo largo del tiempo sin registrar en qué momento de la barra llegó cada componente. Esto reduce la resolución, pero no hace que el modelo vea el futuro. Tanto una barra M1 con una hora incorrecta como una barra dólar con una hora correcta desdibujan sus características, pero sólo una de ellas provoca un sesgo de previsión.

Sobre el etiquetado M1 de hora final frente a hora inicial: Yo no diría que la hora final es sólo marginalmente mejor. Se elimina la trampa específica descrita en la Parte 1 de esta serie, donde las marcas de tiempo por defecto de MetaTrader 5 implica la disponibilidad de información cinco minutos antes del cierre de la barra. Esa es una fuente concreta, medible de rendimiento backtest inflado, no una solución cosmética. Si se soluciona adicionalmente el desenfoque intra-barra es una cuestión aparte.

Su punto más amplio - que el artículo no distingue suficientemente lo que el muestreo basado en la actividad resuelve de lo que no lo hace - es justo. En una versión futura se debería explicar que la ingeniería de rasgos tick a tick, y no la elección de barras, es la respuesta teóricamente completa al desenfoque, a costa de una complejidad de datos y de modelización sustancialmente mayor. Añadiré esa aclaración a la conclusión. Actualmente estoy racionalizando el repositorio de esta serie, pero muy pronto editaré también estos errores.

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.
Redes neuronales en el trading: Segmentación periódica adaptativa (LightGTS) Redes neuronales en el trading: Segmentación periódica adaptativa (LightGTS)
Les invitamos a explorar la innovadora técnica de segmentación adaptativa, una forma de segmentar series temporales de forma flexible en función de su periodicidad inherente. Además, se usan técnicas de codificación eficientes que permiten preservar características semánticas importantes al trabajar con datos de diferentes escalas. Estos métodos descubren nuevas posibilidades para procesar con precisión datos complejos a múltiples escalas, típicos de los mercados financieros, y mejoran significativamente la estabilidad y la validez de las previsiones.
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.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 26): Herramienta de patrones pin bar y envolventes con divergencia del RSI (patrones múltiples) Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 26): Herramienta de patrones pin bar y envolventes con divergencia del RSI (patrones múltiples)
En línea con nuestro objetivo de desarrollar herramientas prácticas basadas en la acción del precio, este artículo analiza la creación de un Asesor Experto (EA) que detecta patrones de «pin bar» y «engulfing», utilizando la divergencia del RSI como señal de confirmación antes de generar cualquier señal de trading.