
Переосмысливаем классические стратегии (Часть V): Анализ нескольких инструментов в валютной паре USDZAR
Введение
Существует бесчисленное множество способов интеграции ИИ в наши торговые стратегии, но, к сожалению, мы не можем оценить каждый из них, прежде чем решим, какому из них доверить свой капитал. Сегодня мы вновь рассмотрим популярную торговую стратегию анализа нескольких инструментов, чтобы определить, можем ли мы улучшить ее с помощью ИИ. Мы предоставим вам информацию, необходимую для принятия обоснованного решения о том, подходит ли данная стратегия для вашего инвестиционного профиля.
Обзор торговой стратегии
Торговые стратегии, использующие анализ нескольких инструментов, в основном основаны на корреляции, которая наблюдается между корзиной инструментов. Корреляция - это мера линейной зависимости между двумя переменными. Однако корреляцию часто ошибочно принимают за указание на взаимосвязь между двумя переменными, что не всегда может соответствовать действительности.
Трейдеры по всему миру используют свое фундаментальное понимание коррелированных активов для принятия инвестиционных решений, оценки уровней рисков и даже в качестве сигнала выхода. Для примера рассмотрим валютную пару USDZAR. Американское правительство является одним из ведущих экспортеров нефти в мире, в то время как, с другой стороны, правительство Южной Африки является крупнейшим в мире экспортером золота.
Поскольку эти сырьевые товары составляют значительную долю в валовом внутреннем продукте этих двух стран, естественно было бы ожидать, что уровни цен на эти сырьевые товары могут частично объяснить флуктуацию в валютной паре USDZAR. Таким образом, если цены на нефть на спотовом рынке выше, чем на золото, можно ожидать, что доллар будет сильнее ранда, и наоборот.
Обзор методологии
Чтобы оценить взаимосвязь, мы экспортировали все наши рыночные данные из нашего терминала MetaTrader 5 с помощью скрипта, написанного на MQL5. Мы обучили различные модели, используя 2 группы возможных входных данных для моделей:
- Обычные котировки OHLC по курсу USDZAR.
- Комбинация цен на нефть и золото.
Из собранных данных следует, что нефть имеет более сильные уровни корреляции с валютной парой USDZAR, чем золото.
Поскольку наши данные были в разных масштабах, перед обучением мы их стандартизировали и нормализовали. Мы провели 10-кратную перекрестную проверку без случайной перестановки данных, чтобы сравнить нашу точность с различными наборами входных данных.
Наши результаты показывают, что первая группа может дать наименьшую погрешность. Наиболее эффективной моделью оказалась линейная регрессия с использованием обычных данных OHLC. Однако в последней группе, 2, наиболее эффективной моделью был алгоритм KNeigborsRegressor.
Мы успешно выполнили настройку гиперпараметров, используя 500 итераций рандомизированного поиска по 5 параметрам модели. Мы провели тестирование на предмет переобучения путем сравнения уровней ошибок нашей настроенной модели с моделью по умолчанию в валидационном наборе, который удерживался во время оптимизации. После обучения обеих моделей на эквивалентных обучающих наборах, мы превзошли модель по умолчанию в валидационном наборе.
Наконец, мы экспортировали нашу настроенную модель в формат ONNX и интегрировали ее в наш советник на MQL5.
Извлечение данных
Я создал удобный скрипт, который поможет извлечь необходимые данные из вашего терминала MetaTrader 5. Просто перетащите скрипт на нужный вам инструмент, и он извлечет данные для вас и разместит их по адресу: “\MetaTrader 5\MQL5\Files\..”
//+------------------------------------------------------------------+ //| 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 //---Amount of data requested input int size = 100000; //How much data should we fetch? //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void OnStart() { //---File name string file_name = "Market Data " + Symbol() + ".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"); } 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)); } } } //+------------------------------------------------------------------+
Исследовательский анализ данных в Python
Начинаем с импорта стандартных библиотек.
#Libraries import pandas as pd import numpy as np import seaborn as sns
Теперь давайте ознакомимся с данными, которые мы извлекли ранее.
#Dollar VS Rand USDZAR = pd.read_csv("/home/volatily/market_data/Market Data USDZAR.csv") #US Oil USOIL = pd.read_csv("/home/volatily/market_data/Market Data US Oil.csv") #SA Gold SAGOLD = pd.read_csv("/home/volatily/market_data/Market Data XAUUSD.csv")
Проверка данных.
USOIL
Рис 1: Наши данные перемещаются ретроспективно
Обратите внимание, что наши временные метки отсчитываются практически от настоящего времени и дальше в прошлое. Это нежелательно для задач машинного обучения. Изменим порядок данных таким образом, чтобы мы прогнозировали будущее, а не прошлое.
#Format the data USDZAR = USDZAR[::-1] USOIL = USOIL[::-1] SAGOLD = SAGOLD[::-1]
Прежде чем мы сможем объединить наши наборы данных, сначала убедимся, что все они используют столбец даты в качестве индексов. Поступая таким образом, мы можем быть уверены, что выбираем только дни, общие для всех наборов данных, в корректном хронологическом порядке.
#Set the indexes USOIL = USOIL.set_index("Time") SAGOLD = SAGOLD.set_index("Time") USDZAR = USDZAR.set_index("Time")
Слияние наборов данных.
#Merge the dataframes merged_df = pd.merge(USOIL,SAGOLD,how="inner",left_index=True,right_index=True,suffixes=(" US OIL"," SA GOLD")) merged_df = pd.merge(merged_df,USDZAR,how="inner",left_index=True,right_index=True)
Определение горизонта прогноза.
#Define the forecast horizon look_ahead = 10
Целью будет будущая цена закрытия пары USDZAR, мы также включим бинарную цель для визуализации.
#Label the data merged_df["Target"] = merged_df["Close"].shift(-look_ahead) merged_df["Binary Target"] = 0 merged_df.loc[merged_df["Close"] < merged_df["Target"],"Binary Target"] = 1
Отбросим любые пустые строки.
#Drop empty rows
merged_df.dropna(inplace=True)
Наблюдение за уровнями корреляции.
#Let's observe the correlation levels merged_df.corr()
Рис 2: Уровни корреляции в нашем наборе данных
Нефть, по-видимому, демонстрирует относительно более сильные уровни корреляции с парой USDZAR, приблизительно -0,4, в то время как золото имеет относительно более слабые уровни корреляции с валютной парой, приблизительно 0,1. Важно помнить, что корреляция не всегда подразумевает наличие взаимосвязи между переменными, иногда корреляция является результатом общей причины, влияющей на обе переменные.
Например, исторически соотношение между золотом и долларом было обратным. Всякий раз, когда доллар обесценивался, трейдеры выводили свои деньги из доллара и вместо него вкладывали их в золото. Исторически это приводило к росту цен на золото всякий раз, когда доллар показывал плохие результаты. Таким образом, общей причиной в этом простом примере могут быть трейдеры, действующие на обоих рынках.
Диаграммы рассеяния помогают нам визуализировать взаимосвязь между 2 переменными, поэтому мы создали диаграмму рассеяния цен на нефть по сравнению с ценами на золото и раскрасили точки в зависимости от того, повысился ли уровень цен USDZAR (красный) или понизился (зеленый). Как можно видеть, в данных нет четкого уровня разделения. На самом деле, ни одна из созданных нами диаграмм рассеяния не предполагает сильной взаимосвязи.
Рис 3: Диаграмма рассеяния цен на золото по сравнению с ценами на нефть
Рис 4: Диаграмма рассеяния цен на нефть по сравнению с ценой закрытия USDZAR
Рис 5: Диаграмма рассеяния цен на золото по сравнению с ценой закрытия USDZAR
Моделирование взаимосвязи
Выполним сброс индекса нашего набора данных, чтобы можно было выполнить кросс-валидацию.
#Reset the index
merged_df.reset_index(inplace=True)
Теперь импортируем библиотеки, которые нам нужны для моделирования взаимосвязи в данных.
#Import the libraries we need from sklearn.linear_model import LinearRegression from sklearn.linear_model import Lasso from sklearn.ensemble import GradientBoostingRegressor from sklearn.ensemble import RandomForestRegressor from sklearn.ensemble import AdaBoostRegressor from sklearn.ensemble import BaggingRegressor from sklearn.neighbors import KNeighborsRegressor from sklearn.svm import LinearSVR from sklearn.neural_network import MLPRegressor from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import root_mean_squared_error from sklearn.preprocessing import RobustScaler
Определение предикторов и цели.
#Define the predictors normal_predictors = ["Open","High","Low","Close"] oil_gold_predictors = ["Open US OIL","High US OIL","Low US OIL","Close US OIL","Open SA GOLD","High SA GOLD","Low SA GOLD","Close SA GOLD"] target = "Target"
Масштабирование данных.
#Scale the data all_predictors = normal_predictors + oil_gold_predictors scaler = RobustScaler() scaled_data = pd.DataFrame(scaler.fit_transform(merged_df.loc[:,all_predictors]),columns=all_predictors,index=np.arange(0,merged_df.shape[0]))
Инициализация моделей.
#Now prepare the models models = [ LinearRegression(), Lasso(), GradientBoostingRegressor(), RandomForestRegressor(), AdaBoostRegressor(), BaggingRegressor(), KNeighborsRegressor(), LinearSVR(), MLPRegressor(hidden_layer_sizes=(10,5),early_stopping=True), MLPRegressor(hidden_layer_sizes=(50,15),early_stopping=True) ] columns = [ "Linear Regression", "Lasso", "Gradient Boosting Regressor", "Random Forest Regressor", "AdaBoost Regressor", "Bagging Regressor", "KNeighbors Regressor", "Linear SVR", "Small Neural Network", "Large Neural Network" ]
Создание экземпляра объекта с временными рядами для кросс-валидации.
#Prepare the time-series split object splits = 10 tscv = TimeSeriesSplit(n_splits=splits,gap=look_ahead)
Создание фрейма данных для хранения наших уровней ошибок.
#Prepare the dataframes to store the error levels normal_error = pd.DataFrame(columns=columns,index=np.arange(0,splits)) new_error = pd.DataFrame(columns=columns,index=np.arange(0,splits))
Теперь проведем кросс-валидацию с использованием вложенного цикла. Первый цикл выполняет перебор нашего списка моделей, в то время, как второй цикл выполняет перекрестную проверку каждой модели и сохраняет уровни ошибок.
#First we iterate over all the models we have available for j in np.arange(0,len(models)): #Now we have to perform cross validation with each model for i,(train,test) in enumerate(tscv.split(scaled_data)): #Get the data X_train = scaled_data.loc[train[0]:train[-1],oil_gold_predictors] X_test = scaled_data.loc[test[0]:test[-1],oil_gold_predictors] y_train = merged_df.loc[train[0]:train[-1],target] y_test = merged_df.loc[test[0]:test[-1],target] #Fit the model models[j].fit(X_train,y_train) #Measure the error new_error.iloc[i,j] = root_mean_squared_error(y_test,models[j].predict(X_test))
Наши уровни ошибок при использовании обычных входных данных модели.
normal_error
Рис. 6: Наши уровни ошибок при прогнозировании с использованием предикторов OHLC
Рис. 7: Наши уровни ошибок при прогнозировании с использованием предикторов OHLC II
Теперь взгляните на наши уровни ошибок, используя только цены на нефть и золото.
new_error
Рис. 8: Наши уровни точности при прогнозировании с использованием цен на нефть и золото
Рис. 9: Наши уровни точности при прогнозировании с использованием цен на нефть и золото II
Посмотрим на наши средние показатели по каждой модели, используя обычные предикторы.
#Let's see our average performance on the normal dataset for i in (np.arange(0,normal_error.shape[0])): print(f"{models[i]} normal error {((normal_error.iloc[:,i].mean()))}")
Lasso() normal error 0.11138143304314707
GradientBoostingRegressor() normal error 0.03472997520534606
RandomForestRegressor() normal error 0.03616484012058101
AdaBoostRegressor() normal error 0.037484107657877755
BaggingRegressor() normal error 0.03670486223028821
KNeighborsRegressor() normal error 0.035113189373409175
LinearSVR() normal error 0.01085610361276552
MLPRegressor(early_stopping=True, hidden_layer_sizes=(10, 5)) normal error 2.558754334716706
MLPRegressor(early_stopping=True, hidden_layer_sizes=(50, 15)) normal error 1.0544369296125597
Теперь оценим наши средние показатели, используя новые предикторы.
#Let's see our average performance on the new dataset for i in (np.arange(0,normal_error.shape[0])): print(f"{models[i]} normal error {((new_error.iloc[:,i].mean()))}")
Lasso() normal error 0.11138143304314707
GradientBoostingRegressor() normal error 0.0893855335909897
RandomForestRegressor() normal error 0.08957454602573789
AdaBoostRegressor() normal error 0.08658796789785872
BaggingRegressor() normal error 0.08887059320664067
KNeighborsRegressor() normal error 0.07696901077705855
LinearSVR() normal error 0.15463529064256165
MLPRegressor(early_stopping=True, hidden_layer_sizes=(10, 5)) normal error 3.8970873719426784
MLPRegressor(early_stopping=True, hidden_layer_sizes=(50, 15)) normal error 0.6958177634524169
Проанализируем изменения точности.
#Let's see our average performance on the normal dataset for i in (np.arange(0,normal_error.shape[0])): print(f"{models[i]} changed by {((normal_error.iloc[:,i].mean()-new_error.iloc[:,i].mean()))/normal_error.iloc[:,i].mean()}%")
Lasso() changed by 0.0%
GradientBoostingRegressor() changed by -1.573728690057642%
RandomForestRegressor() changed by -1.4768406476311784%
AdaBoostRegressor() changed by -1.3099914419240863%
BaggingRegressor() changed by -1.421221271695885%
KNeighborsRegressor() changed by -1.1920256220116057%
LinearSVR() changed by -13.244087580439862%
MLPRegressor(early_stopping=True, hidden_layer_sizes=(10, 5)) changed by -0.5230408480672479%
MLPRegressor(early_stopping=True, hidden_layer_sizes=(50, 15)) changed by 0.34010489967561475%
Выбор признаков
Нашей наиболее эффективной моделью из числа предикторов по нефти и золоту является KNeighbors regressor. Посмотрим, какие показатели для него наиболее важны.
#Our best performing model was the KNeighbors Regressor #Let us perform feature selection to test how stable the relationship is from mlxtend.feature_selection import SequentialFeatureSelector as SFS
Создаем новый экземпляр модели.
#Let us select our best model
model = KNeighborsRegressor()
Используем метод прямого отбора для определения наиболее важных показателей для нашей модели. Теперь предоставим нашей модели доступ сразу ко всем предикторам.
#Create the sequential selector object sfs1 = SFS( model, k_features=(1,len(all_predictors)), forward=True, scoring="neg_mean_squared_error", cv=10, n_jobs=-1 )
Подбор последовательного селектора показателей.
#Fit the sequential selector sfs1 = sfs1.fit(scaled_data.loc[:,all_predictors],merged_df.loc[:,"Target"])
Наблюдение за лучшими показателями, выбранными алгоритмом, может привести нас к выводу, что ни цены на нефть, ни цены на золото не имеют большого значения для прогнозирования USDZAR, поскольку наш алгоритм выбрал только 3 показателя, которые были котировками цен открытия, минимума и закрытия USDZAR.
#Now let us see which predictors were selected
sfs1.k_feature_names_
Настройка гиперпараметров
Попробуем выполнить настройку гиперпараметров с помощью модуля RandomizedSearchCV в scikit-learn. Алгоритм помогает нам выполнить выборку поверхности ответа, которая может быть слишком большой для полной выборки. Когда мы используем модели с большим количеством параметров, общее количество комбинаций входных данных растет значительно быстрее. Поэтому мы предпочитаем алгоритм рандомизированного поиска, когда имеем дело со многими параметрами, имеющими множество возможных значений.
Алгоритм обеспечивает оптимальное соотношение между точностью результатов и временем вычисления. Данное соотношение контролируется путем регулировки количества разрешенных нами циклов. Обратите внимание, что из-за случайного характера алгоритма точное воспроизведение результатов, продемонстрированных в настоящей статье, может оказаться сложной задачей.
Импорт модуля scikit-learn.
#Now we will load the libraries we need
from sklearn.model_selection import RandomizedSearchCV
Подготовим специальное обучение и тестовые наборы.
#Let us see if we can tune the model #First we will create train test splits train_X = scaled_data.loc[:(scaled_data.shape[0]//2),:] train_y = merged_df.loc[:(merged_df.shape[0]//2),"Target"] test_X = scaled_data.loc[(scaled_data.shape[0]//2):,:] test_y = merged_df.loc[(merged_df.shape[0]//2):,"Target"]
Чтобы выполнить настройку параметров, нам нужно передать оценщик, который реализует интерфейс scikit-learn, за которым следует словарь, содержащий ключи, соответствующие параметрам оценщика, и значения, соответствующие диапазону допустимых входных данных для каждого параметра. Оттуда мы указываем, что хотели бы выполнить 5-кратную кросс-валидацию, а затем должны указать метрику оценки как отрицательную среднеквадратичную ошибку.
#Create the tuning object rs = RandomizedSearchCV(KNeighborsRegressor(n_jobs=-1),{ "n_neighbors": [1,2,3,4,5,8,10,16,20,30,60,100], "weights":["uniform","distance"], "leaf_size":[1,2,3,4,5,10,15,20,40,60,90], "algorithm":["ball_tree","kd_tree"], "p":[1,2,3,4,5,6,7,8] },cv=5,n_iter=500,return_train_score=False,scoring="neg_mean_squared_error")
Выполнение настройки параметров на обучающем наборе.
#Let's perform the hyperparameter tuning rs.fit(train_X,train_y)
Посмотрим на полученные нами результаты от наилучших к наихудшим.
#Let's store the results from our hyperparameter tuning tuning_results = pd.DataFrame(rs.cv_results_) tuning_results.loc[:,["param_n_neighbors","param_weights","param_leaf_size","param_algorithm","param_p","mean_test_score"]].sort_values(by="mean_test_score",ascending=False)
Рис. 10: Результаты настройки нашей лучшей модели
Приведены лучшие параметры, обнаруженные нами.
#The best parameters we came across
rs.best_params_
'p': 1,
'n_neighbors': 4,
'leaf_size': 15,
'algorithm': 'ball_tree'}
Проверка на переобучение
Приготовимся сравнить наши индивидуально настроенные модели и модели по умолчанию. Обе модели будут обучены на одинаковых обучающих наборах. Если модель по умолчанию эффективнее нашей настроенной модели в наборе для проверки, это может быть признаком того, что мы переоценили данные обучения. Однако, если наша настроенная модель работает эффективнее, это может свидетельствовать о том, что мы успешно настроили параметры модели без переобучения.
#Create instances of the default model and the custmoized model default_model = KNeighborsRegressor() customized_model = KNeighborsRegressor(p=rs.best_params_["p"],weights=rs.best_params_["weights"],n_neighbors=rs.best_params_["n_neighbors"],leaf_size=rs.best_params_["leaf_size"],algorithm=rs.best_params_["algorithm"])
Измерим точность модели по умолчанию.
#Measure the accuracy of the default model default_model.fit(train_X,train_y) root_mean_squared_error(test_y,default_model.predict(test_X))
Теперь - точность настроенной модели.
#Measure the accuracy of the customized model
customized_model.fit(train_X,train_y)
root_mean_squared_error(test_y,customized_model.predict(test_X))
Похоже, что мы хорошо настроили модель, без наличия переобучения! Теперь подготовимся к экспорту нашей настроенной модели в формат ONNX.
Экспортирование в формат ONNX
Open Neural Network Exchange (ONNX) — это совместимая платформа для создания и развертывания моделей машинного обучения независимо от языка программирования. Используя ONNX, наши модели машинного обучения можно легко использовать на любом языке программирования, если этот язык поддерживает ONNX API. На момент написания данной статьи ONNX API разрабатывается и поддерживается консорциумом крупнейших компаний в мире.
Импортируем необходимые нам библиотеки #Let's prepare to export the customized model to ONNX format import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
Нам необходимо обеспечить масштабирование и нормализацию наших данных таким образом, чтобы мы могли воспроизвести их в терминале MetaTrader 5. Поэтому мы выполним стандартное преобразование, которое позже всегда сможем выполнить в нашем терминале. Вычтем среднее значение каждого столбца, это позволит центрировать наши данные. А затем разделим каждое значение на стандартное отклонение для соответствующего столбца, это поможет нашей модели лучше оценить изменения между переменными в разных масштабах.
#Train the model on all the data we have #But before doing that we need to first scale the data in a way we can repeat in MQL5 scale_factors = pd.DataFrame(columns=all_predictors,index=["mean","standard deviation"]) for i in np.arange(0,len(all_predictors)): scale_factors.iloc[0,i] = merged_df.loc[:,all_predictors[i]].mean() scale_factors.iloc[1,i] = merged_df.loc[:,all_predictors[i]].std() scale_factors
Рис. 12: Некоторые значения, которые мы будем использовать для масштабирования и стандартизации наших данных. Отображаются не все столбцы
Теперь проведем нормализацию и стандартизацию.
for i in all_predictors:
merged_df.loc[:,i] = (merged_df.loc[:,i] - merged_df.loc[:,i].mean()) / merged_df.loc[:,i].std()
Посмотрим на наши данные теперь.
merged_df
Рис. 11: Как выглядят наши данные после масштабирования, отображаются не все столбцы
Инициализируем нашу настроенную модель.
customized_model = KNeighborsRegressor(p=rs.best_params_["p"],weights=rs.best_params_["weights"],n_neighbors=rs.best_params_["n_neighbors"],leaf_size=rs.best_params_["leaf_size"],algorithm=rs.best_params_["algorithm"]) customized_model.fit(merged_df.loc[:,all_predictors],merged_df.loc[:,"Target"])
Определим входную форму нашей модели.
#Define the input shape and type initial_type = [("float_tensor_type",FloatTensorType([1,train_X.shape[1]]))]
Создадим представление ONNX.
#Create an ONNX representation
onnx_model = convert_sklearn(customized_model,initial_types=initial_type)
Сохраним модель ONNX.
#Store the ONNX model onnx_model_name = "USDZAR_FLOAT_M1.onnx" onnx.save_model(onnx_model,onnx_model_name)
Визуализация модели ONNX
Netron — это инструмент визуализации с открытым исходным кодом для моделей машинного обучения. Netron расширяет поддержку множества различных платформ, помимо ONNX, таких как Keras. Мы будем использовать netron, чтобы обеспечить, что наша модель ONNX имеет ту входную и выходную форму, которую мы ожидали.
Импорт модуля netron.
#Let's visualize the model in netron import netron
Теперь можем визуализировать модель с помощью netron.
#Run netron
netron.start(onnx_model_name)
Рис. 12: Характеристики нашей модели ONNX
Рис. 13: Структура нашей модели ONNX
Наша модель ONNX соответствует нашим ожиданиям, входная и выходная формы находятся в точности там, где мы их ожидаем. Теперь можем перейти к созданию советника поверх нашей модели ONNX.
Реализация средствами MQL5
Теперь можем приступить к созданию нашего советника. Начнем с интеграции нашей модели ONNX в наше приложение. При указании файла ONNX в качестве ресурса файл ONNX будет включен в скомпилированную программу с расширением .ex5.
//+------------------------------------------------------------------+ //| USDZAR.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" //+-----------------------------------------------------------------+ //| Require the ONNX file | //+-----------------------------------------------------------------+ #resource "\\Files\\USDZAR_FLOAT_M1.onnx" as const uchar onnx_model_buffer[];
Теперь импортируем торговую библиотеку.
//+-----------------------------------------------------------------+ //| Libraries we need | //+-----------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
Определим входные данные, которыми может управлять конечный пользователь.
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ double input sl_width = 0.4; //How tight should our stop loss be? int input lot_multiple = 10; //How many times bigger than minimum lot should we enter? double input max_risk = 10; //After how much profit/loss should we close?
Теперь нам требуется несколько глобальных переменных.
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ long onnx_model; //Our onnx model double mean_values[12],std_values[12]; //The scaling factors we used for our data vector model_inputs = vector::Zeros(12); //Our model's inputs vector model_forecast = vector::Zeros(1); //Our model's output double bid,ask; //Market prices double minimum_volume; //Smallest lot size double state = 0; //0 means we have no positions, 1 means we have buy position, 2 means we have sell position.
Теперь определим вспомогательные функции для задач, которые нам, возможно, придется выполнять неоднократно. Во-первых, давайте управлять нашими уровнями риска. Если общая прибыль/убыток превысит установленные нами уровни риска, мы автоматически закроем позицию.
//+------------------------------------------------------------------+ //| Check if we have reached our risk level | //+------------------------------------------------------------------+ void check_risk_level(void) { //--- Check if we have surpassed our maximum risk level if(MathAbs(PositionGetDouble(POSITION_PROFIT)) > max_risk) { //--- We should close our positions Trade.PositionClose("USDZAR"); } }
Поскольку у нас есть интегрированная система ИИ, используем ее для обнаружения разворотов. Если наша система прогнозирует, что цена будет двигаться против нас, мы закрываем позицию и предупреждаем конечного пользователя об обнаружении потенциального разворота.
//+------------------------------------------------------------------+ //| Check if there is a reversal may be coming | //+------------------------------------------------------------------+ void check_reversal(void) { if(((state == 1) && (model_forecast[0] < iClose("USDZAR",PERIOD_M1,0))) ||((state == 2) && (model_forecast[0] > iClose("USDZAR",PERIOD_M1,0)))) { //--- There may be a reversal coming Trade.PositionClose("USDZAR"); //--- Give the user feedback Alert("Potential reversal detected"); } }
Теперь нам нужна функция, с помощью которой у нас появится возможность выхода на рынок. Будем считать выход действительным только в том случае, если прогноз нашей модели совпадает с изменениями уровней цен на более высоких таймфреймах.
//+------------------------------------------------------------------+ //| Find an entry opportunity | //+------------------------------------------------------------------+ void find_entry(void) { //---Check for the change in price on higher timeframes if(iClose("USDZAR",PERIOD_D1,0) > iClose("USDZAR",PERIOD_D1,21)) { //--- We're looking for buy oppurtunities if(model_forecast[0] > iClose("USDZAR",PERIOD_M1,0)) { //--- Open the position Trade.Buy(minimum_volume,"USDZAR",ask,(ask - sl_width),(ask + sl_width),"USDZAR AI"); //--- Update the system state state = 1; } } //---Check for the change in price on higher timeframes else if(iClose("USDZAR",PERIOD_D1,0) < iClose("USDZAR",PERIOD_D1,21)) { //--- We're looking for sell oppurtunities if(model_forecast[0] < iClose("USDZAR",PERIOD_M1,0)) { //--- Open sell position Trade.Sell(minimum_volume,"USDZAR",bid,(bid + sl_width),(bid - sl_width),"USDZAR AI"); //--- Update the system state state = 2; } } }
Теперь нам нужна функция для извлечения прогноза из нашей модели. Для этого нам сначала требуется извлечь текущие рыночные цены, а затем преобразовать их путем вычитания среднего значения и деления на стандартное отклонение.
//+------------------------------------------------------------------+ //| Obtain a forecast from our model | //+------------------------------------------------------------------+ void model_predict(void) { //Let's fetch our model's inputs //--- USDZAR model_inputs[0] = ((iOpen("USDZAR",PERIOD_M1,0) - mean_values[0]) / std_values[0]); model_inputs[1] = ((iHigh("USDZAR",PERIOD_M1,0) - mean_values[1]) / std_values[1]); model_inputs[2] = ((iLow("USDZAR",PERIOD_M1,0) - mean_values[2]) / std_values[2]); model_inputs[3] = ((iClose("USDZAR",PERIOD_M1,0) - mean_values[3]) / std_values[3]); //--- XTI OIL US model_inputs[4] = ((iOpen("XTIUSD",PERIOD_M1,0) - mean_values[4]) / std_values[4]); model_inputs[5] = ((iHigh("XTIUSD",PERIOD_M1,0) - mean_values[5]) / std_values[5]); model_inputs[6] = ((iLow("XTIUSD",PERIOD_M1,0) - mean_values[6]) / std_values[6]); model_inputs[7] = ((iClose("XTIUSD",PERIOD_M1,0) - mean_values[7]) / std_values[7]); //--- GOLD SA model_inputs[8] = ((iOpen("XAUUSD",PERIOD_M1,0) - mean_values[8]) / std_values[8]); model_inputs[9] = ((iHigh("XAUUSD",PERIOD_M1,0) - mean_values[9]) / std_values[9]); model_inputs[10] = ((iLow("XAUUSD",PERIOD_M1,0) - mean_values[10]) / std_values[10]); model_inputs[11] = ((iClose("XAUUSD",PERIOD_M1,0) - mean_values[11]) / std_values[11]); //--- Get a prediction OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast); }
Поскольку мы анализируем несколько инструментов, нам необходимо добавить их в обзор рынка.
//+------------------------------------------------------------------+ //| Load the symbols we need and add them to the market watch | //+------------------------------------------------------------------+ void load_symbols(void) { SymbolSelect("XAUUSD",true); SymbolSelect("XTIUSD",true); SymbolSelect("USDZAR",true); }
Нам нужна функция, отвечающая за загрузку наших коэффициентов масштабирования, среднего значения и стандартного отклонения для каждого столбца.
//+------------------------------------------------------------------+ //| Load the scale values | //+------------------------------------------------------------------+ void load_scale_values(void) { //--- Mean //--- USDZAR mean_values[0] = 18.14360511919699; mean_values[1] = 18.145737421580925; mean_values[2] = 18.141568574864074; mean_values[3] = 18.14362306984525; //--- XTI US OIL mean_values[4] = 80.76956702216644; mean_values[5] = 80.7864452112087; mean_values[6] = 80.75236177331661; mean_values[7] = 80.76923546633206; //--- GOLD SA mean_values[8] = 2430.5180384776245; mean_values[9] = 2430.878959640318; mean_values[10] = 2430.1509598494354; mean_values[11] = 2430.5204140526976; //--- Standard Deviation //--- USDZAR std_values[0] = 0.11301636249300206; std_values[1] = 0.11318116432297631; std_values[2] = 0.11288670156099372; std_values[3] = 0.11301994613848391; //--- XTI US OIL std_values[4] = 0.9802409859148413; std_values[5] = 0.9807944310705999; std_values[6] = 0.9802449355481064; std_values[7] = 0.9805961626626833; //--- GOLD SA std_values[8] = 26.397404261230328; std_values[9] = 26.414599597905003; std_values[10] = 26.377605644853944; std_values[11] = 26.395208330942864; }
Наконец, нам нужна функция, отвечающая за загрузку нашего файла ONNX.
//+------------------------------------------------------------------+ //| Load the onnx file from buffer | //+------------------------------------------------------------------+ bool load_onnx_file(void) { //--- Create the model from the buffer onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DEFAULT); //--- The input size for our onnx model ulong input_shape [] = {1,12}; //--- Check if we have the right input size if(!OnnxSetInputShape(onnx_model,0,input_shape)) { Comment("Incorrect input shape, the model has input shape ",OnnxGetInputCount(onnx_model)); return(false); } //--- The output size for our onnx model ulong output_shape [] = {1,1}; //--- Check if we have the right output size if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { Comment("Incorrect output shape, the model has output shape ",OnnxGetOutputCount(onnx_model)); return(false); } //--- Everything went fine return(true); } //+------------------------------------------------------------------+
Теперь, когда мы определили эти вспомогательные функции, можно начать использовать их в нашем советнике. Во-первых, определим поведение нашего приложения при первой загрузке. Начнем с загрузки нашей модели ONNX, подготовим значения масштабирования, а затем получим рыночные данные.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Load the onnx file if(!load_onnx_file()) { return(INIT_FAILED); } //--- Load our scaling values load_scale_values(); //--- Add the symbols we need to the market watch load_symbols(); //--- The smallest lotsize we can use minimum_volume = SymbolInfoDouble("USDZAR",SYMBOL_VOLUME_MIN) * lot_multiple; //--- Everything went fine return(INIT_SUCCEEDED); }
Каждый раз при деактивировании нашей программы нам необходимо освобождать ресурсы, которые мы больше не используем.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release the resources we used for our onnx model OnnxRelease(onnx_model); //--- Remove the expert advisor ExpertRemove(); }
Наконец, всякий раз при изменении цены нам нужно извлечь новый прогноз из нашей модели, получить обновленные рыночные цены и затем либо открыть новую позицию, либо управлять позициями, которые у нас открыты в данный момент.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- We always need a forecast from our model model_predict(); //--- Fetch market prices bid = SymbolInfoDouble("USDZAR",SYMBOL_BID); ask = SymbolInfoDouble("USDZAR",SYMBOL_ASK); //--- If we have no open positions, find an entry if(PositionsTotal() == 0) { //--- Find an entry find_entry(); //--- Reset the system state state = 0; } //--- If we have open postitions, manage them else { //--- Check for a reveral warning from our AI check_reversal(); //--- Check if we have not reached our max risk levels check_risk_level(); } }
Собрав все вышеперечисленное вместе, мы теперь можем наблюдать нашу программу в действии.
Рис. 17: Наш советник
Рис. 14: Входные данные для нашего советника
Рис. 15: Наша программа в действии
Заключение
В настоящей статье мы продемонстрировали, каким образом можно создать советник по нескольким инструментам на базе ИИ. Несмотря на то, что мы получили более низкие уровни ошибок при использовании обычных данных OHLC, это не обязательно означает, что то же самое будет справедливо для всех инструментов, которые есть в вашем терминале MetaTrader 5. Может существовать набор различных инструментов, которые могут вызывать меньшую ошибку, чем котировки USDZAR OHLC.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15570
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Потрясающе, еще один отличный обзор. Спасибо за рабочую тетрадь, теперь у нас есть шаблон для проверки наших собственных корреляций. Очень признателен