Pipelines en MQL5
Introducción: por qué es importante el preprocesamiento de datos
A la hora de diseñar y desarrollar sistemas y modelos de predicción basados en inteligencia artificial, resulta tentador centrarse únicamente en la arquitectura del modelo de aprendizaje profundo o en la complejidad de la estrategia de trading. Sin embargo, uno de los factores determinantes más importantes del rendimiento del modelo a menudo no reside en las propias redes neuronales, sino en la calidad y la coherencia de los datos que se introducen en el modelo. En la práctica, los datos financieros brutos —como las barras OHLC, los volúmenes de ticks o incluso los spreads— no suelen estar «listos» para su uso en un modelo, ni mucho menos. Estos valores «brutos» pueden presentarse a diferentes escalas, contener valores atípicos provocados por shocks repentinos del mercado o incluir características categóricas, como las sesiones de trading; y nada de esto puede ser procesado directamente por modelos matemáticos.
En términos prácticos, esto significa que, si, por ejemplo, estamos entrenando una red neuronal sencilla con rendimientos de precios normalizados e indicadores de volatilidad, entonces el escalador estándar probablemente sería la opción más adecuada, ya que garantiza la comparabilidad entre las distintas características. Por un lado, si, por ejemplo, el modelo es una LSTM de ONNX entrenada en Python con activaciones sigmoides, entonces el escalador min-max podría resultar más adecuado, ya que tiende a ajustarse al rango acotado de la activación. Por otro, si estuviéramos modelando comportamientos de trading durante un evento de noticias de gran impacto, en el que los spreads y los volúmenes pueden dispararse drásticamente, el escalador robusto podría utilizarse para ayudar a preservar la estructura central de los datos, minimizando al mismo tiempo la influencia de esta anomalía.
La conclusión principal es que el preprocesamiento no debe considerarse algo secundario. La elección del escalador influye directamente en la forma en que interactúan las características en las etapas de aprendizaje, y puede marcar la diferencia entre un modelo que generaliza bien y otro que presenta sesgos o es inestable.
Si no se lleva a cabo un preprocesamiento adecuado, incluso la red más sofisticada tendrá dificultades para aprender patrones de forma eficaz. Por ejemplo, una característica como el spread, cuando se mide en fracciones de un pip, puede quedar eclipsada por el volumen, cuya magnitud puede alcanzar los miles. Del mismo modo, asignar valores enteros a las sesiones de trading —por ejemplo, un 0 a Asia, un 1 a Europa y un 2 a EE. UU.— conlleva el riesgo de introducir una codificación categórica que puede inducir a error al modelo.
Por eso, el preprocesamiento puede considerarse la base de los flujos de trabajo robustos de aprendizaje automático. En el ecosistema de Python, la biblioteca SCIKIT-LEARN es conocida por sus herramientas de estandarización, normalización, escalado robusto y codificación «one-hot», todas ellas destinadas a preparar los datos para los modelos «posteriores». Cada paso de transformación hace que las características de entrada terminen contribuyendo de forma equitativa al proceso de aprendizaje. No obstante, en el ámbito del trading, el uso de las bibliotecas MQL5 tal cual pone de manifiesto esta carencia de preprocesamiento. Para poner solución a esta carencia, podemos implementar clases MQL5 que, en cierta medida, reproduzcan la funcionalidad de SCIKIT-LEARN y nos permitan obtener pipelines adaptados a la previsión de series temporales financieras.
Cerrando la brecha del ecosistema
La biblioteca SCIKIT-LEARN de Python se ha convertido, a todos los efectos, en el estándar de referencia del sector en lo que respecta al preprocesamiento en el aprendizaje automático. Con solo unas líneas de código, los desarrolladores pueden aplicar un escalador estándar para centrar las características; un escalador min-max para comprimir las características dentro de un rango fijo; un escalador robusto, que sirve para reducir el efecto desproporcionado de los valores atípicos; o un codificador one-hot, que ayuda a convertir las características en representaciones binarias. Es más, la clase pipeline de SCIKIT-LEARN permite encadenar estos pasos a la perfección, lo cual implica que todos los conjuntos de datos transmitidos al modelo se benefician de las mismas secuencias de transformación. Este mecanismo modular «plug-and-play» ha propiciado una rápida adopción del aprendizaje automático en una amplia variedad de sectores.
Los desarrolladores de MQL5, sin embargo, se enfrentan a una realidad muy diferente. Aunque MQL5 es relativamente eficaz a la hora de gestionar datos de trading, sigue sin ofrecer de forma nativa métodos de preprocesamiento comparables a los de SCIKIT-LEARN. En cada transformación —ya sea escalado, codificación o incluso imputación de valores ausentes—, el código debe escribirse manualmente y, con no poca frecuencia, de forma fragmentada. Este hecho no solo aumenta la probabilidad de que se produzcan errores, sino que también dificulta la reproducción de los resultados de las pruebas o el mantenimiento de la coherencia entre los datos de entrenamiento y los de prueba.
En mi opinión, la solución podría consistir en diseñar una clase de pipeline de preprocesamiento en MQL5 que emule esta filosofía de SCIKIT-LEARN. Si logramos implementar módulos reutilizables como CStandardScaler, CMinMaxScaler, CRobustScaler y COneHotEncoder, podríamos encadenar un pipeline de preprocesamiento en un contenedor. Dicha estructura garantizaría que los datos brutos de las series temporales financieras se sometieran a un proceso de preparación sistemático antes de introducirse en los modelos de aprendizaje profundo. Esto sería así tanto si los modelos están programados de forma nativa en MQL5 como si se importan a través de ONNX. Utilizando esta estructura, los desarrolladores de MQL5 pueden adoptar en MQL5 un flujo de trabajo de Python que les resulta familiar, lo que permite una experimentación más limpia, un desarrollo más rápido y, presumiblemente, sistemas de IA más fiables.
Anatomía de un pipeline de preprocesamiento
Al definir un pipeline de preprocesamiento, se puede pensar en él como una cinta transportadora de datos. Los datos brutos entran por un extremo y, al salir, se han transformado en un formato adecuado para que el modelo los procese. Cada etapa de la cinta transportadora realiza una tarea definida, como completar los valores faltantes, codificar categorías o reescalar las características numéricas. Por lo común, en Python, todo esto está encapsulado en el objeto pipeline de SCIKIT-LEARN. En MQL5, tenemos que diseñar una estructura similar utilizando clases personalizadas. Esta clase se llamará CPreprocessingPipeline. La implementaremos en MQL5 de la siguiente manera:
// Preprocessing Pipeline class CPreprocessingPipeline { private: SPreprocessorStep m_steps[]; IPreprocessor *m_preprocessors[]; int m_step_count; public: CPreprocessingPipeline() : m_step_count(0) {} ~CPreprocessingPipeline() { for(int i = 0; i < m_step_count; i++) delete m_preprocessors[i]; } void AddImputeMedian(int column) { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_IMPUTE_MEDIAN; m_steps[m_step_count].column = column; m_preprocessors[m_step_count] = new CImputeMedian(column); m_step_count++; } void AddImputeMode(int column) { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_IMPUTE_MODE; m_steps[m_step_count].column = column; m_preprocessors[m_step_count] = new CImputeMode(column); m_step_count++; } void AddStandardScaler() { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_STANDARD_SCALER; m_steps[m_step_count].column = -1; m_preprocessors[m_step_count] = new CStandardScaler(); m_step_count++; } void AddRobustScaler() { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_ROBUST_SCALER; m_steps[m_step_count].column = -1; m_preprocessors[m_step_count] = new CRobustScaler(); m_step_count++; } void AddMinMaxScaler(double new_min = 0.0, double new_max = 1.0) { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_MINMAX_SCALER; m_steps[m_step_count].column = -1; m_preprocessors[m_step_count] = new CMinMaxScaler(new_min, new_max); m_step_count++; } void AddOneHotEncoder(int column) { ArrayResize(m_steps, m_step_count + 1); ArrayResize(m_preprocessors, m_step_count + 1); m_steps[m_step_count].type = PREPROCESSOR_ONEHOT_ENCODER; m_steps[m_step_count].column = column; m_preprocessors[m_step_count] = new COneHotEncoder(column); m_step_count++; } bool FitPipeline(matrix &data) { matrix temp; temp.Copy(data); for(int i = 0; i < m_step_count; i++) { matrix out; if(!m_preprocessors[i].Fit(temp)) return false; if(!m_preprocessors[i].Transform(temp, out)) return false; temp.Copy(out); } return true; } bool TransformPipeline(matrix &data, matrix &out) { out.Copy(data); for(int i = 0; i < m_step_count; i++) { matrix temp; if(!m_preprocessors[i].Transform(out, temp)) return false; out.Copy(temp); } return true; } bool FitTransformPipeline(matrix &data, matrix &out) { if(!FitPipeline(data)) return false; return TransformPipeline(data, out); } };
Esta clase actúa, en la práctica, como un contenedor de los pasos de transformación. Los desarrolladores pueden añadir pasos utilizando métodos como AddStandardScaler(), AddRobustScaler(), AddMinMaxScaler() o AddOneHotEncoder(). Cada uno de estos pasos está representado por una clase independiente. Por ejemplo, tenemos la clase CStandardScaler. Estas clases implementan una interfaz coherente mediante el uso de funciones o métodos como Fit(), Transform() y FitTransform(). Una vez montado el pipeline, se «ajusta» a un conjunto de datos de entrenamiento, donde aprende parámetros como medias, medianas, modas o incluso correspondencias entre categorías. Una vez el ajuste se ha realizado con unos resultados satisfactorios en el entrenamiento y en las pruebas del modelo, se puede aplicar a un nuevo conjunto de datos con pasos de transformación similares. Esta coherencia nos permite evitar algunos errores habituales en las pruebas, como la fuga de datos.
La mencionada modularidad del diseño ofrece algunas ventajas. En primer lugar, fomenta la reutilización, ya que el mismo objeto de pipeline puede aplicarse a numerosos experimentos. En segundo lugar, mejora la facilidad de mantenimiento, ya que cada clase de transformación resulta menos dependiente y, por lo tanto, más fácil de depurar. Por último, tiende a favorecer la coherencia, en el sentido de que tanto el entrenamiento como la validación e incluso las pruebas siguen exactamente la misma ruta de transformación. En otras palabras, el pipeline de preprocesamiento aporta un grado de rigor a los flujos de trabajo de aprendizaje automático en MQL5 que, podría decirse, refleja la solidez del ecosistema de Python.
Escalador estándar
El escalador estándar es probablemente una de las herramientas de preprocesamiento más utilizadas en el aprendizaje automático. Su objetivo es centrar cada característica en torno a cero y escalarla según su desviación típica. En matemáticas, definimos esta transformación mediante la siguiente fórmula:

Donde:
- μ o mu es la media de la característica, y
- σ o sigma es su desviación típica.
- x es un punto de datos del conjunto de datos.
El resultado final es que cada característica tiene una media de cero y una desviación típica de uno. Esto tiende a hacer que el conjunto de datos sea más uniforme, reduciendo el riesgo de que determinadas características dominen el proceso de aprendizaje. Programaremos nuestra clase CStandardScaler en MQL5 de la siguiente manera:
// Standard Scaler class CStandardScaler : public IPreprocessor { private: double m_means[]; double m_stds[]; bool m_is_fitted; public: CStandardScaler() : m_is_fitted(false) {} bool Fit(matrix &data) { int rows = int(data.Rows()); int cols = int(data.Cols()); ArrayResize(m_means, cols); ArrayResize(m_stds, cols); for(int j = 0; j < cols; j++) { vector column(rows); for(int i = 0; i < rows; i++) column[i] = data[i][j]; m_means[j] = column.Mean(); m_stds[j] = column.Std(); if(m_stds[j] == 0.0) m_stds[j] = EPSILON; } m_is_fitted = true; return true; } bool Transform(matrix &data, matrix &out) { if(!m_is_fitted) return false; int rows = int(data.Rows()); int cols = int(data.Cols()); out.Init(rows, cols); for(int j = 0; j < cols; j++) for(int i = 0; i < rows; i++) out[i][j] = (!MathIsValidNumber(data[i][j]) ? DBL_MIN : (data[i][j] - m_means[j]) / m_stds[j]); return true; } bool FitTransform(matrix &data, matrix &out) { if(!Fit(data)) return false; return Transform(data, out); } };
En nuestra clase MQL5 anterior, implementamos el comportamiento del escalador estándar calculando las medias y las desviaciones típicas por columnas durante la fase Fit(). Estos valores se almacenan de forma interna para luego aplicarse en la fase Transform(), de modo que cada punto de datos se ajuste en consecuencia. Cuando una característica tiene una varianza igual a cero, se añade un pequeño valor distinto de cero, un épsilon, para evitar la división por cero. Esto garantiza la estabilidad numérica.
En las aplicaciones de trading, el escalador estándar resulta bastante provechoso a la hora de gestionar características de datos que existen en diferentes escalas o marcos temporales. Por ejemplo, pensemos en los spreads. Estos suelen registrarse como fracciones de pip, mientras que, por otro lado, características como el volumen de ticks se registran en miles. Sin ninguna «estandarización», el modelo podría prestar una atención desproporcionada a la variable de mayor escala, simplemente por su tamaño. Cuando «estandarizamos» ambos, el modelo evalúa las características en condiciones relativamente equivalentes.
Un caso de uso sencillo también puede consistir en preparar datos de entrada, como rendimientos logarítmicos, medias móviles y medidas de volatilidad, para una red neuronal. Al aplicar este escalador, las características se normalizan, lo que mejora la capacidad de los modelos —ya sean MLP o incluso SVM— para converger de forma eficiente durante el entrenamiento.
Escalador min-max
Mientras que el escalador estándar normaliza las características a una media de cero y una varianza unitaria, el escalador min-max reescala las características a un rango específico, que suele estar comprendido entre 0 y 1. La fórmula de esta transformación es la siguiente:

Dicha transformación garantiza que todos los valores se mantengan dentro de un rango deseado, lo que puede resultar muy útil cuando se utilizan modelos sensibles a las magnitudes de entrada. Por lo general, esto incluye redes neuronales con funciones de activación sigmoide o tanh. Nuestra clase CMinMaxScaler se implementa en MQL5 de la siguiente manera:
// MinMax Scaler class CMinMaxScaler : public IPreprocessor { private: double m_mins[]; double m_maxs[]; double m_new_min; double m_new_max; bool m_is_fitted; public: CMinMaxScaler(double new_min = 0.0, double new_max = 1.0) : m_new_min(new_min), m_new_max(new_max), m_is_fitted(false) {} bool Fit(matrix &data) { int rows = int(data.Rows()); int cols = int(data.Cols()); ArrayResize(m_mins, cols); ArrayResize(m_maxs, cols); for(int j = 0; j < cols; j++) { vector column(rows); for(int i = 0; i < rows; i++) column[i] = data[i][j]; m_mins[j] = column.Min(); m_maxs[j] = column.Max(); if(m_maxs[j] - m_mins[j] == 0.0) m_maxs[j] += EPSILON; } m_is_fitted = true; return true; } bool Transform(matrix &data, matrix &out) { if(!m_is_fitted) return false; int rows = int(data.Rows()); int cols = int(data.Cols()); out.Init(rows, cols); for(int j = 0; j < cols; j++) for(int i = 0; i < rows; i++) { if(!MathIsValidNumber(data[i][j])) out[i][j] = DBL_MIN; else { double scale = (m_new_max - m_new_min) / (m_maxs[j] - m_mins[j]); out[i][j] = (data[i][j] - m_mins[j]) * scale + m_new_min; } } return true; } bool FitTransform(matrix &data, matrix &out) { if(!Fit(data)) return false; return Transform(data, out); } };
Esta clase implementa este comportamiento determinando, en primer lugar, los valores mínimo y máximo de cada columna durante la fase Fit(). Dichos valores calculados se incorporan a continuación para reescalar los datos durante la fase Transform(). Los desarrolladores pueden especificar límites personalizados, como de -1 a +1 en lugar del típico [0,1], con el fin de ajustarse a las expectativas del modelo utilizado. Al igual que sucede con el escalador estándar, un épsilon garantiza que no se produzcan divisiones por cero en caso de que todos los valores sean idénticos.
Un ejemplo práctico de trading puede consistir en reescalar los precios de cierre antes de introducirlos en un modelo LSTM basado en ONNX. Como las redes neuronales suelen dar mejores resultados cuando las entradas se encuentran dentro de un rango reducido, la normalización min-max garantiza gradientes suaves y una convergencia más rápida. Del mismo modo, cuando se trabaja con indicadores de momentum u osciladores con valores absolutos elevados, el escalador min-max los sitúa en un rango coherente y predecible.
Sin embargo, podría decirse que la principal ventaja del escalador min-max radica en su sencillez y en su capacidad para conservar la forma de la distribución original. A diferencia de la estandarización, que modifica la varianza del conjunto de datos, el escalador min-max se limita a comprimir los valores en un intervalo fijo. No obstante, este método puede ser sensible a los valores atípicos, ya que un solo valor extremo puede distorsionar el escalado de todo el conjunto de datos de características de entrada. En el caso de conjuntos de datos estables o cuando se utiliza este escalador junto con la eliminación de valores atípicos extremos, puede ser la mejor opción a la hora de preparar características/datos de entrada para modelos de aprendizaje profundo.
Escalador robusto
Los mercados son conocidos por su imprevisibilidad y por ser propensos a generar valores atípicos. Las noticias de última hora suelen provocar que los spreads bid-ask se amplíen drásticamente o que el volumen se dispare muy por encima de las medias históricas. En casos como estos, los métodos de preprocesamiento, como el escalador estándar o el escalador min-max, pueden distorsionarse, dado que ambos se basan de forma desproporcionada en la media y en los valores extremos de los datos. Por ello, precisamente en estas situaciones el escalador robusto resulta de gran utilidad.
El escalador robusto centra los datos en torno a su mediana y, a continuación, los escala según su rango intercuartílico, también conocido como IQR. Este IQR se define como la diferencia entre el percentil 75 (Q3) y el percentil 25 (Q1). Por lo tanto, esta transformación se expresa de la siguiente manera:

Donde:
- El IQR es la diferencia entre el percentil 75 (Q3) y el percentil 25 (Q1);
- Median es la mediana del conjunto de datos y;
- x es un punto de datos del conjunto de datos.
Dado que este escalador ignora la influencia de los valores extremos, tiende a ser resistente a los valores atípicos. Con datos de trading, esto implica que, incluso si se produjera una perturbación excepcional en el mercado, la mayoría de las características seguirían teniendo un escalado adecuado para el modelado. Por lo tanto, nuestra implementación de esta clase en MQL5 es la siguiente:
// Robust Scaler class CRobustScaler : public IPreprocessor { private: double m_medians[]; double m_iqrs[]; bool m_is_fitted; public: CRobustScaler() : m_is_fitted(false) {} bool Fit(matrix &data) { int rows = int(data.Rows()); int cols = int(data.Cols()); ArrayResize(m_medians, cols); ArrayResize(m_iqrs, cols); for(int j = 0; j < cols; j++) { vector column(rows); for(int i = 0; i < rows; i++) column[i] = data[i][j]; m_medians[j] = column.Median(); double q25 = column.Quantile(0.25); double q75 = column.Quantile(0.75); m_iqrs[j] = q75 - q25; if(m_iqrs[j] == 0.0) m_iqrs[j] = EPSILON; } m_is_fitted = true; return true; } bool Transform(matrix &data, matrix &out) { if(!m_is_fitted) return false; int rows = int(data.Rows()); int cols = int(data.Cols()); out.Init(rows, cols); for(int j = 0; j < cols; j++) for(int i = 0; i < rows; i++) out[i][j] = (!MathIsValidNumber(data[i][j]) ? DBL_MIN : (data[i][j] - m_medians[j]) / m_iqrs[j]); return true; } bool FitTransform(matrix &data, matrix &out) { if(!Fit(data)) return false; return Transform(data, out); } };
Nuestra clase CRobustScaler de MQL5, mostrada más arriba, calcula las medianas y los cuartiles durante la fase Fit() y, a continuación, aplica el escalado en el paso Transform(). También se utiliza una salvaguarda épsilon para garantizar la estabilidad en caso de que el IQR sea cero. Nuestra implementación permite procesar conjuntos de datos que podrían inducir a error a los modelos debido a picos irregulares en el mercado.
A modo de ejemplo, imagine que entrena un modelo utilizando el volumen de ticks. En sesiones «normales», los volúmenes pueden mantenerse en un rango estable; sin embargo, cuando se difunde una noticia relevante, pueden multiplicarse fácilmente por diez. La estandarización o el escalado min-max ampliarían la distribución de las características y comprimirían los valores «normales» en una franja muy estrecha y casi insignificante. El escalador robusto, sin embargo, se centra en el 50 % central de los datos, lo que suele garantizar que la mayoría de los patrones de características de entrada de un modelo permanezcan intactos. Esto lo convierte, por tanto, en una herramienta idónea para el aprendizaje profundo, donde los modelos pueden encargarse de gestionar distribuciones volátiles y de colas pesadas, que suelen observarse en el mercado de divisas o en algunos mercados de criptomonedas.
Codificación one-hot
En el trading, no todas las características son numéricas. Algunas suelen ser de carácter discreto o categórico, ya que tienden a corresponderse con formas específicas del mercado. Por ejemplo, podríamos clasificar el tiempo en sesiones de trading de Asia, Europa y Estados Unidos. O quizá queramos agrupar los regímenes de mercado en alcistas, bajistas y laterales. Estos ejemplos no son exhaustivos; sin embargo, lo importante aquí es que los modelos de aprendizaje automático no pueden interpretar de forma nativa este tipo de valores categóricos y no numéricos. Y para subrayarlo aún más, el simple hecho de asignar números enteros a cada categoría —por ejemplo, Asia = 0, Europa = 1 y EE. UU. = 2— introduce una falsa sensación de orden y puede sesgar el modelo.
La solución a este problema suele ser la codificación one-hot. Aquí es donde cada categoría se transforma en un vector binario; por ejemplo, nuestras clasificaciones de sesiones de trading para Asia, Europa y EE. UU. se convierten en [1,0,0], [0,1,0] y [0,0,1], respectivamente. Esto suele permitir que los modelos distingan entre categorías sin tener que suponer necesariamente ninguna relación ordinal. Nuestra implementación de esto en MQL5 es la siguiente:
// One-Hot Encoder class COneHotEncoder : public IPreprocessor { private: int m_column; double m_categories[]; bool m_is_fitted; public: COneHotEncoder(int column) : m_column(column), m_is_fitted(false) {} bool Fit(matrix &data) { int rows = int(data.Rows()); vector values; int unique = 0; for(int i = 0; i < rows; i++) { if(!MathIsValidNumber(data[i][m_column])) continue; int idx = CVectorUtils::BinarySearch(values, data[i][m_column]); if(idx == -1) { values.Resize(unique + 1); values[unique] = data[i][m_column]; unique++; } } values.Swap(m_categories); //ArrayCopy(m_categories, values); m_is_fitted = true; return true; } bool Transform(matrix &data, matrix &out) { if(!m_is_fitted) return false; int rows = int(data.Rows()); int cols = int(data.Cols()); int cat_count = ArraySize(m_categories); if(data.Cols() == cols - 1 + cat_count) return false; out.Resize( rows, cols - 1 + cat_count); out.Fill(0.0); for(int i = 0; i < rows; i++) { int out_col = 0; for(int j = 0; j < cols; j++) { if(j == m_column) continue; out[i][out_col] = data[i][j]; out_col++; } for(int k = 0; k < cat_count; k++) if(data[i][m_column] == m_categories[k]) { out[i][out_col + k] = 1.0; break; } } m_is_fitted = true; return true; } bool FitTransform(matrix &data, matrix &out) { if(!Fit(data)) return false; return Transform(data, out); } };
La clase COneHotEncoder anterior implementa esta transformación. Durante la fase Fit(), se efectúa la identificación de categorías únicas en la columna de características seleccionada. Durante la fase Transform(), la columna categórica se sustituye por varias columnas binarias cuyo número corresponde al número de categorías. El resultado es una matriz de características ampliada en la que la información categórica se integra en un formato compatible con los modelos.
Para demostrar lo útil que puede resultar esto, podemos basarnos en los ejemplos anteriores sobre la codificación de las sesiones de trading. Si utilizamos números de sesión sin procesar —0 para Asia, 1 para Europa y 2 para EE. UU.—, una red neuronal podría interpretar erróneamente que la diferencia entre Asia y EE. UU. es mayor que la existente entre Asia y Europa en métricas que no están necesariamente relacionadas con el tiempo. Adoptando la codificación one-hot, cada sesión obtiene una representación independiente y el modelo tiene mayor libertad para aprender comportamientos distintos para cada categoría. Esto resulta fundamental para los modelos de trading, ya que las distintas sesiones, por ejemplo, pueden presentar características únicas en cuanto a liquidez, volatilidad y señales direccionales.
Integrándolo todo
Por separado, los escaladores y los codificadores son potentes; sin embargo, su verdadero potencial reside en combinarlos en un único flujo de trabajo. Este es el pipeline de preprocesamiento. Al combinar varias transformaciones, nos aseguramos de que cada conjunto de datos reciba exactamente el mismo tratamiento antes de llegar al modelo.
Imagine esta situación. Supongamos que estamos preparando un conjunto de datos para el cual hemos identificado cuatro características clave:
- Precio de cierre
- Volumen de ticks
- Spread
- Categoría de sesión de trading (categórico)
El primer paso sería, por lo general, tratar los valores que faltan. Esto es algo que algunos desarrolladores de MQL5 podrían pasar por alto, ya que los datos suelen tomarse tal cual del bróker; sin embargo, es recomendable no dar nunca por sentado que se dispone de todos los datos necesarios. Por ejemplo, si los datos de volumen de ticks están incompletos, podemos aplicar la función AddImputeMedian(1), donde 1 representa el índice de la característica de volumen de ticks dentro de las cuatro características utilizadas. De este modo, las entradas que faltan se sustituirían por la mediana de la columna. Del mismo modo, si al bucket de sesión de trading le faltan algunos datos, podemos aplicar la función AddImputeMode(3) para rellenarlos con la sesión de trading más habitual, a modo de solución intermedia. El desarrollador puede ajustar estas opciones en función de su sistema o modelo; lo que aquí se muestra es meramente a título ilustrativo.
Una vez abordados los datos que faltan, nuestro siguiente paso sería transformar los datos categóricos en un formato binario «imparcial». Para ello, aplicaríamos la función AddOneHotEncoder(3) para expandir la columna de sesión en vectores binarios, asegurándonos al mismo tiempo de que cada sesión quede representada de forma diferenciada.
El siguiente paso, o paso 3, podría consistir en aplicar un escalador. Teniendo en cuenta nuestro conjunto de datos, podemos elegir entre las funciones AddStandardScaler(), AddRobustScaler() o AddminMaxScaler(). En definitiva, este paso tiene por objeto garantizar que todas las características numéricas se ajusten para que sean comparables en escala. Una vez añadidos estos pasos al pipeline, tendríamos que llamar a la función FitPipeline() sobre el conjunto de datos de entrenamiento. Esta función aprendería todos los parámetros necesarios, como la media, la mediana, la moda y las correspondencias entre categorías. Posteriormente, podemos llamar a la función TransformPipeline() tanto en el conjunto de datos de entrenamiento como en el de prueba (o en la optimización y la prueba walk-forward), lo que garantizaría la coherencia sin que se produzca fuga de información futura en el proceso de entrenamiento.
Al final, obtenemos como resultado una matriz de características limpia, escalada y codificada, lista para su uso inmediato en un modelo de aprendizaje profundo, ya esté codificado a medida en MQL5 o importado en formato ONNX. Este pipeline hace que el preprocesamiento sea transparente, reproducible y modular, lo que permite a los desarrolladores centrarse más en las señales o la estrategia y menos en la preparación de datos. Para realizar una demostración de nuestras clases de pipeline destinadas a preparar datos para un modelo, puede utilizar el script que se adjunta al final del artículo. Una prueba realizada con el símbolo USD/JPY nos ofrece los siguientes registros:
2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) RAW (first 6 rows) [rows=2999, cols=4] 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.625000, 6894.000000, 20.000000, 2.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.837000, 14153.000000, 20.000000, 1.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.885000, 16794.000000, 20.000000, 1.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.489000, 8010.000000, 20.000000, 0.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.219000, 6710.000000, 20.000000, 0.000000 2025.09.12 17:05:50.150 Pipeline_Illustration (USDJPY,H4) 147.194000, 13686.000000, 20.000000, 3.000000 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) TRANSFORMED (FitTransform on all) [rows=2999, cols=32] 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.353976, 0.081606, 0.052632, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.363616, 0.167533, 0.052632, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.365798, 0.198795, 0.052632, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.347792, 0.094816, 0.052632, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.335516, 0.079428, 0.052632, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.163 Pipeline_Illustration (USDJPY,H4) 0.334379, 0.162005, 0.052632, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) TRAIN (after TransformPipeline) [rows=2249, cols=32] 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.353976, 0.081606, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.363616, 0.167533, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.365798, 0.198795, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.347792, 0.094816, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.335516, 0.079428, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.334379, 0.162005, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) TEST (after TransformPipeline) [rows=750, cols=32] 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.538217, 0.098806, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.536307, 0.280804, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.545628, 0.163082, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.540217, 0.121817, -0.028571, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.533852, 0.093858, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) 0.532215, 0.071675, 0.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, ... 2025.09.12 17:05:50.170 Pipeline_Illustration (USDJPY,H4) Preprocessing pipeline demo complete.
Comparación de escaladores en trading
La elección del escalador adecuado no suele ser una solución universal. Cada escalador presenta sus puntos fuertes y débiles, y la elección óptima suele depender del tipo de datos y del comportamiento del mercado objetivo. La comparación tabulada en paralelo que figura a continuación puede ayudar a aclarar en qué aspectos destaca cada escalador:
| Escalador | Puntos fuertes | Puntos débiles | Ideal para |
|---|---|---|---|
| Escalador estándar | Adecuado para datos de tipo gaussiano. Genera características con media nula y varianza unitaria. | Sensible a los valores atípicos. | Adecuado para manejar indicadores técnicos o soluciones basadas en la volatilidad. |
| Escalador min-max | Adecuado para activaciones acotadas. Genera datos dentro de un rango fijo, normalmente [0,1]. | Es muy sensible a los valores atípicos, ya que los valores extremos distorsionan el escalado. | Ideal para valores y características de precio, como entrada para una red sigmoide/tanh |
| Escalador robusto | El uso de medianas y del IQR hace más resistente a los valores atípicos. | Puede perder con frecuencia el escalado absoluto. | Adecuado para datos con colas pesadas, volúmenes de ticks y spreads durante situaciones de mercado volátiles. |
Lo que sugiere nuestro resumen anterior es que, si, por ejemplo, estamos entrenando una red neuronal sencilla con rendimientos de precios normalizados e indicadores de volatilidad, el escalador estándar probablemente sería la opción más adecuada, ya que garantiza la comparabilidad entre las distintas características. Por un lado, si, por ejemplo, el modelo es una LSTM de ONNX entrenada en Python con activaciones sigmoides, entonces el escalador min-max podría resultar más adecuado, ya que tiende a ajustarse al rango acotado de la activación. Por otro, si estuviéramos modelando comportamientos de trading durante un evento de noticias de gran impacto, en el que los spreads y los volúmenes pueden dispararse drásticamente, el escalador robusto podría utilizarse para ayudar a preservar la estructura central de los datos, minimizando al mismo tiempo la influencia de esta anomalía.
La conclusión principal es que el preprocesamiento no debe considerarse algo secundario. La elección del escalador influye directamente en la forma en que interactúan las características en las etapas de aprendizaje, y puede marcar la diferencia entre un modelo que generaliza bien y otro que presenta sesgos o es inestable.
Preparación de datos para el aprendizaje profundo
Lo que denominamos «modelos de aprendizaje profundo» o «redes neuronales», ya estén programados en MQL5 o importados a través de ONNX, suele tener requisitos muy estrictos en cuanto a sus datos de entrada. Por ejemplo, un modelo entrenado en Python con datos normalizados tendrá un rendimiento inferior o fallará por completo si se le presentan características sin procesar y sin escalar de datos de MQL5. Esto pone de relieve la importancia de los pipelines de preprocesamiento como algo que, en sentido estricto, no es opcional, sino esencial en lo que respecta al trading profesional y a los flujos de trabajo.
Vamos a considerar un modelo ONNX entrenado en TensorFlow con precios normalizados mediante min-max. Si posteriormente se despliega el mismo modelo en MQL5, pero con datos OHLC sin procesar, no hace falta decir que los pesos dejarán de corresponderse con la escala de esas entradas, lo que dará lugar a previsiones poco precisas. La aplicación del paso del pipeline CMinMaxScaler() con los mismos parámetros de mínimo y máximo que en el entrenamiento con Python garantizará la coherencia entre ambos entornos.
Más allá del escalado, la codificación categórica también cumple una función fundamental. Una red neuronal entrenada con codificación one-hot, como en el ejemplo de las sesiones de trading que hemos visto anteriormente, esperará el mismo formato binario en la fase de inferencia. Si la codificación es inconsistente —por ejemplo, si «Asia» se asignó a [0,1,0] durante el entrenamiento, pero ahora se encuentra en [1,0,0]—, las predicciones del modelo dejarán de tener sentido. Utilizando un pipeline que registra las categorías aprendidas, mitigamos esos riesgos.
La persistencia es otro aspecto clave. Parámetros como las medias, los mínimos y los máximos, así como las correspondencias entre categorías, deben almacenarse tras el entrenamiento y volver a aplicarse posteriormente durante la inferencia. Sin esta constancia, el reentrenamiento o la inferencia corren el riesgo de divergir. En Python, SCIKIT-LEARN ofrece funciones de serialización mediante módulos como joblib o pickle. En MQL5, los desarrolladores podrían conseguir un efecto similar guardando las variables de estado del pipeline desde su formato de arrays en archivos bin o csv y recargándolas durante la inicialización del Asesor Experto.
Retos y buenas prácticas
Aunque contamos con un alto nivel de rigor en los flujos de trabajo de MQL5 gracias a estos pipelines de preprocesamiento, esto conlleva algunos retos que es necesario abordar para garantizar la fiabilidad. De todos ellos, vamos a analizar cinco. El primero es el tratamiento de los valores NaN y cómo gestionar los datos que faltan. Los conjuntos de datos financieros suelen contener valores que faltan debido al cierre de los mercados, a datos de ticks incompletos o a feeds de datos irregulares de los brokers. En los pipelines de MQL5, a menudo se utilizan marcadores de posición como DBL_MIN para señalar estas entradas que faltan. Por lo tanto, resulta fundamental imputar estos valores de forma coherente utilizando alternativas como las medianas, si se trata de datos numéricos, o la moda, si los datos son discretos o categóricos. Esto evita que se introduzcan datos no válidos en el modelo.
El segundo reto que implica el uso de estos pipelines en MQL5 podría ser la prevención de la fuga de datos. Un error habitual es ajustar escaladores o codificadores a todo el conjunto de datos antes de dividirlo en conjuntos de entrenamiento, validación y prueba. Esta práctica puede provocar una fuga de información futura hacia el proceso de entrenamiento e inflar artificialmente las métricas de rendimiento. El probador de estrategias de MQL5 está diseñado, por naturaleza, para funcionar leyendo estrictamente información de precios actual o pasada; sin embargo, si se utilizan datos complementarios procedentes de otras fuentes para alimentar un modelo, deben incorporarse controles adicionales para garantizar que no se produzcan esas fugas. La práctica recomendada sería aplicar siempre Fit() al pipeline únicamente con los datos de entrenamiento y, a continuación, aplicar Transform() por separado tanto al conjunto de entrenamiento como al de prueba.
En tercer lugar, el escalado de tipos de datos mixtos puede resultar complicado. Cuando los conjuntos de datos contienen tanto características numéricas como categóricas, las transformaciones deben aplicarse con precaución. Los codificadores deben utilizarse antes que los escaladores, con el fin de garantizar que las columnas binarias recién creadas se integren correctamente en la nueva matriz numérica.
El cuarto punto a considerar, es que debemos prestar atención a la depuración de las transformaciones. Para comprobar que los resultados de la transformación son correctos, resulta útil mostrar en pantalla las primeras filas de los datos transformados. Las herramientas de depuración, como la función Print(), permiten comprobar rápidamente si la codificación y el escalado se comportan según lo esperado.
Por último, no siempre se puede dar por garantizada la reproducibilidad. Para garantizar la coherencia entre los distintos experimentos, es necesario almacenar los parámetros del pipeline, los valores mínimos, las medianas y las correspondencias de categorías junto con el modelo entrenado. Esto garantiza que se pueda aplicar exactamente el mismo preprocesamiento en el backtesting, en el trading en vivo o incluso en el reentrenamiento.
Seguir estas cinco recomendaciones puede ayudar a los desarrolladores a evitar errores habituales, al garantizar que los pipelines en MQL5 sigan siendo tan robustos como sus equivalentes en Python.
Conclusión
El preprocesamiento no es, desde luego, una parte glamurosa del aprendizaje automático, pero podría decirse que es uno de sus pilares fundamentales. Sin una preparación minuciosa, incluso el modelo de aprendizaje profundo más avanzado se verá en dificultades cuando se enfrente a datos de trading sin procesar, sin escalar o con una codificación incoherente. Para los desarrolladores expertos en MetaTrader 5, este reto ha supuesto tradicionalmente un obstáculo a la hora de aprovechar al máximo los flujos de trabajo de aprendizaje automático. A diferencia de Python, que cuenta con el sólido conjunto de herramientas de SCIKIT-LEARN, MQL5 no ofrece pipelines de preprocesamiento integrados. La solución, tal y como se ha demostrado, podría consistir en crear pipelines de preprocesamiento modulares y reutilizables dentro de MQL5.
De este modo, si estos pipelines no se tratan como utilidades opcionales, sino como componentes esenciales del flujo de trabajo de la IA, los desarrolladores alinearán sus sistemas de trading con el rigor de las prácticas de aprendizaje automático, de modo que la mayoría de los modelos —ya sean nativos o basados en ONNX— dispongan de la mejor base posible para alcanzar el éxito.
| nombre | descripción |
|---|---|
| PipeLine.mqh | Clase base para la funcionalidad del pipeline |
| Pipeline_Illustration.mq5 | Script que hace referencia a la clase base y muestra cómo utilizarla |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/19544
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.
Desarrollo de un indicador personalizado de matriz de rendimiento de cuenta
Sincronización de gráficos para facilitar el análisis técnico
Particularidades del trabajo con números del tipo double en MQL4
Automatización de estrategias de trading en MQL5 (Parte 32): Creación de un sistema para el patrón armónico 5 Drives basado en Price Action
- 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