Пайплайны Codex: от Python до MQL5 для выбора индикаторов — анализ ETF XLF по нескольким кварталам с применением машинного обучения
Введение
В нашем последнем исследовании, посвящённом построению строгой методологии ранжирования индикаторов в различных квартальных режимах, в качестве торгуемого актива мы использовали ETF FXI. Наша цель заключалась в создании чистого модульного пайплайна, который фильтрует шум и вносит определённую структуру в выбор индикаторов для случаев, когда торговая интуиция неизбежно подводит. Однако созданный нами пайплайн имел диагностическое назначение. Он показывал нам, какие индикаторы имели значение и когда именно они были важны. Хотя мы продемонстрировали, как набор индикаторов можно использовать вместе с взвешиванием, пропорциональным рейтинговым оценкам, наш итоговый советник всё же оставлял некоторые пробелы с точки зрения лучшей обобщающей способности, поскольку использованное взвешивание индикаторов, по сути, сводится к одному слою перцептрона, что может быть слишком упрощённым подходом. Мы пытаемся решить эту проблему с помощью многослойного перцептрона.
В этой статье мы по-прежнему используем состояния индикаторов, а не классификации паттернов, но при этом опираемся на те же горизонты будущей доходности, которые рассматривали в прошлый раз, и проверяем, сможем ли преобразовать эту информацию в механизм прогнозирования. Мы стремимся получить компактную нейронную сеть, обученную на наблюдениях индикаторов за несколько кварталов. Затем мы экспортируем эту модель в формат ONNX для непосредственного использования в MQL5. Идея здесь проста: если наш пайплайн уже умеет отделять полезные сигналы от информационного мусора, то модели, которую мы будем обучать на последующих этапах, не придётся «заново учить» то, что мы построили в прошлой статье. Вместо этого она будет работать как специализированный слой инференса, предоставляющий нам вероятностные сигналы о тенденциях нашего тестового актива XLF без необходимости создавать отдельного советника для каждого уникального рыночного режима.
Таким образом, наш целевой результат — производственный цикл, включающий следующие этапы: данные → пайплайн → признаки → модель → ONNX → советник.
XLF
ETF XLF — это секторный ETF от SPDR, представляющий финансовый сектор. В отличие от FXI, с которым мы работали в прошлый раз, с момента запуска у XLF преобладал бычий тренд. За исключением периода глобального финансового кризиса, этот ETF почти всегда рос, и поэтому он даёт нам контрастную площадку для тестирования по сравнению с тем, что было в прошлый раз, когда FXI, подобно валютной паре, на протяжении многих лет вяло держался в районе ценовой отметки 40. Тем не менее у него действительно наблюдаются резкие смены режимов, которые, по всей видимости, обусловлены политикой процентных ставок, кредитными циклами, отдельными шоками ликвидности, а также секторной ротацией.

