English 中文 Español Deutsch 日本語
preview
Инженерия признаков с Python и MQL5 (Часть I): AI-модели для долгосрочного прогнозирования по скользящим средним

Инженерия признаков с Python и MQL5 (Часть I): AI-модели для долгосрочного прогнозирования по скользящим средним

MetaTrader 5Примеры |
502 4
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Если говорить о применении искусственного интеллекта к любой задаче, пожалуй, первой важной ступенью является сбор как можно большего количества полезной информации о реальном мире, которую наша модель сможет использовать. Далее, чтобы описать модели различные признаки данных (в нашем случае это характеристики рынка), необходимо обработать и преобразовать входные данные — этот процесс называется инженерией признаков (feature engineering). В этой серии статей вы узнаете, как преобразовывать рыночные данные для снижения уровня ошибок в моделях. Сегодня мы поговорим о том, как использовать скользящие средние для увеличения горизонта прогнозирования AI-моделей. При этом попытаемся сохранить полный контроль и понимание общей эффективности стратегии.


Обзор стратегии

В прошлый раз, когда мы говорили о прогнозировании скользящих средних с помощью ИИ, я привел доказательства того, что для моделей ИИ проще предсказывать значения скользящих средних, чем будущие уровни цен (вот ссылка на эту статью). Чтобы убедиться в значимости наших выводов, я обучил две идентичные AI-модели на более чем 200 различных рыночных инструментах и сравнил точность прогнозирования цены с точностью прогнозирования скользящих средних. Результаты показали, что при прогнозировании цены точность в среднем снижается на 34% по сравнению с прогнозированием скользящих средних.

В среднем можно ожидать 70% точности при прогнозировании скользящих средних против 52% точности при прогнозировании цены. Всем известно, что в зависимости от выбранного периода индикатор скользящей средней не всегда следует за уровнем цен достаточно близко. Например, цена может снижаться в течение 20 свечей, а скользящие средние на этом интервале будут расти. Такое расхождение нежелательно, поскольку даже при правильном прогнозе направления скользящей средней цена может пойти в противоположную сторону. Примечательно, что уровень расхождения остается стабильным примерно на уровне 31% по всем рыночным инструментам, а точность прогнозирования самих расхождений составила в среднем 68%.

Кроме того, дисперсия точности прогнозирования расхождений и самих случаев расхождения составила 0.000041 и 0.000386 соответственно. Это демонстрирует, что модель обладает устойчивой способностью к самокоррекции. Если вы планируете применять AI в долгосрочных торговых стратегиях, может иметь смысл рассмотреть этот альтернативный подход на старших таймфреймах. Пока что мы ограничимся обсуждением таймфрейма M1, поскольку он обеспечивает достаточный объем данных по всем 297 рынкам для корректного сравнения.

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


Начало работы

Для начала нужно импортировать стандартные библиотеки научных для вычислений в Python.

#Load the libraries we need
import pandas as pd
import numpy as np
import MetaTrader5 as mt5
from sklearn.model_selection import TimeSeriesSplit,cross_val_score
from sklearn.linear_model import LogisticRegression,LinearRegression
import matplotlib.pyplot as plt

Инициализируем терминал MetaTrader 5.

#Initialize the terminal
mt5.initialize()
True

Сколько торговых символов у нас есть?

#The total number of symbols we have
print(f"Total Symbols Available: ",mt5.symbols_total())
Total Symbols Available:  297

Получим названия всех символов.

#Get the names of all pairs
symbols = mt5.symbols_get()
idx = [s.name for s in symbols]

Создадим датафрейм для хранения уровней точности по всем инструментам.

global_params = pd.DataFrame(index=idx,columns=["OHLC Error","MAR Error","Noise Levels","Divergence Error"])
global_params

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

Определим объект разбиения временного ряда.

#Define the time series split object
tscv = TimeSeriesSplit(n_splits=5,gap=10)

Измерим точность по всем инструментам.

#Iterate over all symbols
for i in np.arange(global_params.dropna().shape[0],len(idx)):
    #Fetch M1 Data
    data = pd.DataFrame(mt5.copy_rates_from_pos(cols[i],mt5.TIMEFRAME_M1,0,50000))
    data.rename(columns={"open":"Open","high":"High","low":"Low","close":"Close"},inplace=True)
    #Define our period
    period = 10
    #Add the classical target
    data.loc[data["Close"].shift(-period) > data["Close"],"OHLC Target"] = 1
    #Calculate the returns
    data.loc[:,["Open","High","Low","Close"]] = data.loc[:,["Open","High","Low","Close"]].diff(period)
    data["RMA"] = data["Close"].rolling(period).mean()
    #Calculate our new target
    data.dropna(inplace=True)
    data.reset_index(inplace=True,drop=True)
    data.loc[data["RMA"].shift(-period) > data["RMA"],"New Target"] = 1
    data = data.iloc[0:-period,:]
    #Calculate the divergence target
    data.loc[data["OHLC Target"] != data["New Target"],"Divergence Target"] = 1
    #Noise ratio
    global_params.iloc[i,2] = data.loc[data["New Target"] != data["OHLC Target"]].shape[0] / data.shape[0]
    #Test our accuracy predicting the future close price
    score = cross_val_score(LogisticRegression(),data.loc[:,["Open","High","Low","Close"]],data["OHLC Target"],cv=tscv)
    global_params.iloc[i,0] = score.mean()
    #Test our accuracy predicting the moving average of future returns
    score = cross_val_score(LogisticRegression(),data.loc[:,["Open","Close","RMA"]],data["New Target"],cv=tscv)
    global_params.iloc[i,1] = score.mean()
    #Test our accuracy predicting the future divergence between price and its moving average
    score = cross_val_score(LogisticRegression(),data.loc[:,["Open","Close","RMA"]],data["Divergence Target"],cv=tscv)
    global_params.iloc[i,3] = score.mean()
    print(f"{((i/len(idx)) * 100)}% complete")

#We are done
print("Done")
99.66329966329967% complete
Готово


Анализ результатов

Мы получили рыночные данные и оценили работу нашей модели по двум целевым показателям. Теперь можем подвести итоги наших тестов по всем рынкам. Начнем с оценки того, насколько точно удалось спрогнозировать изменения будущей цены закрытия. На Рис. 2 ниже представлены общие результаты прогнозирования изменения цены закрытия. Красная горизонтальная линия обозначает порог точности в 50%. Средняя точность методики показана синей горизонтальной линией. Как видите, средняя точность находится не сильно выше 50%-го порога, что, откровенно говоря, не самый выдающийся результат.

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

global_params.iloc[:,0].plot()
plt.title("OHLC Accuracy")
plt.xlabel("Market")
plt.ylabel("5-fold Accuracy %")
plt.axhline(global_params.iloc[:,0].mean(),linestyle='--')
plt.axhline(0.5,linestyle='--',color='red')

Рис. 2: Средняя точность прогнозирования цены

Теперь давайте посмотрим на точность при прогнозировании изменений скользящих средних. На рисунке 3 ниже показаны результаты. Здесь снова красной линией показан порог точности в 50%, золотая линия показывает среднюю точность прогнозирования изменений цены, а синяя — среднюю точность прогнозирования изменений скользящей средней. Сказать, что модель лучше предсказывает скользящие средние, — это ничего не сказать. Думаю, этот факт уже не подлежит обсуждению: наши модели однозначно лучше прогнозируют определенные индикаторы, чем сами ценовые уровни.

global_params.iloc[:,1].plot()
plt.title("Moving Average Returns Accuracy")
plt.xlabel("Market")
plt.ylabel("5-fold Accuracy %")
plt.axhline(global_params.iloc[:,1].mean(),linestyle='--')
plt.axhline(global_params.iloc[:,0].mean(),linestyle='--',color='orange')
plt.axhline(0.5,linestyle='--',color='red')

Рис. 3: Точность прогнозирования изменения скользящей средней

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

global_params.iloc[:,2].plot()
plt.title("Noise Level")
plt.xlabel("Market")
plt.ylabel("Percentage of Divergence:Price And Moving Average")
plt.axhline(global_params.iloc[:,2].mean(),linestyle='--')

Рис. 4: Визуализация уровней шума по всем рынкам.

Если между двумя переменными наблюдается почти постоянное соотношение, это может указывать на наличие связи, которую можно формализовать в модели. Давайте теперь проанализируем, насколько точно мы можем прогнозировать расхождение между ценой и скользящей средней. Логика здесь проста: если наша модель прогнозирует снижение скользящей средней, можем ли мы с приемлемой точностью определить, будет ли за цена тоже снижаться или, напротив, она начнет расходиться со скользящей средней? Оказалось, что модель способна спрогнозировать расхождение с достаточно высокой точностью — в среднем около 70%.

global_params.iloc[:,3].plot()
plt.title("Divergence Accuracy")
plt.xlabel("Market")
plt.ylabel("5-fold Accuracy %")
plt.axhline(global_params.iloc[:,3].mean(),linestyle='--')

Рис. 5: Точность прогнозирования расхождений между ценой и скользящей средней

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

Метрика
Точность
Ошибка при прогнозировании доходностей
0.525353
Ошибка при прогнозировании скользящей средней доходностей
0.705468
Уровень шума
0.317187
Ошибка при прогнозировании расхождений
0.682069

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

global_params.sort_values("MAR Error",ascending=False)


Рис 6: Рынки с лучшими результатами модели


Оптимизация для наиболее успешного рынка

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

symbol = "AUDJPY"

Инициализируем доступ в терминал.

#Reach the terminal
mt5.initialize()
True

Далее получим рыночные данные.

data = pd.DataFrame(mt5.copy_rates_from_pos(symbol,mt5.TIMEFRAME_D1,365*2,5000))

Импортируем необходимые библиотеки.

#Standard libraries
import seaborn                 as     sns
from   mpl_toolkits.mplot3d    import Axes3D
from   sklearn.linear_model    import LinearRegression
from   sklearn.neural_network  import MLPRegressor
from   sklearn.metrics         import mean_squared_error
from   sklearn.model_selection import cross_val_score,TimeSeriesSplit

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

#Define the input range
x_min , x_max = 2,100 #Look ahead
y_min , y_max = 2,100 #Period

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

#Sample input range uniformly
x_axis = np.arange(x_min,x_max,2) #Look ahead
y_axis = np.arange(y_min,y_max,2) #Period

Создадим сетку по осям x_axis и y_axis. Эта сетка состоит из двух двумерных массивов, которые содержат все возможные комбинации горизонтов прогноза и периодов, которые мы хотим оценить.

#Create a meshgrid
x , y = np.meshgrid(x_axis,y_axis)

Далее нам нужна функция, которая будет загружать рыночные данные из терминала и размечать их для оценки.

def clean_data(look_ahead,period):
    #Fetch the data from our terminal and clean it up 
    data = pd.DataFrame(mt5.copy_rates_from_pos('AUDJPY',mt5.TIMEFRAME_D1,365*2,5000))
    data['time'] = pd.to_datetime(data['time'],unit='s')
    data['MA'] = data['close'].rolling(period).mean()
    #Transform the data
    #Target
    data['Target'] = data['MA'].shift(-look_ahead) - data['MA']
    #Change in price
    data['close']  = data['close'] - data['close'].shift(period)
    #Change in MA
    data['MA']  = data['MA'] - data['MA'].shift(period)
    data.dropna(inplace=True)
    data.reset_index(drop=True,inplace=True)
    return(data)

Следующая функция выполняет 5-кратную кросс-валидацию (5-fold cross validation) для оценки точности модели:

#Evaluate the objective function
def evaluate(look_ahead,period):
    #Define the model
    model = LinearRegression()
    #Define our time series split
    tscv = TimeSeriesSplit(n_splits=5,gap=look_ahead)
    temp = clean_data(look_ahead,period)
    score = np.mean(cross_val_score(model,temp.loc[:,["Open","High","Low","Close"]],temp["Target"],cv=tscv))
    return(score)

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

#Define the objective
def objective(x,y):
    #Define the output matrix
    results = np.zeros([x.shape[0],y.shape[0]])
    #Fill in the output matrix
    for i in np.arange(0,x.shape[0]):
        #Select the rows
        look_ahead = x[i]
        period     = y[i]
        for j in np.arange(0,y.shape[0]):
            results[i,j] = evaluate(look_ahead[j],period[j])
    return(results)

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

res = objective(x,y)
res = np.abs(res)

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

plt.contourf(x,y,res,100,cmap="jet")
plt.plot(x_axis[res.min(axis=0).argmin()],y_axis[res.min(axis=1).argmin()],'.',color='white')
plt.ylabel("Differencing Period")
plt.xlabel("Forecast Horizon")
plt.title("Linear Regression Accuracy Forecasting AUDJPY Daily Return")

Рис. 7: Визуализация способности модели прогнозировать по уровню цены

Если переключиться с прогнозирования изменения цены на прогнозирование изменения скользящей средней, можно заметить, что оптимальный горизонт предсказания смещается вправо. На Рис. 8 видно, что с прогнозом скользящих средних мы можем вполне надежно предсказывать до 22 шагов вперед, в отличие от одного шага вперед при прогнозе цен.

plt.contourf(x,y,res,100,cmap="jet")
plt.plot(x_axis[res.min(axis=0).argmin()],y_axis[res.min(axis=1).argmin()],'.',color='white')
plt.ylabel("Differencing Period")
plt.xlabel("Forecast Horizon")
plt.title("Linear Regression Accuracy Forecasting AUDJPY Daily Moving Average Return")


Рис. 7: Визуализация способности модели прогнозировать скользящую среднюю

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

При визуализации результатов тестирования в 3D различие между двумя целевыми переменными становится особенно наглядным. Рис. 9 показывает взаимосвязь между изменениями цен и параметрами прогнозирования модели. Здесь видна явная тенденция: по мере увеличения горизонта прогноза точность модели резко падает. Это означает, что в таком виде модели обладают "близорукостью" и не способны разумно прогнозировать интервалы более 20 шагов.

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

#Create a surface plot
fig , ax = plt.subplots(subplot_kw={"projection":"3d"})
fig.set_size_inches(8,8)
ax.plot_surface(x,y,optimal_nn_res,cmap="jet")


Рис. 9: Визуализация взаимосвязи между моделью и изменениями дневной цены AUDJPY.


Рис. 10: Взаимосвязь между моделью и изменениями значения скользящей средней по паре AUDJPY.


Нелинейные преобразования: вейвлет-фильтрация шума

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

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

Вейвлет-фильтрация имеет несколько преимуществ по сравнению с другими популярными методами, такими как быстрое преобразование Фурье (FFT). Напомню, что преобразование Фурье представляет любой сигнал в виде суммы синусоид и косинусоид. К сожалению, оно отфильтровывает высокочастотные компоненты сигнала. Такой метод не подходит, если полезный сигнал как раз находится в высокочастотной области. Поскольку заранее неизвестно, относится ли наш сигнал к высокочастотной или низкочастотной области, нам требуется более гибкое преобразование, способное выполнять фильтрацию в режиме без подкрепления. Вейвлет-преобразование позволяет сохранить полезный сигнал в данных, максимально устраняя шум.

Чтобы продолжить далее, нужно иметь установленные пакеты scikit-learn-image и PyWavelets. Тем, кто планирует создавать полноценное торговое приложение в MQL5, реализация и отладка вейвлет-преобразования с нуля может оказаться довольно трудоёмкой. Поэтому проще продолжать работу без него. Но если вы планируете использовать интеграцию терминала с Python, данный инструмент стоит иметь в своем арсенале.

Мы можем сравнить изменения точности на валидационной выборке, чтобы определить, помогает ли преобразование нашей модели — и, как оказалось, помогает. Обратите внимание, что мы применяем преобразование только к входным данным модели, не изменяя целевую переменную. Она остается в исходном виде. При этом точность на валидационной выборке несколько снизилась. Мы используем вейвлет-преобразование с жестким порогом (hard threshold), при котором все коэффициенты шума обнуляются. В качестве альтернативы можно попробовать мягкий порог (soft threshold), при котором коэффициенты шума стремятся к нулю, но не обязательно становятся строго равными нулю.

#Benchmark Score
np.mean(cross_val_score(LinearRegression(),data.loc[:,["MA"]],data["Target"]))
0.9935846835797412

#Wavelet denoising
data["Denoised"] = denoise_wavelet(
    data["MA"],
    method='BayesShrink',
    mode='hard',
    rescale_sigma=True,
    wavelet_levels = 3,
    wavelet='sym5'
    )
np.mean(cross_val_score(LinearRegression(),np.sqrt(np.log(data.loc[:,["Denoised"]])),data["Target"]))
0.9082244556297641


Построение индивидуальных AI-моделей

Итак, мы определили оптимальные параметры горизонта прогнозирования и идеальный период скользящей средней. Теперь загрузим рыночные данные непосредственно из терминала MetaTrader 5. Это необходимо для того, чтобы наши AI-модели обучались на тех же значениях индикаторов, которые они будут использовать в процессе реальной торговли. Мы стремимся как можно точнее имитировать реальные условия торговли.

В скрипте период скользящей средней установим равным рассчитанному выше оптимальному значению. Кроме того, также загрузим значения индикатора RSI из терминала, чтобы улучшить стабильность поведения нашего советника с AI. Опираясь на прогнозы сразу от двух различных индикаторов, а не одного, наша модель сможет демонстрировать более стабильную работу на длительных интервалах.

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"
#property script_show_inputs

//+------------------------------------------------------------------+
//| Script Inputs                                                    |
//+------------------------------------------------------------------+
input int size = 100000; //How much data should we fetch?

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
int    ma_handler,rsi_handler;
double ma_reading[],rsi_reading[];

//+------------------------------------------------------------------+
//| On start function                                                |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Load indicator
ma_handler  = iMA(Symbol(),PERIOD_CURRENT,40,0,MODE_SMA,PRICE_CLOSE);
rsi_handler = iRSI(Symbol(),PERIOD_CURRENT,30,PRICE_CLOSE);
   
//--- Load the indicator values
CopyBuffer(ma_handler,0,0,size,ma_reading);
CopyBuffer(rsi_handler,0,0,size,rsi_reading);

ArraySetAsSeries(ma_reading,true);
ArraySetAsSeries(rsi_reading,true);

//--- File name
   string file_name = "Market Data " + Symbol() +" MA RSI " +  " As Series.csv";

//--- Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i= size;i>=0;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","Open","High","Low","Close","MA","RSI");
        }

      else
        {
         FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i),
                   iOpen(Symbol(),PERIOD_CURRENT,i),
                   iHigh(Symbol(),PERIOD_CURRENT,i),
                   iLow(Symbol(),PERIOD_CURRENT,i),
                   iClose(Symbol(),PERIOD_CURRENT,i),
                   ma_reading[i],
                   rsi_reading[i]
                  );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+

Наш скрипт готов. Теперь можем накинуть его на нужный рынок и начать работу с рыночными данными. Чтобы результаты наших тестов были достоверными, мы исключили последние два года рыночных данных из созданного CSV-файла. Таким образом, когда мы будем тестировать нашу стратегию на данных за период с 2023 по 2024 годы, результаты будут отражать реальную работу модели на данных, которых она ранее не видела.

#Read in the data
data = pd.read_csv("Market Data AUDJPY MA RSI As Series.csv")
#Let's drop the last two years of data. We'll use that to validate our model in the back test
data = data.iloc[365:-(365 * 2),:]
data

Рис. 11: Обучение модели на выборке рыночных данных за 22 года, за исключением периода 2023-2024 гг.

Разметим данные для обучения модели. Мы хотим научить модель прогнозировать изменения цены на основе изменений технических индикаторов. Чтобы помочь модели изучить эту взаимосвязь, преобразуем входные данные таким образом, чтобы отражать текущее состояние индикаторов. Например, для индикатора RSI мы определим три возможных состояния:

  1. Выше 70.
  2. Ниже 30.
  3. Между 70 и 30.
Если текущее значение RSI больше 70, тогда в первой колонке RSI 1 будет установлено значение 1, а в остальных — 0. Аналогичным образом мы обработаем и скользящую среднюю, но для нее определим только два состояния — рост и снижение. Такие преобразования позволят модели сосредоточиться на критических изменениях индикаторов — точно так же, как их интерпретируем мы.
#MA States
data["MA 1"] = 0
data["MA 2"] = 0
data.loc[data["MA"] > data["MA"].shift(40),"MA 1"] = 1
data.loc[data["MA"] <= data["MA"].shift(40),"MA 2"] = 1

#RSI States
data["RSI 1"] = 0
data["RSI 2"] = 0
data["RSI 3"] = 0
data.loc[data["RSI"] < 30,"RSI 1"] = 1
data.loc[data["RSI"] > 70,"RSI 2"] = 1
data.loc[(data["RSI"] >= 30) & (data["RSI"] <= 70),"RSI 3"] = 1

#Target
data["Target"]    = data["Close"].shift(-22) - data["Close"]
data["MA Target"] = data["MA"].shift(-22) - data["MA"]

#Clean up the data
data = data.dropna()
data = data.iloc[40:,:]
data = data.reset_index(drop=True)

Перейдем к измерению точности.

from sklearn.linear_model import Ridge
from sklearn.model_selection import TimeSeriesSplit,cross_val_score

С преобразованиями мы можем видеть среднее изменение цены при переходе индикатора RSI через три выделенные нами зоны. Коэффициенты нашей линейной модели можно интерпретировать как среднее изменение цены, соответствующее каждому из трех состояний RSI. Интересно, что полученные результаты иногда могут противоречить классическим методикам использования индикатора. Например, наша модель Ridge (гребневая регрессия) обучилась тому, что при значении RSI выше 70 уровни цен, как правило, начинают снижаться; напротив, когда RSI меньше 70 — цены, как правило, продолжают расти.

#Our model can suggest optimal ways of using the RSI indicator
#Our model has learned that on average price tends to fall the RSI reading is less than 30 and increases otherwises
model = Ridge()

model.fit(data.loc[:,["RSI 1","RSI 2","RSI 3"]] , data["Target"])

model.coef_
array([-0.19297857,  0.14816216,  0.04481641])

Наша модель Ridge способна делать прогнозы будущих цен, опираясь исключительно на текущее состояние RSI.

#RSI state
np.mean(cross_val_score(Ridge(),data.loc[:,["RSI 1","RSI 2","RSI 3"]] , data["Target"],cv=tscv))
-0.025569914370219736

Аналогичным образом, модель обучилась собственным правилам для торговли на основе поведения скользящей средней. Первый коэффициент модели отрицателен — это означает, что если скользящая средняя за 40 свечей возрастает, то далее она, как правило, начинает снижаться. Второй коэффициент положителен. Это говорит о противоположной динамике: когда 40-дневная скользящая средняя AUDJPY снижается за последние 40 свечей — в дальнейшем она, как правило, растет. Таким образом, на основе исторических данных, загруженных из терминала, модель самостоятельно изучила стратегию возврата к среднему(mean reversion).

#Our model can suggest optimal ways of using the RSI indicator
#Our model has learned that on average price tends to fall the RSI reading is less than 30 and increases otherwises
model = Ridge()

model.fit(data.loc[:,["MA 1","MA 2"]] , data["Target"])

model.coef_
array([-0.15572796,  0.15572796])

Модель работает еще лучше, если задать ей текущее состояние индикатора скользящей средней.

#MA state
np.mean(cross_val_score(Ridge(),data.loc[:,["MA 1","MA 2"]] , data["Target"],cv=tscv))
-0.009645886983465935


Конвертация модели в формат ONNX

Мы определили оптимальные входные параметры для прогноза скользящей средней и можем подготовить модель к конвертации в формат ONNX. Open Neural Network Exchange (ONNX) позволяет разрабатывать модели машинного обучения в независимой от языка среде. ONNX — это протокол с открытым исходным кодом, который позволяет создавать и разворачивать модели на любом языке программирования, поддерживающем ONNX API.

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

#Fetch clean data
new_data = clean_data(140,130)

Импортируем необходимые библиотеки.

import onnx
from   skl2onnx import convert_sklearn
from   skl2onnx.common.data_types import FloatTensorType

Обучим модель RSI на всех имеющихся у нас данных.

#First we will export the RSI model
rsi_model = Ridge()
rsi_model.fit(data.loc[:,['RSI 1','RSI 2','RSI 3']],data.loc[:,'Target'])

Обучим модель скользящей средней на всех данных.

#Finally we will export the MA model
ma_model = Ridge()
ma_model.fit(data.loc[:,['MA 1','MA 2']],data.loc[:,'MA Target'])

Определим входную форму нашей модели и сохраним ее на диск.

initial_types = [('float_input', FloatTensorType([1, 3]))]
onnx.save(convert_sklearn(rsi_model,initial_types=initial_types,target_opset=12),"AUDJPY D1 RSI AI F22 P40.onnx")

initial_types = [('float_input', FloatTensorType([1, 2]))]
onnx.save(convert_sklearn(ma_model,initial_types=initial_types,target_opset=12),"AUDJPY D1 MA AI F22 P40.onnx")


Реализация средствами MQL5

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

Кроме того, мы реализуем систему трейдинг-стопов для управления рисками. Для определения уровней стоп-лоссов и тейк-профитов будем использовать индикатор Average True Range (ATR). Основой торговой стратегии будут каналы по скользящим средним.

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

Начнем с импорта ранее созданного файла ONNX.

//+------------------------------------------------------------------+
//|                                                    GBPUSD AI.mq5 |
//|                                        Gamuchirai Zororo Ndawana |
//|                          https://www.mql5.com/en/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Load our resources                                               |
//+------------------------------------------------------------------+
#resource  "\\Files\\AUDJPY D1 MA AI F22 P40.onnx" as const uchar onnx_buffer[];
#resource  "\\Files\\AUDJPY D1 RSI AI F22 P40.onnx" as const uchar rsi_onnx_buffer[];

Затем импортируем нужные библиотеки.

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;
#include <Trade\OrderInfo.mqh>
class COrderInfo;

Теперь определим нужные глобальные переменные.

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
long     onnx_model;
int      ma_handler,state;
double   bid,ask,vol;
vectorf  model_forecast   = vectorf::Zeros(1);
vectorf  rsi_model_output = vectorf::Zeros(1);
double   min_volume,max_volume_increase, volume_step, buy_stop_loss, sell_stop_loss,atr_stop,risk_equity;
double   take_profit = 0;
double   close_price[3],atr_reading[],ma_buffer[];
long     min_distance,login;
int      atr,close_average,ticket_1,ticket_2;
bool     authorized = false;
double   margin,lot_step;
string   currency,server;
bool     all_closed =true;
int      rsi_handler;
long     rsi_onnx_model;
double   indicator_reading[];
ENUM_ACCOUNT_TRADE_MODE account_type;
const double  stop_percent = 1;

Определим наши входные переменные.

//+------------------------------------------------------------------+
//| Technical indicators                                             |
//+------------------------------------------------------------------+
input group "Money Management"
input int    lot_multiple     = 10; // How big should the lot size be?
input double profit_target = 0;     // Profit Target
input double loss_target   = 0;     // Max Loss Allowed

input group "Money Management"
const int    atr_period = 200;      //ATR Period
input double atr_multiple =2.5;     //ATR Multiple

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

int OnInit()
  {
//Authorization
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program");
      return(INIT_FAILED);
     }

   else
      if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
        {
         Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading");
         return(INIT_FAILED);
        }

      else
        {
         Comment("This License is Genuine");
         setup();
        }
//Everything was okay
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   OnnxRelease(onnx_model);
   OnnxRelease(rsi_onnx_model);
   IndicatorRelease(atr)
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//Update technical data
   update();

   if(PositionsTotal() == 0)
     {
      check_setup();
     }

   if(PositionsTotal() > 0)
     {
      check_atr_stop();
     }
  }

Чтобы получить прогноз с помощью нашей модели, необходимо определить текущие состояния RSI и индикатора скользящей средней.

//+------------------------------------------------------------------+
//| Get a prediction from our model                                  |
//+------------------------------------------------------------------+
int model_predict(void)
  {
//MA Forecast
   vectorf  model_inputs = vectorf::Zeros(2);
   vectorf  rsi_model_inputs = vectorf::Zeros(3);
   CopyBuffer(ma_handler,0,0,40,ma_buffer);

   if(ma_buffer[0] > ma_buffer[39])
     {
      model_inputs[0] = 1;
      model_inputs[1] = 0;
     }

   else
      if(ma_buffer[0] < ma_buffer[39])
        {
         model_inputs[1] = 1;
         model_inputs[0] = 0;
        }

//RSI Forecast
   CopyBuffer(rsi_handler,0,0,1,indicator_reading);

   if(indicator_reading[0] < 30)
     {
      rsi_model_inputs[0] = 1;
      rsi_model_inputs[1] = 0;
      rsi_model_inputs[2] = 0;
     }


   else
      if(indicator_reading[0] >70)
        {
         rsi_model_inputs[0] = 0;
         rsi_model_inputs[1] = 1;
         rsi_model_inputs[2] = 0;
        }

      else
        {
         rsi_model_inputs[0] = 0;
         rsi_model_inputs[1] = 0;
         rsi_model_inputs[2] = 1;
        }

//Model predictions
   OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast);
   OnnxRun(rsi_onnx_model,ONNX_DEFAULT,rsi_model_inputs,rsi_model_output);


//Evaluate model output for buy setup
   if(((rsi_model_output[0] > 0)  && (model_forecast[0] > 0)))
     {
      //AI Models forecast
      Comment("AI Forecast: UP");
      return(1);
     }

//Evaluate model output for a sell setup
   if((rsi_model_output[0] < 0) && (model_forecast[0] < 0))
     {
      Comment("AI Forecast: DOWN");
      return(-1);
     }

//Otherwise no position was found
   return(0);
  }

Обновим глобальные переменные. Удобнее выполнять эти обновления за один вызов функции, а не выполнять весь код напрямую в обработчике OnTick().

//+------------------------------------------------------------------+
//| Update our market data                                           |
//+------------------------------------------------------------------+
void update(void)
  {
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   buy_stop_loss = 0;
   sell_stop_loss = 0;
   static datetime time_stamp;
   datetime time = iTime(_Symbol,PERIOD_CURRENT,0);
   check_price(3);
   CopyBuffer(atr,0,0,1,atr_reading);
   CopyBuffer(ma_handler,0,0,1,ma_buffer);
   ArraySetAsSeries(atr_reading,true);
   atr_stop = ((min_volume + atr_reading[0]) * atr_multiple);
//On Every Candle
   if(time_stamp != time)
     {

      //Mark the candle
      time_stamp = time;
      OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,min_volume,ask,margin);
     }
  }

Загрузим необходимые ресурсы: технические индикаторы, информацию о счетах и рынках, и т.д.

//+------------------------------------------------------------------+
//| Load resources                                                   |
//+------------------------------------------------------------------+
bool setup(void)
  {
//Account Info
   currency = AccountInfoString(ACCOUNT_CURRENCY);
   server = AccountInfoString(ACCOUNT_SERVER);
   login = AccountInfoInteger(ACCOUNT_LOGIN);

//Indicators
   atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);

//Setup technical indicators
   ma_handler   =iMA(Symbol(),PERIOD_CURRENT,40,0,MODE_SMA,PRICE_LOW);
   vol          = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) * lot_multiple;
   rsi_handler  = iRSI(Symbol(),PERIOD_CURRENT,30,PRICE_CLOSE);

//Market Information
   min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   max_volume_increase = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX) / SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
   lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);

//Define our ONNX model
   ulong ma_input_shape [] = {1,2};
   ulong rsi_input_shape [] = {1,3};
   ulong output_shape [] = {1,1};

//Create the model
   onnx_model     = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
   rsi_onnx_model = OnnxCreateFromBuffer(rsi_onnx_buffer,ONNX_DEFAULT);

   if((onnx_model == INVALID_HANDLE) || (rsi_onnx_model == INVALID_HANDLE))
     {
      Comment("[ERROR] Failed to load AI module correctly");
      return(false);
     }

//Validate I/O
   if((!OnnxSetInputShape(onnx_model,0,ma_input_shape)) || (!OnnxSetInputShape(rsi_onnx_model,0,rsi_input_shape)))
     {
      Comment("[ERROR] Failed to set input shape correctly: ",GetLastError());
      return(false);
     }

   if((!OnnxSetOutputShape(onnx_model,0,output_shape)) || (!OnnxSetOutputShape(rsi_onnx_model,0,output_shape)))
     {
      Comment("[ERROR] Failed to load AI module correctly: ",GetLastError());
      return(false);
     }
//Everything went fine
   return(true);
  }

Итак, получилось вот такое приложение.

//+------------------------------------------------------------------+
//|                                                    GBPUSD AI.mq5 |
//|                                        Gamuchirai Zororo Ndawana |
//|                          https://www.mql5.com/en/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Load our resources                                               |
//+------------------------------------------------------------------+
#resource  "\\Files\\AUDJPY D1 MA AI F22 P40.onnx" as const uchar onnx_buffer[];
#resource  "\\Files\\AUDJPY D1 RSI AI F22 P40.onnx" as const uchar rsi_onnx_buffer[];

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;
#include <Trade\OrderInfo.mqh>
class COrderInfo;

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
long     onnx_model;
int      ma_handler,state;
double   bid,ask,vol;
vectorf  model_forecast   = vectorf::Zeros(1);
vectorf  rsi_model_output = vectorf::Zeros(1);
double   min_volume,max_volume_increase, volume_step, buy_stop_loss, sell_stop_loss,atr_stop,risk_equity;
double   take_profit = 0;
double   close_price[3],atr_reading[],ma_buffer[];
long     min_distance,login;
int      atr,close_average,ticket_1,ticket_2;
bool     authorized = false;
double   margin,lot_step;
string   currency,server;
bool     all_closed =true;
int      rsi_handler;
long     rsi_onnx_model;
double   indicator_reading[];
ENUM_ACCOUNT_TRADE_MODE account_type;
const double  stop_percent = 1;

//+------------------------------------------------------------------+
//| Technical indicators                                             |
//+------------------------------------------------------------------+
input group "Money Management"
input int    lot_multiple     = 10; // How big should the lot size be?
input double profit_target = 0;     // Profit Target
input double loss_target   = 0;     // Max Loss Allowed

input group "Money Management"
input int    bb_period = 36;        //Bollinger band period
input int    ma_period = 4;         //Moving average period
const int    atr_period = 200;      //ATR Period
input double atr_multiple =2.5;      //ATR Multiple

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//Authorization
   if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
      Comment("Press Ctrl + E To Give The Robot Permission To Trade And Reload The Program");
      return(INIT_FAILED);
     }

   else
      if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
        {
         Comment("Reload The Program And Make Sure You Clicked Allow Algo Trading");
         return(INIT_FAILED);
        }

      else
        {
         Comment("This License is Genuine");
         setup();
        }
//--- Everything was okay
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   OnnxRelease(onnx_model);
   OnnxRelease(rsi_onnx_model);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Update technical data
   update();

   if(PositionsTotal() == 0)
     {
      check_setup();
     }

   if(PositionsTotal() > 0)
     {
      check_atr_stop();
     }
  }
//+------------------------------------------------------------------+
//| Get a prediction from our model                                  |
//+------------------------------------------------------------------+
int model_predict(void)
  {
//MA Forecast
   vectorf  model_inputs = vectorf::Zeros(2);
   vectorf  rsi_model_inputs = vectorf::Zeros(3);
   CopyBuffer(ma_handler,0,0,40,ma_buffer);

   if(ma_buffer[0] > ma_buffer[39])
     {
      model_inputs[0] = 1;
      model_inputs[1] = 0;
     }

   else
      if(ma_buffer[0] < ma_buffer[39])
        {
         model_inputs[1] = 1;
         model_inputs[0] = 0;
        }

//RSI Forecast
   CopyBuffer(rsi_handler,0,0,1,indicator_reading);

   if(indicator_reading[0] < 30)
     {
      rsi_model_inputs[0] = 1;
      rsi_model_inputs[1] = 0;
      rsi_model_inputs[2] = 0;
     }


   else
      if(indicator_reading[0] >70)
        {
         rsi_model_inputs[0] = 0;
         rsi_model_inputs[1] = 1;
         rsi_model_inputs[2] = 0;
        }

      else
        {
         rsi_model_inputs[0] = 0;
         rsi_model_inputs[1] = 0;
         rsi_model_inputs[2] = 1;
        }

//Model predictions
   OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast);
   OnnxRun(rsi_onnx_model,ONNX_DEFAULT,rsi_model_inputs,rsi_model_output);


//Evaluate model output for buy setup
   if(((rsi_model_output[0] > 0)  && (model_forecast[0] > 0)))
     {
      //AI Models forecast
      Comment("AI Forecast: UP");
      return(1);
     }

//Evaluate model output for a sell setup
   if((rsi_model_output[0] < 0) && (model_forecast[0] < 0))
     {
      Comment("AI Forecast: DOWN");
      return(-1);
     }

//Otherwise no position was found
   return(0);
  }

//+------------------------------------------------------------------+
//| Check for valid trade setups                                     |
//+------------------------------------------------------------------+
void check_setup(void)
  {
   int res = model_predict();

   if(res == -1)
     {
      Trade.Sell(vol,Symbol(),bid,0,0,"VD V75 AI");
      state = -1;
     }

   else
      if(res == 1)
        {
         Trade.Buy(vol,Symbol(),ask,0,0,"VD V75 AI");
         state = 1;
        }
  }

//+------------------------------------------------------------------+
//| Update our market data                                           |
//+------------------------------------------------------------------+
void update(void)
  {
   ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   buy_stop_loss = 0;
   sell_stop_loss = 0;
   static datetime time_stamp;
   datetime time = iTime(_Symbol,PERIOD_CURRENT,0);
   check_price(3);
   CopyBuffer(atr,0,0,1,atr_reading);
   CopyBuffer(ma_handler,0,0,1,ma_buffer);
   ArraySetAsSeries(atr_reading,true);
   atr_stop = ((min_volume + atr_reading[0]) * atr_multiple);
//On Every Candle
   if(time_stamp != time)
     {

      //Mark the candle
      time_stamp = time;
      OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,min_volume,ask,margin);
     }
  }

//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Load resources                                                   |
//+------------------------------------------------------------------+
bool setup(void)
  {
//Account Info
   currency = AccountInfoString(ACCOUNT_CURRENCY);
   server = AccountInfoString(ACCOUNT_SERVER);
   login = AccountInfoInteger(ACCOUNT_LOGIN);

//Indicators
   atr = iATR(_Symbol,PERIOD_CURRENT,atr_period);

//Setup technical indicators
   ma_handler   =iMA(Symbol(),PERIOD_CURRENT,40,0,MODE_SMA,PRICE_LOW);
   vol          = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN) * lot_multiple;
   rsi_handler  = iRSI(Symbol(),PERIOD_CURRENT,30,PRICE_CLOSE);

//Market Information
   min_volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   max_volume_increase = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX) / SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
   min_distance = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL);
   lot_step = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);

//Define our ONNX model
   ulong ma_input_shape [] = {1,2};
   ulong rsi_input_shape [] = {1,3};
   ulong output_shape [] = {1,1};

//Create the model
   onnx_model     = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);
   rsi_onnx_model = OnnxCreateFromBuffer(rsi_onnx_buffer,ONNX_DEFAULT);

   if((onnx_model == INVALID_HANDLE) || (rsi_onnx_model == INVALID_HANDLE))
     {
      Comment("[ERROR] Failed to load AI module correctly");
      return(false);
     }

//--- Validate I/O
   if((!OnnxSetInputShape(onnx_model,0,ma_input_shape)) || (!OnnxSetInputShape(rsi_onnx_model,0,rsi_input_shape)))
     {
      Comment("[ERROR] Failed to set input shape correctly: ",GetLastError());
      return(false);
     }

   if((!OnnxSetOutputShape(onnx_model,0,output_shape)) || (!OnnxSetOutputShape(rsi_onnx_model,0,output_shape)))
     {
      Comment("[ERROR] Failed to load AI module correctly: ",GetLastError());
      return(false);
     }
//--- Everything went fine
   return(true);
  }

//+------------------------------------------------------------------+
//| Close all our open positions                                     |
//+------------------------------------------------------------------+
void close_all()
  {
   if(PositionsTotal() > 0)
     {
      ulong ticket;
      for(int i =0;i < PositionsTotal();i++)
        {
         ticket = PositionGetTicket(i);
         Trade.PositionClose(ticket);
        }
     }
  }

//+------------------------------------------------------------------+
//| Update our trailing ATR stop                                     |
//+------------------------------------------------------------------+
void check_atr_stop()
  {

   for(int i = PositionsTotal() -1; i >= 0; i--)
     {

      string symbol = PositionGetSymbol(i);
      if(_Symbol == symbol)
        {

         ulong ticket = PositionGetInteger(POSITION_TICKET);
         double position_price = PositionGetDouble(POSITION_PRICE_OPEN);
         double type = PositionGetInteger(POSITION_TYPE);
         double current_stop_loss = PositionGetDouble(POSITION_SL);

         if(type == POSITION_TYPE_BUY)
           {
            double atr_stop_loss = (ask - (atr_stop));
            double atr_take_profit = (ask + (atr_stop));

            if((current_stop_loss < atr_stop_loss) || (current_stop_loss == 0))
              {
               Trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
              }
           }

         else
            if(type == POSITION_TYPE_SELL)
              {
               double atr_stop_loss = (bid + (atr_stop));
               double atr_take_profit = (bid - (atr_stop));
               if((current_stop_loss > atr_stop_loss) || (current_stop_loss == 0))
                 {
                  Trade.PositionModify(ticket,atr_stop_loss,atr_take_profit);
                 }
              }
        }
     }
  }

//+------------------------------------------------------------------+
//| Close our open buy positions                                     |
//+------------------------------------------------------------------+
void close_buy()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal();i++)
        {
         if(PositionGetSymbol(i) == _Symbol)
           {
            ticket = PositionGetTicket(i);
            type = (int)PositionGetInteger(POSITION_TYPE);
            if(type == POSITION_TYPE_BUY)
              {
               Trade.PositionClose(ticket);
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Close our open sell positions                                    |
//+------------------------------------------------------------------+
void close_sell()
  {
   ulong ticket;
   int type;
   if(PositionsTotal() > 0)
     {
      for(int i = 0; i < PositionsTotal();i++)
        {
         if(PositionGetSymbol(i) == _Symbol)
           {
            ticket = PositionGetTicket(i);
            type = (int)PositionGetInteger(POSITION_TYPE);
            if(type == POSITION_TYPE_SELL)
              {
               Trade.PositionClose(ticket);
              }
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Get the most recent price values                                 |
//+------------------------------------------------------------------+
void check_price(int candles)
  {
   for(int i = 0; i < candles;i++)
     {
      close_price[i] = iClose(_Symbol,PERIOD_CURRENT,i);
     }
  }
//+------------------------------------------------------------------+

Теперь протестируем наш торговый алгоритм на истории, используя данные, которые не использовались при обучении модели. Для тестов возьмем период с начала января 2023 года по 28 июня 2024 года, дневные котировки пары AUDJPY. Параметр Forward установим в значение No, поскольку выбранные даты не входили в обучающую выборку модели.


Рис. 12: Символ и таймфрейм, которые мы будем использовать для тестирования торговой стратегии.

Дополнительно, чтобы сымитировать реальные торговые условия, установим параметр Delays в значение Random delay. Этот параметр управляет задержкой между размещением ордера и его исполнением. Задание случайной задержки приближает симуляцию к реальным рыночным условиям, поскольку задержки исполнения обычно непостоянны. Также выберем режим моделирования рынка по реальным тикам. Такой режим замедляет тестирование, поскольку терминалу потребуется загрузить подробные рыночные данные от брокера через интернет.

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

Рис. 13: Параметры тестирования.

Рис. 14: Результаты тестирования стратегии на данных, которые ранее не использовались для обучения модели.

Рис. 15: Дополнительные детали тестирования на новых рыночных данных.


Заключение

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


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

Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Too Chee Ng
Too Chee Ng | 27 мар. 2025 в 10:43

Результат выглядит многообещающе, будем пробовать.

Пожалуйста, побольше подобных материалов.

Спасибо.

npats2007
npats2007 | 13 июн. 2025 в 15:03
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 10 июл. 2025 в 09:31
Too Chee Ng # :

Результаты выглядят многообещающе.

Побольше таких вещей, пожалуйста.

Больше подобных вещей, пожалуйста. Спасибо.

Не за что, Ту Че Нг.

Определенно, еще многое можно сказать, учитывая такое сильное начало.


Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana | 10 июл. 2025 в 09:33
npats2007 # :

Некоторые изображения не отображаются...

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

От начального до среднего уровня: Плавающая точка От начального до среднего уровня: Плавающая точка
Эта статья является кратким введением к понятию числа с плавающей точкой. Поскольку этот текст очень сложный, советую вам прочитать его спокойно и внимательно. Не рассчитывайте быстро освоить систему с плавающей точкой, она становится понятной только со временем, по мере появления опыта использования. Но эта статья поможет вам понять, почему ваше приложение иногда выдает результат, отличный от ожидаемого.
Разработка системы репликации (Часть 75): Новый Chart Trade (II) Разработка системы репликации (Часть 75): Новый Chart Trade (II)
В этой статье мы расскажем о классе C_ChartFloatingRAD. Это то, что позволяет Chart Trade работать. Однако на этом объяснение не закончится. Мы завершим его в следующей статье, так как содержание данной статьи довольно объемное и требует глубокого понимания. Представленные здесь материалы предназначены только для обучения. Ни в коем случае не рассматривайте его как окончательное приложение, целью которого не является изучение представленных концепций.
Разработка системы репликации (Часть 76): Новый Chart Trade (III) Разработка системы репликации (Часть 76): Новый Chart Trade (III)
В этой статье мы рассмотрим, как работает недостающий код из предыдущей статьи, DispatchMessage. Здесь мы введем тему следующей статьи. По этой причине важно понять, как работает данная процедура, прежде чем переходить к следующей теме. Представленные здесь материалы предназначены только для обучения. Ни в коем случае не рассматривайте это приложение как окончательное, цели которого будут иные, кроме изучения представленных концепций.
Создание Python-классов для торговли в MetaTrader 5, аналогичных представленным в MQL5 Создание Python-классов для торговли в MetaTrader 5, аналогичных представленным в MQL5
Python-пакет MetaTrader 5 предлагает простой способ создания торговых приложений для платформы MetaTrader 5 на языке Python. Будучи мощным и полезным инструментом данный модуль не так прост как язык программирования MQL5, когда дело касается разработки решений для алгоритмической торговли. В данной статье мы создадим классы для торговли, аналогичные предлагаемым в языке MQL5, чтобы создать схожий синтаксис и сделать разработку торговых роботов на Python такой же простой как и на MQL5.