Возможности Мастера MQL5, которые вам нужно знать (Часть 76): Использование паттернов Awesome Oscillator и каналов конвертов с обучением с учителем
Введение
В предыдущей статье, мы представили комбинацию индикаторов Awesome-Oscillator и Envelope-Channels, и в результате тестирования этой пары 7-8 из 10 паттернов показали положительный walk-forward-результат на двухлетнем тестовом окне. После представления пары индикаторов мы обычно изучаем, какое влияние, если таковое имеется, может оказать машинное обучение на эффективность этих индикаторных сигналов. Данная статья не является исключением, поэтому мы рассмотрим, как паттерны 4, 8 и 9 могут быть изменены, если мы добавим к их сигналам сеть с обучением с учителем как фильтр. Мы используем сверточную нейронную сеть (CNN), размер ядер/каналов которой определяется ядром скалярного произведения (dot product kernel) с механизмом внимания во времени (cross-time attention).
Ядро скалярного произведения с механизмом внимания во времени
Это ядро представляет собой разновидность механизма внимания, в котором, имея две последовательности, обычно являющиеся признаками из разных временных шагов или слоев, вычисляется оценка внимания между каждой парой с помощью их скалярного произведения. Такой подход выявляет взаимосвязи во времени и показывает, как прошлый или будущий признак соотносится с текущим. Если у вас есть следующие две последовательности:
X=[x1,x2,...,xT]
Y=[y1,y2,...,yS]
Для каждой пары (xi,yj) скалярное произведение вычисляется следующим образом:
Scorei,j = xi⋅yj
В качестве альтернативы можно применить функцию softmax к j или i для получения весов внимания, которые затем используются для смешивания/перевзвешивания признаков во времени. Этот подход экономичен по числу параметров — не требуется дополнительное обучение параметров, помимо проекций. Она отличается гибкостью, поскольку может обрабатывать последовательности переменной длины, нерегулярную выборку и межмодальные/временные взаимодействия. Ее преимущество также заключается в том, что она фокусируется на важных моментах, а не только на локальных сверточных областях. Ее применение включает прогнозирование временных рядов, особенно там, где важна задержка, анализ видео, где кадры связаны во времени, анализ речи и любую область с нетривиальными временными зависимостями.
Так почему же мы выбираем именно это ядро для определения размеров ядра/каналов нашей сверточной нейронной сети? Что касается ядер, то они обладают пространственными и временными характеристиками. Во-первых, ядра внимания показывают, какие временные шаги более важны. Возможно, это не так актуально в случаях с небольшими, узко сфокусированными ядрами, однако по мере расширения ядра фиксируется больше контекста. CNN также по своей природе локальны, поэтому механизм внимания может адаптивно расширять/фокусировать эффективное рецептивное поле, влияя на размер ядра в зависимости от временной динамики последовательности данных.
Что касается каналов, ядро внимания может определить, какие каналы или измерения признаков "привлекают наибольшее внимание". Это руководство по обрезке каналов. Если внимание постоянно игнорирует определенные каналы, они становятся балластом и фактически сокращают общее число каналов. Это также показатель расширения. Если внимание рассеяно и отсутствует четкий фокус, то может потребоваться более насыщенное представление информации с большим количеством каналов. Таким образом, наш подход, как и в предыдущих статьях, где мы использовали улучшения, применяемые в сверточных нейронных сетях, заключается в динамической архитектуре сверточной нейронной сети вместо статических размеров ядра/каналов, подходящих для всех случаев. Мы используем паттерны внимания в ходе обучения и инференса для переконфигурации CNN с целью получения более эффективных моделей для нестационарных данных финансовых рынков.
Несмотря на наш динамичный подход, направленный на повышение адаптивности и производительности модели, существуют некоторые недостатки, которые стоит упомянуть. Во-первых, у нас есть вычислительные затраты O(N^2) с длиной входной последовательности данных, тогда как статические CNN обычно требуют затрат O(N). Для длинных последовательностей это нецелесообразно. Во-вторых, мы имеем дело с парадоксом интерпретируемости. Показатели внимания — это хорошо, однако их объединение с динамическим выбором ядра/канала делает модель более сложной для интерпретации и осмысленного извлечения информации, которую можно было бы применять при переносе выводов на внешние сценарии.
Кроме того, они не всегда лучше, поскольку некоторые тесты показали, что в чисто локальных задачах, таких как обнаружение границ на изображениях, классические сверточные нейронные сети могут превосходить модели, сильно опирающиеся на механизм внимания. Кроме того, требования к данным для ядер внимания значительно выше, чем для классических сверточных нейронных сетей, особенно на этапах обучения/оптимизации, чтобы выявить значимые зависимости между временными интервалами. Наконец, существует риск переобучения. Если архитектурные изменения направляются вниманием, то чрезмерная адаптивность может привести к погоне за шумом или случайностью в тестируемых данных.
Возможные альтернативы нашему подходу включают: расширенные свертки, где рецептивное поле расширяется без увеличения размера ядра для охвата широких, но разреженных временных различий; или динамические/адаптивные свертки, где веса ядра устанавливаются на основе характера входных данных или сигналов внимания; или мы могли бы использовать глубинно-разделимые свертки, где нашей переменной является только размер канала, а нашей целью — вычислительная эффективность; или мы могли бы использовать блоки сжатия и возбуждения; или временные сверточные сети. Каждый из этих подходов представляет собой новый поворот в нашем методе, имеющий свои преимущества и недостатки, но здесь они представлены для полноты картины, демонстрируя возможности настройки сверточных нейронных сетей.
Сеть
После этого краткого введения в ядро с механизмом внимания во времени на основе скалярного произведения, давайте разберем код буквально по косточкам. Ниже представлен код этой нейронной сети в виде класса:
import torch import torch.nn as nn import torch.nn.functional as F class DotProductAttentionConv1D(nn.Module): def __init__(self, input_length=100): super(DotProductAttentionConv1D, self).__init__() self.input_length = input_length # Deeper and wider design with attention self.kernel_sizes, self.channels = self._design_architecture() self.conv_layers = nn.ModuleList() self.attention_layers = nn.ModuleList() # For cross-time attention in_channels = 1 for i, (out_channels, kernel_size) in enumerate(zip(self.channels, self.kernel_sizes)): # Convolutional block conv_layer = nn.Sequential( nn.Conv1d(in_channels, out_channels, kernel_size=kernel_size, padding=kernel_size // 2), nn.BatchNorm1d(out_channels), nn.ReLU(), nn.Dropout(0.2)) self.conv_layers.append(conv_layer) # Attention block (cross-time attention) if i % 2 == 0: # Apply attention to every other layer attn_layer = nn.MultiheadAttention(embed_dim=out_channels, num_heads=4) self.attention_layers.append(attn_layer) else: self.attention_layers.append(None) in_channels = out_channels # Fully connected head (same as original) self.head = nn.Sequential( nn.AdaptiveAvgPool1d(1), nn.Flatten(), nn.Linear(in_channels, 128), nn.ReLU(), nn.Dropout(0.2), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 1), nn.Sigmoid() ) def _dot_product_kernel(self, x): """Dot product similarity kernel with positional encoding""" # x shape: (B, C, L) x = x.permute(0, 2, 1) # (B, L, C) # Compute dot product attention attention_scores = torch.bmm(x, x.transpose(1, 2)) # (B, L, L) attention_weights = F.softmax(attention_scores / (x.size(-1) ** 0.5), dim=-1) return torch.bmm(attention_weights, x).permute(0, 2, 1) # (B, C, L) def _design_architecture(self): # Simulate attention response pattern num_layers = 10 kernel_sizes = [3 + (i % 4) * 2 for i in range(num_layers)] # cyclic 3, 5, 7, 9 channels = [32 * (i + 1) for i in range(num_layers)] # 32, 64, ..., 320 return kernel_sizes, channels def forward(self, x): x = x.unsqueeze(1) # (B, 1, L) for conv_layer, attn_layer in zip(self.conv_layers, self.attention_layers): x = conv_layer(x) if attn_layer is not None: # Reshape for attention (MultiheadAttention expects seq_len first) attn_input = x.permute(2, 0, 1) # (L, B, C) attn_output, _ = attn_layer(attn_input, attn_input, attn_input) x = attn_output.permute(1, 2, 0) # (B, C, L) # Also apply dot product kernel x = self._dot_product_kernel(x) return self.head(x)
Во-первых, наш класс наследует от nn.Module, стандарта для нейронных сетей, использующих PyTorch. Параметр input_length задает ожидаемую длину последовательности, однако при внесении изменений класс по-прежнему может принимать входные последовательности переменной длины. Директива super гарантирует инициализацию базового класса, что является очень важным шагом для работы всей "магии" в PyTorch, поэтому ее определенно не стоит пропускать. Таким образом, мы переходим к архитектуре, ядру и размерам каналов.
Как уже упоминалось выше, мы используем динамический подход. место жестко заданных значений модель алгоритмически выбирает размеры ядер и число каналов, что позволяет адаптироваться к паттернам внимания во входных данных, обрабатываемых на каждом прямом проходе. В качестве рекомендации, было бы целесообразно связать статистику внимания из предыдущих запусков или метаобучения с фактическим выбором ядра/канала для каждого слоя.
После выполнения функции проектирования и архитектуры мы переходим к построению слоев. Здесь функция ModuleList позволяет построить сеть переменной глубины. Этот шаг очень важен при динамическом наложении слоев. Затем мы переходим к циклу for, описывающему послойное наложение. На этом этапе для каждого сверточного слоя размер ядра определяется отдельно. Это позволяет использовать широкие или узкие фильтры, в зависимости от ситуации. Ядро внимания впоследствии может подсказать оптимальный выбор. padding, равный kernel_size // 2, обеспечивает обеспечивает одинаковое заполнение, при этом длина выходных данных равна длине входных данных для большинства ядер. Это крайне важно при работе с временными рядами, где необходимо согласовать временные шаги. Наконец, пакетная нормализация, активация ReLU и Drop-out — классический подход глубокого обучения — помогают стабилизировать обучение, ускорить сходимость и регуляризовать модель.
Внутри цикла for, помимо упомянутого выше блока кода свертки, у нас также есть "блок внимания". Здесь мы поочередно добавляем многоголовые (multi-head) слои внимания, ядро трансформера, которое улавливает все зависимости. Мы выполняем это добавление поочередно, а не для каждого слота слоя, главным образом для экономии вычислительных ресурсов. Это позволяет модели переключаться между локальной CNN и глобальным анализом. Наконец, параметру входящих каналов (in-channels) присваивается значение параметра исходящих каналов (out-channels). Это происходит потому, что каждый канал сопоставления признаков становится измерением встраивания, объединяя представления CNN и механизма внимания.
Затем мы переходим к заключительной части функции инициализации класса, где определяем выходной полносвязный блок. Здесь мы используем адаптивное пулинг-преобразование (adaptive pooling) для уменьшения временной размерности до 1 независимо от длины входных данных, поскольку размер выходных данных фиксирован, а затем разворачиваем тензор (Flatten) для подачи в полносвязные слои. Это плотные слои, представляющие собой набор нелинейных преобразований, которые в итоге дают единственный сигмоидный выходной сигнал. Это можно интерпретировать как бинарный прогноз или нормализованный балл, но в конечном итоге это наш прогноз ценового тренда, где значения ниже 0,5 указывают на медвежий тренд, а значения выше 0,5 — на бычий. Метод Dropout предотвращает переобучение на этапе плотного слоя (dense layer stage).
Рассмотрев инициализацию класса этой сети, далее рассмотрим функцию ядра скалярного произведения. При вычислении ядра первым делом мы переставляем размерности входа в формат (B, L, C) для упрощения скалярного произведения по последовательности входных векторов данных. Затем мы выполняем пакетное матричное умножение (batch matrix multiplication, BMM), в ходе которого эффективно вычисляем все попарные скалярные произведения для каждого пакета. Графические процессоры могут еще больше ускорить этот процесс. Затем мы выполняем масштабирование softmax, стандартное для ядра внимания, где температура представляет собой квадратный корень из размера входного вектора или размерности вложения.
Затем мы выполняем заключительное умножение матриц пакетов, где объединяем признаки из разных моментов времени с учетом веса, заданного механизмом внимания. Более того, мы затем повторно переставляем результат умножения, чтобы восстановить его форму до входного вектора, а также подготовить его для дальнейшего использования. Именно эта функция обеспечивает сети возможность отслеживать изменения во времени, что выходит за рамки возможностей обычной свертки, и мы называем её в сети функцией прямого прохода.
Следующая наша специальная функция внутри класса сети — это вспомогательный инструмент для проектирования архитектуры. В предыдущих статьях мы уже использовали аналогичные функции при настройке CNN, и в данном случае мы циклически перебираем различные размеры ядра, чтобы имитировать вариацию, основанную на механизме внимания. Мы также неуклонно увеличиваем количество каналов — это классический подход глубокого обучения, при котором мы стремимся изучать более абстрактные и многомерные представления на более поздних слоях.
Последней функцией класса сети, как обычно, является функция прямого прохода (forward pass). Первым делом мы предварительно обрабатываем входной вектор, добавляя дополнительную промежуточную размерность, или канал, которая является обязательным условием для одномерных сверток. После этого для каждого слоя мы начинаем выполнять свертку, в ходе которой извлекаем локальные признаки из входных данных. Затем мы обрабатываем условное внимание, которое представляет собой проверку наличия слоя внимания. Напомним, что в функции инициализации этого класса мы назначали эти слои внимания поочередно, а не в каждом отдельном слоте.
Если у нас есть слой внимания, мы его перестраиваем, поскольку многоузловой механизм PyTorch ожидает определенный формат (канал, пакет, длина), а у нас есть (пакет, канал, длина). Получив результат работы механизма внимания, мы выполняем вычисление скалярного произведения, вызывая функцию, определенную выше. Также следует отметить, что мы мы используем и блок внимания в стиле Transformer, и dot-product kernel в прямом проходе, однако в качестве альтернативного подхода можно использовать только один из них. Такое переключение можно осуществить, чтобы определить, какой алгоритм обеспечивает более высокую точность модели. Полученные данные затем передаются в узел для классификации/регрессии.
Краткое изложение кода по разделам
| Раздел | Что он делает | Почему важен |
|---|---|---|
| __init__ + super | Базовая настройка класса | Необходим для работы PyTorch |
| self.kernel_sizes, channels | Динамическая разработка CNN | Индивидуально настроенное, адаптивное извлечение признаков |
| Слои ModuleList | Модульная, расширяемая архитектура | Масштабируемость, экспериментирование |
| Сверточные блоки | Извлечение локальных признаков | Преимущества традиционных CNN, пакетная нормализация и т. д. |
| Блоки MultiheadAttention | Глобальные межвременные зависимости | Улавливает зависимости на большом расстоянии |
| _dot_product_kernel | Явное вычисление внимания | Избыточный/взаимосвязанный сигнал для межвременных связей |
| Полностью подключенный узел | Агрегация, вывод | Гибкий подход к решению задач (классификация, регрессия и т. д.) |
| forward logic (прямая логика) | Поток данных через сеть | Обеспечивает правильное выполнение каждой операции |
| _design_architecture | Как выбираются ядра/каналы | Может быть ориентирован на данные или на внимание. |
Функция Awesome Oscillator
Функции индикаторов, используемые в MQL5, не импортируются при использовании модуля MetaTrader 5 в Python. Поэтому нам всегда приходится использовать существующие библиотеки или писать собственный код. Мы использовали второй вариант, поэтому будем придерживаться его и реализуем нашу функцию Awesome Oscillator на Python следующим образом:
def Awesome_Oscillator(df: pd.DataFrame, short_period: int = 5, long_period: int = 34) -> pd.DataFrame: """ Calculate the Bill Williams Awesome Oscillator (AO) and append it to the input DataFrame. AO = SMA(Median Price, short_period) - SMA(Median Price, long_period) Args: df (pd.DataFrame): DataFrame with 'high' and 'low' columns. short_period (int): Short period for SMA (default 5). long_period (int): Long period for SMA (default 34). Returns: pd.DataFrame: Input DataFrame with 'AO' column added. """ required_cols = {'high', 'low'} if not required_cols.issubset(df.columns): raise ValueError("DataFrame must contain 'high' and 'low' columns") if not all(p > 0 for p in [short_period, long_period]): raise ValueError("Period values must be positive integers") result_df = df.copy() median_price = (result_df['high'] + result_df['low']) / 2 short_sma = median_price.rolling(window=short_period).mean() long_sma = median_price.rolling(window=long_period).mean() result_df['AO'] = short_sma - long_sma return result_df
В число импортированных нами модулей для этой функции входит pandas для работы с таблицами/временными рядами — он хорошо справляется с обработкой фреймов данных, а также с применением приемов скользящего окна. Мы также импортируем NumPy, основу для быстрых математических вычислений, которая широко используется внутри системы, хотя мы и не указываем ее здесь явно. Наша сигнатура функции, строка текста после def, является понятной и содержит подсказки о типах входных данных, необходимых для вызова функции. Эта ясность сочетается с автозавершением кода в IDE, поскольку значения по умолчанию для периодов усреднения предопределены. Они соответствуют классическим настройкам Билла Уильямса.
После проверки сигнатуры мы приступаем к проверке входных данных функции, начиная с необходимых. Фрейм данных. Необходимо наличие столбцов high (высокий) и low (низкий). Это проверка корректности, которая предотвращает появление скрытых ошибок в дальнейшем, если отсутствуют столбцы. Кроме того, мы проверяем, являются ли введенные периоды допустимыми беззнаковыми целыми числами. Использование понятных пользователю сообщений об ошибках позволяет предотвратить преждевременное возникновение ошибок, что способствует проведению содержательной диагностики. Такой защитный подход к проверке входных данных очень важен для предотвращения появления значений NaN. Никогда не доверяйте данным извне.
Далее мы создаем копию входного фрейма данных, чтобы гарантировать сохранение исходных данных в их первозданном виде. Это "неразрушающий" метод, и он необходим для конвейеров, в которых индикаторные функции расположены на нескольких уровнях. Затем мы вычисляем медианную цену на основе скопированного набора данных. Это ключевой параметр для AO, отражающий среднюю цену сделки за бар, а не только цену закрытия, и предназначен для предотвращения резких колебаний цены открытия/закрытия. Затем мы вычисляем краткосрочные и долгосрочные сглаженные скользящие средние. Использование метода rolling().mean() создает скользящее среднее, которое является плавным сигналом тренда. В основе логики осциллятора лежит сравнение короткого и длинного среднего. Он измеряет импульс как расстояние между быстрым и медленным трендом.
Наконец, мы добавляем к скопированному фрейму данных новый столбец AO. Это фактическое значение AO. Когда положительная краткосрочная динамика превышает долгосрочную, это свидетельствует о бычьем настрое. Снижение отрицательного краткосрочного импульса является медвежьим сигналом или признаком ослабления бычьего ралли. Как отмечалось в предыдущей статье, AO управляется нулевой линией, поэтому пересечения имеют существенное значение.
Функция канала конвертов
Мы вводим реализацию конвертов на Python следующим образом:
def Envelope_Channels(df: pd.DataFrame, period: int = 20, deviation: float = 0.025) -> pd.DataFrame: """ Calculate Envelope Channels (Upper & Lower Bands) and append to the input DataFrame. Envelope Channels = SMA(close, period) * (1 ± deviation) Args: df (pd.DataFrame): DataFrame with 'close' column. period (int): Period for SMA calculation (default 20). deviation (float): Deviation as a decimal (e.g., 0.025 for 2.5%, default 0.025). Returns: pd.DataFrame: Input DataFrame with 'Envelope_Upper' and 'Envelope_Lower' columns added. """ required_cols = {'close'} if not required_cols.issubset(df.columns): raise ValueError("DataFrame must contain 'close' column") if period <= 0 or deviation < 0: raise ValueError("Period must be positive and deviation non-negative") result_df = df.copy() sma = result_df['close'].rolling(window=period).mean() result_df['Envelope_Upper'] = sma * (1 + deviation) result_df['Envelope_Lower'] = sma * (1 - deviation) result_df['Envelope_Mid'] = 0.5 * (result_df['Envelope_Upper'] + result_df['Envelope_Lower']) return result_df
Как и в случае с AO, сигнатура определяется по типу, и для входных параметров периода и отклонения используются "разумные" значения по умолчанию. Наша проверка входных данных сосредоточена на столбце закрытия. Это наше единственное требование к фрейму данных, заключающееся в защите от получения "мусора на входе — мусора на выходе". Мы также проверяем, чтобы период входного сигнала не был равен нулю и чтобы отклонение было неотрицательным. При несоблюдении этих требований возникает ошибка, приводящая к завершению выполнения скрипта.
Мы также создаем копию входного фрейма данных, которую называем фреймом результирующих данных, что является надежной практикой, как уже говорилось выше и в предыдущих статьях. Первый расчет производится для сглаженной скользящей средней цены закрытия. Конверты строятся на основе этого базового принципа. Более гладкая поверхность SMA означает меньший эффект "пилы", но большее запаздывание. SMA служит центром тяжести полос. Имея в распоряжении эти базовые данные, мы переходим к расчету верхнего и нижнего буферов конверта. Это каналы.
Цена выше верхней границы может сигнализировать о перекупленности, ниже — о перепроданности. Отклонение отображается в десятичном формате, по умолчанию — 2,5% или 0,025. Выбор значения отклонения должен быть оптимальным для торгуемого актива, поскольку это, как правило, очень чувствительная величина. Для более волатильных активов допустимые отклонения могут достигать 5 процентов. Дополнительный буфер, который мы добавляем к фрейму данных, — это средняя линия конверта, и для получения ее значения мы просто берем среднее значение верхней и нижней полос.
Признаки
Мы тестируем 3 из 10 сигнальных паттернов, которые мы представили в предыдущей статье. В данной статье мы называем эти сигнальные паттерны признаками, поскольку они служат входными данными для сети, но для наших целей эти два названия можно использовать взаимозаменяемо. Как уже говорилось в предыдущей статье, и как это было и в этой серии статей, каждая характеристика представляет собой вектор, объединяющий сигналы двух индикаторов. Это последовательность битов, состоящая из нулей и единиц. В предыдущих статьях мы рассматривали возможность расширения размера наших функций с двух значений и сопоставления их с каждым показанием проверки индикатора для получения бычьего или медвежьего сигнала. Результаты тестирования оказались плачевными по сравнению с тем, что мы получали и получаем, когда сосредотачиваемся на общем сигнале бычьего и медвежьего тренда.
Мы возвращаемся к признакам 4, 8 и 9. Таким образом, общая структура этих функциональных возможностей, которую мы здесь используем, не сильно отличается от той, которую мы применяли ранее. Каждая функция выводит двумерный массив NumPy, форма которого соответствует количеству строк в фрейме данных, а количество столбцов равно двум. Индекс [0] каждой строки соответствует уникальному бычьему паттерну (паттерну покупки); в то время как индекс [1] каждой строки соответствует уникальному медвежьему паттерну (паттерну продажи). В нашей системе 1 означает наличие паттерна покупки/продажи, а 0 — его отсутствие. Каждый паттерн представляет собой комбинацию сигналов от двух индикаторов: AO и канала конверта. При сравнении нескольких столбцов мы используем shift[n] в каждом фрейме данных. Таким образом, каждый загруженный фрейм данных должен содержать достаточное количество информации.
Признак 4
Подводя итог, основная логика этой модели заключается в том, что мы отмечаем бычий сигнал, когда AO формирует падение выше нуля, а цена тяготеет к нижней половины конверта. Напротив, медвежий сигнал возникает, когда AO формирует пик ниже нуля, а цена находится в верхней половине конверта. Реализация в Python:
def feature_4(df): """ //+------------------------------------------------------------------+ //| Check for Pattern 4. | //+------------------------------------------------------------------+ """ feature = np.zeros((len(df), 2)) feature[:, 0] = ((df['AO'].shift(2) > df['AO'].shift(1)) & (df['AO'].shift(1) < df['AO']) & (df['AO'].shift(1) > 0.0) & (df['close'].shift(2) >= df['Envelope_Mid'].shift(2)) & (df['close'].shift(2) <= df['Envelope_Lower'].shift(2)) & (df['close'].shift(1) >= df['Envelope_Mid'].shift(1)) & (df['close'].shift(1) <= df['Envelope_Lower'].shift(1)) & (df['close'] >= df['Envelope_Mid']) & (df['close'] <= df['Envelope_Lower'])).astype(int) feature[:, 1] = ((df['AO'].shift(2) < df['AO'].shift(1)) & (df['AO'].shift(1) > df['AO']) & (df['AO'].shift(1) < 0.0) & (df['close'].shift(2) <= df['Envelope_Mid'].shift(2)) & (df['close'].shift(2) >= df['Envelope_Upper'].shift(2)) & (df['close'].shift(1) <= df['Envelope_Mid'].shift(1)) & (df['close'].shift(1) >= df['Envelope_Upper'].shift(1)) & (df['close'] <= df['Envelope_Mid']) & (df['close'] >= df['Envelope_Upper'])).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
В приведенном выше коде мы начинаем с подготовки выходного массива таким образом, чтобы он отражал/предполагал отсутствие сигналов, заполняя его нулями и обеспечивая наличие двух столбцов. Затем мы присваиваем индекс первому столбцу, проверяя, выполнены ли все требования бычьего индикатора. Для того чтобы любой столбец получил оценку 1, необходимо соблюдение как требований к AO, так и требований к оболочке. Условиями для формирования фигуры AO являются снижение, а затем рост импульса или бычий разворот. Эта V-образная форма также должна полностью находиться выше нуля или в бычьей зоне. Условия для формирования конверта: цена находится вблизи или ниже средней линии конверта, но не в зоне перепроданности. Это можно приравнять к коррекции, но не к пробою, что хорошо для затухания тренда или его возобновления.
Присвоение одному столбцу значения 1 с большой долей вероятности означает, что в другом столбце будет значение ноль, поскольку эти сигналы являются зеркальным отражением друг друга. Таким образом, наша медвежья логика представляет собой инверсию рассмотренной выше. Пик AO должен находиться ниже нуля, а цена - в верхней половине конверта. Последнее, что мы делаем в функции признака 4, это присваиваем нули первым двум строкам, поскольку сравнения проводились с использованием до двух индексов. Как уже отмечалось ранее, это предотвращает случайные сигналы от NaN, возникающие в результате сдвига в начале серии.
Мы проводим оптимизацию и тесты прямого запуска для признака 4, когда дополняем его нашей сверточной нейронной сетью, указанной выше, в качестве фильтра. Как уже говорилось в предыдущей статье, наш символ — USD/JPY, таймфрейм — 30 минут, и, как всегда, по крайней мере в этом году, тестовый период — 2023 и 2024 годы. Функция лучше проходит форвард-тест, хотя и с убытком, по сравнению с тем, что мы видели в предыдущей статье. Отчет:


Признак 8
Как уже говорилось в предыдущей статье, этот сигнальный паттерн имеет бычий сигнал AO, и цена стабильно растет, при этом AO находится выше нуля, а цена отходит от нижней границы конверта. Согласно медвежьей логике, AO и цена находятся в нисходящем движении, при этом AO опускается ниже нуля, а цена падает ниже нижней границы. Реализуем эту функцию на Python следующим образом:
def feature_8(df): """ //+------------------------------------------------------------------+ //| Check for Pattern 8. | //+------------------------------------------------------------------+ """ feature = np.zeros((len(df), 2)) feature[:, 0] = ((df['AO'].shift(2) > 0.0) & (df['AO'].shift(1) > df['AO'].shift(2)) & (df['AO'] > df['AO'].shift(1)) & (df['close'].shift(2) > df['Envelope_Lower'].shift(2)) & (df['close'].shift(1) > df['close'].shift(2)) & (df['close'] > df['close'].shift(1))).astype(int) feature[:, 1] = ((df['AO'].shift(2) < 0.0) & (df['AO'].shift(1) < df['AO'].shift(2)) & (df['AO'] > df['AO'].shift(1)) & (df['close'].shift(2) < df['Envelope_Lower'].shift(2)) & (df['close'].shift(1) < df['close'].shift(2)) & (df['close'] < df['close'].shift(1))).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
Таким образом, согласно приведенному выше коду, мы присваиваем первому индексу значение 1, подтверждающее наличие бычьего сигнала, если AO продолжает двигаться вверх, а цена демонстрирует явное ускорение из положительной зоны. Нам также необходимо подтвердить, что цена остается выше нижнего конверта и что это не фальшивый отскок. Второй индекс получает 1, если AO изначально имеет отрицательное значение, падая, но затем, возможно, повышается в результате коррекции, в то время как цена продолжает падать в сильном нисходящем тренде ниже нижнего предела. Затем мы обнуляем первые 2 индекса, как и в случае с признаком 4. При тестировании признака 8 нам по-прежнему не удается получить прибыльный форвард-тест. Отчет:


Признак 9
Основная логика нашей финальной сигнальной модели, как упоминалось в предыдущей статье, заключается в том, что AO разворачивается, но остается выше нуля, в то время как цена опускается к средней линии и отскакивает, формируя бычий сигнал. В случае медвежьего сигнала, AO повышается, но остается ниже нуля, цена поднимается до средней линии, но не может преодолеть ее. Реализация в Python:
def feature_9(df): """ //+------------------------------------------------------------------+ //| Check for Pattern 9. | //+------------------------------------------------------------------+ """ feature = np.zeros((len(df), 2)) feature[:, 0] = ((df['AO'].shift(2) > df['AO'].shift(1)) & (df['AO'].shift(1) > df['AO']) & (df['AO'] > 0.0) & (df['close'].shift(2) > df['Envelope_Mid'].shift(2)) & (df['close'].shift(1) <= df['Envelope_Mid'].shift(1)) & (df['close'] > df['Envelope_Mid'])).astype(int) feature[:, 1] = ((df['AO'].shift(2) < df['AO'].shift(1)) & (df['AO'].shift(1) < df['AO']) & (df['AO'] < 0.0) & (df['close'].shift(2) < df['Envelope_Mid'].shift(2)) & (df['close'].shift(1) >= df['Envelope_Mid'].shift(1)) & (df['close'] < df['Envelope_Mid'])).astype(int) feature[0, :] = 0 feature[1, :] = 0 return feature
Наша философия внедрения соответствует двум функциям, которые мы рассмотрели выше, поэтому я не буду повторять уже обсужденные ключевые моменты. Проверка нашего финального паттерна показывает, что мы получаем благоприятный вариант форвард-теста. Отчет представлен ниже:


Заключение
Мы исследовали интеграцию машинного обучения, в частности, путем применения CNN с ядром скалярного произведения и механизмом внимания во времени, для улучшения сигналов AO и каналов конверта. В ходе наших тестовых запусков мы сосредоточились на трех ранее протестированных паттернах (4, 8 и 9), чтобы установить, может ли добавление расширенной нейронной фильтрации улучшить их точность прогнозирования.
Наш динамический метод CNN оказался плодотворным, особенно для признаков 4 и 9, где мы превратили ранее неудачные форвард-тесты в запуски с меньшими убытками. Механизм внимания нашего ядра позволил сети динамически адаптировать как размеры ядра, так и параметры канала в зависимости от временной значимости, что в итоге позволило выявить более глубокие закономерности в поведении рынка. Такая адаптивность оказалась для нас очень полезной, поскольку мы работаем на нестационарных финансовых рынках. Дело в том, что жесткие модели обычно показывают худшие результаты из-за изменений волатильности, динамики трендов или рыночного режима.
Однако не все результаты были положительными. Признак 8, в значительной степени опирающийся на устойчивый направленный импульс, не принес прибыли даже с учетом улучшенной адаптивности CNN. Это говорит о том, что некоторые сигналы имеют ограниченную ценность, независимо от улучшений модели, что подчеркивает важность выбора индикаторов и сигналов в торговой стратегии. Помимо обучения с учителем, которое мы рассмотрели в этой статье, мы можем вернуться к обучению с подкреплением как средству улучшения наших сигналов. В будущих статьях этот аспект можно будет рассмотреть в контексте обсуждаемых здесь функций.
| файл | описание |
|---|---|
| WZ-76.mq5 | Созданный Мастером советник, в заголовок которого включены использованные файлы |
| SignalWZ-76.mqh | Класс пользовательского сигнала, используемый Мастером MQL5 в сборке |
| 76-4.onnx | Экспортируемая сеть для сигнального паттерна 4 |
| 76-8.onnx | Экспортируемая сеть для сигнального паттерна 8 |
| 76-9.onnx | Экспортируемая сеть для сигнального паттерна 9 |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18878
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Создаем объемные 3D бары на MQL5
Торговые инструменты MQL5 (Часть 25): Расширяем поддержку нескольких распределений с интерактивным переключением
Моделирование рынка: Первые шаги на SQL в MQL5 (III)
Адаптивный индикатор Malaysian Engulfing (Часть 1): Обнаружение паттернов и валидация ретеста
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования