English 中文 Español Deutsch 日本語 Português
preview
Поиск произвольных паттернов валютных пар на Python с использованием MetaTrader 5

Поиск произвольных паттернов валютных пар на Python с использованием MetaTrader 5

MetaTrader 5Трейдинг |
1 197 1
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Введение в анализ паттернов на Форекс

Что видит новичок, впервые увидев графики валютных пар? Множество внутридневных колебаний, повышение и затухание волатильности, смена трендов и многое другое. Взлеты, падения, зигзаги — как в этом разобраться? Также и я начинал свое знакомство с Форекс и погрузился в изучение анализа ценовых паттернов.

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

Но как же их найти? Как отличить настоящий паттерн от случайного шума? Вот тут-то и начинается самое интересное. Я решил создать свою собственную систему анализа паттернов, используя Python и MetaTrader 5. Этакий симбиоз математики и программирования для покорения Форекса.

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


Настройка окружения: установка необходимых библиотек и подключение к MetaTrader 5

Итак, первая наша задача — установка Python. Его можно скачать с официального сайта python.org. Скачивайте, устанавливайте. Также необходимо установить галочку "Add Python to PATH". 

Следующий важный шаг  — библиотеки. Нам понадобится несколько. Главная — MetaTrader 5. Ещё pandas — для работы с данными. И пожалуй, numpy. Открываем командную строку и пишем:

pip install MetaTrader5 pandas numpy matplotlib pytz

Первым делом нужно установить сам MetaTrader 5. Скачиваете с официального сайта, сайта вашего брокера, устанавливаете. Ничего сложного.

А вот теперь нужно найти путь к терминалу. Обычноэто что-то вроде "C:\Program Files\MetaTrader 5\terminal64.exe". Запомните этот путь, он нам пригодится.

Теперь открываем Python и пишем:

import MetaTrader5 as mt5

if not mt5.initialize(path="C:/Program Files/MetaTrader 5/terminal64.exe"):
    print("MetaTrader 5 initialization failed.")
    mt5.shutdown()
else:
    print("MetaTrader 5 initialized successfully.")

Запускаем и, если видим принт об успешной инициализации терминала, значит, мы все сделали правильно.

Хотите убедиться, что всё работает? Давайте попробуем получить немного данных:

import MetaTrader5 as mt5
import pandas as pd
from datetime import datetime

if not mt5.initialize():
    print("Oops! Something went wrong.")
    mt5.shutdown()

eurusd_ticks = mt5.copy_ticks_from("EURUSD", datetime.now(), 10, mt5.COPY_TICKS_ALL)
ticks_frame = pd.DataFrame(eurusd_ticks)
print("Look at this beauty:")
print(ticks_frame)

mt5.shutdown()

Если вы видите таблицу с данными — поздравляю! Вы только что сделали свой первый шаг в мир алгоритмической торговли на Форекс при помощи Python. Это не так сложно, как кажется.


Структура кода: основные функции и их назначение

Итак, начинаем разбирать структуру кода. Это полноценная система для анализа паттернов на валютном рынке. 

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

Следующая функция — calculate_winrate_and_frequency. Эта функция анализирует найденные закономерности — тут и частота появлений, и винрейт, а также — сортировка паттернов.

Также немаловажную роль играет функция process_currency_pair. Это достаточно важный обработчик. Он загружает данные, просматривает их, ищет паттерны различной длины, и так же выдает топ-300 паттернов для продаж и покупок. Что касается начала кода, то тут инициализация, настройка параметров, интервал графика (ТФ) и временной период (у меня с 1990 пол 2024).

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


Получение данных с MetaTrader 5: функция copy_rates_range

Первая наша функция — это получение данных от терминала. Давайте взглянем на код:

import MetaTrader5 as mt5
import pandas as pd
import time
from datetime import datetime, timedelta
import pytz

# List of major currency pairs
major_pairs = ['EURUSD']

# Setting up data request parameters
timeframe = mt5.TIMEFRAME_H4
start_date = pd.Timestamp('1990-01-01')
end_date = pd.Timestamp('2024-05-31')

def process_currency_pair(symbol):
    max_retries = 5
    retries = 0
    while retries < max_retries:
        try:
            # Loading OHLC data
            rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
            if rates is None:
                raise ValueError("No data received")
            ohlc_data = pd.DataFrame(rates)
            ohlc_data['time'] = pd.to_datetime(ohlc_data['time'], unit='s')
            break
        except Exception as e:
            print(f"Error loading data for {symbol}: {e}")
            retries += 1
            time.sleep(2)  # Wait before retrying

    if retries == max_retries:
        print(f"Failed to load data for {symbol} after {max_retries} attempts")
        return

    # Further data processing...

Что происходит в данном коде?  Сначала мы определяем наши валютные пары. Сейчас у нас только EURUSD, но вы можете добавить и другие. Затем мы устанавливаем временной интервал. H4 — это 4 часа. Это оптимальный вариант времени. 

Далее — даты. С 1990 по 2024 год. Нам понадобиться много исторических котировок. Чем больше данных, тем точнее наш анализ. Теперь к главному — функции process_currency_pair. Она загружает данные, используя copy_rates_range.

Что получаем в итоге? DataFrame с историческими данными. Время, открытие, закрытие, максимум, минимум — всё что необходимо для работы.

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


Обработка временных рядов: преобразование данных OHLC в направления движения цены

Вернемся к нашей основной задаче. Превратить хаотичные колебания рынка Форекс в нечто более упорядоченное  — тренды и развороты. Как мы это делаем? По довольно понятной схеме: мы превращаем цены в направления.

Вот наш код:

# Fill missing values with the mean
ohlc_data.fillna(ohlc_data.mean(), inplace=True)

# Convert price movements to directions
ohlc_data['direction'] = np.where(ohlc_data['close'].diff() > 0, 'up', 'down')

Что здесь происходит? Во-первых, мы заполняем пропуски. Пропуски могут значительно ухудшить наш конечный результат. Мы заполняем их средними значениями. 

А теперь самое интересное. Мы создаем новый столбец 'direction'. С помощью данного столбца мы переводим ценовые данные в данные, имитирующие поведение трендов. Он работает элементарно:

  • Если текущая цена закрытия выше предыдущей — пишем 'up'.
  • Если ниже — пишем 'down'.

Довольно простая формулировка, но достаточно эффективная. Теперь вместо сложных чисел у нас простая последовательность 'up' и 'down'. Такая последовательность намного легче для человеческого восприятия. Но зачем нам это нужно? Эти 'up' и 'down' — строительные блоки для наших паттернов. Именно из них мы будем собирать полное представление о том, что происходит на рынке.


Алгоритм поиска паттернов: функция find_patterns

Итак, у нас есть последовательность 'up' и 'down'. Далее мы будем искать в этой последовательности повторяющиеся узоры — паттерны.

Вот функция find_patterns:

def find_patterns(data, pattern_length, direction):
    patterns = defaultdict(list)
    last_pattern = None
    last_winrate = None
    last_frequency = None

    for i in range(len(data) - pattern_length - 6):
        pattern = tuple(data['direction'][i:i+pattern_length])
        if data['direction'][i+pattern_length+6] == direction:
            patterns[pattern].append(True)
        else:
            patterns[pattern].append(False)

    # Check last prices for pattern match
    last_pattern_tuple = tuple(data['direction'][-pattern_length:])
    if last_pattern_tuple in patterns:
        last_winrate = np.mean(patterns[last_pattern_tuple]) * 100
        last_frequency = len(patterns[last_pattern_tuple])
        last_pattern = last_pattern_tuple

    return patterns, last_pattern, last_winrate, last_frequency

Как все это работает?

  • Мы создаем словарь patterns. Это послужит своеобразной библиотекой, где мы будем хранить все найденные паттерны.
  • Затем мы начинаем перебирать данные. Мы берем пример данных длиной pattern_length (это может быть 3, 4, 5 и так далее до 25) и смотрим, что происходит через 6 баров после него.
  • Если через 6 баров цена двигается в нужном направлении (то есть вверх для паттернов покупки или вниз для паттернов продажи), мы записываем True. Если нет - False.
  • Мы делаем это для всех возможных примеров данных. У нас должны получиться аналогичные схемы: "up-up-down" - True, "down-up-up" - False и так далее.
  • Далее мы проверяем, не формируется ли сейчас какой-нибудь паттерн, попадавшийся ранее. Если да, мы вычисляем его винрейт (процент успешных отработок) и частоту встречаемости.

Вот так мы превращаем простую последовательность 'up' и 'down' в довольно эффективный инструмент прогнозирования. Но это еще не всё. Дальше мы будем фильтровать эти паттерны, выбирать самые эффективные, анализировать их.


Расчет статистики паттернов: WinRate и частота встречаемости

Теперь, когда мы получили некоторое количество паттернов, нам необходимо отобрать самые лучшие.

Давайте взглянем на наш код:

def calculate_winrate_and_frequency(patterns):
    results = []
    for pattern, outcomes in patterns.items():
        winrate = np.mean(outcomes) * 100
        frequency = len(outcomes)
        results.append((pattern, winrate, frequency))
    results.sort(key=lambda x: x[1], reverse=True)
    return results

Здесь мы берем каждый паттерн и его результаты (мы упоминали их ранее как True и False), а затем вычисляем винрейт — это наш процент продуктивности. Если паттерн сработал 7 раз из 10, его винрейт 70%. Считаем и частоту — это количество раз, когда паттерн встретился. Чем чаще, тем надежнее наша статистика. Всё это складываем в список results. И в конце — сортировка. Лучшие паттерны ставим в начало списка.


Фильтрация результатов: отбор значимых паттернов

Теперь у нас есть достаточное количество данных. Но понадобятся нам далеко не все, необходимо провести фильтрацию.

filtered_buy_results = [result for result in all_buy_results if result[2] > 20]
filtered_sell_results = [result for result in all_sell_results if result[2] > 20]

filtered_buy_results.sort(key=lambda x: x[1], reverse=True)
top_300_buy_patterns = filtered_buy_results[:300]

filtered_sell_results.sort(key=lambda x: x[1], reverse=True)
top_300_sell_patterns = filtered_sell_results[:300]

Подобным образом мы устанавливаем фильтрацию. Сначала мы отсеиваем все паттерны, которые встретились менее 20 раз. Как показывает статистика, редкие паттерны менее надежны. 

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


Работа с различными длинами паттернов: от 3 до 25

Теперь нам необходимо отобрать вариации паттернов, которые будут статистически и регулярно приносить прибыль при торговле. Варианты различаются длиной. Они могут состоять как из 3 так и из 25 движений цены. Проверим все возможные:

pattern_lengths = range(3, 25)  # Pattern lengths from 3 to 25
all_buy_patterns = {}
all_sell_patterns = {}

for pattern_length in pattern_lengths:
    buy_patterns, last_buy_pattern, last_buy_winrate, last_buy_frequency = find_patterns(ohlc_data, pattern_length, 'up')
    sell_patterns, last_sell_pattern, last_sell_winrate, last_sell_frequency = find_patterns(ohlc_data, pattern_length, 'down')
    all_buy_patterns[pattern_length] = buy_patterns
    all_sell_patterns[pattern_length] = sell_patterns

Мы запускаем наш фильтр поиска паттернов для каждой длины от 3 до 25. Почему именно так? Паттерны меньше трех движений слишком ненадежны — мы упоминали это ранее. А длиннее 25 — встречаются слишком редко. Для каждой длины мы ищем паттерны и на покупку, и на продажу. 

Но зачем нам столько разных длин? Короткие паттерны могут уловить быстрые развороты рынка, а длинные — показать долгосрочные тренды. Мы пока не знаем заранее, что будет эффективнее, поэтому проверяем всё.


Анализ паттернов на покупку и продажу

Теперь, когда у нас есть определенная подборка паттернов разной длины, пора определить, какие из них действительно работают. 

Вот наш код в действии:

all_buy_results = []
for pattern_length, patterns in all_buy_patterns.items():
    results = calculate_winrate_and_frequency(patterns)
    all_buy_results.extend(results)

all_sell_results = []
for pattern_length, patterns in all_sell_patterns.items():
    results = calculate_winrate_and_frequency(patterns)
    all_sell_results.extend(results)

Мы берем каждый паттерн — и на покупку, и на продажу — и фильтруем через нашу функцию расчета винрейта и частоты. 

Но мы не просто считаем статистику. Мы ищем разницу между паттернами на покупку и продажу. Почему это важно? Потому что рынок может вести себя по-разному при росте и падении. Иногда паттерны на покупку работают лучше, иногда — на продажу.

Далее мы перейдем к следующему шагу — сравним паттерны разной длины между собой. Может оказаться, что для определения точки входа в рынок лучше работают короткие паттерны, а для определения долгосрочного тренда — длинные. Так же может происходить и наоборот. Именно поэтому мы анализируем всё и не отбрасываем ничего раньше времени.

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

Но помните, друзья, даже самый лучший паттерн — не гарантия успеха. Рынок полон неожиданностей. Наша задача  увеличить шансы на успех, и именно это мы делаем, анализируя паттерны со всех сторон.


Заглядывая в будущее: прогнозирование на основе последних паттернов

Теперь пришло время конкретных прогнозов. Давайте взглянем на наш код-прорицатель:

if last_buy_pattern:
    print(f"\nLast buy pattern for {symbol}: {last_buy_pattern}, Winrate: {last_buy_winrate:.2f}%, Frequency: {last_buy_frequency}")
    print(f"Forecast: Price will likely go up.")
if last_sell_pattern:
    print(f"\nLast sell pattern for {symbol}: {last_sell_pattern}, Winrate: {last_sell_winrate:.2f}%, Frequency: {last_sell_frequency}")
    print(f"Forecast: Price will likely go down.")

Мы смотрим на последний сформировавшийся паттерн и пытаемся предугадать будущее. Мы смотрим на последний сформировавшийся паттерн и проводим наш торговый анализ.

Обратите внимание, мы рассматриваем два сценария: паттерн на покупку и паттерн на продажу. Почему? Потому что рынок — это вечное противостояние быков и медведей, покупателей и продавцов. Мы должны быть готовы к любому повороту событий.

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

Частота тоже играет важную роль. Паттерн, который встречается часто, более надежен, чем редкий паттерн.

Довольно интересная часть — это наш прогноз. "Price will likely go up" или "Price will likely go down".  Эти прогнозы приносят некоторое удовлетворение от проделанной работы. Однако помните, друзья мои, даже самый точный прогноз — это всего лишь вероятность, а не гарантия. Рынок Форекс довольно сложно прогнозируется. Новости, экономические события, даже твиты влиятельных людей могут изменить направление движения цены в считанные секунды.

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


Рисуем будущее: визуализация лучших паттернов и прогнозов

Давайте добавим в наш код немного магии визуализации:

import matplotlib.pyplot as plt

def visualize_patterns(patterns, title, filename):
    patterns = patterns[:20]  # Берем топ-20 для наглядности
    patterns.reverse()  # Разворачиваем список для правильного отображения на графике

    fig, ax = plt.subplots(figsize=(12, 8))
    
    winrates = [p[1] for p in patterns]
    frequencies = [p[2] for p in patterns]
    labels = [' '.join(p[0]) for p in patterns]

    ax.barh(range(len(patterns)), winrates, align='center', color='skyblue', zorder=10)
    ax.set_yticks(range(len(patterns)))
    ax.set_yticklabels(labels)
    ax.invert_yaxis()  # Инвертируем ось Y для отображения лучших паттернов сверху

    ax.set_xlabel('Winrate (%)')
    ax.set_title(title)

    # Добавляем частоту встречаемости
    for i, v in enumerate(winrates):
        ax.text(v + 1, i, f'Freq: {frequencies[i]}', va='center')

    plt.tight_layout()
    plt.savefig(filename)
    plt.close()

# Визуализируем топ паттерны для покупки и продажи
visualize_patterns(top_300_buy_patterns, f'Top 20 Buy Patterns for {symbol}', 'top_buy_patterns.png')
visualize_patterns(top_300_sell_patterns, f'Top 20 Sell Patterns for {symbol}', 'top_sell_patterns.png')

# Визуализируем последний паттерн и прогноз
def visualize_forecast(pattern, winrate, frequency, direction, symbol, filename):
    fig, ax = plt.subplots(figsize=(8, 6))
    
    ax.bar(['Winrate'], [winrate], color='green' if direction == 'up' else 'red')
    ax.set_ylim(0, 100)
    ax.set_ylabel('Winrate (%)')
    ax.set_title(f'Forecast for {symbol}: Price will likely go {direction}')

    ax.text(0, winrate + 5, f'Pattern: {" ".join(pattern)}', ha='center')
    ax.text(0, winrate - 5, f'Frequency: {frequency}', ha='center')

    plt.tight_layout()
    plt.savefig(filename)
    plt.close()

if last_buy_pattern:
    visualize_forecast(last_buy_pattern, last_buy_winrate, last_buy_frequency, 'up', symbol, 'buy_forecast.png')
if last_sell_pattern:
    visualize_forecast(last_sell_pattern, last_sell_winrate, last_sell_frequency, 'down', symbol, 'sell_forecast.png')

Мысоздалидвефункции: visualize_patterns и visualize_forecast. Первая рисует информативный горизонтальный бар-чарт с топ-20 паттернами, их винрейтами и частотой встречаемости. Вторая создает наглядное представление нашего прогноза на основе последнего паттерна.

Для паттернов мы используем горизонтальные столбцы, из-за того, что паттерны могут быть длинными, и так их удобнее читать. Цвет у нас приятный, для восприятия человеческим глазом, небесно-голубой.

Мы сохраняем наши шедевры в PNG файлы.


Тестирование и бэктестинг системы анализа паттернов

Мы создали нашу систему анализа паттернов, но как узнать, работает ли она на самом деле? Для этого, нужно протестировать её на исторических данных. 

Вот наш код, необходимый для этой задачи:

def simulate_trade(data, direction, entry_price, take_profit, stop_loss):
    for i, row in data.iterrows():
        current_price = row['close']
        
        if direction == "BUY":
            if current_price >= entry_price + take_profit:
                return {'profit': take_profit, 'duration': i}
            elif current_price <= entry_price - stop_loss:
                return {'profit': -stop_loss, 'duration': i}
        else:  # SELL
            if current_price <= entry_price - take_profit:
                return {'profit': take_profit, 'duration': i}
            elif current_price >= entry_price + stop_loss:
                return {'profit': -stop_loss, 'duration': i}
    
    # Если цикл завершился без достижения TP или SL, закрываем по текущей цене
    last_price = data['close'].iloc[-1]
    profit = (last_price - entry_price) if direction == "BUY" else (entry_price - last_price)
    return {'profit': profit, 'duration': len(data)}

def backtest_pattern_system(data, buy_patterns, sell_patterns):
    equity_curve = [10000]  # Начальный капитал $10,000
    trades = []
    
    for i in range(len(data) - max(len(p[0]) for p in buy_patterns + sell_patterns)):
        current_data = data.iloc[:i+1]
        last_pattern = tuple(current_data['direction'].iloc[-len(buy_patterns[0][0]):])
        
        matching_buy = [p for p in buy_patterns if p[0] == last_pattern]
        matching_sell = [p for p in sell_patterns if p[0] == last_pattern]
        
        if matching_buy and not matching_sell:
            entry_price = current_data['close'].iloc[-1]
            take_profit = 0.001  # 10 pips
            stop_loss = 0.0005  # 5 pips
            trade_result = simulate_trade(data.iloc[i+1:], "BUY", entry_price, take_profit, stop_loss)
            trades.append(trade_result)
            equity_curve.append(equity_curve[-1] + trade_result['profit'] * 10000)  # Умножаем на 10000 для конвертации в доллары
        elif matching_sell and not matching_buy:
            entry_price = current_data['close'].iloc[-1]
            take_profit = 0.001  # 10 pips
            stop_loss = 0.0005  # 5 pips
            trade_result = simulate_trade(data.iloc[i+1:], "SELL", entry_price, take_profit, stop_loss)
            trades.append(trade_result)
            equity_curve.append(equity_curve[-1] + trade_result['profit'] * 10000)  # Умножаем на 10000 для конвертации в доллары
        else:
            equity_curve.append(equity_curve[-1])
    
    return equity_curve, trades

# Проводим бэктест
equity_curve, trades = backtest_pattern_system(ohlc_data, top_300_buy_patterns, top_300_sell_patterns)

# Визуализация результатов бэктеста
plt.figure(figsize=(12, 6))
plt.plot(equity_curve)
plt.title('Equity Curve')
plt.xlabel('Trades')
plt.ylabel('Equity ($)')
plt.savefig('equity_curve.png')
plt.close()

# Расчет статистики бэктеста
total_profit = equity_curve[-1] - equity_curve[0]
win_rate = sum(1 for trade in trades if trade['profit'] > 0) / len(trades) if trades else 0
average_profit = sum(trade['profit'] for trade in trades) / len(trades) if trades else 0

print(f"\nBacktest Results:")
print(f"Total Profit: ${total_profit:.2f}")
print(f"Win Rate: {win_rate:.2%}")
print(f"Average Profit per Trade: ${average_profit*10000:.2f}")
print(f"Total Trades: {len(trades)}")

Что здесь происходит? Функция simulate_trade — это наш симулятор отдельной сделки. Она следит за ценой и закрывает сделку при достижении take profit или stop loss. 

А backtest_pattern_system — это более важная функция. Она проходит по историческим данным, шаг за шагом, день за днем, проверяя, не сформировался ли один из наших паттернов. Нашла паттерн на покупку? Покупаем. На продажу? Продаем.

Мы используем фиксированные take profit в 100 пунктов и stop loss в 50 пунктов. Так как нам необходимо установить границы удовлетворительной прибыли — не слишком много, чтобы не рисковать выше лимита, но и не слишком мало, чтобы дать прибыли возможность вырасти.

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

Реализуем поиск паттернов средствами языка MQL5. Вот наш код:

//+------------------------------------------------------------------+
//|                                       PatternProbabilityIndicator|
//|                                                 Copyright 2024   |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, Your Name Here"
#property link      "https://www.mql5.com"
#property version   "1.06"
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2

//--- plot BuyProbability
#property indicator_label1  "BuyProbability"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrGreen
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2

//--- plot SellProbability
#property indicator_label2  "SellProbability"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrRed
#property indicator_style2  STYLE_SOLID
#property indicator_width2  2

//--- input parameters
input int      InpPatternLength = 5;    // Pattern Length (3-10)
input int      InpLookback     = 1000;  // Lookback Period (100-5000)
input int      InpForecastHorizon = 6;  // Forecast Horizon (1-20)

//--- indicator buffers
double         BuyProbabilityBuffer[];
double         SellProbabilityBuffer[];

//--- global variables
int            g_pattern_length;
int            g_lookback;
int            g_forecast_horizon;
string         g_patterns[];
int            g_pattern_count;
int            g_pattern_occurrences[];
int            g_pattern_successes[];
int            g_total_bars;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- validate inputs
   if(InpPatternLength < 3 || InpPatternLength > 10)
   {
      Print("Invalid Pattern Length. Must be between 3 and 10.");
      return INIT_PARAMETERS_INCORRECT;
   }
   
   if(InpLookback < 100 || InpLookback > 5000)
   {
      Print("Invalid Lookback Period. Must be between 100 and 5000.");
      return INIT_PARAMETERS_INCORRECT;
   }

   if(InpForecastHorizon < 1 || InpForecastHorizon > 20)
   {
      Print("Invalid Forecast Horizon. Must be between 1 and 20.");
      return INIT_PARAMETERS_INCORRECT;
   }

   //--- indicator buffers mapping
   SetIndexBuffer(0, BuyProbabilityBuffer, INDICATOR_DATA);
   SetIndexBuffer(1, SellProbabilityBuffer, INDICATOR_DATA);
   
   //--- set accuracy
   IndicatorSetInteger(INDICATOR_DIGITS, 2);
   
   //--- set global variables
   g_pattern_length = InpPatternLength;
   g_lookback = InpLookback;
   g_forecast_horizon = InpForecastHorizon;
   
   //--- generate all possible patterns
   if(!GeneratePatterns())
   {
      Print("Failed to generate patterns.");
      return INIT_FAILED;
   }
   
   g_total_bars = iBars(_Symbol, PERIOD_CURRENT);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   //--- check for rates total
   if(rates_total <= g_lookback + g_pattern_length + g_forecast_horizon)
   {
      Print("Not enough data for calculation.");
      return 0;
   }

   int start = (prev_calculated > g_lookback + g_pattern_length + g_forecast_horizon) ? 
               prev_calculated - 1 : g_lookback + g_pattern_length + g_forecast_horizon;
   
   if(ArraySize(g_pattern_occurrences) != g_pattern_count)
   {
      ArrayResize(g_pattern_occurrences, g_pattern_count);
      ArrayResize(g_pattern_successes, g_pattern_count);
   }
   
   ArrayInitialize(g_pattern_occurrences, 0);
   ArrayInitialize(g_pattern_successes, 0);
   
   // Pre-calculate patterns for efficiency
   string patterns[];
   ArrayResize(patterns, rates_total);
   for(int i = g_pattern_length; i < rates_total; i++)
   {
      patterns[i] = "";
      for(int j = 0; j < g_pattern_length; j++)
      {
         patterns[i] += (close[i-j] > close[i-j-1]) ? "U" : "D";
      }
   }
   
   // Main calculation loop
   for(int i = start; i < rates_total; i++)
   {
      string current_pattern = patterns[i];
      
      if(StringLen(current_pattern) != g_pattern_length) continue;
      
      double buy_probability = CalculateProbability(current_pattern, true, close, patterns, i);
      double sell_probability = CalculateProbability(current_pattern, false, close, patterns, i);
      
      BuyProbabilityBuffer[i] = buy_probability;
      SellProbabilityBuffer[i] = sell_probability;
   }
   
   // Update Comment with pattern statistics if total bars changed
   if(g_total_bars != iBars(_Symbol, PERIOD_CURRENT))
   {
      g_total_bars = iBars(_Symbol, PERIOD_CURRENT);
      UpdatePatternStatistics();
   }
   
   return(rates_total);
}

//+------------------------------------------------------------------+
//| Generate all possible patterns                                   |
//+------------------------------------------------------------------+
bool GeneratePatterns()
{
   g_pattern_count = (int)MathPow(2, g_pattern_length);
   if(!ArrayResize(g_patterns, g_pattern_count))
   {
      Print("Failed to resize g_patterns array.");
      return false;
   }
   
   for(int i = 0; i < g_pattern_count; i++)
   {
      string pattern = "";
      for(int j = 0; j < g_pattern_length; j++)
      {
         pattern += ((i >> j) & 1) ? "U" : "D";
      }
      g_patterns[i] = pattern;
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| Calculate probability for a given pattern                        |
//+------------------------------------------------------------------+
double CalculateProbability(const string &pattern, bool is_buy, const double &close[], const string &patterns[], int current_index)
{
   if(StringLen(pattern) != g_pattern_length || current_index < g_lookback)
   {
      return 50.0; // Return neutral probability on error
   }

   int pattern_index = ArraySearch(g_patterns, pattern);
   if(pattern_index == -1)
   {
      return 50.0;
   }

   int total_occurrences = 0;
   int successful_predictions = 0;
   
   for(int i = g_lookback; i > g_pattern_length + g_forecast_horizon; i--)
   {
      int historical_index = current_index - i;
      if(historical_index < 0 || historical_index + g_pattern_length + g_forecast_horizon >= ArraySize(close))
      {
         continue;
      }

      if(patterns[historical_index] == pattern)
      {
         total_occurrences++;
         g_pattern_occurrences[pattern_index]++;
         if(is_buy && close[historical_index + g_pattern_length + g_forecast_horizon] > close[historical_index + g_pattern_length])
         {
            successful_predictions++;
            g_pattern_successes[pattern_index]++;
         }
         else if(!is_buy && close[historical_index + g_pattern_length + g_forecast_horizon] < close[historical_index + g_pattern_length])
         {
            successful_predictions++;
            g_pattern_successes[pattern_index]++;
         }
      }
   }
   
   return (total_occurrences > 0) ? (double)successful_predictions / total_occurrences * 100 : 50;
}

//+------------------------------------------------------------------+
//| Update pattern statistics and display in Comment                 |
//+------------------------------------------------------------------+
void UpdatePatternStatistics()
{
   string comment = "Pattern Statistics:\n";
   comment += "Pattern Length: " + IntegerToString(g_pattern_length) + "\n";
   comment += "Lookback Period: " + IntegerToString(g_lookback) + "\n";
   comment += "Forecast Horizon: " + IntegerToString(g_forecast_horizon) + "\n\n";
   comment += "Top 5 Patterns:\n";
   
   int sorted_indices[];
   ArrayResize(sorted_indices, g_pattern_count);
   for(int i = 0; i < g_pattern_count; i++) sorted_indices[i] = i;
   
   // Use quick sort for better performance
   ArraySort(sorted_indices);
   
   for(int i = 0; i < 5 && i < g_pattern_count; i++)
   {
      int idx = sorted_indices[g_pattern_count - 1 - i];  // Reverse order for descending sort
      double win_rate = g_pattern_occurrences[idx] > 0 ? 
                        (double)g_pattern_successes[idx] / g_pattern_occurrences[idx] * 100 : 0;
      
      comment += g_patterns[idx] + ": " +
                 "Occurrences: " + IntegerToString(g_pattern_occurrences[idx]) + ", " +
                 "Win Rate: " + DoubleToString(win_rate, 2) + "%\n";
   }
   
   Comment(comment);
}

//+------------------------------------------------------------------+
//| Custom function to search for a string in an array               |
//+------------------------------------------------------------------+
int ArraySearch(const string &arr[], string value)
{
   for(int i = 0; i < ArraySize(arr); i++)
   {
      if(arr[i] == value) return i;
   }
   return -1;
}

Как это выглядит на графике:


Создаем советник по обнаружению паттернов и торговле

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

Ключевые компоненты советника:

  • Генерация паттернов: EA использует бинарное представление ценовых движений (восходящее или нисходящее), создавая все возможные комбинации для заданной длины паттерна.
  • Статистический анализ: Советник проводит ретроспективный анализ, оценивая частоту появления каждого паттерна и его прогностическую эффективность.
  • Динамическая адаптация: EA постоянно обновляет статистику паттернов, адаптируясь к изменяющимся рыночным условиям.
  • Принятие торговых решений: На основе выявленных наиболее эффективных паттернов для покупки и продажи, советник открывает, закрывает или удерживает позиции.
  • Параметризация: EA предоставляет возможность настройки ключевых параметров, таких как длина паттерна, период анализа, горизонт прогнозирования и минимальное количество появлений паттерна для его учета.

Всего я сделал 4 варианта советника: первый по концепции статьи, он открывает сделки по паттернам, и закрывает при обнаружении нового лучшего паттерна в противоположном направлении. Второй — такой же, но мультивалютный: работает с 10 наиболее ликвидными парами Форекс, согласно статистике Всемирного банка. Третий — такой же, но он закрывает сделки по прохождению ценой количества баров больше горизонта прогноза. Ну и последний — закрытие по тейку и стопу.

Вот код первого советника, остальные будут в закрепленных файлах:

//+------------------------------------------------------------------+
//|                                  PatternProbabilityExpertAdvisor |
//|                                Copyright 2024, Evgeniy Koshtenko |
//|                          https://www.mql5.com/ru/users/koshtenko |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, Evgeniy Koshtenko"
#property link      "https://www.mql5.com/ru/users/koshtenko"
#property version   "1.00"

#include <Trade\Trade.mqh>            // Подключаем торговый класс CTrade

//--- input parameters
input int      InpPatternLength = 5;    // Pattern Length (3-10)
input int      InpLookback     = 1000;  // Lookback Period (100-5000)
input int      InpForecastHorizon = 6;  // Forecast Horizon (1-20)
input double   InpLotSize = 0.1;        // Lot Size
input int      InpMinOccurrences = 30;  // Minimum Pattern Occurrences

//--- global variables
int            g_pattern_length;
int            g_lookback;
int            g_forecast_horizon;
string         g_patterns[];
int            g_pattern_count;
int            g_pattern_occurrences[];
int            g_pattern_successes[];
int            g_total_bars;
string         g_best_buy_pattern = "";
string         g_best_sell_pattern = "";

CTrade trade;                         // Используем торговый класс CTrade
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- validate inputs
   if(InpPatternLength < 3 || InpPatternLength > 10)
   {
      Print("Invalid Pattern Length. Must be between 3 and 10.");
      return INIT_PARAMETERS_INCORRECT;
   }
   
   if(InpLookback < 100 || InpLookback > 5000)
   {
      Print("Invalid Lookback Period. Must be between 100 and 5000.");
      return INIT_PARAMETERS_INCORRECT;
   }

   if(InpForecastHorizon < 1 || InpForecastHorizon > 20)
   {
      Print("Invalid Forecast Horizon. Must be between 1 and 20.");
      return INIT_PARAMETERS_INCORRECT;
   }

   //--- set global variables
   g_pattern_length = InpPatternLength;
   g_lookback = InpLookback;
   g_forecast_horizon = InpForecastHorizon;
   
   //--- generate all possible patterns
   if(!GeneratePatterns())
   {
      Print("Failed to generate patterns.");
      return INIT_FAILED;
   }
   
   g_total_bars = iBars(_Symbol, PERIOD_CURRENT);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   if(!IsNewBar()) return;
   
   UpdatePatternStatistics();
   
   string current_pattern = GetCurrentPattern();
   
   if(current_pattern == g_best_buy_pattern)
   {
      if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
      {
         trade.PositionClose(_Symbol);
      }
      if(!PositionSelect(_Symbol))
      {
         trade.Buy(InpLotSize, _Symbol, 0, 0, 0, "Buy Pattern: " + current_pattern);
      }
   }
   else if(current_pattern == g_best_sell_pattern)
   {
      if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
      {
         trade.PositionClose(_Symbol);
      }
      if(!PositionSelect(_Symbol))
      {
         trade.Sell(InpLotSize, _Symbol, 0, 0, 0, "Sell Pattern: " + current_pattern);
      }
   }
}

//+------------------------------------------------------------------+
//| Generate all possible patterns                                   |
//+------------------------------------------------------------------+
bool GeneratePatterns()
{
   g_pattern_count = (int)MathPow(2, g_pattern_length);
   if(!ArrayResize(g_patterns, g_pattern_count))
   {
      Print("Failed to resize g_patterns array.");
      return false;
   }
   
   for(int i = 0; i < g_pattern_count; i++)
   {
      string pattern = "";
      for(int j = 0; j < g_pattern_length; j++)
      {
         pattern += ((i >> j) & 1) ? "U" : "D";
      }
      g_patterns[i] = pattern;
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| Update pattern statistics and find best patterns                 |
//+------------------------------------------------------------------+
void UpdatePatternStatistics()
{
   if(ArraySize(g_pattern_occurrences) != g_pattern_count)
   {
      ArrayResize(g_pattern_occurrences, g_pattern_count);
      ArrayResize(g_pattern_successes, g_pattern_count);
   }
   
   ArrayInitialize(g_pattern_occurrences, 0);
   ArrayInitialize(g_pattern_successes, 0);
   
   int total_bars = iBars(_Symbol, PERIOD_CURRENT);
   int start = total_bars - g_lookback;
   if(start < g_pattern_length + g_forecast_horizon) start = g_pattern_length + g_forecast_horizon;
   
   double close[];
   ArraySetAsSeries(close, true);
   CopyClose(_Symbol, PERIOD_CURRENT, 0, total_bars, close);
   
   string patterns[];
   ArrayResize(patterns, total_bars);
   ArraySetAsSeries(patterns, true);
   
   for(int i = 0; i < total_bars - g_pattern_length; i++)
   {
      patterns[i] = "";
      for(int j = 0; j < g_pattern_length; j++)
      {
         patterns[i] += (close[i+j] > close[i+j+1]) ? "U" : "D";
      }
   }
   
   for(int i = start; i >= g_pattern_length + g_forecast_horizon; i--)
   {
      string current_pattern = patterns[i];
      int pattern_index = ArraySearch(g_patterns, current_pattern);
      
      if(pattern_index != -1)
      {
         g_pattern_occurrences[pattern_index]++;
         if(close[i-g_forecast_horizon] > close[i])
         {
            g_pattern_successes[pattern_index]++;
         }
      }
   }
   
   double best_buy_win_rate = 0;
   double best_sell_win_rate = 0;
   
   for(int i = 0; i < g_pattern_count; i++)
   {
      if(g_pattern_occurrences[i] >= InpMinOccurrences)
      {
         double win_rate = (double)g_pattern_successes[i] / g_pattern_occurrences[i];
         if(win_rate > best_buy_win_rate)
         {
            best_buy_win_rate = win_rate;
            g_best_buy_pattern = g_patterns[i];
         }
         if((1 - win_rate) > best_sell_win_rate)
         {
            best_sell_win_rate = 1 - win_rate;
            g_best_sell_pattern = g_patterns[i];
         }
      }
   }
   
   Print("Best Buy Pattern: ", g_best_buy_pattern, " (Win Rate: ", DoubleToString(best_buy_win_rate * 100, 2), "%)");
   Print("Best Sell Pattern: ", g_best_sell_pattern, " (Win Rate: ", DoubleToString(best_sell_win_rate * 100, 2), "%)");
}

//+------------------------------------------------------------------+
//| Get current price pattern                                        |
//+------------------------------------------------------------------+
string GetCurrentPattern()
{
   double close[];
   ArraySetAsSeries(close, true);
   CopyClose(_Symbol, PERIOD_CURRENT, 0, g_pattern_length + 1, close);
   
   string pattern = "";
   for(int i = 0; i < g_pattern_length; i++)
   {
      pattern += (close[i] > close[i+1]) ? "U" : "D";
   }
   
   return pattern;
}

//+------------------------------------------------------------------+
//| Custom function to search for a string in an array               |
//+------------------------------------------------------------------+
int ArraySearch(const string &arr[], string value)
{
   for(int i = 0; i < ArraySize(arr); i++)
   {
      if(arr[i] == value) return i;
   }
   return -1;
}

//+------------------------------------------------------------------+
//| Check if it's a new bar                                          |
//+------------------------------------------------------------------+
bool IsNewBar()
{
   static datetime last_time = 0;
   datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(current_time != last_time)
   {
      last_time = current_time;
      return true;
   }
   return false;
}

Что касается результатов тестов, то на евро-долларе они такие:

И детально:

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


Потенциальные улучшения: расширение временных рамок и добавление индикаторов

Продолжаем наши размышления. Система, конечно приносит положительные результаты, но как их улучшить, и реально ли это?

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

timeframes = [mt5.TIMEFRAME_H4, mt5.TIMEFRAME_D1, mt5.TIMEFRAME_W1, mt5.TIMEFRAME_MN1]
for tf in timeframes:
    ohlc_data = get_ohlc_data(symbol, tf, start_date, end_date)
    patterns = find_patterns(ohlc_data)

Больше данных — больше шума. Нам нужно научиться фильтровать этот шум, чтобы получить более четкие данные.

Давайте расширим ряд анализируемых признаков. В мире трейдинга это добавление технических индикаторов. RSI, MACD, Bollinger Bands – наиболее часто используемые инструменты.

def add_indicators(data):
    data['RSI'] = ta.RSI(data['close'])
    data['MACD'] = ta.MACD(data['close']).macd()
    data['BB_upper'], data['BB_middle'], data['BB_lower'] = ta.BBANDS(data['close'])
    return data

ohlc_data = add_indicators(ohlc_data)

Индикаторы могут помочь нам подтвердить сигналы наших паттернов. Или же, можно дополнительно искать паттерны на индикаторах.


Заключение

Вот мы и закончили нашу работу над поиском и анализом паттернов. Мы создали систему, которая ищет закономерности в хаосе рынка. Мы научились визуализировать наши результаты, проводить бэктесты, планировать о будущих улучшениях. Но самое главное — мы научились мыслить как трейдеры-аналитики. Мы не просто следуем за толпой, мы ищем свой путь, свои паттерны, свои возможности.

Помните, рынок — это продукт действий живых людей. Он растет и меняется. И наша задача заключается в том, чтобы меняться вместе с ним. Сегодняшние паттерны могут не работать завтра, но это не повод отчаиваться. Это повод учиться, адаптироваться и расти. Используйте эту систему как отправную точку. Экспериментируйте, улучшайте, создавайте свое. Может быть, именно вы найдете тот самый паттерн, который откроет двери к успешной торговле!

Удачи вам на этом увлекательном пути! Пусть ваши паттерны всегда будут прибыльными, а убытки — лишь уроками на пути к успеху. До новых встреч в мире Форекс!

Прикрепленные файлы |
PredictPattern.py (10.35 KB)
AutoPattern.mq5 (18.98 KB)
PatternEA.mq5 (16.14 KB)
PatternEAMult.mq5 (9.59 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
linfo2
linfo2 | 9 мая 2025 в 02:09
Спасибо Евгению, отличный шаблон для оценки идеи с питоном. очень признателен
Разработка системы репликации (Часть 52): Всё усложняется (IV) Разработка системы репликации (Часть 52): Всё усложняется (IV)
В этой статье мы изменим указатель мыши, чтобы иметь возможность взаимодействовать с индикатором управления, поскольку он работает нестабильно.
Разработка системы репликации (Часть 51): Все усложняется (III) Разработка системы репликации (Часть 51): Все усложняется (III)
В данной статье мы разберемся с одним из самых сложных вопросов сферы программирования на MQL5: как правильно получить ID графика, и почему иногда объекты не строятся на графике. Представленные здесь материалы носят исключительно дидактический характер. Ни в коем случае нельзя рассматривать приложение ни с какой иной целью, кроме как для изучения и освоения представленных концепций.
Нейросети в трейдинге: Контрастный Трансформер паттернов (Окончание) Нейросети в трейдинге: Контрастный Трансформер паттернов (Окончание)
В последней статье нашей серии мы рассмотрели фреймворк Atom-Motif Contrastive Transformer (AMCT), который использует контрастное обучение для выявления ключевых паттернов на всех уровнях — от базовых элементов до сложных структур. В этой статье мы продолжаем реализацию подходов AMCT средствами MQL5.
Высокочастотная арбитражная торговая система на Python с использованием MetaTrader 5 Высокочастотная арбитражная торговая система на Python с использованием MetaTrader 5
Создаем легальную в глазах брокеров арбитражную систему, которая создает тысячи синтетических цен на рынке Форекс, анализирует их, и успешно торгует в прибыль.