English Deutsch 日本語
preview
Возможности Мастера MQL5, которые вам нужно знать (Часть 79): Использование осцилляторов Gator и A/D в обучении с учителем

Возможности Мастера MQL5, которые вам нужно знать (Часть 79): Использование осцилляторов Gator и A/D в обучении с учителем

MetaTrader 5Торговые системы |
39 0
Stephen Njuki
Stephen Njuki

Введение

В двух предыдущих статьях мы, как обычно, протестировали десять сигнальных паттернов при использовании осцилляторов Gator и накопления/распределения. При этом, как и в предыдущих статьях, мы выявили три стабильно отстающих паттерна: 0, 3 и 4. Вместо того чтобы отбрасывать эти паттерны, мы рассмотрим, может ли обучение с учителем улучшить их результаты. Мы используем CNN, дополненную ядерной регрессией и сходством по скалярному произведению, и проверяем, способна ли сеть такой архитектуры извлечь скрытый потенциал из сигналов, которые поначалу могут казаться слабыми. Как и в двух предыдущих статьях, все тесты проводятся на паре GBP/JPY и на 30-минутном таймфрейме.



Ядерная регрессия со сходством по скалярному произведению

В качестве модели обучения с учителем мы выбрали CNN, использующую ядерную регрессию и сходство по скалярному произведению. Ядерная регрессия оценивает выходные представления каждого слоя CNN, взвешивая наблюдаемые входные данные по степени их сходства с конкретной точкой запроса. Это сходство количественно оценивается с помощью скалярного произведения, используемого в качестве ядерной функции. В нашей модели, код которой приведен в следующем разделе, метод _dot_product_kernel_regression вычисляет сходство между всеми временными позициями на карте признаков с помощью функции torch.bmm(x, x.T). Затем к результату применяется нормализация Softmax, во многом аналогичная механизму самовнимания.

Скалярное произведение выступает адаптивной к данным мерой сходства: его высокое значение означает, что две позиции имеют одинаковый паттерн активации по каналам и поэтому сильнее влияют на взвешенную сумму. Это можно представить так: каждая позиция получает "голоса", число которых пропорционально ее сходству с позицией запроса. Слой CNN можно представить как таблицу, в которой строки соответствуют временным шагам, а столбцы – признакам. В этой модели одна строка соответствует "позиции". Таким образом, "позиция запроса" – это текущая строка, которая обновляется. В процессе обновления эта строка сравнивается со всеми остальными позициями, чтобы количественно оценить степень их сходства. Такие обновления происходят в ходе обратного распространения ошибки.

Математически новые веса можно определить следующим образом. Для векторов признаков xi и xj вес wij определяется следующим образом:


Где:

  • d – размерность канала;
  • exp – экспоненциальная функция.

Наша матрица весов выполняет нелокальное сглаживание по всей последовательности, не ограничиваясь, в отличие от свертки, соседними позициями. Благодаря этому модель способна выявлять дальние зависимости и корреляции в пространстве признаков, что потенциально повышает качество решения задач, связанных с временными рядами и структурированными данными. Сходство по скалярному произведению стоит использовать, когда задача содержит значимые корреляции между несмежными признаками и требуется механизм, аналогичный механизму внимания, но без добавления проекций запроса, ключа и значения. Этот подход особенно хорошо сочетается с CNN: сеть извлекает локальные признаки, а ядерная регрессия добавляет глобальный контекст.


Выбор размеров ядер и количества каналов CNN на основе сходства по скалярному произведению

Ядерная регрессия на основе скалярного произведения показывает, где и при каком разрешении корреляции наиболее сильны. Настройка размеров ядер CNN и количества каналов в соответствии со структурой сходства дает два преимущества. Во-первых, это помогает соотнести рецептивные поля с длиной корреляции. Это особенно актуально для больших ядер, когда высокая степень сходства сохраняется на больших расстояниях. Во-вторых, это позволяет выбирать количество каналов в соответствии со сложностью корреляций. Большее число каналов выделяется при существенном различии паттернов сходства, а меньшее число – при их большей однородности. В результате можно получить архитектуру, одновременно ориентированную на данные и специфику задачи, а не на фиксированные эвристики.

В нашей реализации, код которой приведен в следующих разделах, размеры ядер циклически меняются в последовательности [3,5,7,9], а количество каналов увеличивается с 32 до 320. Каждый второй слой применяет регрессию на основе скалярного произведения, благодаря чему слои "видят" глобальные карты корреляций. Если карты сходства показывают широкие пики, в последующих сверточных слоях можно использовать более крупные ядра. Если карта показывает резкие пики, может быть достаточно меньших ядер, что позволит сосредоточиться на локальных уточнениях. Такой подход можно сравнить с адаптивной настройкой рецептивного поля на основе глобального корреляционного сигнала.

При использовании этого подхода в CNN стоит предварительно пропустить данные через небольшую CNN и блок ядерной регрессии на основе скалярного произведения. Анализ длины корреляций может стать основой для выбора размеров ядер и количества каналов основной CNN. Кроме того, для ранних слоев при локальном обнаружении границ и паттернов предпочтительнее использовать меньшие ядра, а размеры ядер последующих слоев выбирать по наблюдаемой ширине распределения сходства.


Возможные недостатки и альтернативы

Главный недостаток используемого нами ядра скалярного произведения вполне ожидаем – это высокие вычислительные затраты. Вычислительная сложность сходства по скалярному произведению составляет O(L2) при длине последовательности L. Для длинных сигналов или больших наборов входных признаков это может быть ресурсоемко. Еще одна проблема – чрезмерное сглаживание. Если веса распределены слишком равномерно, признаки могут стать слишком однородными, что приведет к потере мелких деталей. Еще одна проблема – нестабильность обучения. При высоких значениях сходства отсутствие нормализации может привести к взрывному росту весов. Наконец, затраты памяти на хранение полной матрицы сходства могут оказаться неприемлемо высокими. 

Однако эти проблемы можно смягчить, например используя локальные окна внимания. Вместо вычисления сходства между всеми позициями с учетом приведенной выше оценки вычислительной сложности сравнения ограничиваются фиксированным окном вокруг каждой позиции запроса. Это сократит вычислительные затраты и потребление памяти, сосредоточив обработку на близлежащем релевантном контексте. Кроме того, можно использовать температурное масштабирование. Для этого значения сходства умножаются на 1/𝜏, где 𝜏 – температура. Более низкая температура приводит к более резким и избирательным весам, при которых доминируют несколько позиций или строк в слое. Более высокое значение 𝜏 дает более сглаженные и равномерно распределенные веса по всей сетке слоя. Температура становится дополнительным гиперпараметром.

Наконец, перед выполнением ядерной регрессии можно применить понижающую дискретизацию. Для этого перед вычислением скалярного произведения длина входной последовательности данных L уменьшается с помощью пулинга или сверток с шагом. Уменьшение L должно ускорить вычисление сходства и снизить потребление памяти, сохранив возможность выявлять крупномасштабные корреляции.


Альтернативные способы улучшения CNN на основе скалярного произведения

Помимо ядерной регрессии на основе скалярного произведения, можно использовать гибридные сверточные нейронные сети с механизмом самовнимания. В таком варианте регрессия на основе скалярного произведения используется как нелокальный блок наряду со слоями CNN, а не как стандартный слой внимания. Этот подход отличается от нашей реализации, исходный код которой приведен в следующем разделе, тем, что в нашей модели ядерная регрессия периодически применяется в стеке сверточных слоев. В гибридной архитектуре этот блок размещается в параллельных ветвях с последующим слиянием выходов. Еще один вариант – использовать ядерную регрессию для многомасштабного слияния признаков: она применяется к признакам из нескольких слоев CNN, после чего результаты объединяются. Отличие от нашего подхода в том, что ядерная регрессия применяется только к одному слою за раз. Многомасштабное слияние объединяет глобальный контекст из разных слоев и уровней разрешения. 

Ядерную регрессию также можно было бы использовать в предтекстовых задачах в целях регуляризации. В этом сценарии выходы ядерной регрессии обучаются решать вспомогательные задачи наряду с основной, например предсказывать эмбеддинг следующего шага. Это отличается от изложенного ниже подхода: в нашей реализации ядро используется только для уточнения признаков, а в данном случае также выполняет функцию регуляризации. Наконец, ядерную регрессию можно было бы применить в подходе, который мы рассматривали в предыдущих статьях. Динамический выбор размера ядра. В этом подходе статистика сходства, полученная ядерной регрессией, используется для настройки размеров сверточных ядер во время обучения и инференса. В описанном ниже подходе размеры ядер задаются заранее.


Сеть

Ниже приведен код класса нашей сети, в котором реализована ядерная регрессия на основе скалярного произведения:

class DotProductKernelRegressionConv1D(nn.Module):
    def __init__(self, input_length=100):
        super().__init__()
        self.input_length = input_length

        self.kernel_sizes, self.channels = self._design_architecture()

        self.conv_layers = nn.ModuleList()
        self.use_kernel_regression = []  # <-- Python list for markers

        in_channels = 1
        for i, (out_channels, kernel_size) in enumerate(zip(self.channels, self.kernel_sizes)):
            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)

            self.use_kernel_regression.append(i % 2 == 0)
            in_channels = out_channels

        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_regression(self, x):
        """
        Dot product kernel regression.
        For each time position, outputs a weighted sum of all positions using dot product similarity as weights.
        """
        # x: (B, C, L)
        x_ = x.permute(0, 2, 1)  # (B, L, C)
        # Dot product similarity
        sim = torch.bmm(x_, x_.transpose(1, 2))  # (B, L, L)
        # Optionally normalize for stability (like attention)
        weights = F.softmax(sim / (x_.size(-1) ** 0.5), dim=-1)  # (B, L, L)
        out = torch.bmm(weights, x_)  # (B, L, C)
        return out.permute(0, 2, 1)  # (B, C, L)

    def _design_architecture(self):
        num_layers = 10
        kernel_sizes = [3 + (i % 4) * 2 for i in range(num_layers)]
        channels = [32 * (i + 1) for i in range(num_layers)]
        return kernel_sizes, channels

    def forward(self, x):
        x = x.unsqueeze(1)  # (B, 1, L)
        for conv_layer, use_kr in zip(self.conv_layers, self.use_kernel_regression):
            x = conv_layer(x)
            if use_kr:
                x = self._dot_product_kernel_regression(x)
        return self.head(x)

В начале приведенного выше листинга объявляются каркас класса и конструктор. Класс наследуется от nn.Module, что позволяет использовать отслеживание параметров PyTorch, а также методы .to(device), .eval() и т.д. Значение input_length сохраняется, хотя свертки не зависят от длины входной последовательности. Это полезно для контрольных проверок, экспорта в ONNX и последующей реализации логики, зависящей от длины входной последовательности. Далее мы переходим к архитектурной схеме, в которой задаем размеры ядер свертки и число каналов. Это важно, поскольку в одном месте задаются план рецептивных полей (размеры ядер свертки) и план емкости сети (число каналов). Такое размещение в одном месте дает два преимущества. Во-первых, размеры ядер свертки можно согласовать с характером затухания сходства, наблюдаемым в ядерной регрессии. Во-вторых, это позволяет масштабировать модель при более сложных паттернах сходства.

При проектировании функции _design_architecture следует опираться на данные. Можно проанализировать пакет данных, измерить затухание сходства в блоке регрессии и на этой основе увеличить размеры ядер свертки. Кроме того, можно связать число каналов с энтропией распределения сходства. В таком случае более высокая энтропия будет означать большее число каналов, необходимых для моделирования большей вариативности данных.

Далее код определяет стек сверточных слоев и метки, указывающие, где применяется регрессия. Это важно, поскольку ModuleList регистрирует слои и тем самым обеспечивает сохранение и загрузку их весов. Выбор паддинга, равного половине размера ядра свертки, позволяет почти полностью сохранить длину последовательности. Это важно, поскольку на следующих этапах ядерная регрессия предполагает постоянную длину последовательности.

Пакетная нормализация, активация ReLU и слои прореживания (dropout) также повышают устойчивость модели: пакетная нормализация стабилизирует обучение, ReLU добавляет нелинейность, а слои прореживания обеспечивают регуляризацию. Булев параметр use_kernel_regression позволяет не применять вычислительно затратную ядерную регрессию в каждом слое, а использовать ее только там, где она полезна. При сильном влиянии ядерной регрессии вокруг блоков conv_layer можно включить остаточные связи, чтобы упростить оптимизацию.

Далее мы определяем выходной блок предсказания. Такая последовательность слоев важна, поскольку включает несколько ключевых компонентов. Во-первых, AdaptiveAvgPool1d(1) формирует для каждого канала глобальное сводное представление, не зависящее от длины последовательности. Это важно при переменной длине входных данных. Иными словами, он сжимает временной ряд переменной длины каждого канала до одного среднего значения. В результате получается сводный вектор фиксированного размера. Благодаря этому выход сети не зависит от длины входной последовательности. Это особенно важно, когда число временных шагов различается между образцами.

MLP преобразует сводный вектор признаков в скалярное значение на выходе. Это подходит для регрессии в диапазоне от 0.0 до 1.0. Поэтому в данном случае целесообразно использовать Sigmoid; для неограниченного регрессионного выхода можно выбрать Identity, а для многоклассового – LogSoftmax. Если требуется различать направления, например длинные и короткие позиции, Sigmoid можно заменить двухголовой выходной структурой. Первый выходной блок может формировать вероятностный выход с помощью Sigmoid. Второй выходной блок может оценивать величину, используя функцию активации Softplus. Если требуется интерпретируемость, агрегированный вектор можно использовать для расчета важности каждого из каналов.

После этого мы определяем основную функцию ядерной регрессии. Сначала мы переставляем оси входного тензора x, приводя его к формату "размер пакета – число каналов – длина". После этого скалярное произведение применяется к представлениям каналов в каждой позиции. Поэтому C, то есть число каналов, становится внутренней размерностью. Затем мы строим нелокальную матрицу сходства для всех позиций с вычислительной сложностью порядка квадрата длины последовательности. Это матрица sim. Затем мы определяем веса, масштабируя матрицу sim делением на квадратный корень из числа каналов, чтобы с ростом размерности каналов логиты не принимали чрезмерно большие значения. Активация Softmax преобразует исходные значения сходства в вероятностный симплекс. Каждая строка слоя становится выпуклой комбинацией всех строк. На следующем этапе выполняется ядерная регрессия: эти веса умножаются на транспонированный входной тензор x с переставленными осями. После этого каждая позиция заимствует информацию у сходных позиций по всему окну.

Для длинных последовательностей используйте локальные окна или блочно-диагональный механизм внимания, методы низкого ранга, такие как Nyström или Linformer, либо понижайте разрешение, выполняйте ядерную регрессию и затем восстанавливайте разрешение. Для прогнозирования или каузальных задач добавление каузальной маски перед применением Softmax помогает предотвратить утечку данных и использование будущей информации. Если заметно чрезмерное сглаживание, можно ввести гиперпараметр температуры:

weights = F.softmax(sim / (tau * x_.size(-1) ** 0.5), dim=-1)

x = x + alpha * self._dot_product_kernel_regression(x)

Следующая функция задает архитектуру модели. Эта функция циклически задает размеры ядер свертки от 3 до 9, обеспечивая многомасштабный охват, чтобы каждый этап учитывал глобальный контекст ядерной регрессии. Линейный рост числа каналов повышает емкость глубоких слоев сети, где признаки становятся более абстрактными. Рекомендуется адаптировать цикл к измеренной ширине сходства: чем она больше, тем сильнее смещение в сторону больших ядер свертки в последующих слоях. Число каналов не следует увеличивать без учета данных; вместо этого нужно контролировать разреженность активаций и энтропию ядерной регрессии, чтобы распределять каналы там, где они необходимы.

Последняя функция класса сети реализует прямой проход. Здесь сначала добавляется измерение, чтобы привести данные к ожидаемому формату "пакет – канал – длина". В одномерных сверточных нейронных сетях число входных каналов изначально равно 1. Чередование локальной свертки и глобальной ядерной регрессии образует эффективную архитектурную схему. После обнаружения локальных паттернов информация распространяется в соответствии с усвоенным сходством. Затем на следующих сверточных слоях выполняется локальное уточнение. Возврат self.head() сохраняет граф прямого прохода простым и удобным для экспорта в ONNX и развертывания.

Если требуется улучшить распространение градиента, до и после ядерной регрессии или остаточных связей можно добавить нормализацию слоев.


Реализация индикатора

Чтобы использовать сигнальные паттерны из двух предыдущих статей как входные данные для описанной выше модели сверточной нейронной сети с ядерной регрессией, нам нужен эквивалент индикатора на Python. В Python есть несколько готовых модулей индикаторов, однако собственная реализация с нуля оказалась столь же несложной и позволяет полностью контролировать решения, принимаемые при расчете значений индикатора. Как и в предыдущих статьях, мы кодируем показания индикатора как бычьи или медвежьи сигналы в двумерном векторе, принимающем только значения 0 и 1. Здесь полезна ядерная регрессия, поскольку извлеченные признаки кодируют структурные паттерны баров и осцилляторов. Удаленные позиции со сходными паттернами или представлениями, близкими по скалярному произведению, могут усиливать друг друга. Поэтому ядерная регрессия (KR) позволяет модели заимствовать статистическую силу у сходных участков временного ряда. Это особенно ценно, когда каждый паттерн сам по себе относительно редок или слаб.

Кроме того, ранний выбор места применения KR помогает распространить простейшие паттерны по сети. Применение KR на поздних этапах объединяет паттерны более высокого уровня. Разумно протестировать менее глубокую KR на ранних этапах с меньшим числом каналов и более глубокую KR на поздних этапах с большим числом каналов.


Осциллятор Gator

Эта функция формирует компоненты Alligator Билла Вильямса, используя сглаженные скользящие средние медианной цены и их прямые сдвиги для "челюсти", "зубов" и "губ". Это уже рассматривалось в предыдущих статьях. Мы выводим два буфера: модуль разности между "челюстью" и "зубами" с меткой "gator up", а также отрицательный модуль разности между скользящими средними "зубов" и "губ". Мы также добавляем цветовые состояния, отражающие расширение и сжатие. Такова общая логика функции, которую мы реализуем в Python:

def Gator_Oscillator(df: pd.DataFrame,
                     jaw_period: int = 13, jaw_shift: int = 8,
                     teeth_period: int = 8, teeth_shift: int = 5,
                     lips_period: int = 5, lips_shift: int = 3) -> pd.DataFrame:
    """
    Calculate the Bill Williams Gator Oscillator and append the columns to the input DataFrame.

    Adds color columns for each bar:
    - Gator_Up_Color: 'green' for increasing, 'red' for decreasing, else previous color.
    - Gator_Down_Color: 'green' for increasing (less negative), 'red' for decreasing (more negative), else previous color.

    Args:
        df (pd.DataFrame): DataFrame with 'high' and 'low' columns.
        jaw_period (int): Jaw period (default 13).
        jaw_shift (int): Jaw shift (default 8).
        teeth_period (int): Teeth period (default 8).
        teeth_shift (int): Teeth shift (default 5).
        lips_period (int): Lips period (default 5).
        lips_shift (int): Lips shift (default 3).

    Returns:
        pd.DataFrame: Input DataFrame with 'Gator_Up', 'Gator_Down', 'Gator_Up_Color', 'Gator_Down_Color'.
    """
    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 [jaw_period, jaw_shift, teeth_period, teeth_shift, lips_period, lips_shift]):
        raise ValueError("Period and shift values must be positive integers")

    result_df = df.copy()
    median_price = (result_df['high'] + result_df['low']) / 2

    def smma(series, period):
        smma_vals = []
        smma_prev = series.iloc[0]
        smma_vals.append(smma_prev)
        for price in series.iloc[1:]:
            smma_new = (smma_prev * (period - 1) + price) / period
            smma_vals.append(smma_new)
            smma_prev = smma_new
        return pd.Series(smma_vals, index=series.index)

    jaw = smma(median_price, jaw_period).shift(jaw_shift)
    teeth = smma(median_price, teeth_period).shift(teeth_shift)
    lips = smma(median_price, lips_period).shift(lips_shift)

    result_df['Gator_Up'] = (jaw - teeth).abs()
    result_df['Gator_Down'] = -(teeth - lips).abs()

    # Color logic
    up_vals = result_df['Gator_Up'].values
    down_vals = result_df['Gator_Down'].values

    up_colors = ['green']  # Start with green (or change to None/'grey' if you want)
    for i in range(1, len(up_vals)):
        if pd.isna(up_vals[i]) or pd.isna(up_vals[i-1]):
            up_colors.append(up_colors[-1])
        elif up_vals[i] > up_vals[i-1]:
            up_colors.append('green')
        elif up_vals[i] < up_vals[i-1]:
            up_colors.append('red')
        else:
            up_colors.append(up_colors[-1])

    down_colors = ['green']  # Start with green for "less negative" (getting closer to zero is "increasing")
    for i in range(1, len(down_vals)):
        if pd.isna(down_vals[i]) or pd.isna(down_vals[i-1]):
            down_colors.append(down_colors[-1])
        elif down_vals[i] > down_vals[i-1]:
            down_colors.append('green')  # "less negative" = "up"
        elif down_vals[i] < down_vals[i-1]:
            down_colors.append('red')    # "more negative" = "down"
        else:
            down_colors.append(down_colors[-1])

    result_df['Gator_Up_Color'] = up_colors
    result_df['Gator_Down_Color'] = down_colors

    return result_df

Если рассматривать код построчно, сначала функция защищается от некорректных входных данных и недопустимых гиперпараметров. Значения сдвига должны быть положительными, поскольку линии Alligator намеренно смещаются вперед. Они должны визуально опережать цену. Затем мы создаем рабочую копию входного датафрейма и рассчитываем вектор медианной цены. Это сохраняет исходные данные без изменений, а медианная цена снижает шум по сравнению с использованием только цены закрытия. Это в целом соответствует общепринятой практике использования Alligator.

После этого мы определяем реализацию сглаженного скользящего среднего. SMMA – рекурсивный метод сглаживания, который реагирует медленнее обычной EMA и поэтому подходит для фильтрации рваных временных рядов. Инициализация первого значения через series.iloc[0] позволяет избежать длительного периода разогрева. После этого мы задаем линии Alligator и величины их прямого сдвига. Периоды уменьшаются от "челюстей" к "зубам" и затем к "губам". Все линии сдвинуты вперед, поэтому "рот" индикатора визуально открывается или закрывается с опережением относительно текущего бара.

После этого мы определяем два выходных буфера: gator up и gator down. В них записывается абсолютная величина разности, то есть степень раскрытия "челюстей" Alligator. Для визуализации барам присваивается положительный или отрицательный знак. Наконец, мы реализуем логику цветового кодирования, учитывающую динамику спреда: расширяется он или сужается. Нижний бар отображается ниже нулевой линии, поэтому его значения отрицательны. Если бар движется вверх, это не означает, что он стал положительным: его значение лишь стало менее отрицательным. При обработке NaN сохраняются предыдущие состояния, чтобы избежать мерцания в начале нестабильного участка.


Осциллятор A/D

Этот осциллятор рассчитывает линию накопления/распределения на основе произведения множителя денежного потока на объем. Он оценивает соотношение давления покупателей и продавцов по положению цены закрытия в диапазоне бара с учетом объема или тикового объема. Реализуем это в Python следующим образом:

def AD_Oscillator(df: pd.DataFrame, fast_period: int = 5, slow_period: int = 13) -> pd.DataFrame:
    """
    Calculate the Accumulation/Distribution Oscillator (A/D Oscillator) and append it to the input DataFrame.

    A/D Oscillator = EMA(ADL, fast_period) - EMA(ADL, slow_period)

    ADL (Accumulation/Distribution Line) is calculated as:
    Money Flow Multiplier = [(Close - Low) - (High - Close)] / (High - Low)
    Money Flow Volume = Money Flow Multiplier * Volume
    ADL = cumulative sum of Money Flow Volume

    Args:
        df (pd.DataFrame): DataFrame with 'high', 'low', 'close', 'volume' columns.
        fast_period (int): Fast EMA period (default 3).
        slow_period (int): Slow EMA period (default 10).

    Returns:
        pd.DataFrame: Input DataFrame with 'ADL' and 'AD_Oscillator' columns added.
    """
    required_cols = {'high', 'low', 'close', 'tick_volume'}
    if not required_cols.issubset(df.columns):
        raise ValueError("DataFrame must contain 'high', 'low', 'close', 'tick_volume' columns")
    if not all(p > 0 for p in [fast_period, slow_period]):
        raise ValueError("Period values must be positive integers")
    if fast_period >= slow_period:
        raise ValueError("fast_period must be less than slow_period")

    result_df = df.copy()
    high = result_df['high']
    low = result_df['low']
    close = result_df['close']
    volume = result_df['tick_volume']

    # Avoid divide by zero, replace zero ranges with np.nan
    range_ = high - low
    range_ = range_.replace(0, pd.NA)

    mfm = ((close - low) - (high - close)) / range_
    mfv = mfm * volume
    result_df['ADL'] = mfv.cumsum()

    fast_ema = result_df['ADL'].ewm(span=fast_period, adjust=False).mean()
    slow_ema = result_df['ADL'].ewm(span=slow_period, adjust=False).mean()
    result_df['AD_Oscillator'] = fast_ema - slow_ema

    return result_df

Как и при разборе Gator выше, первые строки кода проверяют наличие во входном датафрейме всех четырех полей цены и объема. Поскольку, как и в предыдущих статьях, мы тестируем валютные пары, в качестве объема используем тиковый, а не реальный объем по причинам, указанным ранее. Затем мы проверяем, что это "корректно заданный осциллятор": медленный период должен быть больше быстрого. Затем мы извлекаем данные для расчета диапазона, исключая деление на ноль. Если цена максимума равна цене минимума, например у свечей доджи или при внерыночных котировках, диапазон равен нулю, что может привести к чрезмерно большим значениям множителя. 

После этого мы рассчитываем множитель денежного потока и объем. Значение множителя ограничено диапазоном: высокие значения близки к +1, а низкие – к -1. Этот буфер обозначается как mfm, а его эквивалент с учетом объема – как mfv. Функция cumsum накапливает это давление, взвешенное по объему, с течением времени, формируя линию A/D. Затем мы рассчитываем буферы быстрой и медленной EMA для линии A/D. Если параметру adjust присвоено значение false, получается стандартная рекурсивная EMA, к которой привыкло большинство трейдеров. Разность этих буферов усиливает сигнал: положительные значения указывают, что недавнее накопление может опережать долгосрочный тренд.


Выбранные сигнальные паттерны

Каждая из рассматриваемых функций паттернов: Функции 0, 3 и 4 возвращают массив NumPy формы (длина_входного_датафрейма, 2). Как и в предыдущих статьях, столбец [:, 0] кодирует длинный (бычий) сигнал на основе значений индикатора, определенных в предыдущей статье. Если паттерн бычий, записывается 1, в противном случае – 0. В то же время столбец [:, 1] кодирует короткий (медвежий) сигнал: 1 записывается при наличии паттерна, а 0 – при отсутствии медвежьего сигнала. Во всех трех сигнальных паттернах объединяются цвета осциллятора Gator для верхней и нижней гистограмм, ограничения Price Action на основе сравнений цен максимума, минимума и закрытия с функцией shift, а также моментум и давление по объему через индикатор A/D с аналогичными сравнениями со сдвигом. Во всех функциях первые строки, число которых зависит от глубины сравнения, обнуляются, чтобы избежать ложных срабатываний.

Признак 0

Как отмечалось в предпоследней статье, этот сигнал представляет собой дивергенцию Gator и пробой, подтвержденный индикатором A/D. Реализация этого признака в Python выглядит следующим образом:

def feature_0(df):
    """
//+------------------------------------------------------------------+
//| Check for Pattern 0.                                             |
//+------------------------------------------------------------------+

    """
    feature = np.zeros((len(df), 2))
    
    cond_1 = df['Gator_Up_Color'] == 'red'
    cond_2 = df['Gator_Down_Color'] == 'green'

    feature[:, 0] = (cond_1 &
                     cond_2 &
                     (df['high'].shift(2) > df['high'].shift(3)) &
                     (df['high'].shift(1) >= df['high'].shift(2)) &
                     (df['close'] > df['high'].shift(1)) &
                     (df['AD_Oscillator'].shift(1) > df['AD_Oscillator'].shift(2)) &
                     (df['AD_Oscillator'] <= df['AD_Oscillator'].shift(1))).astype(int)
    
    feature[:, 1] = (cond_1 &
                     cond_2 &
                     (df['low'].shift(2) < df['low'].shift(3)) &
                     (df['low'].shift(1) <= df['low'].shift(2)) &
                     (df['close'] < df['low'].shift(1)) &
                     (df['AD_Oscillator'].shift(1) < df['AD_Oscillator'].shift(2)) &
                     (df['AD_Oscillator'] >= df['AD_Oscillator'].shift(1))).astype(int)
    
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

В приведенном выше листинге создается двоичная матрица из 2 столбцов для флагов длинных и коротких позиций. Затем мы определяем основной цветовой фильтр Gator. По сути, требуется, чтобы верхняя гистограмма сужалась (была красной), а нижняя расширялась (была зеленой). Далее мы формально определяем условия для длинных позиций в столбце 0. Здесь мы кодируем состояние Gator, используя заранее заданные цветовые условия для определения режима. Для условия на покупку также проверяются повышающиеся максимумы, подтверждающие формирование движения, а пробой подтверждается моментумом по резкому росту индикатора A/D. Все эти показатели важны для подтверждения бычьей направленности, поскольку требуется сочетание режима, формирования движения, пробоя и контролируемого импульсного подтверждения. Это структурированный, слабый, но устойчивый сигнал.

Для коротких позиций используется зеркальная логика: нисходящий пробой после движения вниз подтверждается замедлением распределения. Наконец, все неопределенные из-за сдвига значения устанавливаются равными 0, чтобы избежать ложных срабатываний. Тестирование только этого паттерна в советнике, собранном Мастером, после экспорта сети, обрабатывающей эти выходные данные через ONNX, дает следующий отчет:

r0

c0

Несмотря на значительное число прибыльных сделок, этот сигнал, по-видимому, все же не проходит форвардное тестирование за 2024 год с прибылью.

Признак 3

Второй сигнальный паттерн основан на переходе от сжатия к расширению при импульсе внутри бара. Реализуем это в Python следующим образом:

def feature_3(df):
    """
//+------------------------------------------------------------------+
//| Check for Pattern 3.                                             |
//+------------------------------------------------------------------+
    """
    feature = np.zeros((len(df), 2))
    
    cond_1 = df['Gator_Up_Color'].shift(1) == 'red'
    cond_2 = df['Gator_Down_Color'].shift(1) == 'red'
    cond_3 = df['Gator_Up_Color'] == 'red'
    cond_4 = df['Gator_Down_Color'] == 'green'
    
    feature[:, 0] = (cond_1 &
                     cond_2 &
                     cond_3 &
                     cond_4 &
                     (df['close']-df['low'].shift(1) > 0.5*(df['high'].shift(1)-df['low'].shift(1))) &
                     (df['AD_Oscillator'].shift(2) > df['AD_Oscillator']) &
                     (df['AD_Oscillator'] > df['AD_Oscillator'].shift(1))).astype(int)
    
    feature[:, 1] = (cond_1 &
                     cond_2 &
                     cond_3 &
                     cond_4 &
                     (df['high'].shift(1)-df['close'] < 0.5*(df['high'].shift(1)-df['low'].shift(1))) &
                     (df['AD_Oscillator'].shift(2) < df['AD_Oscillator']) &
                     (df['AD_Oscillator'] < df['AD_Oscillator'].shift(1))).astype(int)
    
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

Основное условие состоит в том, что предыдущие бары обеих гистограмм Gator окрашены в красный цвет, после чего одна из гистограмм меняет цвет на зеленый, указывая на переход, как описано в предпоследней статье. Подробное описание этих паттернов уже было дано в той статье, поэтому перейдем к отчету о тестировании того же паттерна, когда нейронная сеть используется как фильтр. Получаем следующий отчет:

r3

c3

Стратегия почти успешно проходила форвардное тестирование до последней серии убыточных сделок. В наших системах всегда используются уровни тейк-профита без стоп-лосса, поэтому причиной стала необходимость закрывать убыточные сделки по сигналу разворота.

Признак 4

Последний рассматриваемый сигнальный паттерн – продолжение после смены цвета с простым подтверждением по моментуму. Если предыдущий бар расширялся, при этом верхняя гистограмма была зеленой, а нижняя красной, а затем на следующих барах верхняя гистограмма становится красной, указывая на сжатие, тогда как нижняя остается зеленой, это может указывать на смену режима осциллятора Gator. Также используются проверка ценового отвержения и моментума индикатора A/D, уже описанные в предыдущих частях. Реализация этого сигнала в Python выглядит следующим образом:

def feature_4(df):
    """
//+------------------------------------------------------------------+
//| Check for Pattern 4.                                             |
//+------------------------------------------------------------------+
    """
    feature = np.zeros((len(df), 2))
    
    cond_1 = df['Gator_Up_Color'].shift(1) == 'green'
    cond_2 = df['Gator_Down_Color'].shift(1) == 'red'
    cond_3 = df['Gator_Up_Color'] == 'red'
    cond_4 = df['Gator_Down_Color'] == 'green'
    
    feature[:, 0] = (cond_1 &
                     cond_2 &
                     cond_3 &
                     cond_4 &
                     (df['close'] > df['close'].shift(1)) &
                     (df['low'].shift(1) > df['low']) &
                     (df['AD_Oscillator'] > df['AD_Oscillator'].shift(1))).astype(int)
    
    feature[:, 1] = (cond_1 &
                     cond_2 &
                     cond_3 &
                     cond_4 &
                     (df['close'] < df['close'].shift(1)) &
                     (df['high'].shift(1) < df['high']) &
                     (df['AD_Oscillator'] < df['AD_Oscillator'].shift(1))).astype(int)
    
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

При тестировании этого сигнала нейронная сеть служит дополнительным фильтром для советника из предыдущих статей. Получаем следующий отчет:

r4

c4

Из трех сигналов, к которым мы вернулись в этой статье, только этот заметно улучшил результаты по сравнению с первым тестированием. Еще раз отметим, что период тестирования ограничен всего 2 годами, поэтому тем, кто хочет развивать этот сигнал дальше, следует провести самостоятельную проверку. Паттерн короткого сигнала для признака 4 на ценовом графике может выглядеть следующим образом:

p4


Заключение

Модели машинного обучения с обучением с учителем, такие как рассмотренная в этой статье сверточная нейронная сеть, дополненная ядерной регрессией и сходством по скалярному произведению, могут способствовать усилению слабых сигнальных паттернов. Хотя не все протестированные паттерны улучшились в равной мере, признак 4 выделился явным улучшением результатов. Это потенциально подтверждает пригодность такого подхода в качестве фильтра для советников. Тем не менее, ограниченный горизонт тестирования не позволяет делать далеко идущие выводы, хотя метод показывает, что адаптивные архитектуры способны извлекать полезную информацию из сигналов с ненадежной историей.

имя описание
WZ-79.mq5 Советник, собранный Мастером, в заголовке которого указаны используемые файлы. Руководство по Мастеру приведено здесь.
SignalWZ-79.mqh Файл класса сигнала, используемый Мастером для создания советника
79_0.onnx Экспортированная нейронная сеть для сигнального паттерна 0
79_3.onnx Экспортированная нейронная сеть для сигнального паттерна 3
79_4.onnx Экспортированная нейронная сеть для сигнального паттерна 4

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/19220

Прикрепленные файлы |
WZ-79.mq5 (6.91 KB)
SignalWZ_79.mqh (17.35 KB)
79_0.onnx (7548.33 KB)
79_3.onnx (7548.33 KB)
79_4.onnx (7548.33 KB)
Анализ CSV-данных (Часть 1): Механизм экспорта CSV при многоядерной оптимизации в MQL5 Анализ CSV-данных (Часть 1): Механизм экспорта CSV при многоядерной оптимизации в MQL5
При многоядерной оптимизации в MetaTrader 5 результаты могут незаметно теряться, когда параллельные агенты одновременно обращаются к одному CSV-файлу. Повторно используемый механизм экспорта MQL5 применяет итерационную спин-блокировку, чтобы надёжно открыть файл и добавлять строки без потерь. Он сохраняет пользовательские метрики, включая коэффициент Сортино, среднюю продолжительность сделки и показатели качества сигналов — запаздывание и частота ложных переворотов (whipsaws) — в сводном CSV-файле для последующей аналитической обработки.
Можно ли запустить DOOM в MetaTrader 5: DLL, рендеринг и ввод через MQL5? Можно ли запустить DOOM в MetaTrader 5: DLL, рендеринг и ввод через MQL5?
В этой статье показано, как запустить DOOM в MetaTrader 5, интегрировав нативную DLL Windows с советником MQL5. Мы рассмотрим сборку DLL, рендеринг буфера кадра в реальном времени с помощью ResourceCreate, обработку ввода с клавиатуры с обходным решением для отпускания клавиши через GetAsyncKeyState, а также запуск игрового цикла в фоновом потоке. Эти методы напрямую применимы к пользовательской визуализации, мостам к внешним данным и надёжной интеграции MQL5 с нативным кодом.
Кодирование свечных моделей (Часть 2): Моделирование движения цены в виде упорядоченной последовательности Кодирование свечных моделей (Часть 2): Моделирование движения цены в виде упорядоченной последовательности
Разработка инструментов на основе перестановок в MQL5 обеспечивает систематический способ анализа комбинаций свечных паттернов для торговых стратегий. В этой статье представлен калькулятор и генератор перестановок, предназначенные для вычисления и перечисления всех возможных упорядоченных последовательностей свечей из бычьих и медвежьих наборов, с повторениями или без них. Генерируя исчерпывающие комбинации паттернов, трейдеры могут проводить анализ на основе данных для выявления высоковероятных рыночных паттернов и улучшения принятия решений в автоматизированных торговых системах.
Система аудита счета в MQL5 (Часть 1): Проектирование пользовательского интерфейса Система аудита счета в MQL5 (Часть 1): Проектирование пользовательского интерфейса
В этой статье мы разработаем слой пользовательского интерфейса системы аудита счёта в MQL5 с использованием классов CChartObject. Создадим размещаемую на графике информационную панель, отображающую основные показатели: начальный и конечный баланс, чистую прибыль, общее количество сделок, число прибыльных и убыточных сделок, процент прибыльных сделок, выведенные средства и оценку эффективности в виде звёзд. Кнопка меню позволяет отображать и скрывать панель и восстанавливает торговлю в один клик. В результате получается удобная основа для дальнейшего развития системы аудита счёта.