Судя по всему, на протяжении многих кварталов его динамика чередуется между плавным ростом и резкими скачками волатильности. Обучая и оценивая модели на XLF, мы показываем состоятельность нашего пайплайна: индикаторы должны оставаться устойчивыми в различных рыночных подрежимах, а конструирование признаков — выдерживать шумные переходные периоды. ONNX-модель, которая здесь является нашим конечным продуктом, должна выдерживать исполнение в реальном времени в MQL5, не полагаясь на благоприятные условия спокойных рынков.
Доработки пайплайна
Этот пайплайн индикаторов, который мы разработали в предыдущей статье, уже взял на себя большую часть сложной работы, выполнив такие задачи, как очистка данных, разбиение на кварталы, расчёт значений индикаторов и оценка их полезности. Можно, пожалуй, сказать, что его слабое место — обобщение: умение объединять усвоенное в ограниченном временном окне и показывать сопоставимые результаты на ранее не встречавшихся рыночных режимах или тестовых окнах. Именно в этом заключается основной аргумент в пользу такого расширения пайплайна или альтернативного подхода к нему.
Поэтому наша задача здесь заключается в том, чтобы рассматривать выходные данные пайплайна — нормализованные показания индикаторов и состояния, полученные на основе паттернов, — в качестве структурированных признаков для прогнозирующей сети. Эта сеть не заменит пайплайн, но унаследует его строгую структуру и надстроит над ним статистический слой. Наш подход также позволяет избежать подхода с подачей сырых цен напрямую в сеть, который при обучении склонен к переобучению.
Таким образом, наша итоговая архитектура представляет собой многоэтапный конвейер, в котором логика индикаторов формирует признаки, расчёты будущей доходности дают метки/целевые значения для обучения, а многослойный перцептрон обучается этой взаимосвязи. ONNX просто служит каналом для передачи модели из Python в MQL5. Этот процесс построен так, чтобы все компоненты оставались независимыми, благодаря чему конкретные изменения — будь то в индикаторах, типах паттернов или временных горизонтах — не требуют «хирургического вмешательства» в код модели. Нашу архитектуру можно представить следующим образом.
Кроме того, после обучения мы проведем поствалидационный прогон, чтобы оценить вероятную эффективность нашей модели. Для этого мы возьмем самые последние 20% набора данных. Мы останавливаем процесс валидации, как только результаты тестирования выходят на плато, после чего экспортируем модель в формате ONNX для MQL5.
Конструирование признаков
В моей предыдущей статье по FXI набор данных строился на основе стандартной тройки индикаторов, объединяющей метрики тренда, моментума и волатильности. В качестве этой тройки мы использовали индикаторы MACD, RSI и «Полосы Боллинджера». Чтобы тщательнее протестировать наш пайплайн или «раздвинуть его границы», в этой статье мы рассмотрим альтернативный набор индикаторов: FrAMA, Parabolic SAR, «Аллигатор» и TRIX. Они выбраны не только из-за своей новизны, что важно при исследовании, но и с учетом того, какой аспект рынка представляет каждый из них.
Индикатор FrAMA хорошо выявляет ускорения тренда, SAR, как следует из его названия, — развороты, «Аллигатор» очень полезен для обнаружения сближения цены и скользящей средней (MA), а TRIX помогает оценивать чувствительность цены к изменению моментума. Как и в предыдущей статье, легко поддаться соблазну включить их все, аргументируя это тем, что каждый из них является специализированным индикатором; однако главная цель нашего пайплайна — определить, какой из них наиболее полезен для XLF. Однако объединение этих индикаторов с учетом их весов значимости может хорошо сработать с многослойным перцептроном, поскольку он, как правило, лучше извлекает взаимосвязи между индикаторами, чем наш исходный пайплайн с ранговым взвешиванием.
Задача пайплайна останется прежней: вычислять ранговые веса значимости этих индикаторов бар за баром по квартальным сегментам, а затем передавать полученные числовые состояния в единую обучающую матрицу. Однако, в отличие от схемы, которую мы использовали в прошлой статье с FXI, здесь наши индикаторы формируют многоколоночные выходные данные, такие как линии «Челюсть», «Зубы» и «Губы» индикатора «Аллигатор», а также рекурсивные компоненты, как при сглаживании переключений SAR с помощью FrAMA. Все эти признаки необходимо выровнять бар за баром или по времени и рассматривать как полноценный источник признаков. Мы не сглаживаем индикаторы, чтобы привести их к единому виду, и также следим за тем, чтобы не возникала утечка будущих значений в более ранние строки. Поэтому при подготовке к использованию модели MLP мы реализуем конструирование признаков следующим образом. Исходный код для загрузки ценовых данных из MetaTrader 5 здесь не приводится, поскольку он уже был рассмотрен выше.
# python libraries import MetaTrader5 as mt5 from dataclasses import dataclass from typing import Callable, Dict, Tuple import pandas as pd import numpy as np from datetime import datetime, time from pandas.tseries.holiday import USFederalHolidayCalendar from IndicatorsAll import FRAMA, Parabolic_SAR, Alligator, TRIX # :contentReference[oaicite:1]{index=1} # … # same mt5 price load and resampling as last article # … # Apply mask df_market = df_resampled[market_mask].copy() # Compute the alternative indicators df = FRAMA(df_market, period=16) df = Parabolic_SAR(df) df = Alligator(df) df = TRIX(df, period=15, signal=9) # Build feature set feature_cols = [ "FRAMA", "SAR", "Alligator_Jaw", "Alligator_Teeth", "Alligator_Lips", "TRIX", "TRIX_signal" ] # Drop any rows with NaNs caused by indicator warm-up periods X = df[feature_cols].dropna() print("Feature matrix shape:", X.shape)
Матрица признаков, которую мы получаем на выходе выше, намеренно нестандартна. В ней объединены адаптивное сглаживание, динамика фрактальной размерности, согласование тренда по нескольким линиям и темпы изменения моментума с тройным сглаживанием. Таким образом, этап обучения в нашем пайплайне получает более чистую и информативную основу входных данных, которая, по всей видимости, отражает разнообразную динамику рынков.
Конструирование меток
Практически все индикаторы, какими бы необычными они ни были, не принесут трейдерам особой пользы, если не смогут указывать на значимый будущий результат. Именно для этого существует конструирование меток. С его помощью мы преобразуем необработанные данные о движении цены в чистую, пригодную для обучения целевую метку, которую ONNX-модель впоследствии сможет попытаться спрогнозировать. Кроме того, поскольку наш набор индикаторов теперь широко охватывает адаптивное выявление тренда с помощью FrAMA, сигналы разворота с помощью SAR, структурное выравнивание с помощью Alligator и перелом моментума с помощью TRIX, нам нужна метка, которая в сжатой форме поощряет модель за правильное выявление будущего продолжения направления или его срыва.
Для обеспечения согласованности с сегментацией по кварталам, которую мы ввели в предыдущей статье, мы рассчитываем будущую доходность за фиксированный период. После этого мы сводим их к классификации по одному из трех классов: «бычий», «медвежий» или «нейтральный». Очень важно также, какие именно пороговые значения мы используем, и некоторые трейдеры могут упускать этот момент из виду. Если диапазон слишком узкий, то наша модель может усвоить много шума. Если диапазон слишком широк, то наша модель может фактически оказаться не лучше подбрасывания монеты. Поэтому в нашей реализации мы используем простой диапазон ±X%, где X по умолчанию равен 0,2 процента. По сути, это гиперпараметр, и читателям следует попробовать настроить его — например, по кварталам и т. п. — так, как будет наиболее эффективно. Для начала рассмотрим два основных подхода к конструированию меток — регрессию и классификацию, как показано в приведенном ниже коде. Позже мы определим, какая целевая функция лучше всего подходит для нашей MLP.
horizon = 8 # Compute forward returns df["fwd_return"] = df["close"].shift(-horizon) / df["close"] - 1.0 up_th = 0.002 # +0.20% down_th = -0.002 # -0.20% def classify(r): if r > up_th: return 2 # bullish -> class index 2 elif r < down_th: return 0 # bearish -> class index 0 else: return 1 # neutral -> class index 1 df["label_cls"] = df["fwd_return"].apply(classify) # Regression target df["label_reg"] = df["fwd_return"] # Clean training rows Y_cls = df["label_cls"].dropna() Y_reg = df["label_reg"].dropna() print("Y_cls head:", Y_cls.head()) print("Y_reg head:", Y_reg.head()) # Build feature matrix X = df[feature_cols] # Build labels Y_cls = df["label_cls"] Y_reg = df["label_reg"] # Features (whatever you defined earlier) X = df[feature_cols] # Find rows where ALL are valid valid_mask = X.notna().all(axis=1) & Y_cls.notna() & Y_reg.notna() X_final = X[valid_mask] Y_cls_final = Y_cls[valid_mask] Y_reg_final = Y_reg[valid_mask] print("Final shapes:", X_final.shape, Y_cls_final.shape, Y_reg_final.shape) # Classification labels Y_cls = df["label_cls"] # If you still use regression labels: Y_reg = df["label_reg"] # or df["fwd_return"] valid_mask = X.notna().all(axis=1) & Y_cls.notna() X_final = X[valid_mask] Y_cls_final = Y_cls[valid_mask] print("Unique class labels:", sorted(Y_cls_final.unique())) print("Y_reg head:", Y_reg.head())
Наша схема с двумя метками выбрана намеренно, поскольку может оказаться, что адаптивные индикаторы, такие как FrAMA и TRIX, хорошо подходят для прогнозирования величины движения, тогда как индикаторы разворота, такие как SAR, лучше подходят для определения направления. Оставляя открытыми оба варианта, мы сохраняем возможность проверить, какая цель моделирования обеспечивает наиболее стабильное качество.
Правила нормализации и целостности входных данных
Если пайплайн индикаторов определяет, на основе чего мы обучаемся, то нормализация определяет, как наша модель это воспринимает. Значения FrAMA могут колебаться в непосредственной близости от цены актива, значения TRIX могут находиться в очень малых процентных диапазонах, компоненты индикатора Alligator также могут значительно варьироваться в зависимости от уровня волатильности, тогда как значения SAR, хотя и схожи с ценой, могут легко резко подскакивать или падать при каждом развороте. Таким образом, в этом разнообразном пуле входных данных задействовано множество факторов. Таким образом, без какого-либо масштабирования MLP будет воспринимать признак с наибольшей величиной как «самый громкий голос» в комнате. Мы этого не хотим.
Применяемая нами стратегия нормализации также должна учитывать временной порядок. Значения индикаторов выводятся в хронологическом порядке, а на их основе в MQL5 впоследствии принимаются торговые решения. Это означает, что, хотя во время обучения у нас есть большой объём данных, применяемое масштабирование должно использовать только информацию из прошлого. Например, глобальное масштабирование всего набора данных, часто используемое в «сказочных мирах» Kaggle, может случайно протечь информацией о будущих сдвигах волатильности в более ранние строки, наделив нашу модель непреднамеренной способностью к предвидению. Чтобы этого избежать, мы используем скользящие масштабировщики либо масштабировщики, обученные на обучающей выборке. Это гарантирует, что наша модель будет вести себя одинаково — по крайней мере, с точки зрения её конструкции — как при тестировании на исторических данных, так и при развёртывании в реальных условиях. Поэтому для решения этой проблемы мы вносим в пайплайн следующие дополнения.
import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader from sklearn.preprocessing import StandardScaler, MinMaxScaler # After defining feature_cols and before splitting alligator_cols = ["Alligator_Jaw", "Alligator_Teeth", "Alligator_Lips"] trix_cols = ["TRIX", "TRIX_signal"] minmax_cols = ["FRAMA", "SAR"] std_cols = alligator_cols + trix_cols # After valid_mask and X_final = X[valid_mask] split = int(len(X_final) * 0.8) X_train_df = X_final.iloc[:split] X_val_df = X_final.iloc[split:] y_train_df = Y_cls_final.iloc[:split] y_val_df = Y_cls_final.iloc[split:] std_scaler = StandardScaler() minmax_scaler = MinMaxScaler() X_train_scaled = X_train_df.copy() X_train_scaled[minmax_cols] = minmax_scaler.fit_transform(X_train_df[minmax_cols]) X_train_scaled[std_cols] = std_scaler.fit_transform(X_train_df[std_cols]) X_val_scaled = X_val_df.copy() X_val_scaled[minmax_cols] = minmax_scaler.transform(X_val_df[minmax_cols]) X_val_scaled[std_cols] = std_scaler.transform(X_val_df[std_cols]) # Then convert to tensors X_train = torch.tensor(X_train_scaled.values, dtype=torch.float32) X_val = torch.tensor(X_val_scaled.values, dtype=torch.float32) y_train = torch.tensor(y_train_df.values, dtype=torch.long) y_val = torch.tensor(y_val_df.values, dtype=torch.long)
Z-оценка, или стандартизация, обычно лучше всего работает для компонентов TRIX и Alligator. С другой стороны, масштабирование по методу min-max демонстрирует лучшие результаты при работе с дискретными значениями SAR и FrAMA, поскольку эти значения, как правило, остаются ограниченными заданными диапазонами в определённых структурных областях. Независимо от выбранного варианта масштабирования, его необходимо сериализовать вместе с экспортом в ONNX, чтобы в MQL5 поступали входные данные, обработанные точно таким же образом. Если этого не произойдёт, модель может «галлюцинировать», как указано выше.
Разработка модели
Учитывая, что матрица входных данных нормализована и у нас уже есть «честные признаки», теперь нам нужно спроектировать MLP-модель, способную использовать эти данные без «галлюцинаций» и переобучения. Часто именно на этом этапе большинство ML-проектов в трейдинге начинают чрезмерно разрастаться: в MLP добавляют множество слоёв, а также подключают более сложные типы слоёв. Зачастую они могут оказаться ненужными. Главное преимущество нашего пайплайна заключается в том, что индикаторы FrAMA, SAR, Alligator и TRIX уже выполнили часть работы по извлечению сигнала. Таким образом, наша модель не ставит перед собой задачу выявлять тренд, волатильность или динамику разворотов. Его основная задача и сложность заключаются в объединении этих индикаторов для улучшения обобщающей способности по сравнению с пайплайном, который мы рассматривали в предыдущей статье.
Поэтому компактная MLP может идеально подойти для этой роли. Она будет быстрой, прозрачной, легко отлаживаемой и — что самое важное — будет демонстрировать стабильное поведение после экспорта в ONNX для использования в MQL5. В отличие от рекуррентных или сверточных сетей, наша простая MLP не будет сбоить из-за отсутствующих данных при подборе гиперпараметров на лету — например, окна горизонта — или даже при корректировке параметров входных индикаторов. Хотя эти проблемы не всегда возникают в более сложных сетях, они всегда представляют собой риск, который необходимо учитывать заранее.
Наша MLP имеет весьма простую структуру: в ней используется три скрытых слоя, каждый из которых имеет разумный размер, чтобы избежать чрезмерного количества параметров. Мы стремимся к тому, чтобы размер экспортируемой ONNX-модели составлял примерно 3 МБ. Этого должно быть достаточно, чтобы отражать нелинейные взаимосвязи, например когда моментум TRIX меняет направление, SAR дает резкие ложные развороты, а FrAMA, скажем, находится в фазе сжатия. Кроме того, мы выбираем активационные функции, обеспечивающие плавные градиенты и предсказуемое поведение при экспорте в ONNX, при этом мы не используем необычные слои и неподдерживаемые операции. Ниже приведена наша реализация этого на Python.
# Update model class for larger size class XLF_MLP(nn.Module): def __init__(self, input_dim, output_dim=3): super().__init__() hidden = 512 self.net = nn.Sequential( nn.Linear(input_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, output_dim) ) def forward(self, x): return self.net(x) # Example construction: input_dim = X_train.shape[1] model = XLF_MLP(input_dim=input_dim) print(model)Наша MLP проста. Ее цель заключается не в прогнозировании рынка, а в определении вероятности продолжения движения или разворота на заранее заданном временном горизонте. Цель здесь — поставить скромную, но пригодную для торговли задачу, если такая вообще существует. После экспорта через ONNX принимающий советник получит чистое распределение вероятностей, а не сырые значения индикаторов или непрозрачные эвристики. Таким образом, модель становится завершающим элементом нашего пайплайна.
Обучение модели
После того как мы определили модель, выровняли входные признаки и очистили метки, обучение MLP должно стать относительно простой задачей. Конечно, это предполагает, что мы сможем избежать некоторых типичных ловушек, таких как временная утечка, перемешивание меток или даже обучение модели на данных, с которыми она на практике никогда не столкнется. Благодаря шагам, которые мы предприняли ранее при формировании входных тензоров значений X и Y для модели, эти два набора данных синхронизированы таким образом, что X запаздывает относительно Y. Это всегда важно, поскольку все наши входные индикаторы используют внутреннее сглаживание и сравнения со сдвигом, что может незаметно нарушить процесс обучения в Python, если не обеспечить надлежащее выравнивание.
В конечном итоге советник, который будет использовать эту ONNX-модель, ожидает на выходе классификацию: «бычий», «медвежий» или «нейтральный». С учетом этого в выполняемом нами тестовом запуске используются классификационные метки, которые будет легко интегрировать в MetaTrader 5. Тем не менее в нашем коде мы сохраняем вариант регрессионного формирования меток как вторичную цель для экспериментов. Регрессия иногда позволяет выявить тонкие различия в силе прогноза, которые классификационный подход мог бы упустить. Поэтому, если итоговый советник рассматривает два разных пула индикаторов в качестве входных данных, это различие может помочь выбрать между ними при равенстве оценок.
Наш цикл обучения ориентирован на стабильность: мы не используем планировщики, функции потерь на пять страниц или многочисленные настройки гиперпараметров. Торговые модели должны хорошо работать за счет предсказуемости. Поэтому мы используем Adam, умеренную скорость обучения, мини-батчи, а также раннюю остановку на основе значения потерь на валидации. Наша цель — не совершенство само по себе, а способность лучше обобщать. Нам нужна модель, достаточно устойчивая, чтобы выдерживать смены рыночных режимов ETF XLF. Вот как мы реализуем обучение.
train_ds = TensorDataset(X_train, y_train) val_ds = TensorDataset(X_val, y_val) train_loader = DataLoader(train_ds, batch_size=64, shuffle=True) val_loader = DataLoader(val_ds, batch_size=64, shuffle=False) criterion = torch.nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) for epoch in range(30): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() # validation model.eval() with torch.no_grad(): val_loss = sum(criterion(model(xv), yv) for xv, yv in val_loader) print(f"Epoch {epoch}: val_loss={val_loss.item():.4f}")
Валидация
Обучение любой модели, особенно на Python, где вычислительные затраты минимальны, относительно просто. Самое сложное — а это, по сути, и есть главная цель обучения — доказать, что то, чему модель научилась при обучении, работает в разных рыночных режимах. Мы пытаемся достичь этой цели на примере XLF — ETF, ценовая динамика которого во многом определяется политикой процентных ставок, кредитными циклами, ликвидностью и множеством других факторов. Поэтому, чтобы оценить этот MLP на данном секторном ETF финансового сектора, мы применяем подход walk-forward-валидации с учетом кварталов. По сути, мы оцениваем, насколько хорошо модель работает, когда ей приходится обучаться на данных текущего режима, а затем прогнозировать следующий. Это должно имитировать развертывание в реальных условиях: при тестировании в MetaTrader 5 такой порядок фактически обеспечивается бесшовно, однако в Python нужны дополнительные «защитные меры», чтобы гарантировать, что это действительно происходит.
Точки данных входных индикаторов в матрице признаков по-разному влияют на эту устойчивость при прогнозировании. Кварталы с разными рыночными режимами отличаются друг от друга. В одних кварталах лучше будет отрабатывать фрактальное сжатие, в других — развороты моментума, а в третьих — ускорение тренда. В идеале надлежащая валидация должна оценивать результативность во всех этих различных рыночных режимах, а не исходить из того, что средняя результативность, полученная при тестировании лишь на нескольких из них, дает достаточный ориентир на будущее.
Мы провели валидацию только на последних 20 % тестового окна с 01.01.2020 по 01.12.2025. Очевидно, что это не очень репрезентативно для всех рыночных режимов; однако прагматичным решением могло бы стать обучение с разбиением 60 к 40 на данных с момента запуска в декабре 1998 года, чтобы последние 40 % охватывали более широкий спектр рыночных режимов. Тем не менее, наша схема обучения выглядит следующим образом.
Используемый нами процесс с поступательно сдвигающимся окном оценки потенциально может выявить участки, где модель дает сбой, — например, при волатильной секторной ротации. Кроме того, он позволяет выявить, где модель показывает наилучшие результаты; согласно нашим тестам, это происходит в циклах постепенного повышения ставок, когда большинство индикаторов ведет себя так, как и следовало ожидать. Таким образом, конечным результатом наших тестовых прогонов становится не показатель точности как таковой, а профиль. Показатель того, насколько хорошо модель адаптируется при переходе от одного рыночного режима к другому. В идеале любая ONNX-модель, развернутая в MQL5, должна пройти эту серию испытаний, прежде чем её можно будет рассматривать для использования в реальной торговле.
Экспорт через ONNX
Когда тестируемый MLP успешно проходит валидацию с учетом кварталов, следующим шагом становится преобразование его в переносимый формат, который могут использовать MQL5 и советники MetaTrader 5. Стандарт ONNX делает это возможным, поскольку задает ограниченный и предсказуемый набор операторов и гарантирует согласованное поведение на разных платформах. Экспорт нашей модели будет успешным только в том случае, если в ней используются операции, поддерживаемые средой выполнения ONNX. К счастью, наша архитектура весьма проста по причинам, изложенным выше, и поэтому линейные слои, активации ReLU и единственная выходная головка не должны представлять никаких сложностей для ONNX. Таким образом, экспорт является детерминированным, а отладка в MQL5 становится вполне выполнимой.
Тем не менее, как и при любом экспорте в ONNX, прежде чем передавать модель дальше, нам необходимо проверить и зафиксировать точные формы входных и выходных тензоров. Кроме того, выполненную выше нормализацию входной матрицы данных необходимо перенести в MetaTrader 5. Экспорт в формат ONNX лишь предоставляет нам сеть в MetaTrader 5, но не выполняет никакой нормализации входных данных. Это означает, что эту задачу нам нужно решить в MQL5 до выполнения прямого прохода. Однако экспорт в Python выглядит следующим образом.
import torch.onnx dummy = torch.randn(1, X_train.shape[1], dtype=torch.float32) onnx_path = "xlf_model.onnx" torch.onnx.export( model, dummy, onnx_path, input_names=["features"], outputimport torch.onnx dummy = torch.randn(1, X_train.shape[1], dtype=torch.float32) onnx_path = "xlf_model.onnx" torch.onnx.export( model, dummy, onnx_path, input_names=["features"], output_names=["logits"], dynamic_axes={"features": {0: "batch"}}, opset_version=12 ) print("Model exported to:", onnx_path) import onnx import onnxruntime as ort # Create an ONNX Runtime session session = ort.InferenceSession(onnx_path) # Run inference Running inference.= session.get_inputs()[0].name output_name = session.get_outputs()[0].name input_data = np.random.randn(1, X_train.shape[1]).astype(np.float32)# torch.randn(1, X_train.shape[1], dtype=torch.float32) #input_data = input_data.to(device) output = session.run([output_name], {input_name: input_data}) for i in session.get_inputs(): print(f"inputs: {i.name}, Shape: {i.shape}, Type: {i.type}") for o in session.get_outputs(): print(f"outputs: {o.name}, Shape: {o.shape}, Type: {o.type}") _names=["logits"], dynamic_axes={"features": {0: "batch"}}, opset_version=12 ) print("Model exported to:", onnx_path)
Использование в MQL5
Если нет возможности подавать данные в экспортированную ONNX-модель для построения прогнозов, она бесполезна. В этом отношении MQL5 весьма полезен: он предоставляет обёртку над средой выполнения ONNX в виде лаконичного минимального API, поэтому развертывание этой модели в советнике менее трудоёмко, чем управление бэкендом на Python. Главное — убедиться, что советник воссоздает именно тот вектор признаков, который мы использовали при обучении на Python. Выравнивание должно быть точным, бар за баром, без единой ошибки. Здесь легко ошибиться, если применить несоответствующее масштабирование входных данных или использовать другие параметры индикаторов.
Реализованная нами схема работы советника проста. Мы рассчитываем индикаторы на каждом новом баре; затем применяем к каждому индикатору параметры масштабирования в соответствии с теми, что использовались при тестировании в Python. После этого мы формируем вектор признаков в том же порядке, что и в Python, вызываем сессию ONNX, а затем интерпретируем выходной вектор. По сути, перед нами трехэтапный процесс. Подготовить признаки, выполнить инференс и, наконец, преобразовать логиты в торговое решение. Наш советник не выполняет обучение и даже не обновляет веса нейронной сети во время торговли — нам нужен лишь стабильный и воспроизводимый интерфейс. Ниже приведена сокращённая версия нашего кода, в которой показаны функции, выполняющие эти три шага.
Подготовка признаков.
//+------------------------------------------------------------------+ //| Detecting the "weighted" direction | //+------------------------------------------------------------------+ double CSignalXLF::Direction(void) { m_x.Fill(0.0f); m_y.Fill(0.0f); vector _z(5), _z_out(5); _z.Fill(0.0); m_frama.Refresh(-1); _z[0] = m_frama.Main(X()); m_sar.Refresh(-1); _z[1] = m_sar.Main(X()); m_alligator.Refresh(-1); _z[2] = m_alligator.Jaw(X()); _z[3] = m_alligator.Lips(X()); _z[4] = m_alligator.Teeth(X()); vector _mm(2), _mm_out(2); m_trix.Refresh(-1); _mm[0] = m_trix.Main(X()); _mm[1] = m_trix.Main(X() + 1); if(NormalizeZScore(_z, _z_out) && NormalizeMinMax(_mm, _mm_out)) { for(int i = 0; i < 5; i++) { m_x[i] = float(_z_out[i]); } for(int i = 0; i < 2; i++) { m_x[5 + i] = float(_mm_out[i]); } //printf(__FUNCSIG__); ////Print(" z out: ", _z_out); ////Print(" mm out: ", _mm_out); //Print(" in x: ", m_x); m_y = Infer(m_x); return(LongCondition() - ShortCondition()); } return(0.0); }
Выполнение инференса.
//+------------------------------------------------------------------+ //| Inference Pass. | //+------------------------------------------------------------------+ vectorf CSignalXLF::Infer(vectorf &X) { vectorf _y(__CLASSES); _y.Fill(0.0); //Print(" x in: ", __FUNCTION__, X); ResetLastError(); if(!OnnxRun(m_handle, ONNX_NO_CONVERSION, X, _y)) { printf(__FUNCSIG__ + " failed to get y forecast, err: %i", GetLastError()); } vectorf _yy(_y.Size()); _yy.Fill(0.0); if(_y.Size() == 3) { Softmax(_y, _yy); } else { _yy.Copy(_y); } //printf(__FUNCSIG__); //Print(" y out: ", _yy); return(_yy); }
Интерпретация и использование выходных данных.
//+------------------------------------------------------------------+ //| "Voting" that price will grow. | //+------------------------------------------------------------------+ int CSignalXLF::LongCondition(void) { if(fabs(m_y[0]) > fabs(m_y[2]))//0.5) { //printf(__FUNCSIG__); //Print(" in y: ", m_y); //return(int(100.0 * (fabs(m_y[0]) - 0.5) / 0.5)); return(int(100.0 * fabs(m_y[0]))); } return(0); } //+------------------------------------------------------------------+ //| "Voting" that price will fall. | //+------------------------------------------------------------------+ int CSignalXLF::ShortCondition(void) { if(fabs(m_y[0]) < fabs(m_y[2]))//0.5) { //printf(__FUNCSIG__); //Print(" in y: ", m_y); //return(int(100.0 * fabs(m_y[0]) / 0.5)); return(int(100.0 * fabs(m_y[2]))); } return(0); }
Используя мастер MQL5, для которого главным условием является наличие пользовательского класса сигналов, подобного тому, что мы используем выше, мы избавляемся от необходимости учитывать некоторые важные, но рутинные требования, характерные для любого советника. К ним относятся средства контроля рисков, связанных с шумом в цене, возвращаемой брокером, проверка спреда, предотвращение дублирования ордеров при открытии позиции и т. д. В рамках данного тестирования мы сосредоточиваемся исключительно на взаимодействии с ONNX. Наш советник представляет собой «тонкую» обёртку, которая принимает значения индикаторов, нормализует их в признаки, передаёт в ONNX-модель для инференса, обрабатывает выходные логиты, а затем принимает торговое решение. Это можно обобщить с помощью приведенной ниже блок-схемы.
При использовании варианта с классификационной меткой ONNX-модель выдает три логита. Они должны соответствовать вероятностям бычьего, нейтрального или медвежьего сценария. Однако это логиты, а еще не вероятности. Если рассматривать их в сыром виде, значения зачастую не только содержат отрицательные числа, но и по модулю часто превышают единицу; кроме того, если запустить тест, в этих выходных данных обычно наблюдается слабая изменчивость на протяжении многих баров, а это означает, что в таком виде из них нельзя получить четкий сигнал. Чтобы сделать их пригодными для использования, необходимо применить функцию softmax к выходу ONNX-модели. Мы поступаем следующим образом.
//+------------------------------------------------------------------+ //| Compute softmax probabilities from logits | //| logits[] : input raw scores (e.g. from your network) | //| probs[] : output probabilities (same length as logits) | //+------------------------------------------------------------------+ void CSignalXLF::Softmax(vectorf &Logits, vectorf &Probs) { int _size = int(Logits.Size()); if(_size == 0) return; Probs.Init(_size); Probs.Fill(0.0); // 1) Find max logit for numerical stability float _max_logit = Logits.Max(); // 2) Exponentiate shifted logits and sum float _sum_exp = 0.0; for(int i = 0; i < _size; i++) { Probs[i] = float(MathExp(Logits[i] - _max_logit)); _sum_exp += Probs[i]; } // 3) Normalize to get probabilities if(_sum_exp <= 0.0f) { // Failsafe: uniform distribution if something goes wrong float _p = 1.0f / _size; for(int i = 0; i < _size; i++) Probs[i] = _p; return; } float _inv_sum = 1.0f / _sum_exp; for(int i = 0; i < _size; i++) Probs[i] *= _inv_sum; }
После этого советник, собранный с помощью мастера и использующий этот пользовательский класс сигналов, должен быть готов к тестированию и дальнейшей доработке. Код этого класса приведен ниже.
Заключение
В этой статье мы начали с рассмотрения потенциальных ограничений ранжирования индикаторов и завершили созданием готового к развертыванию торгового движка на базе ONNX, протестированного на ETF XLF. Мы взяли исследовательский пайплайн, включавший аккуратную подготовку данных, квартальную сегментацию, выбор индикаторов и конструирование признаков, чтобы создать модель, которая нативно работает в среде MQL5 без постоянных зависимостей от Python.
Ключевой вывод здесь может заключаться в том, что машинное обучение становится по-настоящему полезным только в сочетании со строгой структурой. Индикаторы помогают извлекать данные; масштабирование/нормализация сохраняет целостность этих данных; валидация с учетом кварталов обосновывает применимость; а ONNX обеспечивает беспрепятственное развертывание. Наша конечная система задумывалась не как «черный ящик» или «фабрика сигналов» на основе грубого перебора, а как компактное решение с объяснимым слоем принятия решений, сформированным на основе поведения за несколько кварталов и способным адаптироваться по мере изменения рыночных режимов.
| Имя | Описание |
|---|---|
| XLF.mq5 | Советник, собранный с помощью мастера, в заголовке которого перечислены используемые файлы |
| SignalXLF.mqh | Файл пользовательского класса сигналов, используемый при сборке в мастере |
| xlf-cls-model.onnx | Классификационная ONNX-модель |
| xlf-reg-model.onnx | Регрессионная ONNX-модель |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20595
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
От начального до среднего уровня: Перегрузка операторов (IV)
Разработка инструментария для анализа Price Action (Часть 74): Создание советника MQL5 на основе буферов индикатора
Возможности Мастера MQL5, которые вам нужно знать (Часть 97): Использование выпуклой оболочки и миниатюрной GRU-сети в пользовательском классе трейлинг-стопа
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования


