
Переосмысливаем классические стратегии (Часть IX): Анализ на нескольких таймфреймах (II)
Существует множество таймфреймов, которые могут использовать трейдеры. Для нового участника сообщества или любого, кто может учиться торговать, выбор может оказаться трудным. Даже опытные трейдеры часто спорят и делятся различными точками зрения, пытаясь определить, какой таймфрейм является оптимальным. Попытаемся объективно ответить на этот вопрос, определив оптимальный таймфрейм как таймфрейм, который минимизирует уровень ошибок нашей модели ИИ.
В сегодняшнем обсуждении мы обратим внимание на распределение остатков нашей модели по 11 таймфреймам; мы наблюдали 2 области с низким уровнем ошибок на месячном и часовом таймфреймах. Однако очевидного паттерна в распределении уровней ошибок модели нет, по-видимому, она достигает максимума и минимума на часовом таймфрейме. Прежде чем мы сможем окончательно ответить на извечный вопрос: «Какой таймфрейм лучше всего использовать?", мы должны быть достаточно уверены в том, что при смене рынков распределение остатков не изменится. Кроме того, в будущем нам следует рассмотреть возможность проведения исчерпывающего поиска по всем доступным таймфреймам.
Обзор торговой методологии
Хотя паттерн, создаваемый ценовыми свечами, может показаться сильно отличающимся на всех таймфреймах, в любой момент времени на каждом из них предлагается только одна цена. Трейдеры часто анализируют сразу множество таймфреймов, чтобы получить ценную информацию о текущем состоянии рынка. Если тренд ослабевает, мы, скорее всего, увидим противоречивое ценовое движение на таймфреймах, более низких, чем использованные нами для открытия нашей сделки. Кроме того, эти признаки слабости всегда сначала будут заметны на более низком таймфрейме, прежде чем они станут заметны на более высоких таймфреймах.
Как правило, большинство стратегий, включающих анализ нескольких таймфреймов, направлены на то, чтобы понять настроения рынка на более высоких таймфреймах. Некоторые успешные трейдеры ищут формирование хорошо известных паттернов ценового действия на более высоких таймфреймах, таких как свечные паттерны бычьего поглощения. Традиционно наличие или отсутствие этих свечных паттернов служило сигналом для трейдеров, которые искали наборы значений с высокой вероятностью. Мы хотели алгоритмически определить, какой таймфрейм даёт нам надежные уровни ошибок при прогнозировании пары EURUSD.
На каком-то уровне мы все, как правило, ценим интуицию, которая подсказывает, что чем дальше в будущее вы пытаетесь заглянуть, тем сложнее становится задача. Результаты нашего сегодняшнего анализа ставят под сомнение это убеждение на фундаментальном уровне. Прежде чем вы сможете понять, почему я это говорю, или насколько обоснованы эти выводы, мы должны сначала обсудить используемую методологию.
Обзор методологии
Чтобы наш тест был справедливым, мы должны были получить одинаковый объем данных за каждый таймфрейм. Ограничивающим фактором на этом этапе было количество баров, доступных на месячном таймфрейме. Всего 400 баров ежемесячных данных составлены примерно за 33 года. Таких старых рынков всего несколько, что может повлиять на наше понимание наилучших таймфреймов для всех возможных рынков. Однако в рамках нашего обсуждения пара EURUSD обладает богатым набором данных, на которые мы можем положиться.
Мы извлекли 400 строк ежемесячных ценовых котировок из терминала MetaTrader 5. Затем мы извлекли 400 соответствующих строк будущей стоимости пары EURUSD. Этот двухэтапный процесс повторялся в течение оставшихся 10 таймфреймов. Для проведения такого анализа мной выбрано следующее:
- W1
- D1
- H12
- H8
- H4
- H1
- M30
- M15
- M5
- M1
Должен признаться, я ожидал увидеть сильные уровни корреляции, особенно между таймфреймами, которые периодически находятся близко друг к другу. Однако по всей выборке наблюдались лишь умеренные уровни корреляции. Единственными интересными корреляционными парами, которые могут заслуживать дальнейшего анализа, были:
- Текущая цена Н4 и будущая цена Н8
- Текущая цена М1 и будущая цена Н4
- Текущая цена М1 и будущая цена М5
Напомним, что наши входные данные содержали 22 столбца, естественно, полученная нами корреляционная матрица была большой и не будет полностью отображена в нашем обсуждении. Основываясь на наших данных, мы смогли создать 11 наборов входных данных для тестирования. После моделирования наших данных мы обнаружили, что наши модели лучше всего работают в месячном и часовом таймфреймах. Этот результат был достаточно нелогичным. Нашей целью было продвинуться на 20 шагов в будущее. 20 месяцев в будущее - это период в 1 год и 8 месяцев. Наша модель может предсказать изменения цены в течение года с большей точностью, чем изменения цены за 20 минут.
Заинтригованные двумя таймфреймами с низкой погрешностью, мы преобразовали данные о ценах в периодические доходы и впоследствии провели тесты на причинность по Грейнджеру для этих доходов. Мы наблюдали значительные p-значения, указывающие на то, что почасовая доходность по Грейнджеру привела к месячной доходности. Этот тест доказывает, что мы можем моделировать ежемесячную доходность, используя почасовую доходность с помощью модели векторной авторегрессии (VAR).
Мы использовали библиотеку искажения (выравнивания) временных рядов, чтобы выровнять и найти сходство между месячными и почасовыми данными. Наш алгоритм смог найти много точек сходства между данными. Это придало нам уверенности в процессе выбора, и мы приступили к успешной настройке параметров нашей ежемесячной модели и экспорту нашей модели в формат ONNX.
Наконец, я реализовал советника, который делает прогнозы по ожидаемым уровням цен на месячном таймфрейме, а затем совершает свои сделки на часовом таймфрейме. Система может переключаться между закрытием своих позиций на основе прогнозируемых разворотов ИИ или с использованием скользящих средних. Мы использовали технический анализ для фиксации времени ввода данных в наши позиции.
Извлечение нужных нам данных
Начнем с импорта библиотеки MetaTrader 5 и нескольких других необходимых нам библиотек.
#Import the libraries we need import pandas as pd import numpy as np import seaborn as sns import MetaTrader5 as mt5 from sklearn.model_selection import cross_val_score,train_test_split,TimeSeriesSplit from sklearn.metrics import mean_squared_error import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression
Теперь тест, сможем ли мы добраться до терминала.
#Initialize the terminal
mt5.initialize()
Определим таймфреймы, которые мы хотели бы протестировать.
#Declare the time-frames we are interested in
time_frames = [mt5.TIMEFRAME_MN1,
mt5.TIMEFRAME_W1,
mt5.TIMEFRAME_D1,
mt5.TIMEFRAME_H12,
mt5.TIMEFRAME_H8,
mt5.TIMEFRAME_H4,
mt5.TIMEFRAME_H1,
mt5.TIMEFRAME_M30,
mt5.TIMEFRAME_M15,
mt5.TIMEFRAME_M5,
mt5.TIMEFRAME_M1
]
Сколько баров данных мы должны получить?
#How many bars should we fetch fetch = 400
Давайте спрогнозируем 20 шагов в будущее.
#How far into the future should we forecast? look_ahead = 20
Определим столбцы нашего фрейма данных.
#Create our dataframe inputs = ["MN","W","D","H12","H8","H4","H1","M30","M15","M5","M1"] target = [] for i in np.arange(0,len(inputs)): target.append(inputs[i] + " Target")
Создадим фрейм данных, содержащий наши цены.
columns = inputs + target
prices = pd.DataFrame(columns=columns,index=np.arange(0,fetch))
Рис. 1: Некоторые из входных данных в нашем фрейме данных
Рис. 2: Некоторые из целей в нашем фрейме данных
Нам нужен фрейм данных для хранения наших уровней ошибок.
#The columns for our error levels data frame. error_columns = [] for i in np.arange(0,len(inputs)): error_columns.append(inputs[i]) #Create a dataframe to store our error levels error_levels = pd.DataFrame(columns=error_columns,index=[0]) test_error_levels = pd.DataFrame(columns=error_columns,index=[0])
Извлекаем нужные нам данные.
for i in np.arange(0,len(time_frames)): print(i) prices.iloc[:,i] = pd.DataFrame(mt5.copy_rates_from_pos("EURUSD",time_frames[i],look_ahead,fetch)).loc[:,"close"] prices.iloc[:,i+10] = pd.DataFrame(mt5.copy_rates_from_pos("EURUSD",time_frames[i],0,fetch)).loc[:,"close"]
Разведочный анализ данных
Давайте проанализируем уровни корреляции в нашем фрейме данных. Отметим сильные уровни корреляции между таймфремами H12 и H8. Какие еще уровни корреляции привлекают ваше внимание?
fig, ax = plt.subplots(figsize=(15,15)) sns.heatmap(prices.corr(),annot=True,ax=ax)
Рис. 3: Несколько значений из корреляционной матрицы, которые мы получили
Построение диаграммы рассеяния цен закрытия месяца и недели показало неясную тенденцию. По большей части, похоже, что данные имеют общую тенденцию к росту.
sns.scatterplot(data=prices,x="MN Close",y="W Close")
Рис. 4: Диаграмма рассеяния цен закрытия месяца и недели
Мы преобразовали наши ценовые данные в данные о периодической доходности и снова построили диаграмму рассеяния. На этот раз прослеживается общая тенденция, и, похоже, наши доходы группируются вокруг 0.
sns.scatterplot(data=prices.pct_change(),x="MN Close",y="W Close")
Рис. 5: Диаграмма рассеяния наших результатов на различных таймфреймах
Когда мы строили блочные диаграммы наших результатов за разные таймфреймы, мы могли наблюдать другую тенденцию. Разброс в наших результатах уменьшается по мере того, как мы переходим от месячных таймфреймов к более низким. Аналогично, средняя доходность по всем таймфреймам близка к 0. Это также может быть истолковано как указание на то, что если мы пытаемся максимизировать доходность портфеля, следует рассмотреть более высокие таймфреймы.
Рис. 6: Блочная диаграмма наших результатов по различным таймфреймам
Подготовка к моделированию данных
Теперь подготовимся к тому, чтобы начать моделирование данных. Во-первых, нам нужно выполнить учебно-тренировочное разделение наших данных.
#Create train test splits X_train_mn,X_test_mn,y_train_mn,y_test_mn = train_test_split(prices.loc[:,["MN"]],prices.loc[:,"MN Target"],test_size=0.5,shuffle=False) X_train_w,X_test_w,y_train_w,y_test_w = train_test_split(prices.loc[:,["W"]],prices.loc[:,"W Target"],test_size=0.5,shuffle=False) X_train_d,X_test_d,y_train_d,y_test_d = train_test_split(prices.loc[:,["D"]],prices.loc[:,"D Target"],test_size=0.5,shuffle=False) X_train_h12,X_test_h12,y_train_h12,y_test_h12 = train_test_split(prices.loc[:,["H12"]],prices.loc[:,"H12 Target"],test_size=0.5,shuffle=False) X_train_h8,X_test_h8,y_train_h8,y_test_h8 = train_test_split(prices.loc[:,["H8"]],prices.loc[:,"H8 Target"],test_size=0.5,shuffle=False) X_train_h4,X_test_h4,y_train_h4,y_test_h4 = train_test_split(prices.loc[:,["H4"]],prices.loc[:,"H4 Target"],test_size=0.5,shuffle=False) X_train_h1,X_test_h1,y_train_h1,y_test_h1 = train_test_split(prices.loc[:,["H1"]],prices.loc[:,"H1 Target"],test_size=0.5,shuffle=False) X_train_m30,X_test_m30,y_train_m30,y_test_m30 = train_test_split(prices.loc[:,["M30"]],prices.loc[:,"M30 Target"],test_size=0.5,shuffle=False) X_train_m15,X_test_m15,y_train_m15,y_test_m15 = train_test_split(prices.loc[:,["M15"]],prices.loc[:,"M15 Target"],test_size=0.5,shuffle=False) X_train_m5,X_test_m5,y_train_m5,y_test_m5 = train_test_split(prices.loc[:,["M5"]],prices.loc[:,"M5 Target"],test_size=0.5,shuffle=False) X_train_m1,X_test_m1,y_train_m1,y_test_m1 = train_test_split(prices.loc[:,["M1"]],prices.loc[:,"M1 Target"],test_size=0.5,shuffle=False)
Теперь сохраним эти разделения в списки.
train_X = [ X_train_mn, X_train_w, X_train_d, X_train_h12, X_train_h8, X_train_h4, X_train_h1, X_train_m30, X_train_m15, X_train_m5, X_train_m1 ] test_X = [ X_test_mn, X_test_w, X_test_d, X_test_h12, X_test_h8, X_test_h4, X_test_h1, X_test_m30, X_test_m15, X_test_m5, X_test_m1 ]
Повторим вышеописанную процедуру для целевых значений.
train_y = [ y_train_mn, y_train_w, y_train_d, y_train_h12, y_train_h8, y_train_h4, y_train_h1, y_train_m30, y_train_m15, y_train_m5, y_train_m1, ] test_y = [ y_test_mn, y_test_w, y_test_d, y_test_h12, y_test_h8, y_test_h4, y_test_h1, y_test_m30, y_test_m15, y_test_m5, y_test_m1, ]
Выполним кросс-валидацию каждой модели.
#Record our error for i in np.arange(0,len(train_X)): #Fit the model model = LinearRegression() cv_score = cross_val_score(model,train_X[i],train_y[i],cv=5) error_levels.iloc[0,i] = np.mean(cv_score * -1) #Record validation error model.fit(train_X[i],train_y[i]) test_error_levels.iloc[0,i] = mean_squared_error(test_y[i],model.predict(test_X[i]))
Наши соответствующие уровни ошибок.
error_levels
MN | W | D | H12 | H8 | H4 | H1 | M30 | M15 | M5 | M1 |
---|---|---|---|---|---|---|---|---|---|---|
0.719131 | 3.979435 | 3.897228 | 5.023601 | 5.218168 | 40.406227 | 0.196244 | 18.264356 | 3.680168 | 20.331821 | 3.540946 |
Давайте визуализируем нашу аппроксимацию распределения остатка нашей модели.
fig, ax = plt.subplots(figsize=(7,4)) sns.barplot(error_levels,ax=ax)
Рис. 7: Визуализация уровней ошибок нашей модели
Значимость признаков
Теперь, когда мы определили наши оптимальные таймфреймы, давайте попробуем определить, может ли существовать какая-либо причинно-следственная связь между этими двумя таймфреймами. В 1969 году сэр Клайв Грейнджер (Sir Clive Granger) предложил тест, позволяющий эмпирически определить, имеют ли причинно-следственную связь данные двух временных рядов друг с другом, даже в тех случаях, когда прошлые значения 1 временного ряда повлияли на будущие значения последнего. Проще говоря, тест Грейнджера считается пройденным, если мы можем задерживать значения одного временного ряда и использовать его для прогнозирования будущего значения второго без существенного снижения дисперсии наших прогнозов.
С момента своего создания тест Грейнджера претерпел множество изменений и улучшений. В наше время он широко используется во всех отраслях - от неврологии до финансов. Использование этого теста уже более полувека находится в центре многочисленных дискуссий в академических кругах. Основная проблема заключается в предположениях о линейности, которые неявно вытекают из теста Грейнджера. Следовательно, если действительно существует причинно-следственная связь, являющаяся нелинейной, тест Грейнджера опровергнет ее существование. Кроме того, на практике тест обычно сводится к двумерным задачам. Иными словами, мы редко используем тест Грейнджера для решения больших задач с более чем двумя наборами данных временных рядов.
Рис. 8: Покойный британский экономист сэр Клайв Грейнджер
Для начала импортируем библиотеку statsmodels, а затем запустим тест. Тест выполняется на версиях H1 Close с запаздыванием. Тест считается пройденным, если мы получаем p-значения < 0,05, что мы и сделали на первом лаге. Все последующие лаги не прошли тест и мы можем исключить наличие какой-либо причинно-следственной связи, выходящей за рамки первого лага.
from statsmodels.tsa.stattools import grangercausalitytests result = grangercausalitytests(prices[['H1 Close','MN Close']].pct_change().dropna(), maxlag=4)
number of lags (no zero) 1
ssr based F test: F=4.4913 , p=0.0347 , df_denom=395, df_num=1
ssr based chi2 test: chi2=4.5254 , p=0.0334 , df=1
likelihood ratio test: chi2=4.4999 , p=0.0339 , df=1
parameter F test: F=4.4913 , p=0.0347 , df_denom=395, df_num=1
Granger Causality
number of lags (no zero) 2
ssr based F test: F=2.2706 , p=0.1046 , df_denom=392, df_num=2
ssr based chi2 test: chi2=4.5991 , p=0.1003 , df=2
likelihood ratio test: chi2=4.5727 , p=0.1016 , df=2
parameter F test: F=2.2706 , p=0.1046 , df_denom=392, df_num=2
Причинность по Грейнджеру обычно работает в одну сторону. Давайте убедимся в этом, проверив наличие причинно-следственной связи в обратном направлении. Ни одно из полученных p-значений не было значимым, что убеждает нас в том, что причинно-следственная связь действительно работает в одном направлении так, как мы ожидаем.
result = grangercausalitytests(prices[['MN Close','H1 Close']].pct_change().dropna(), maxlag=4)
Granger Causality
number of lags (no zero) 1
ssr based F test: F=0.0188 , p=0.8909 , df_denom=395, df_num=1
ssr based chi2 test: chi2=0.0190 , p=0.8905 , df=1
likelihood ratio test: chi2=0.0190 , p=0.8905 , df=1
parameter F test: F=0.0188 , p=0.8909 , df_denom=395, df_num=1
Granger Causality
number of lags (no zero) 2
ssr based F test: F=2.2182 , p=0.1102 , df_denom=392, df_num=2
ssr based chi2 test: chi2=4.4930 , p=0.1058 , df=2
likelihood ratio test: chi2=4.4678 , p=0.1071 , df=2
parameter F test: F=2.2182 , p=0.1102 , df_denom=392, df_num=2
Granger Causality
number of lags (no zero) 3
ssr based F test: F=1.7310 , p=0.1601 , df_denom=389, df_num=3
ssr based chi2 test: chi2=5.2863 , p=0.1520 , df=3
likelihood ratio test: chi2=5.2513 , p=0.1543 , df=3
parameter F test: F=1.7310 , p=0.1601 , df_denom=389, df_num=3
Granger Causality
number of lags (no zero) 4
ssr based F test: F=1.4694 , p=0.2108 , df_denom=386, df_num=4
ssr based chi2 test: chi2=6.0148 , p=0.1980 , df=4
likelihood ratio test: chi2=5.9694 , p=0.2014 , df=4
parameter F test: F=1.4694 , p=0.2108 , df_denom=386, df_num=4
Динамическое выравнивание временных рядов также позволяет нам найти сходство между двумя наборами данных временных рядов. Этот алгоритм также может быть использован для выравнивания рядов разной длины. Нами применён этот алгоритм для нахождения точки сходства между месячными и часовыми значениями наших данных. Алгоритм выполняет эту задачу путем минимизации специальной функции стоимости, которая измеряет разницу между двумя рядами. Начнем с импорта необходимых нам библиотек.
#Let's calculate the simillarities between our time series data from dtaidistance import dtw from dtaidistance import dtw_visualisation as dtwvis
Теперь найдем сходство между этими результатами.
series_1 = prices["MN Close"].pct_change(periods=1).dropna().reset_index(drop=True) * 100 series_2 = prices["H1 Close"].pct_change(periods=1).dropna().reset_index(drop=True) * 100 path = dtw.warping_path(series_1, series_2) dtwvis.plot_warping(series_1, series_2, path)
Рис. 9: Визуализация сходства между месячными и часовыми значениями результатов
Настройка параметров
Теперь настроим параметры нашей Глубокой нейронной сети таким образом, чтобы превзойти контрольные показатели, установленные нашей линейной регрессией. Обратите внимание, что из-за особенностей процедур оптимизации, используемых для обучения DNN (Глубокой нейронной сети), результаты, полученные в этом разделе статьи, могут быть сложными для воспроизведения. На самом деле, мы провели этот тест 5 раз, и нам не удалось превзойти линейную модель в двух тестах.
Импортируем необходимые нам библиотеки и инициализируем модель.
#Let's try to outperform our linear regression model from sklearn.neural_network import MLPRegressor from sklearn.model_selection import RandomizedSearchCV #Let's tune our model model = MLPRegressor(max_iter=500)
Определим пространство параметров.
#Tuner tuner = RandomizedSearchCV( model, { "activation" : ["relu","logistic","tanh","identity"], "solver":["adam","sgd","lbfgs"], "alpha":[0.1,0.01,0.001,0.0001,0.00001,0.00001,0.0000001,0.000000001,0.000000000000001], "tol":[0.1,0.01,0.001,0.0001,0.00001,0.000001,0.0000001,0.000000001,0.000000000000001], "learning_rate":['constant','adaptive','invscaling'], "learning_rate_init":[1,0.1,0.0001,0.000001,100,10000,1000000,1000000000,100,1000], "shuffle": [True,False], "hidden_layer_sizes":[(1,4),(1,4,5),(1,8,10),(2,5),(8),(10,12),(5,10,4)] }, n_iter=100, cv=5, n_jobs=-1, scoring="neg_mean_squared_error" )
Устанавливаем тюнер.
tuner.fit(X_train_mn,y_train_mn)
Лучшие параметры, обнаруженные нами.
tuner.best_params_
'solver': 'lbfgs',
'shuffle': True,
'learning_rate_init': 1,
'learning_rate': 'adaptive',
'hidden_layer_sizes': (2, 5),
'alpha': 1e-05,
'activation': 'identity'}
Более глубокая оптимизация
Проведем более глубокий поиск оптимальных параметров с помощью библиотеки SciPy.
#Deeper optimization
from scipy.optimize import minimize
Создадим структуры данных для фиксации нашего прогресса.
#Create a dataframe to store our accuracy current_error_rate = pd.DataFrame(index = np.arange(0,5),columns=["Current Error"]) optimization_progress = []
Определим целевую функцию, подлежащую минимизации. Мы хотим минимизировать среднеквадратичную ошибку нашей модели.
#Define the objective function def objective(x): #The parameter x represents a new value for our neural network's settings #In order to find optimal settings, we will perform 10 fold cross validation using the new setting #And return the average RMSE from all 10 tests #We will first turn the model's Alpha parameter, which controls the amount of L2 regularization model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=x[0], tol=x[1], learning_rate_init=x[2]) #Now we will cross validate the model for i,(train,test) in enumerate(tscv.split(X_train_mn)): #Train the model model.fit(X_train_mn.loc[train[0]:train[-1],:],y_train_mn.loc[train[0]:train[-1]]) #Measure the RMSE current_error_rate.iloc[i,0] = mean_squared_error(y_train_mn.loc[test[0]:test[-1]],model.predict(X_train_mn.loc[test[0]:test[-1],:])) #Record the progress made by the optimizer optimization_progress.append(current_error_rate.iloc[:,0].mean()) #Return the Mean CV RMSE return(current_error_rate.iloc[:,0].mean())
Укажем начальную точку для процедуры оптимизации и укажем большие границы таким образом, чтобы мы могли аппроксимировать глобальную оптимизацию.
#Define the starting point pt = [tuner.best_params_["alpha"],tuner.best_params_["tol"],tuner.best_params_["learning_rate_init"]] bnds = ((0.000000001,10000000000),(0.0000000001,10000000000),(0.000000001,10000000000))
Оптимизация нашей модели DNN.
#Searchin deeper for parameters result = minimize(objective,pt,method="TNC",bounds=bnds)
Похоже, оптимизация проведена успешно.
result
success: True
status: 2
fun: 0.04257403904271943
x: [ 4.864e-05 1.122e-03 9.999e-01]
nit: 1
jac: [ 1.298e+04 1.806e+02 -3.371e+03]
nfev: 92
Сохраним наши оптимальные значения.
optima_y = result.fun
optima_x = optimization_progress.index(optima_y)
inputs = np.arange(0,len(optimization_progress))
Визуализируем прогресс, достигнутый в ходе процедуры оптимизации.
plt.scatter(inputs,optimization_progress) plt.plot(optima_x,optima_y,'s',color='r') plt.axvline(x=optima_x,ls='--',color='red') plt.axhline(y=optima_y,ls='--',color='red') plt.title("Minimizing Training MSE")
Рис. 10: Результаты процедуры оптимизации TNC
Тестирование на переобучение
Посмотрим, действительно ли мы можем превзойти нашу линейную модель по умолчанию.
#Test for overfitting benchmark = LinearRegression() default_model = MLPRegressor(max_iter=200) random_search_model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=tuner.best_params_["alpha"], tol=tuner.best_params_["tol"], learning_rate_init=tuner.best_params_["learning_rate_init"], max_iter=200 ) lbfgs_model = MLPRegressor(hidden_layer_sizes=tuner.best_params_["hidden_layer_sizes"], activation=tuner.best_params_["activation"], learning_rate=tuner.best_params_["learning_rate"], solver=tuner.best_params_["solver"], shuffle=tuner.best_params_["shuffle"], alpha=result.x[0], tol=result.x[1], learning_rate_init=result.x[2], max_iter=200 )
Обучение моделей на обучающей выборке.
#Fit the models
benchmark.fit(X_train_mn,y_train_mn)
default_model.fit(X_train_mn,y_train_mn)
random_search_model.fit(X_train_mn,y_train_mn)
lbfgs_model.fit(X_train_mn,y_train_mn)
Подготовьтесь к тому, чтобы зафиксировать наши результаты кросс-валидации.
#Record our cross val scores
models = [benchmark,
default_model,
random_search_model,
lbfgs_model
]
val_error = pd.DataFrame(columns=["Linear Reg","Default NN","Random Search NN","TNC NN"],index=[0])
Выполним кросс-валидацию каждой модели.
for i in np.arange(0,len(models)): val_error.iloc[0,i] = np.mean(cross_val_score(models[i],X_test_mn,y_test_mn,cv=5,n_jobs=-1)) * -1
Наша ошибка при проверке ясно показывает, что наша нейронная сеть, оптимизированная TNC, была наиболее эффективной.
val_error
Линейная рег | Дефолтная НС | Случайный поиск НС | TNC НС |
---|---|---|---|
3.323741 | 3.987083 | 3.314776 | 3.283775 |
Экспортирование в формат ONNX
Теперь подготовимся к экспорту нашей модели в формат ONNX. ONNX (Open Neural Network Exchange) — и представляет собой протокол с открытым исходным кодом для представления любой модели машинного обучения в виде дерева узлов, представляющих вычисления и поток данных после каждого вычисления. ONNX позволяет нам создавать и использовать модели машинного обучения на разных языках программирования, при условии, что эти языки реализуют спецификацию ONNX.
Для начала импортируем библиотеку ONNX.
#Preparing to export to ONNX import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
Подготовка модели.
#Fit the model on all the data we have model = MLPRegressor( solver= 'lbfgs', shuffle= True, activation= 'identity', learning_rate= 'adaptive', hidden_layer_sizes= (2, 5), alpha= 4.864e-05, tol= 1.122e-03, learning_rate_init= 9.999e-01, )
Подгоним модель по всем имеющимся у нас данным.
model.fit(prices[["MN Close"]],prices.loc[:,"MN Target"])
Определим входную форму нашей модели.
#Define the input types for our ONNX model initial_types = [("float_input",FloatTensorType([1,1]))]
Создадим ONNX-представление модели.
# Create the ONNX representation onnx_model = convert_sklearn(model,initial_types=initial_types,target_opset=12)
Экспортируем модель в ONNX формат.
# Save the ONNX model onnx.save_model(onnx_model,"EURUSD MN1 AI.onnx")
Теперь представим нашу модель в формате ONNX, чтобы убедиться, что наши входные данные имеют верный размер.
import netron
netron.start("EURUSD MN1 AI.onnx")
Рис. 11: Визуализация нашей модели DNN
Рис. 12: Форма ввода-вывода нашей модели
Реализация средствами MQL5
Теперь нам надо реализовать наш торговый алгоритм на MQL5. Мы хотим, чтобы наш торговый алгоритм мог переключаться между закрытием позиций с помощью простых скользящих средних и использованием прогнозов искусственного интеллекта. Кроме того, мы хотим управлять нашей моделью ИИ с помощью технического анализа. Для начала мы импортируем модель ONNX, которую мы экспортировали выше.
//+------------------------------------------------------------------+ //| EURUSD MTF 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 the ONNX resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD MN1 AI.onnx" as const uchar onnx_buffer[];
Теперь определим наш пользовательский счетчик, чтобы указать, как пользователь хотел бы закрывать позиции.
//+-------------------------------------------------------------------+ //| Define our custom type | //+-------------------------------------------------------------------+ enum close_type { MA_CLOSE = 0, // Moving Averages Close AI_CLOSE = 1 // AI Auto Close };
Давайте создадим входные данные, чтобы мы могли изменить способ закрытия позиций нашим приложением, а также увидеть, какой из них лучше.
//+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input close_type user_close_type = AI_CLOSE; // How should we close our positions?
Нам нужно импортировать торговый класс.
//+------------------------------------------------------------------+ //| Libraries we need | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
Создадим глобальные переменные, которые понадобятся нам на протяжении всей нашей программы.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ long onnx_model; vectorf model_input = vectorf::Zeros(1); vectorf model_output = vectorf::Zeros(1); double bid,ask; int ma_hanlder; double ma_buffer[]; int bb_hanlder; double bb_mid_buffer[]; double bb_high_buffer[]; double bb_low_buffer[]; int rsi_hanlder; double rsi_buffer[]; int system_state = 0,model_state=0;
Когда наше приложение будет загружено, мы сначала создадим нашу ONNX-модель из созданного ранее буфера. Затем назначим наши технические хэндлы индикаторов.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Load our ONNX function if(!load_onnx_model()) { return(INIT_FAILED); } //--- Load our technical indicators bb_hanlder = iBands("EURUSD",PERIOD_D1,30,0,1,PRICE_CLOSE); rsi_hanlder = iRSI("EURUSD",PERIOD_D1,14,PRICE_CLOSE); ma_hanlder = iMA("EURUSD",PERIOD_D1,20,0,MODE_EMA,PRICE_CLOSE); //--- return(INIT_SUCCEEDED); }
Если наш советник будет удален с графика, мы должны освободить ресурсы, которые больше не используем.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release the resources we don't need release_resources(); }
Итак, всякий раз, когда мы получаем обновленные цены, сначала будем сохранять новые технические данные, делать прогноз на основе нашей модели, а затем показывать важную статистику пользователю. Если у нас нет открытых позиций, мы будем следовать прогнозам нашей модели. В противном случае мы будем следовать входным данным пользователя, чтобы определить, следует ли держать наши позиции открытыми или закрытыми.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Update market data update_market_data(); //--- Fetch a prediction from our model model_predict(); //--- Display stats display_stats(); //--- Find a position if(PositionsTotal() == 0) { if(model_state == 1) check_bullish_setup(); else if(model_state == -1) check_bearish_setup(); } //--- Manage the position we have else { //--- How should we close our positions? if(user_close_type == MA_CLOSE) { ma_close_positions(); } else { ai_close_positions(); } } } //+------------------------------------------------------------------+
Теперь определим, как наша система искусственного интеллекта должна закрывать свои сделки. Если наша система искусственного интеллекта обнаружит, что уровни цен изменятся таким образом, который противоречит утверждению нашей позиции, мы закроем наши сделки.
//+------------------------------------------------------------------+ //| Close whenever our AI detects a reversal | //+------------------------------------------------------------------+ void ai_close_positions(void) { if(system_state != model_state) { Alert("Reversal detected by our AI system,closing open positions"); Trade.PositionClose("EURUSD"); } }
С другой стороны, если мы будем полагаться на скользящую среднюю при закрытии наших позиций, то нам надо закрывать любые сделки на продажу, если цена закрытия выше скользящей средней, и наоборот для наших сделок на продажу.
//+------------------------------------------------------------------+ //| Close whenever price reverses the moving average | //+------------------------------------------------------------------+ void ma_close_positions(void) { //--- Is our buy position possibly weakening? if(system_state == 1) { if(iClose("EURUSD",PERIOD_D1,0) < ma_buffer[0]) Trade.PositionClose("EURUSD"); } //--- Is our sell position possibly weakening? if(system_state == -1) { if(iClose("EURUSD",PERIOD_D1,0) > ma_buffer[0]) Trade.PositionClose("EURUSD"); } }
Для открытия сделки нам сначала потребуется пробой полосы Боллинджера, за которым последует подтверждение от индикатора RSI, и, наконец, нам также надо видеть скользящую среднюю справа по отношению к цене.
//+------------------------------------------------------------------+ //| Check bearish setup | //+------------------------------------------------------------------+ void check_bearish_setup(void) { if(iClose("EURUSD",PERIOD_D1,0) < bb_low_buffer[0]) { if(50 > rsi_buffer[0]) { if(iClose("EURUSD",PERIOD_D1,0) < ma_buffer[0]) { Trade.Sell(0.3,"EURUSD",bid,0,0,"EURUSD MTF AI"); system_state = -1; } } } } //+------------------------------------------------------------------+ //| Check bullish setup | //+------------------------------------------------------------------+ void check_bullish_setup(void) { if(iClose("EURUSD",PERIOD_D1,0) > bb_high_buffer[0]) { if(50 < rsi_buffer[0]) { if(iClose("EURUSD",PERIOD_D1,0) > ma_buffer[0]) { Trade.Buy(0.3,"EURUSD",ask,0,0,"EURUSD MTF AI"); system_state = 1; } } } }
В настоящее время эта функция будет отображать только прогноз модели.
//+------------------------------------------------------------------+ //| Display account stats | //+------------------------------------------------------------------+ void display_stats(void) { Comment("Forecast: ",model_output[0]); }
Извлечение прогноза из нашей модели и сохранение его, используя двоичный флаг.
//+------------------------------------------------------------------+ //| Fetch a prediction from our model | //+------------------------------------------------------------------+ void model_predict(void) { //--- Get inputs model_input.CopyRates("EURUSD",PERIOD_MN1,COPY_RATES_CLOSE,0,1); //--- Fetch a prediction from our model OnnxRun(onnx_model,ONNX_DEFAULT,model_input,model_output); //--- Store the model's prediction as a flag if(model_output[0] > model_input[0]) { model_state = -1; } else if(model_output[0] < model_input[0]) { model_state = 1; } }
Теперь определим функцию, которая освободит ненужные нам ресурсы.
//+------------------------------------------------------------------+ //| Release the resources we don't need | //+------------------------------------------------------------------+ void release_resources(void) { OnnxRelease(onnx_model); IndicatorRelease(ma_hanlder); IndicatorRelease(rsi_hanlder); IndicatorRelease(bb_hanlder); ExpertRemove(); }
Всякий раз, когда указывается новая цена, эта функция будет вызываться для обновления имеющихся у нас рыночных данных.
//+------------------------------------------------------------------+ //| Update our market data | //+------------------------------------------------------------------+ void update_market_data(void) { //--- Update all our technical data bid = SymbolInfoDouble("EURUSD",SYMBOL_BID); ask = SymbolInfoDouble("EURUSD",SYMBOL_ASK); CopyBuffer(ma_hanlder,0,0,1,ma_buffer); CopyBuffer(rsi_hanlder,0,0,1,rsi_buffer); CopyBuffer(bb_hanlder,0,0,1,bb_mid_buffer); CopyBuffer(bb_hanlder,1,0,1,bb_high_buffer); CopyBuffer(bb_hanlder,2,0,1,bb_low_buffer); }
Наконец, давайте определим функцию, которая создаст нашу ONNX-модель из определенного нами ранее буфера.
//+------------------------------------------------------------------+ //| Load our ONNX model | //+------------------------------------------------------------------+ bool load_onnx_model(void) { //--- Create the ONNX model from our buffer onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT); //--- Validate the model if(onnx_model == INVALID_HANDLE) { //--- Give feedback Comment("Failed to create the ONNX model"); //--- We failed to create the model return(false); } //--- Specify the I/O shapes ulong input_shape[] = {1,1}; ulong output_shape[] = {1,1}; //--- Validate the I/O shapes if(!(OnnxSetInputShape(onnx_model,0,input_shape)) || !(OnnxSetOutputShape(onnx_model,0,output_shape))) { //--- Give feedback Comment("We failed to define the correct input shapes"); //--- We failed to define the correct I/O shape return(false); } return(true); } //+------------------------------------------------------------------+
Рис. 13: Входные данные нашего советника
Рис. 14: Наш советник в действии
Рис. 15: Результаты бэк-тестирования нашей стратегии
Рис. 16: Результаты форвард-тестирования нашей стратегии
Заключение
В настоящей статье показано, что месячный и часовой таймфреймы являются наиболее стабильными для прогнозирования пары EURUSD. Мы не можем быть уверены, что это справедливо для каждого существующего рынка. Аналогичным образом, в будущем мы также должны рассмотреть возможность поиска большего количества возможных таймфреймов, чтобы убедиться, что мы не упускаем из виду какие-либо оптимальные таймфреймы. Кроме того, мы можем внести дополнительные коррективы в наш подход к поиску более низких показателей ошибок. Например, нам может быть интересно понять, существует ли комбинация таймфреймов, которая могла бы еще больше снизить уровень наших ошибок.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15972
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования