Конвейеры обработки данных (пайплайны) в MQL5
Введение: почему важна предварительная обработка данных
При проектировании и построении систем и моделей прогнозирования на основе искусственного интеллекта возникает соблазн сосредоточиться только на архитектуре модели глубокого обучения или на сложности торговой стратегии. Однако одним из важнейших факторов, определяющих производительность модели, часто являются не сами нейронные сети, а также качество и согласованность данных, поступающих в модель. На практике необработанные финансовые данные, такие как OHLC-бары, тиковые объемы или даже спреды, часто далеки от того, чтобы быть «готовыми для моделирования». Эти «сырые» значения могут существовать в разных масштабах, содержать вызванные внезапными рыночными потрясениями выбросы или включать в себя такие категориальные характеристики, как торговые сессии; математические модели не могут учитывать все это без предварительной обработки данных.
Как следует из приведенного выше обзора, если, например, мы обучаем простую нейронную сеть на нормализованных показателях доходности цен и волатильности, то подходящим выбором будет, вероятно, Standard-Scaler, поскольку этот алгоритм обеспечивает сопоставимость между различными признаками. В соответствии с другим подходом, если, например, модель представляет собой ONNX LSTM, обученную на Python с сигмоидными функциями активации, то алгоритм масштабирования min-max может оказаться более подходящим, поскольку он, как правило, соответствует ограниченному диапазону активации. С другой стороны, если мы моделировали торговое поведение во время важного новостного события, когда спреды и объемы могут резко возрасти, то надежный алгоритм масштабирования может помочь сохранить центральную структуру данных, минимизируя при этом влияние этой аномалии.
Главный вывод здесь заключается в том, что предварительную обработку не следует рассматривать как нечто второстепенное. Выбор параметра масштабирования непосредственно влияет на то, как признаки взаимодействуют на этапах обучения, и может иметь решающее значение в отношении разницы между моделью, которая хорошо обобщает данные, и моделью, которая является предвзятой или нестабильной.
В отсутствие надлежащей предварительной обработки даже самая сложная нейронная сеть будет испытывать трудности с эффективным изучением закономерностей. Например, такой параметр, как разброс, измеряемый в долях процентных пунктов, может затмеваться объемом, величина которого может исчисляться тысячами. Аналогичным образом, присвоение целых чисел торговым сессиям, таким как Азия (a 0), Европа (a 1) и США (a 2), сопряжено с риском внесения категориальных данных, которые могут ввести модель в заблуждение.
Именно поэтому предварительную обработку можно считать основой надежных рабочих процессов в машинном обучении. В экосистеме Python библиотека SCIKIT-LEARN известна своими инструментами для стандартизации, нормализации, надежного масштабирования, а также кодирования one-hot, которые служат для подготовки данных в моделях последующих этапов. Каждый этап преобразования означает, что входные признаки в конечном итоге вносят значительный вклад в процесс обучения. Однако в торговле работа с библиотеками MQL5 в их текущем виде демонстрирует отсутствие предварительной обработки. Для решения этой проблемы мы можем реализовать классы MQL5, которые в некоторой степени повторяют функциональность SCIKIT-LEARN, и в итоге получить конвейеры обработки данных, адаптированные для прогнозирования финансовых временных рядов.
Преодоление разрыва между экосистемами
Библиотека SCIKIT-LEARN в Python во всех отношениях общепринятым отраслевым стандартом в области предварительной обработки данных для машинного обучения. Написав минимальное количество кода, разработчики могут применять инструмент Standard-Scaler для центрирования признаков, инструмент масштабирования Min-Max-Scaler для сжатия признаков в пределах фиксированного диапазона, инструмент масштабирования Robust-Scaler, который служит для уменьшения чрезмерного скопления выбросов, или средство кодирования One-Hot-Encoder, которое помогает преобразовывать признаки в бинарные представления. Более того, класс конвейера у SCIKIT-LEARN позволяет беспрепятственно объединять эти этапы в единую цепочку, и это означает, что все наборы данных, передаваемые в модель, проходят одинаковую последовательность преобразований. Этот модульный механизм типа «подключи и работай» способствовал быстрому внедрению машинного обучения в самых разных отраслях.
В то же время разработчиков MQL5 обескураживает совершенно иная реальность. Хотя язык MQL5 относительно эффективен в обработке торговых данных, он по-прежнему не предоставляет встроенные методы предварительной обработки, сравнимые с SCIKIT-LEARN. Для каждого преобразования — будь то масштабирование, кодирование или даже заполнение пропущенных значений — кодирование приходится выполнять вручную и часто фрагментарно. Это не только повышает вероятность внесения ошибок, но и затрудняет воспроизведение результатов тестирования или поддержание согласованности между обучающими и тестовыми данными.
Решение, по моему мнению, может заключаться в проектировании класса конвейера предварительной обработки в MQL5, который воспроизводит концептуальный подход SCIKIT-LEARN. Если мы сможем реализовать такие многократно используемые модули, как CStandardScaler, CMinMaxScaler, CRobustScaler и COneHotEncoder, то сможем объединить конвейер предварительной обработки в контейнер. Такая структура обеспечит систематическую подготовку исходных данных финансовых временных рядов перед их использованием в моделях глубокого обучения. Это будет справедливо независимо от того, написаны модели изначально на языке MQL5 или импортированы через ONNX. Используя такой подход, разработчики MQL5 могут адаптировать привычный для Python рабочий процесс к MQL5, что открывает возможности для более эффективного экспериментирования, ускорения разработки и, предположительно, создания более надежных систем искусственного интеллекта.
Структура конвейера предварительной обработки
При определении пайплайна предварительной обработки данных его можно рассматривать как физический конвейер для перемещения данных. Исходные данные поступают на один конец, а к моменту выхода преобразуются в формат, подходящий для использования моделью. На каждом этапе конвейерной ленты выполняется определенная задача, например: заполнение пропущенных значений, кодирование категорий или масштабирование числовых характеристик. Обычно в Python все это инкапсулировано в объекте pipeline библиотеки SCIKIT-LEARN. В MQL5 нам необходимо разработать аналогичную структуру, используя пользовательские классы. Наш класс будет называться CPreprocessingPipeline. Реализуем это в MQL5 следующим образом:
// 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); } };
Этот класс фактически служит контейнером для этапов преобразования. Разработчики могут добавлять этапы, используя такие методы, как AddStandardScaler(), AddRobustScaler(), AddMinMaxScaler() или AddOneHotEncoder(). Каждый из этих этапов представлен отдельным классом. Возьмем, например, класс CStandardScaler. Эти классы далее реализуют согласованный интерфейс, используя такие функции или методы, как Fit(), Transform() и FitTransform(). После сборки конвейер обработки данных «подгоняется» под обучающий набор данных, где он изучает такие параметры, как средние значения, медианы, моды или даже сопоставление категорий. После выполнения этих действий и достижения удовлетворительной производительности при обучении и тестировании модели ее можно применить к новому набору данных с аналогичными этапами преобразования. Такая согласованность позволяет избежать некоторых таких распространенных ошибок в тестировании, как утечка данных.
Подобная модульность конструкции дает ряд преимуществ. Во-первых, это способствует повторному использованию объектов, поскольку один и тот же объект конвейера может применяться во многих экспериментах. Во-вторых, это повышает удобство сопровождения кода, поскольку каждый класс преобразования становится менее зависимым от других и, следовательно, лучше поддается отладке. Наконец, это способствует обеспечению согласованности в том смысле, что и обучение, и проверка, и даже тестирование проходят по одному и тому же пути преобразования. Иными словами, конвейер предварительной обработки вносит в рабочие процессы машинного обучения MQL5 определенный уровень дисциплины, который, по всей видимости, повторяет надежность экосистемы Python.
Инструмент Standard-Scaler
Стандартный алгоритм масштабирования — пожалуй, один из наиболее широко используемых инструментов предварительной обработки данных в машинном обучении. Его цель — центрировать каждое свойство относительно нуля и масштабировать его в соответствии со стандартным отклонением. В математике это преобразование определяется следующей формулой:

где:
- μ (или мю) — это среднее значение свойства,
- σ (или сигма) — это его стандартное отклонение.
- x — информационная точка в наборе данных.
В результате этого среднее значение каждой характеристики равно нулю, а стандартное отклонение равно единице. Это, как правило, делает набор данных более однородным, снижая риск доминирования отдельных признаков в процессе обучения. Наш класс CStandardScaler написан на языке MQL5 следующим образом:
// 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); } };
В приведенном выше классе MQL5 достигаем целей стандартного инструмента масштабирования, вычисляя средние значения и стандартные отклонения по столбцам на этапе Fit(). Эти значения сохраняются внутри системы, а затем применяются на этапе Transform(), чтобы каждая информационная точка была соответствующим образом скорректирована. Когда дисперсия признака становится нулевой, к нему добавляется небольшое ненулевое значение (эпсилон), чтобы избежать деления на ноль. Это обеспечивает численную стабильность.
В торговых приложениях алгоритм масштабирования Standard Scaler весьма полезен при обработке характеристик данных, существующих в разных масштабах или на разных таймфреймах. Рассмотрим, например, спреды. Обычно эти данные записываются в виде доли пипса, тогда как, с другой стороны, такие характеристики, как объем тиков, регистрируются в тысячах. Без какой-либо «стандартизации» модель может уделять непропорционально больше внимания переменной большего масштаба просто из-за ее размера. Когда мы «стандартизируем» оба подхода, модель оценивает характеристики на относительно равных условиях.
В качестве простого примера использования можно также привести подготовку таких входных данных для нейронной сети, как логарифмическая доходность, скользящие средние и показатели волатильности. Применение этого масштабирующего алгоритма приводит к нормализации признаков, что повышает потенциал моделей, будь то многослойные перцептроны (MLP) или даже методы опорных векторов (SVM), для эффективной сходимости в процессе обучения.
Алгоритм масштабирования Min Max Scaler
В то время как алгоритм масштабирования Standard Scaler нормализует признаки до нулевого среднего значения и единичной дисперсии, алгоритм масштабирования Min Max Scaler изменяет масштабы свойств до определенного диапазона, обычно от 0 до 1. Формула этого преобразования выглядит следующим образом:

Такое преобразование обеспечивает ограничение всех значений желаемым диапазоном, что может быть довольно полезно при использовании моделей, чувствительных к величине входных данных. Обычно сюда входят нейронные сети с сигмоидальной или тангенциальной функциями активации. Наш класс CMinMaxScaler реализован на языке MQL5 следующим образом:
// 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); } };
Этот класс реализует такое поведение, определяя сначала минимальное и максимальное значения каждого столбца на этапе Fit(). Затем определенные таким образом значения используются для изменения масштабов данных на этапе Transform(). Разработчики могут задавать пользовательские границы, например от -1 до +1 вместо типичного диапазона [0,1], чтобы соответствовать ожиданиям применяемой модели. Как и в случае со стандартным коэффициентом масштабирования, эпсилон гарантирует, что деление на ноль не произойдет даже в том случае, если все значения одинаковы.
Практический пример из реального трейдинга может включать в себя изменение масштабов цен закрытия перед их передачей в модель LSTM на основе ONNX. Учитывая, что нейронные сети, как правило, показывают лучшие результаты, когда входные данные ограничены узким диапазоном, нормализация по принципу минимума и максимума обеспечивает плавные градиенты и более быструю сходимость. Похожим образом, при работе с индикаторами импульса или осцилляторами с большими абсолютными значениями, коэффициент масштабирования Min-Max Scaler приводит их в стабильный и предсказуемый диапазон.
Однако, пожалуй, главное преимущество алгоритма масштабирования min-max заключается в его простоте и способности сохранять форму предшествующего распределения. В отличие от стандартизации, которая изменяет дисперсию набора данных, алгоритм масштабирования min-max просто сжимает значения в фиксированный интервал. Тем не менее, этот метод может быть чувствителен к выбросам, поскольку одно экстремальное значение может исказить масштабирование всего набора свойств входных данных. Для стабильных наборов данных или при использовании такого алгоритма масштабирования в сочетании с удалением выбросов он может быть лучшим вариантом при подготовке признаков или входных данных для моделей глубокого обучения.
Алгоритм масштабирования Robust Scaler
Рынки известны своей непредсказуемостью и способностью порождать аномальные явления. Внезапные новостные события часто приводят к резкому расширению спредов между ценами покупки и продажи или к значительному увеличению объема торгов, превышающему исторические средние значения. В подобных случаях такие методы предварительной обработки, как алгоритм масштабирования Standard Scaler или алгоритм масштабирования Min Max Scaler, могут искажаться, поскольку оба они в непропорциональной степени зависят от среднего и экстремальных значений данных. Именно в таких ситуациях алгоритм масштабирования Robust Scaler оказывается бесценным.
Алгоритм масштабирования Robust Scaler центрирует данные по медиане, а затем масштабирует их по межквартильному диапазону (interquartile range, IQR). Межквартильный диапазон (IQR) определяется как разница между 75-м процентилем (Q3) и 25-м процентилем (Q1). Поэтому такое преобразование выражается следующим образом:

где:
- IQR — это разница между 75-м процентилем (Q3) и 25-м процентилем (Q1);
- Median — это медиана набора данных;
- x — это информационная точка в наборе данных.
Поскольку этот алгоритм не учитывает влияние экстремальных значений, он обычно устойчив к выбросам. В контексте торговых данных это означает, что даже в случае редко встречающегося рыночного шока большинство свойств останутся в пределах значений, допустимым образом масштабированных для моделирования. Таким образом, наша реализация этого класса на языке MQL5 выглядит следующим образом:
// 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); } };
Наш описанный выше класс MQL5 CRobustScaler вычисляет медианы и квартили на этапе Fit(), а затем применяет масштабирование на этапе Transform(). Кроме того, для обеспечения стабильности в случае, если межквартильный размах (IQR) окажется равным нулю, используется механизм защиты на основе значения эпсилон.* Наша реализация позволяет обрабатывать наборы данных, которые могут вводить модели в заблуждение из-за нерегулярных скачков на рынке.
Для наглядности представьте, что вы обучаете модель при помощи данных об объеме тиков. В «обычных» сессиях объемы могут оставаться в стабильном диапазоне, однако, когда в новостные ленты попадает выпуск новостей, они могут легко увеличиться в 10 раз. Стандартизация или масштабирование по принципу минимума-максимума растянули бы распределение признаков и сжали «нормальные» значения до очень узкого, почти незначительного диапазона. Однако алгоритм Robust Scaler концентрируется на центральных 50 процентах данных, что, как правило, гарантирует сохранение в неизменном состоянии большинства паттернов входных признаков в модели. Таким образом, это делает его подходящим для глубокого обучения, где моделям можно поручать обработку волатильных распределений с тяжелыми хвостами, которые часто встречаются на валютном рынке или на некоторых рынках криптовалют.
Кодирование с одним активным состоянием
В торговле не все признаки являются числовыми. Некоторые из них часто носят дискретный или категориальный характер, поскольку, как правило, соответствуют конкретным формам рынка. Например, мы можем разделить время на торговые сессии Азии, Европы и США. Либо можем разделить рыночные режимы на бычий, медвежий и неактивный (flat). Это не исчерпывающие примеры, однако суть в том, что модели машинного обучения не могут интерпретировать такие категориальные, нечисловые значения. Чтобы еще больше подчеркнуть это, простое присвоение каждой категории целых чисел, например, Азия = 0, Европа = 1 и США = 2, создает ложное ощущение порядка и может исказить модель.
Решением этой проблемы часто становится кодирование с одним активным состояние (one-hot encoding). Здесь каждая категория преобразуется в бинарный вектор — например, классификация торговых сессий для Азии, Европы и США становится [1,0,0], [0,1,0] и [0,0,1] соответственно. Обычно это позволяет моделям различать категории, не обязательно предполагая наличие каких-либо порядковых отношений. Наша реализация этого принципа в MQL5 выглядит следующим образом:
// 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); } };
Класс CONeHotEncoder, описанный выше, реализует это преобразование. На этапе Fit() выполняется идентификация уникальных категорий в выбранном столбце признаков. На этапе Transform() столбец с категориальными данными заменяется несколькими бинарными столбцами, число которых соответствует количеству категорий. В результате получается расширенная матрица признаков, в которой категориальная информация встраивается в формат, удобный для модели.
Чтобы продемонстрировать, насколько это может быть полезно, можем остановиться на приведенных выше примерах кодирования торговых сессий. Если используем исходные номера сессий, то есть 0 для Азии, 1 для Европы и 2 для США, нейронная сеть может ошибочно интерпретировать разницу между Азией и США как большую, чем между Азией и Европой, по показателям, которые не обязательно связаны со временем. Благодаря использованию кодирования с одним активным состоянием каждая сессия получает независимое представление, и модель получает больше свободы для изучения различных типов поведения для каждой категории. Это имеет решающее значение для торговых моделей, учитывая, что разные торговые сессии, например, могут демонстрировать уникальные сигналы ликвидности, волатильности и направления движения цены.
Соберем все вместе
Отдельно масштабирующие и кодирующие инструменты обладают мощными возможностями, однако их настоящий потенциал раскрывается при совместном использовании в рамках единого рабочего процесса. Это конвейер предварительной обработки. Объединяя несколько преобразований, мы обеспечиваем прохождение каждым набором данных точной обработки, прежде чем попасть в модель.
Рассмотрим этот сценарий. Предположим, мы готовим набор данных, для которого определили 4 ключевых признака:
- Цена закрытия
- Тиковый объем
- Спред
- Сегмент торговой сессии (по категориям)
Первым шагом, как правило, является обработка пропущенных значений. Это аспект, которым некоторые разработчики MQL5 могут пренебречь, поскольку данные часто берутся у брокера в том виде, в каком они есть. Однако никогда не следует предполагать, что все необходимые данные присутствуют. Например, если данные об объеме тиков неполные, мы можем применить функцию AddImputeMedian(1), где 1 представляет собой индекс признака объема тиков среди тех 4 признаков, которые мы используем. Это позволит заменить пропущенные входные данные медианой столбца. Сходным образом, если в сегменте торговых сессий отсутствуют некоторые данные, мы можем использовать функцию AddImputeMode(3), чтобы в качестве компромисса заполнить наиболее распространенную торговую сессию. Эти варианты выбора могут быть настроены разработчиком в зависимости от его системы/модели; представленная здесь информация носит исключительно иллюстративный характер.
После устранения недостающих данных следующим шагом будет преобразование категориальных данных в «беспристрастный» бинарный формат. В завершение этого этапа применим функцию AddOneHotEncoder(3), чтобы развернуть столбец сессий в бинарные векторы, обеспечив при этом уникальное представление каждой сессии.
Следующим шагом, или шагом 3, может быть применение инструмента масштабирования. С нашим набором данных можно выбрать одну из функций: AddStandardScaler(), AddRobustScaler() или AddminMaxScaler(). В конечном итоге, этот этап предназначен для обеспечения настройки всех числовых характеристик таким образом, чтобы они были сопоставимы по масштабу. После добавления этих этапов в конвейер нам может потребоваться вызов функции FitPipeline() для обучающего набора данных. Эта функция позволит изучить все необходимые параметры, такие как среднее значение (Mean), медиана (Median), мода (Mode) и соответствие категорий табличным данным (Category Mappings). Позже мы сможем вызвать функцию TransformPipeline() как для обучающего, так и для тестового наборов данных (или для оптимизации и прямого обхода), что обеспечит согласованность без утечки будущей информации в процесс обучения.
В итоге, результатом нашей работы является чистая, масштабированная и закодированная матрица признаков, которая сразу же готова к использованию в модели глубокого обучения — независимо от того, закодирована она с помощью пользовательского MQL5 или импортирована из ONNX. Этот конвейер делает предварительную обработку прозрачной, воспроизводимой и модульной, что позволяет разработчикам уделять больше внимания сигналам или стратегии и меньше — первичной обработке данных. Продемонстрировать работу классов нашего конвейера предварительной подготовки данных для модели можно с помощью скрипта, прикрепленного в конце этой статьи. Тестовый запуск для символа USD JPY дает нам следующие результаты:
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.
Сравнение стратегий масштабирования в трейдинге
Выбор подходящего инструмента масштабирования не так уж часто становится решением на все случаи жизни. У каждого инструмента масштабирования данных есть свои сильные и слабые стороны, и оптимальный выбор обычно зависит от типа данных и поведения целевого рынка. Приведенная ниже таблица сравнения поможет прояснить, в чем каждый из этих инструментов превосходит другие:
| Инструмент масштабирования | Сильные стороны | Слабые стороны | Лучше всего подходит для |
|---|---|---|---|
| Инструмент Standard-Scaler | Подходит для данных с гауссовым распределением. Создает признаки с нулевым средним значением и единичной дисперсией. | Чувствителен к выбросам. | Умеет работать с техническими индикаторами или решениями, основанными на волатильности. |
| Алгоритм масштабирования Min Max Scaler | Подходит для ограниченных активаций. Выдает данные в фиксированном диапазоне, обычно [0,1]. | Очень чувствителен к выбросам, поскольку экстремальные значения искажают масштабирование. | По соотношению цены и качества, а также функциональности, идеально подходит для работы с сигмоидной/тангенциальной нейронной сетью. |
| Алгоритм масштабирования Robust Scaler | Использование медиан и межквартильного диапазона делает метод менее устойчивым к выбросам. | Существует вероятность частых нарушений абсолютной точности масштабирования. | Подходит для анализа данных с медленно убывающим хвостом, тиковых объемов и спредов в условиях нестабильности рынка. |
Как следует из приведенного выше обзора, если, например, мы обучаем простую нейронную сеть на нормализованных показателях доходности цен и волатильности, то подходящим выбором будет, вероятно, Standard-Scaler, поскольку этот алгоритм обеспечивает сопоставимость между различными признаками. В соответствии с другим подходом, если, например, модель представляет собой ONNX LSTM, обученную на Python с сигмоидными функциями активации, то алгоритм масштабирования min-max может оказаться более подходящим, поскольку он, как правило, соответствует ограниченному диапазону активации. С другой стороны, если мы моделировали торговое поведение во время важного новостного события, когда спреды и объемы могут резко возрасти, то надежный алгоритм масштабирования может помочь сохранить центральную структуру данных, минимизируя при этом влияние этой аномалии.
Главный вывод здесь заключается в том, что предварительную обработку не следует рассматривать как нечто второстепенное. Выбор параметра масштабирования непосредственно влияет на то, как признаки взаимодействуют на этапах обучения, и может иметь решающее значение в отношении разницы между моделью, которая хорошо обобщает данные, и моделью, которая является предвзятой или нестабильной.
Подготовка данных для глубокого обучения
То, что мы называем моделями глубокого обучения, или нейронные сети, независимо от того, написаны они на языке MQL5 или импортированы через ONNX, как правило, предъявляют строгие требования к своим входным данным. Например, модель, обученная на языке Python на нормализованных данных, будет работать хуже или полностью потерпит неудачу, если ей будут предоставлены необработанные и не масштабированные признаки данных MQL5. Это подчеркивает важность конвейеров предварительной обработки, которые, строго говоря, являются не возможной опцией, а необходимостью для серьезной торговли и рабочих процессов.
Рассмотрим модель ONNX, обученную в TensorFlow на нормализованных ценах с использованием алгоритма min-max. Если ту же модель впоследствии развернуть в MQL5, но с необработанными данными OHLC, само собой разумеется, что веса окажутся не согласованными с масштабированием этих входных данных, а это приведет к неточным прогнозам. Применение шага конвейера CMinMaxScaler() с идентичными настройками min/max, как и при обучении на Python, гарантирует согласованность между двумя средами.
Помимо масштабирования, важную функцию выполняет категориальное кодирование. Нейронная сеть, обученная с использованием кодирования с одним активным состоянием, как в примере с торговыми сессиями, который мы рассматривали выше, будет ожидать при выводе того же бинарного формата. Если при кодировании не сохранится последовательность, например, когда Азия в процессе обучения отображалась в [0,1,0], а теперь находится в [1,0,0], то прогнозы модели станут бессмысленными. Используя конвейер обработки данных, который регистрирует полученные категории, мы снижаем подобные риски.
Длительность хранения данных — еще один ключевой аспект. Параметры, такие как средние значения, минимумы, максимумы, а также соответствия категорий табличным данным, необходимо сохранять после обучения, а затем повторно применять во время формирования выводов. Без такой стабилизации переобучение или вывод знаний рискуют всё больше и больше отдалиться. В Python библиотека SCIKIT-LEARN обеспечивает сериализацию с помощью таких модулей, как joblib или pickle. В MQL5 разработчики могли добиться аналогичного эффекта, сохраняя переменные состояния конвейера из формата массивов в файлы типа bin или csv и загружая их повторно во время инициализации советника.
Проблемы и передовой опыт
Несмотря на то, что благодаря этим конвейерам предварительной обработки мы обеспечиваем высокую точность рабочих процессов MQL5, это сопряжено с рядом проблем, которые необходимо решить для обеспечения надежности. Из них мы рассмотрим пять. Первым делом рассмотрим обработку нечисловых значений (NaN) и работу с отсутствующими данными. В наборах финансовых данных обычно содержатся пропущенные значения из-за закрытия рынка, неполных данных о ценах или нерегулярных потоков данных от брокеров. В конвейерах обработки MQL5 часто используются заполнители, такие как DBL_MIN, для обозначения отсутствующих записей. Поэтому крайне важно последовательно подставлять эти значения, используя такие альтернативные методы, как медиана, если данные числовые, или мода, если данные дискретные/категориальные. Это предотвращает ввод в модель некорректных данных.
Вторая проблема с этими конвейерами в MQL5 может заключаться в предотвращении утечки данных. Распространенная ошибка заключается в том, что масштабирующие алгоритмы или кодировщики применяются ко всему набору данных до его разделения на обучающий, проверочный и тестовый наборы данных. Такая практика может привести к утечке будущей информации в процесс обучения и искусственному завышению показателей эффективности. Тестер стратегий MQL5 по своей сути рассчитан на работу исключительно с текущими или старыми данными о ценах, но в случае использования и получения дополнительных данных для передачи в модель, необходимы дополнительные проверки, чтобы исключить подобные утечки. Наилучшей практикой было бы всегда применять конвейер Fit() только к обучающим данным, а затем использовать метод Transform() для обучающего и тестового наборов данных отдельно.
Во-третьих, масштабирование данных разных типов может представлять собой сложную задачу. Когда наборы данных содержат как числовые, так и категориальные признаки, преобразования следует применять с осторожностью. Для обеспечения корректной интеграции вновь созданных бинарных столбцов в новую числовую матрицу необходимо использовать сначала кодировщики, а затем инструменты масштабирования.
В-четвертых, необходимо уделять внимание отладке преобразований. Для проверки корректности результатов преобразования полезно напечатать первые несколько строк преобразованных данных. Инструменты отладки, такие как функция Print(), позволяют быстро выявить, правильно ли работают процессы кодирования и масштабирования.
Наконец, обеспечение воспроизводимости не всегда можно считать само собой разумеющимся. Для обеспечения согласованности результатов в разных экспериментах параметры конвейера обработки данных, минимумы, медианы и сопоставление категорий необходимо хранить вместе с обученной моделью. Это гарантирует, что точно такую же предварительную обработку можно применять при тестировании моделей на исторических данных, в реальной торговле или даже при переобучении.
Следование этим пяти рекомендациям поможет разработчикам избежать распространенных ошибок, обеспечив такой же уровень надежности конвейеров обработки данных в MQL5, как у их аналогов в Python.
Заключение
Предварительная обработка данных, безусловно, не является самой привлекательной частью машинного обучения. Но, пожалуй, это один из его важнейших этапов. Без тщательной подготовки даже самая продвинутая модель глубокого обучения столкнется с необработанными, не масштабированными или непоследовательно закодированными торговыми данными. Для разработчиков, хорошо знакомых с MetaTrader 5, эта проблема традиционно являлась препятствием для полного использования рабочих процессов машинного обучения. В отличие от Python, который предлагает мощный инструментарий SCIKIT-LEARN, MQL5 не имеет встроенных конвейеров предварительной обработки. Решение, как это было показано, может заключаться в построении модульных, многократно используемых конвейеров предварительной обработки данных в рамках языка MQL5.
При этом, рассматривая эти компоненты не как необязательные утилиты, а как неотъемлемые элементы рабочего процесса ИИ, разработчики смогут привести свои торговые системы в соответствие со строгими требованиями машинного обучения, чтобы у большинства моделей — как собственных, так и основанных на ONNX — была наилучшая возможная основа для успеха.
| название | описание |
|---|---|
| PipeLine.mqh | Базовый класс для функциональности конвейера |
| Pipeline_Illustration.mq5 | Скрипт, который ссылается на базовый класс и демонстрирует его использование. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19544
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Разработка инструментария для анализа движения цен (Часть 21): Поиск разворотов рыночной структуры
Нейросети в трейдинге: Гибридные модели прогнозирования с управляемой смесью распределений (Окончание)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования