Guía de aprendizaje automático para MetaTrader 5 (Parte 1): Correcciones relacionadas con la fuga de datos y las marcas de tiempo
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

| Time | Open | High | Low | Close |
|---|---|---|---|---|
|
|
|
|
|
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)
- 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.

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.


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.

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:
- 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.
- 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.
- 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.
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
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Utilizando redes neuronales en MetaTrader
Redes neuronales en el trading: Segmentación periódica adaptativa (LightGTS)
Particularidades del trabajo con números del tipo double 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)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
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"?
¿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 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.
Ahora entiendo el significado del desenfoque.
Ahora entiendo el significado del desenfoque.