English Deutsch 日本語
preview
Возможности Мастера MQL5, которые вам нужно знать (Часть 70): Использование паттернов SAR и RVI с сетью экспоненциального ядра

Возможности Мастера MQL5, которые вам нужно знать (Часть 70): Использование паттернов SAR и RVI с сетью экспоненциального ядра

MetaTrader 5Интеграция |
223 0
Stephen Njuki
Stephen Njuki

Введение

В предыдущей статье мы представили взаимодополняющую пару - Parabolic SAR и осциллятор индекс относительной бодрости (Vigour Index oscillator, RVI). В ходе тестирования 10 паттернов три не смогли пройти форвард-тестирование. Это паттерны 1, 2 и 6. Индексация паттернов от 0 до 9 позволяет нам легко вычислить значение ассоциативного массива (map value), которое дает право на их использование советником. Например, если паттерн имеет индекс 1, то нам нужно установить параметр PatternsUsed равным 2 в степени 1, что в сумме дает 2. 

Если индекс равен 2, то это 2 в степени 2, что дает 4, и так далее. Максимальное значение, которое может быть присвоено параметру, составляет 1023, поскольку у нас всего 10 параметров. Любое число от 0 до 1023, не являющееся чистой степенью 2, будет представлять собой комбинацию этих паттернов, и читатель может изучить возможность настройки советника для использования нескольких паттернов. Однако, основываясь на наших аргументах и результатах исследований, представленных в предыдущих статьях, мы решили пока не рассматривать этот аспект в рамках данной серии публикаций. 

Как и было обещано в одной из недавних статей, мы сейчас попытаемся "реабилитировать" три сигнальных паттерна 1, 2 и 6, которые в предыдущей статье не смогли форвард-тест, с помощью обучения с учителем. Применяя машинное обучение к этим сигналам индикатора MQL5, мы используем Python для кодирования и обучения сетевой модели. Это объясняется той эффективностью, которую он обеспечивает даже без графического процессора. При использовании Python мы полагаемся на модуль Python от MetaTrader, который позволяет нам подключаться к серверу брокера MetaTrader после ввода имени пользователя и пароля для входа. 

После установления соединения через модуль Python для MetaTrader 5 мы получаем доступ к данным о ценах брокера. В Python также есть библиотеки для технических индикаторов, но они требуют установки и часто имеют несколько специфический формат. К счастью, внедрение нашей собственной системы с нуля — относительно простой процесс. Поэтому начнем с реализации наших двух индикаторов, SAR и RVI, на языке Python.


Функция Parabolic SAR

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

def SAR(df: pd.DataFrame, af: float = 0.02, af_max: float = 0.2) -> pd.DataFrame:
    """
    Calculate Parabolic SAR indicator and append it as 'SAR' column to the input DataFrame.
    
    Args:
        df (pd.DataFrame): DataFrame with 'high', 'low', 'close' columns
        af (float): Acceleration factor, default 0.02
        af_max (float): Maximum acceleration factor, default 0.2
        
    Returns:
        pd.DataFrame: Input DataFrame with new 'SAR' column
    """
    if not all(col in df.columns for col in ['high', 'low', 'close']):
        raise ValueError("DataFrame must contain 'high', 'low', 'close' columns")
    if af <= 0 or af_max <= 0 or af > af_max:
        raise ValueError("Invalid acceleration factors")
    if df.empty:
        raise ValueError("DataFrame is empty")

    result_df = df.copy()
    result_df['SAR'] = 0.0
    
    sar = df['close'].iloc[0]
    ep = df['high'].iloc[0]
    af_current = af
    trend = 1 if len(df) > 1 and df['close'].iloc[1] > df['close'].iloc[0] else -1
    
    for i in range(1, len(df)):
        prev_sar = sar
        high, low = df['high'].iloc[i], df['low'].iloc[i]
        
        if trend > 0:
            sar = prev_sar + af_current * (ep - prev_sar)
            if low < sar:
                trend = -1
                sar = ep
                ep = low
                af_current = af
            else:
                if high > ep:
                    ep = high
                    af_current = min(af_current + af, af_max)
        else:
            sar = prev_sar + af_current * (ep - prev_sar)
            if high > sar:
                trend = 1
                sar = ep
                ep = high
                af_current = af
            else:
                if low < ep:
                    ep = low
                    af_current = min(af_current + af, af_max)
        
        result_df.loc[i, 'SAR'] = sar
    
    return result_df

Приведенная выше функция принимает фрейм данных pandas, содержащий столбцы с ценами high (максимальная), low (минимальная) и close (закрытия), а также два параметра типа float для коэффициента ускорения. В итоге вычисленные значения добавляются в качестве столбца к входному фрейму данных в виде нового столбца с меткой SAR. Его общая логика предназначена для отслеживания направления тренда и обновления SAR на основе экстремальной точки и коэффициента ускорения.

Если мы посмотрим на детали кода, то увидим, что начнем с создания копии входного фрейма данных, чтобы избежать внесения изменений в оригинал. Функция извлекает столбцы с высокими и низкими значениями данных в виде массивов NumPy для эффективных вычислений. Затем мы инициализируем массив sar нулями для хранения значений sar. Это важно, поскольку обеспечивает целостность данных и подготавливает структуру для вычислений SAR. Использование .copy() предотвращает непредвиденные побочные эффекты. Кроме того, массивы NumPy, как правило, демонстрируют лучшую производительность по сравнению с сериями pandas при выполнении итеративных вычислений.

После этого мы устанавливаем начальный тренд как восходящий с параметром trend = 1. Экстремальная точка также соответствует начальному максимуму, а начальная цена SAR соответствует первоначальному минимуму. Коэффициент ускорения начинается с входного значения af-start. Это крайне важно, поскольку определяет отправную точку для вычислений SAR, предполагая начальный восходящий тренд. Выбор значения low[0] для начального SAR соответствует роли SAR как стоп-лосса в восходящем тренде, поскольку оно ниже минимальной цены на величину веса параметра af-start, который корректирует его вычитание из минимума в соответствии с волатильностью рынка.

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

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

Это важно, поскольку обнаружение разворота тренда является ключевой функцией SAR при определении точек входа/выхода. Это условие гарантирует, что SAR останется ниже цены в восходящем тренде. Проверить чувствительность к развороту можно, отрегулировав параметр af-increment.

Если бычий тренд сохранится, нам необходимо обновить значения буфера SAR и, что более важно, увеличить значение af-increment. Индекс SAR по-прежнему ограничен минимальным значением, равным текущему уровню или двум последним минимумам, чтобы предотвратить его выход за пределы ценового диапазона. Кроме того, как только максимальное значение превысит крайнюю точку, крайняя точка обновляется, а коэффициент ускорения также увеличивается при условии, что он не превышает своего предельного значения, установленного параметром af-max. Этот этап важен, поскольку он гарантирует, что SAR останется действительным уровнем стоп-лосса и будет ускоряться только в соответствии с трендом. Использование двух предыдущих минимумов действительно повышает надежность, однако может потребоваться некоторая корректировка при работе с более короткими таймфреймами.

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

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

Подводя итог, можно сказать, что SAR лучше всего подходит для трендовых рынков, где он может помочь в определении уровней стоп-лосса и точек разворота. Он не подходит для рынков с боковым движением или нестабильной динамикой. Настройка параметров, хотя и не столь критична для SAR, может быть важна для некоторых активов, поскольку начальное значение и шаг приращения коэффициента ускорения могут потребовать корректировки по сравнению с их комфортными значениями 0,02 и 0,2. Также будет весьма полезной проверка достоверности путем тестирования на исторических данных брокера. Главное ограничение SAR заключается в его склонности к запаздыванию на быстро меняющихся рынках, что может приводить к появлению множества ложных сигналов.


Функция RVI

Главная цель RVI — отслеживать силу тренда, что, по нашему мнению, является синонимом импульса. Это делается путем сравнения положения цены закрытия относительно торгового диапазона и сглаживания этого диапазона с помощью скользящих средних. Это осциллятор, который может служить для подтверждения тренда путем проверки импульса или выявления дивергенций. Реализация в Python:

def RVI(df: pd.DataFrame, period: int, signal_period: int) -> pd.DataFrame:
    """
    Calculate Relative Vigor Index (RVI) with signal line and append as 'RVI' and 'RVI_Signal' columns.
    
    Args:
        df (pd.DataFrame): DataFrame with 'open', 'high', 'low', 'close' columns
        period (int): Lookback period for RVI calculation
        signal_period (int): Lookback period for signal line calculation
        
    Returns:
        pd.DataFrame: Input DataFrame with new 'RVI' and 'RVI_Signal' columns
    """
    # Input validation
    if not all(col in df.columns for col in ['open', 'high', 'low', 'close']):
        raise ValueError("DataFrame must contain 'open', 'high', 'low', 'close' columns")
    if period < 1 or signal_period < 1:
        raise ValueError("Period and signal period must be positive")
        
    # Create a copy to avoid modifying the input DataFrame
    result_df = df.copy()
    
    # Calculate price change and range
    close_open = df['close'] - df['open']
    high_low = df['high'] - df['low']
    
    # Calculate SMA for numerator and denominator
    num = close_open.rolling(window=period).mean()
    denom = high_low.rolling(window=period).mean()
    
    # Calculate RVI
    result_df['RVI'] = num / denom * 100
    
    # Calculate signal line (SMA of RVI)
    result_df['RVI_Signal'] = result_df['RVI'].rolling(window=signal_period).mean()
    
    return result_df

В приведенном выше коде мы, как правило, используем фрейм данных pandas со столбцами high, low и close. Кроме того, входные данные включают period для скользящего среднего SMA буфера сигнала. В результате обработки к столбцам фрейма данных добавляется сглаженный сигнал RVI, полученный совместно с сигналом RVI. По сути, логика расчета RVI выглядит следующим образом: close - open/high - low. Затем сигнал сглаживается с помощью 4-периодного SMA, и мы генерируем сигнальную линию с заданным пользователем периодом SMA.

Если мы теперь рассмотрим всё сделанное построчно, то первое, что мы делаем в нашей функции, — это создаем копию входного фрейма данных. Это служит способом сохранения исходных данных, позволяя предотвратить непреднамеренные изменения во входном фрейме данных. Использование функции копирования легко помогает нам достичь этой цели и избежать непредвиденных "побочных эффектов".

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

Для обработки ошибок, возникающих при делении на ноль, мы заменяем бесконечные значения, возникающие при делении на ноль (когда верхнее значение равно нижнему), на NaN. Затем каждое значение NaN заменяется нулем. Это важно для обеспечения численной устойчивости, а также для предотвращения ошибок в последующих вычислениях. Это упрощенное решение, поскольку читатель может рассмотреть альтернативные подходы к обработке в случае, если нулевые значения чрезмерно искажают выходной буфер RVI.

После этого мы переходим к сглаживанию RVI, а также определяем сигнальную линию. Сглаживание осуществляется с помощью 4-периодного метода SMA к исходным данным RVI для снижения восприимчивости к шуму и даже к нулевым значениям, которые могут встречаться, как упоминалось выше. Это помогает создать линию индикатора RVI. После установки этих параметров мы также применяем заданный пользователем период SMA для дополнительного сглаживания уже сглаженного сигнала RVI с целью генерации сигнальной линии. Этот этап крайне важен, поскольку сглаживание делает RVI более понятным для интерпретации. Сигнальная линия также помогает нам более точно определить пересечения. Часто для сглаживания исходного сигнала RVI используется фиксированный 4-периодный SMA, однако всегда есть возможность отрегулировать период сигнала SMA, если требуется точная настройка. Можно рассматривать периоды в диапазоне от 6 до 14.

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

В заключение, индекс RVI используется для подтверждения силы тренда, выступая в качестве индикатора импульса. Он также позволяет выявлять расхождения на ранних стадиях, например, когда цена достигает новых максимумов, а индекс RVI — нет. Кроме того, пересечения с сигнальной линией способствуют формированию вторичных бычьих или медвежьих прогнозов. Настройка параметров RVI в основном сводится к регулировке "периода" SMA, используемого для сглаживания RVI в сигнальной линии. Более короткие периоды в диапазоне 6 могут использоваться для более быстрых/частых сигналов, тогда как периоды около 14 служат для более плавных сигналов. Проверка путем анализа пересечений и расхождений RVI на исторических данных имеет важное значение для установления надежности сигнала. Основным ограничением RVI является его склонность к запаздыванию из-за используемого двойного сглаживания. Кроме того, в условиях низкой волатильности ему может быть сложно генерировать значимые сигналы. 


Комплементарность SAR RVI

Эта пара индикаторов, как и большинство рассмотренных нами в этой серии, выбрана потому, что каждый индикатор дополняет другой. SAR — это индикатор следования за трендом, отображаемый на ценовом графике. RVI — это осциллятор, отображаемый в отдельном окне и используемый в основном для измерения импульса. Индекс SAR лучше подходит для установки стоп-лоссов, в то время как индекс RVI лучше подходит для подтверждения трендов или выявления дивергенций.

Обе реализации выполнены на Python с использованием библиотеки pandas для обработки данных. Роль NumPy заключается в ускорении вычислений. Логика SAR, являющаяся итеративной, как правило, более сложна из-за разворотов тренда. С другой стороны, векторизованные операции RVI проще, однако они подвержены сценариям деления на ноль. Таким образом, эффективность этой пары индикаторов повышается за счет использования массивов NumPy для SAR с учетом применения итеративных вычислений, а также за счет фреймов данных pandas при обработке векторизованного RVI.

В качестве дополнительных мер, которые читатель может реализовать для этой пары, можно рассмотреть тестирование на исторических данных в Python с использованием таких библиотек, как zipline; визуализацию точечных диаграмм SAR, а также RVI с сигнальной линией, особенно для трейдеров, торгующих вручную; обработку ошибок путем дополнения приведенного выше кода проверкой входных данных, например, проверкой наличия необходимых столбцов данных в фрейме данных pandas или проверкой правильности периода индикатора; и, наконец, для каждого индикатора можно добавить расширения. Например, можно использовать дополнение RVI с функцией обнаружения расхождений, или же альтернативные методы сглаживания.


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

Признак 1

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

В Python, как и в MQL5, каждому из этих паттернов необходимо присвоить функцию, которая выдает либо true, либо false. True, если сигнал паттерна присутствует, и false, если нет. Эти результаты представляются в виде бинарных сигналов из нулей и единиц, поскольку они объединяют торговые стратегии, основанные на двух индикаторах и данных о ценах. Каждая функция создает массив признаков с двумя столбцами. В первом столбце (column-0) регистрируются бычьи сигналы. Во втором столбце (column-1) регистрируются медвежьи. В каждом столбце регистрируется 0, если сигнал отсутствует, и 1, если сигнал присутствует.

Как уже упоминалось в предыдущей статье, признак 1 (feature-1) выявляет тренды, когда цена остается выше/ниже SAR, а RVI одновременно растет/падает. В нем рассматривается устойчивый симбиоз между SAR и ценой, а также импульс RVI. Реализуем эту функцию на Python следующим образом:

def feature_1(sar_df, rvi_df, price_df):
    """
    
    """
    feature = np.zeros((len(sar_df), 2))
    
    feature[:, 0] = ((price_df['low'].shift(1) > sar_df['SAR'].shift(1)) &
                     (price_df['low'] > sar_df['SAR']) &
                     (rvi_df['RVI'] > rvi_df['RVI'].shift(1))).astype(int)
    
    feature[:, 1] = ((price_df['high'].shift(1) < sar_df['SAR'].shift(1)) &
                     (price_df['high'] < sar_df['SAR']) &
                     (rvi_df['RVI'] < rvi_df['RVI'].shift(1))).astype(int)
    
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

Первое, что мы делаем в приведенном выше коде, — это инициализируем массив, заполненный нулями, в виде [длина входных фреймов данных, 2]. Два выделенных в конце "столбца" предназначены для фиксации бычьих и медвежьих сигналов соответственно. Этот шаг важен, поскольку он обеспечивает чистоту данных для хранения наших бинарных сигналов и позволяет избежать наличия неинициализированных значений. Использование np.zeros повышает эффективность и предотвращает неожиданные результаты при выполнении численных операций, поскольку значения NaN могут автоматически использоваться для заполнения пустого массива.

После завершения инициализации мы устанавливаем значение для бычьего сигнала. В столбце 0 мы присваиваем значение 1, если предыдущий минимум находится выше текущего уровня SAR в условиях устойчивого бычьего тренда; и текущий минимум также находится выше уровня SAR; при этом текущий RVI выше предыдущего значения указывает на восходящий импульс. Использование ВСЕХ этих сигналов важно для выявления сильного бычьего тренда, что подтверждают как SAR для тренда, так и RVI для импульса. Важно при этом убедиться, что shift(1) правильно выравнивает данные. Отсутствие данных может привести к серьезным ошибкам. Использование функции astype(int) помогает преобразовывать логические значения в двоичные, 0 и 1.

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

Таким образом, для второго столбца мы устанавливаем медвежий сигнал, когда предыдущий максимум находится ниже предыдущего SAR, а текущий максимум также находится ниже текущего SAR. Это проявляется, когда текущий RVI ниже предыдущего, что является признаком снижения динамики. Благодаря взаимодополняемости индексов SAR и RVI, все эти паттерны в совокупности определяют сильный медвежий тренд. Аналогично вышеописанной оптимистичной логике, нам необходимо проверить выравнивание данных, а также обработать значения NaN из переменной shift.

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

r1

c1

Похоже, что внедрение обучения с учителем приносит лучшие результаты. Ниже представлен график, полученный для паттерна 1 без использования обучения с учителем:

c1_old

Признак 2

Третий паттерн также не прошел форвард-тест. Как обсуждалось в предыдущей статье, этот паттерн позволяет выявлять потенциальные развороты, когда цена пересекает SAR, а RVI демонстрирует некоторый импульс. Он также учитывает направление движения цены, используя цену закрытия в качестве дополнительного ориентира. Реализация в Python:

def feature_2(sar_df, rvi_df, price_df):
    """

    """
    feature = np.zeros((len(sar_df), 2))
    
    feature[:, 0] = ((price_df['high'].shift(1) <= sar_df['SAR'].shift(1)) &
                     (price_df['low'] >= sar_df['SAR']) &
                     (rvi_df['RVI'] > rvi_df['RVI'].shift(1)) &
                     (price_df['close'].shift(1) > price_df['close'])).astype(int)
    
    feature[:, 1] = ((price_df['low'].shift(1) >= sar_df['SAR'].shift(1)) &
                     (price_df['high'] <= sar_df['SAR']) &
                     (rvi_df['RVI'].shift(1) > rvi_df['RVI']) &
                     (price_df['close'] > price_df['close'].shift(1))).astype(int)
    
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

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

Далее мы переходим к определению условий для возникновения бычьего сигнала. Для того чтобы мы зарегистрировали единицу, должны быть выполнены все эти условия, в противном случае присваивается ноль. Поэтому работа с массивами, заполненными нулями, — это безопасный способ избежать ложных входов. Итак, если предыдущий максимум находится на уровне или ниже предыдущего SAR, а текущий минимум находится на уровне или выше текущего SAR, это означает, что произошел разворот тренда; при этом RVI растет, и, наконец, цена закрытия снижается, подтверждая контекст дивергенции. Это помогает выявлять бычьи развороты, когда цена пробивает SAR с учетом импульса RVI. Для этого паттерна необходимо выполнить несколько условий, и все они должны быть подтверждены, прежде чем мы присвоим значение true или 1. 

В условиях медвежьего тренда мы устанавливаем эти значения на втором индексе массива NumPy признака. При медвежьем значении для подтверждения медвежьего тренда предыдущий минимум находится на уровне или ниже предыдущего SAR; текущий максимум находится на уровне или ниже текущего SAR; RVI падает; а цена закрытия растет. Сигнальный паттерн 2 указывает на медвежью дивергенцию, и важно обеспечить согласованность данных во всех наборах данных. Затем мы обнуляем начальные строки нашего массива признаков, присваивая им нули, чтобы учесть использование сравнений со сдвигом. Это предотвращает некорректные сигналы, в которых нет данных с задержкой. Это стандартная практика для временных рядов, имеющих задержки.

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

r2

c2

Ниже представлен график, полученный для паттерна 2 без использования обучения с учителем:

c2_old

Признак 6

Наша последний признак из 10, рассмотренных в предыдущей статье, основан на сигнальном паттерне 6. В этом методе для подтверждения используется пересечение RVI с сигнальной линией, а не только импульс RVI. Основное внимание уделяется сильным трендовым сигналам, в которых наблюдаются пересечения SAR и RVI. Реализация в Python:

def feature_6(sar_df, rvi_df, price_df):
    """

    """
    feature = np.zeros((len(sar_df), 2))
    
    feature[:, 0] = ((price_df['low'].shift(1) > sar_df['SAR'].shift(1)) &
                     (price_df['low'] > sar_df['SAR']) &
                     (rvi_df['RVI'] > rvi_df['RVI_Signal']) &
                     (rvi_df['RVI'].shift(1) < rvi_df['RVI_Signal'].shift(1))).astype(int)
    
    feature[:, 1] = ((price_df['high'].shift(1) < sar_df['SAR'].shift(1)) &
                     (price_df['high'] < sar_df['SAR']) &
                     (rvi_df['RVI'] < rvi_df['RVI_Signal']) &
                     (rvi_df['RVI'].shift(1) > rvi_df['RVI_Signal'].shift(1))).astype(int)
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

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

После этого мы определяем условия для бычьего сигнала, которые регистрируются на первом индексе, равном нулю. Мы регистрируем 1, если предыдущий минимум находится выше SAR; текущий минимум находится выше текущего SAR; текущий RVI также находится выше своей сигнальной линии; и, наконец, предыдущий RVI был ниже своего сигнала.

После определения бычьего паттерна мы также формируем медвежий, который возникает, когда предыдущий максимум находится ниже предыдущего SAR; паттерн подтверждается, если текущий максимум также находится ниже текущего SAR; а текущий RVI находится ниже своей сигнальной линии; при этом предыдущий RVI был выше нулевой границы. Ниже представлен отчет с результатами тестирования. Похоже, результаты форвард-тестирования снова улучшились.

r6

c6

Ниже представлен график, полученный для паттерна 6 без использования обучения с учителем:

c6_old


Сеть

В ходе тестирования, используя три описанных выше паттерна, мы задействовали одномерную сверточную нейронную сеть. Она имеет три сверточных слоя, в каждом из которых количество фильтров и размер ядра увеличиваются экспоненциально. Затем следует максимальный пул (max-pooling), которая представляет собой операцию выравнивания. Наконец, мы используем полносвязные слои для получения конечного результата. Реализуем это следующим образом на Python:

class ExpConv1DNetwork(nn.Module):
    def __init__(self, input_length, input_channels=1, base_filters=16, base_kernel_size=3, exp_base=2):
        super(ExpConv1DNetwork, self).__init__()
        self.conv_layers = nn.ModuleList()
        self.pool_layers = nn.ModuleList()
        
        for i in range(3):
            filters = int(base_filters * (exp_base ** i))
            kernel_size = int(base_kernel_size * (exp_base ** i)) | 1
            self.conv_layers.append(
                nn.Conv1d(
                    in_channels=input_channels if i == 0 else int(base_filters * (exp_base ** (i-1))),
                    out_channels=filters,
                    kernel_size=kernel_size,
                    padding='same'
                )
            )
            # Use smaller kernel size for pooling to prevent size reduction to 0
            self.pool_layers.append(nn.MaxPool1d(kernel_size=2, ceil_mode=True))
        
        self.flatten_size = self._get_flatten_size(input_length, input_channels, base_filters, exp_base)
        self.flatten = nn.Flatten()
        self.dense1 = nn.Linear(self.flatten_size, 128)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(0.5)
        self.dense2 = nn.Linear(128, 1)
    
    def _get_flatten_size(self, input_length, input_channels, base_filters, exp_base):
        current_length = input_length
        current_channels = input_channels
        
        for i in range(3):
            current_channels = int(base_filters * (exp_base ** i))
            # Update length after pooling
            current_length = (current_length + 1) // 2  # Ceiling division for ceil_mode=True
        
        return current_channels * current_length
    
    def forward(self, x):
        for conv, pool in zip(self.conv_layers, self.pool_layers):
            x = self.relu(conv(x))
            x = pool(x)
        x = self.flatten(x)
        x = self.relu(self.dense1(x))
        x = self.dropout(x)
        x = self.dense2(x)
        return x

Нейронная сеть ожидает одномерный входной сигнал, имеющий форму, определяемую параметрами batch-size, input-channels и input-length. Ее также можно настроить с помощью таких параметров, как base-filters, base-kernel-size и exp-base. Наш выходной сигнал представляет собой единый скалярный параметр, обученный принимать значения от 0 до 1, где 0 означает прогнозирование медвежьего тренда, а 1 — бычьего. Класс играет ключевую роль в настройке производительности наших обновленных сигнальных паттернов. Но пока оставим его усовершенствование на потом.


Заключение

Мы изучили потенциальные преимущества добавления обучения с учителем к сигнальным паттернам, которые не прошли форвард-тест при работе с ограниченным окном данных по паре GBP CHF, составляющим всего 2 года. По результатам тестов, проведенных опять же в очень ограниченных условиях, наблюдаются положительные результаты по сравнению с тем, что мы получили в предыдущей статье. Это обнадеживает. Однако, как всегда, читателю необходимо провести собственное, более тщательное исследование, прежде чем принимать во внимание какие-либо из представленных здесь идей. 


Файл Описание
WZ-70.mq5 Файл, заголовок которого указывает на файлы, используемые в сборке мастера.
SignalWZ_70.mqh Пользовательский файл класса сигнала, используемый Мастером MQL5
70_1.onnx Экспортированная модель сети для паттерна 1
70_2.onnx Экспортированная модель сети для паттерна 2
70_6.onnx Экспортированная модель сети для паттерна 6

Прилагаемый код предназначен для компиляции в советника с помощью Мастера MQL5. Детали описаны здесь.

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

Прикрепленные файлы |
WZ-70.mq5 (6.92 KB)
SignalWZ_70.mqh (14.34 KB)
70_1.onnx (153.86 KB)
70_2.onnx (153.86 KB)
70_6.onnx (153.86 KB)
Внедрение в MQL5 практических модулей из других языков (Часть 1): Создание библиотеки SQLite3 как в Python Внедрение в MQL5 практических модулей из других языков (Часть 1): Создание библиотеки SQLite3 как в Python
Модуль sqlite3 в Python предлагает простой способ работы с базами данных SQLite, быстрый и удобный. В этой статье мы создадим подобный модуль поверх встроенных функций MQL5 для работы с базами данных, чтобы упростить работу с базами данных SQLite3 в MQL5 так же, как это реализовано в Python.
Знакомство с языком MQL5 (Часть 37): Освоение API и функции WebRequest в языке MQL5 (XI) Знакомство с языком MQL5 (Часть 37): Освоение API и функции WebRequest в языке MQL5 (XI)
В этой статье мы покажем, как с помощью языка MQL5 отправлять аутентифицированные запросы к API Binance, чтобы получать баланс счета по всем активам. Вы узнаете, как использовать свой API-ключ, время сервера и подпись для безопасного доступа к данным аккаунта, а также как сохранять ответ в файл для дальнейшего использования.
От начального до среднего уровня: Объекты (I) От начального до среднего уровня: Объекты (I)
В данной статье мы начнём рассматривать, как можно работать с объектами непосредственно на графике. Это делается с помощью кода, специально разработанного для демонстрации. Работа с объектами очень интересна и доставляет немало удовольствия. Поскольку это будет наш первый контакт, начнём с чего-нибудь очень простого.
Моделирование рынка (Часть 21): Первые шаги на SQL (IV) Моделирование рынка (Часть 21): Первые шаги на SQL (IV)
Многие из вас, возможно, обладают гораздо большим опытом в области работы с базами данных, чем я, и, следовательно, имеют другое мнение. Поскольку было необходимо дать объяснение, почему базы данных создаются именно так, как они создаются, и нужно объяснить, почему SQL имеет именно такой формат и, прежде всего, почему появились первичные ключи и внешние ключи, поэтому пришлось оставить некоторые вещи немного абстрактными.