English 中文 Español Deutsch 日本語 Português
preview
Создание самооптимизирующихся советников на языках MQL5 и Python (Часть II): Настройка глубоких нейронных сетей

Создание самооптимизирующихся советников на языках MQL5 и Python (Часть II): Настройка глубоких нейронных сетей

MetaTrader 5Примеры | 28 апреля 2025, 11:23
181 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

Введение

Участники нашего сообщества стремятся интегрировать ИИ в свои торговые стратегии, что требует настройки ИИ-моделей для конкретных рынков. Каждая ИИ-модель имеет настраиваемые параметры, существенно влияющие на ее производительность; настройки, оптимальные для одного рынка, могут не подходить для другого. В этой статье будет показано, как настроить ИИ-модели таким образом, чтобы они превосходили настройки по умолчанию, используя алгоритмы оптимизации, в частности алгоритм Нелдера-Мида. Применим этот алгоритм для тонкой настройки глубокой нейросети с помощью данных из терминала MetaTrader5, а затем экспортируем оптимизированную модель в формате ONNX для использования в советнике. Для не знакомых с этими концепциями дадим подробные объяснения на протяжении всей статьи.


Алгоритм оптимизации Нелдера-Мида

Алгоритм Нелдера-Мида — популярный вариант для решения шумных, недифференцируемых и нелинейных многомодальных задач оптимизации. Названный в честь его создателей, Джона Нелдера и Роджера Мида, этот алгоритм был представлен в их статье 1965 года, под названием: «Симплексный метода минимизации функций». Его можно использовать как для одномерных, так и для многомерных задач оптимизации.

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

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

Джон Нелдер

Рис. 1. Джон Нелдер


Роджер Мид

Рис. 2. Роджер Мид


Приступим

Начнем с извлечения необходимых данных из терминала MetaTrader 5. Сначала откроем терминал MetaTrader 5 и выберем в контекстном меню иконку «Символ». Там выберем столбцы и ищем символ, который вы хотите использовать. После запроса данных просто нажмите на «Экспорт», и данные будут доступны в формате CSV.

Поиск нужных данных.

Рис. 3. Поиск нужных данных

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

#import libraries we need
import pandas as pd
import numpy as np
from numpy.random import randn,rand
import seaborn as sns

Затем прочитаем подготовленные нами данные.

#Read in our market data
brent = pd.read_csv("/home/volatily/market_data/Market Data UK Brent Oil.csv", sep="\t")

Нужно маркировать наши данные.

#Preparing to label the data
look_ahead = 20

#Defining the target
brent["Target"] = brent["Close"].shift(-look_ahead)

#Drop missing values
brent.dropna(inplace=True)

Импортируем библиотеки, необходимые для оптимизации.

#In this article we will cover some techniques for hyper-parameter tuning 
from scipy.optimize import minimize
from sklearn.neural_network import MLPRegressor
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import root_mean_squared_error
import time

Теперь создадим объект перекрестной проверки временных рядов.

#Define the time series split parameters
splits = 5
gap = look_ahead

#Create the time series split object
tscv = TimeSeriesSplit(n_splits=splits,gap=gap)

#Create a dataframe to store our accuracy 
current_error_rate = pd.DataFrame(index = np.arange(0,splits),columns=["Current Error"])

Определим предикторы и цели для нашей модели.

#Define the predictors and the target
predictors = ["Open","High","Low","Close"]
target = "Target"

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

#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=(5,2),alpha=x[0],early_stopping=True,shuffle=False,learning_rate_init=x[1],tol=x[2])
    #Now we will cross validate the model
    for i,(train,test) in enumerate(tscv.split(brent)):
        #The data
        X_train = brent.loc[train[0]:train[-1],predictors]
        y_train = brent.loc[train[0]:train[-1],target]
        X_test  = brent.loc[test[0]:test[-1],predictors]
        y_test  = brent.loc[test[0]:test[-1],target]
        #Train the model
        model.fit(X_train,y_train)
        #Measure the RMSE
        current_error_rate.iloc[i,0] = root_mean_squared_error(y_test,model.predict(X_test))
    #Return the Mean CV RMSE
    return(current_error_rate.iloc[:,0].mean())

Напомним, что алгоритм Нелдера-Мида требует начальной точки отсчета. Чтобы найти подходящую начальную точку, выполним линейный поиск по интересующим нас параметрам. Используем цикл for для измерения точности с параметрами, установленными на 0.1, затем на 0.01 и т. д. Это поможет нам определить потенциально удачную начальную точку для алгоритма.

#Let us measure how much time this takes.
start = time.time()

#Create a dataframe to measure the error rates
starting_point_error = pd.DataFrame(index=np.arange(0,21),columns=["Average CV RMSE"])
starting_point_error["Iteration"] = np.arange(0,21)

#Let us first find a good starting point for our optimization algorithm
for i in np.arange(0,21):
    #Set a new starting point
    new_starting_point = (10.0 ** -i)
    #Store error rates
    starting_point_error.iloc[i,0] = objective([new_starting_point,new_starting_point,new_starting_point]) 

#Record the time stamp at the end
stop = time.time()

#Report the amount of time taken
print(f"Completed in {stop - start} seconds")
Выполнено за 312.29402351379395 секунды

Рассмотрим теперь уровни ошибок.

Средний коэффициент вариации среднеквадратического отклонения (CV RMSE)
Итерация
0.91546
0
0.267167
1
14.846035
2
15.763264 3
56.820397 4
75.202923 5
72.562681
6
64.33746
7
88.980977
8
83.791834
9
82.871215
10
88.031151
11
65.532539
12
78.177191
13
85.063947
14
88.631589
15
74.369735
16
86.133656
17
90.482654
18
102.803612
19
74.636781
20

Как видим, мы прошли оптимальную область между итерациями 0 и 2. С тех пор наша ошибка продолжала увеличиваться. Ту же информацию мы можем наблюдать визуально.

sns.lineplot(data=starting_point_error,x="Iteration",y="Average CV RMSE")

Частота появления ошибок

Рис. 4. Визуализация результатов линейного поиска

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

pt = abs(((10 ** -1) + rand(3) * ((10 ** 0) - (10 ** -1))))
pt
array([0.75747551, 0.34066536, 0.26214705])

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

start = time.time()
result = minimize(objective,pt,method="nelder-mead")
stop = time.time()
print(f"Task completed in {stop - start} seconds")
Задача выполнена за 1332.9911317825317 секунды

Интерпретируем результат оптимизации

результат
сообщение: Превышено максимальное количество оценок функции.
       success: False
        status: 1
           fun: 0.12022686955703668
             x: [ 7.575e-01  3.577e-01  2.621e-01]
           nit: 225
          nfev: 600
 final_simplex: (array([[ 7.575e-01,  3.577e-01,  2.621e-01],
                       [ 7.575e-01,  3.577e-01,  2.621e-01],
                       [ 7.575e-01,  3.577e-01,  2.621e-01],
                       [ 7.575e-01,  3.577e-01,  2.621e-01]]), array([ 1.202e-01,  2.393e-01,  2.625e-01,  8.978e-01])

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

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

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

Теперь сравним модель по умолчанию и построенную нами оптимизированную под определенные требования модель.

#Let us compare our customised model and the defualt model
custom_model = MLPRegressor(hidden_layer_sizes=(5,2),alpha=result.x[0],early_stopping=True,shuffle=False,learning_rate_init=result.x[1],tol=result.x[2])

#Default model
default_model = MLPRegressor(hidden_layer_sizes=(5,2))

Подготовим объект разбиения тайм-серии.

#Define the time series split parameters
splits = 10
gap = look_ahead

#Create the time series split object
tscv = TimeSeriesSplit(n_splits=splits,gap=gap)

#Create a dataframe to store our accuracy 
model_error_rate = pd.DataFrame(index = np.arange(0,splits),columns=["Default Model","Custom Model"])

Теперь проведем перекрестную проверку каждой модели.

#Now we will cross validate the model
for i,(train,test) in enumerate(tscv.split(brent)):
    #The data
    X_train = brent.loc[train[0]:train[-1],predictors]
    y_train = brent.loc[train[0]:train[-1],target]
    X_test  = brent.loc[test[0]:test[-1],predictors]
    y_test  = brent.loc[test[0]:test[-1],target]
    #Our model
    model = MLPRegressor(hidden_layer_sizes=(5,2),alpha=result.x[0],early_stopping=True,shuffle=False,learning_rate_init=result.x[1],tol=result.x[2])
    #Train the model
    model.fit(X_train,y_train)
    #Measure the RMSE
    model_error_rate.iloc[i,1] = root_mean_squared_error(y_test,model.predict(X_test))

Посмотрим на показатели ошибок.

model_error_rate
Модель по умолчанию
Оптимизированная модель
0.153904    
0.550214
0.113818     
0.501043
82.188345    
0.52897
0.114108   
0.117466
0.114718    
0.112892
77.508403    
0.258558
0.109191    
0.304262
0.142143    
0.363774
0.163161    
0.153202
0.120068    
2.20102

Кроме того, визуализируем результаты.

model_error_rate["Default Model"].plot(legend=True)
model_error_rate["Custom Model"].plot(legend=True)

Визуализация производительности модели

Рис. 5. Визуализация производительности нашей оптимизированной модели

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

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

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

#Now we will prepare to export our neural network into ONNX format
from skl2onnx.common.data_types import FloatTensorType
from skl2onnx import convert_sklearn
import onnxruntime as ort
import netron

Определим для нашей модели форму входных данных, не забывая, что наша модель принимает данные на 4 входа.

#Define the input types
initial_type = [("float_input",FloatTensorType([1,4]))]

Подгонка оптимизированной нами модели.

#Fit our custom model
custom_model.fit(brent.loc[:,["Open","High","Low","Close"]],brent.loc[:,"Target"])

Создание ONNX-представления нашей глубокой нейросети стало проще благодаря библиотеке skl2onnx.

#Create the onnx represantation
onnx = convert_sklearn(custom_model,initial_types=initial_type,target_opset=12)

Определим имя файла ONNX.

#The name of our ONNX file
onnx_filename = "Brent_M1.onnx"

Теперь запишем файл ONNX.

#Write out the ONNX file
with open(onnx_filename,"wb") as f:
    f.write(onnx.SerializeToString())

Рассмотрим параметры нашей ONNX-модели.

#Now let us inspect our ONNX model
onnx_session = ort.InferenceSession(onnx_filename)
input_name   = onnx_session.get_inputs()[0].name
output_name = onnx_session.get_outputs()[0].name

Посмотрим на форму входных данных.

for i, input_tensor in enumerate(onnx_session.get_inputs()):
    print(f"{i + 1}. Имя: {input_tensor.name}, Тип данных: {input_tensor.type}, Форма: {input_tensor.shape}")
1. Имя: float_input, Тип данных: tensor(float), Форма: [1, 4]

Обратите внимание на форму результата работы нашей модели.

for i, output_tensor in enumerate(onnx_session.get_outputs()):
    print(f"{i + 1}. Имя: {output_tensor.name}, Тип данных: {output_tensor.type}, Форма: {output_tensor.shape}")
1. Имя: variable, Тип данных: tensor(float), Форма: [1, 1]

Теперь можно получить визуальное представление о нашей модели с помощью Netron. Эти действия помогают обеспечивать соответствие входных и выходных форм ONNX нашим ожиданиям.

#We can also inspect our model visually using netron.
netron.start(onnx_filename)

Визуализация в Netron

Рис. 6. ONNX-представление нашей нейросети


Метаданные нашего файла ONNX

Рис. 7. Метаданные нашей ONNX-модели

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


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

Завершив конфигурирование нашей ONNX-модели, можем приступить к созданию советника на языке MQL5.

Принципиальная схема MQL5-приложения

Рис. 8. План-схема нашего советника

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

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

//+------------------------------------------------------------------+
//|                                   Custom Deep Neural Network.mq5 |
//|                                        Gamuchirai Zororo Ndawana |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Load the ONNX model                                              |
//+------------------------------------------------------------------+
#resource "\\Files\\Brent_M1.onnx" as const uchar ModelBuffer[];

Затем загрузим торговую библиотеку, которая необходима для управления позициями.

//+------------------------------------------------------------------+
//| Libraries we need                                                |
//+------------------------------------------------------------------+
#include <Trade/Trade.mqh>
CTrade Trade;

Теперь можно перейти к созданию глобальных переменных для нашей программы.

//+------------------------------------------------------------------+
//| Gloabal variables                                                |
//+------------------------------------------------------------------+
long model; //The handler for our ONNX model
vector forecast = vector::Zeros(1); //Our model's forecast

const int states = 3; //The total number of states the system can be in
vector state = vector::Zeros(states); //The state of our system

int mfi_handler,wpr_handler; //Handlers for our technical indicators
vector mfi_reading,wpr_reading; //The values of our indicators will be kept in vectors

double minimum_volume, trading_volume; //Smallest lot size allowed & our calculated lotsize
double ask_price, bid_price; //Market rates

Определим пользовательские входные данные, которые позволят изменять поведение советника.

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int mfi_period = 20; //Money Flow Index Period
input int wpr_period = 30; //Williams Percent Range Period

input int lot_multiple = 20; //How big should our lot sizes be?
input double sl_width = 2; //How tight should the stop loss be?

input double max_profit = 10; //Close the position when this profit level is reached.
input double max_loss = 10; //Close the position when this loss level is reached.

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

В зависимости от текущего состояния приложение будет иметь доступ к разным функциям.

//+------------------------------------------------------------------+
//| This function is responsible for updating the system state       |
//+------------------------------------------------------------------+
void update_state(int index)
  {
//--- Reset the system state
   state = vector::Zeros(states);
//--- Now update the current state
   state[index] = 1;
  }

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

//+------------------------------------------------------------------+
//| This function will ensure that user inputs are valid             |
//+------------------------------------------------------------------+
bool valid_inputs(void)
  {
//--- Let us validate the inputs the user passed
   return((mfi_period > 0)&&(wpr_period > 0) && (max_profit >= 0) && (max_loss >= 0) && (lot_multiple >= 0) && (sl_width >= 0));
  }

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

//+------------------------------------------------------------------+
//| This function will check our profit levels                       |
//+------------------------------------------------------------------+
void check_profit_level(void)
  {
//--- Let us check if the user set a max profit/loss limit
   if(max_loss > 0 || max_profit > 0)
     {
      //--- If true, let us inspect whether we have passed the limit.
      if((PositionGetDouble(POSITION_PROFIT) > max_profit) || (PositionGetDouble(POSITION_PROFIT) < (max_loss * -1)))
        {
         //--- Close the position
         Trade.PositionClose(Symbol());
        }
     }
  }

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

//+------------------------------------------------------------------+
//| If we predict a reversal, let's close our positions              |
//+------------------------------------------------------------------+
void find_reversal(void)
  {
//--- We have a position
   if(((state[1] == 1) && (forecast[0] < iClose(Symbol(),PERIOD_CURRENT,0))) || ((state[2] == 1) && (forecast[0] > iClose(Symbol(),PERIOD_CURRENT,0))))
     {
      Trade.PositionClose(Symbol());
     }
  }

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

//+------------------------------------------------------------------+
//| This function will determine if we have a valid entry            |
//+------------------------------------------------------------------+
void find_entry(void)
  {
//--- First we want to know if the higher timeframes are moving in the same direction we want to go
   double higher_time_frame_trend = iClose(Symbol(),PERIOD_W1,16) - iClose(Symbol(),PERIOD_W1,0);

//--- If price levels appreciated, the difference will be negative
   if(higher_time_frame_trend < 0)
     {
      //--- We may be better off only taking buy opportunities
      //--- Buy opportunities are triggered when the model's prediction is greater than the current price
      if(forecast[0] > iClose(Symbol(),PERIOD_CURRENT,0))
        {
         //--- We will use technical indicators to time our entries
         bullish_sentiment();
        }
     }

//--- If price levels depreciated, the difference will be positive
   if(higher_time_frame_trend > 0)
     {
      //--- We may be better off only taking sell opportunities
      //--- Sell opportunities are triggered when the model's prediction is less than the current price
      if(forecast[0] < iClose(Symbol(),PERIOD_CURRENT,0))
        {
         //--- We will use technical indicators to time our entries
         bearish_sentiment();
        }
     }
  }

Теперь мы добрались до функции, которая отвечает за интерпретацию наших технических индикаторов. Есть разные способы интерпретации этих индикаторов; тем не менее, я предпочитаю центрировать их вокруг значения 50. При этом значения выше 50 подтверждают бычьи настроения, а значения ниже 50 указывают на медвежьи. Воспользуемся индикатором Money Flow Index (MFI, индекс денежного потока) в качестве индикатора объема, а Williams Percent Range (WPR, Williams %R, процентный диапазон Уильямса) — в качестве индикатора силы тренда.

//+------------------------------------------------------------------+
//| This function will interpret our indicators for buy signals      |
//+------------------------------------------------------------------+
void bullish_sentiment(void)
  {
//--- For bullish entries we want strong volume readings from our MFI
//--- And confirmation from our WPR indicator
   wpr_reading.CopyIndicatorBuffer(wpr_handler,0,0,1);
   mfi_reading.CopyIndicatorBuffer(mfi_handler,0,0,1);
   if((wpr_reading[0] > -50) && (mfi_reading[0] > 50))
     {
      //--- Get the ask price
      ask_price = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      //--- Make sure we have no open positions
      if(PositionsTotal() == 0)
         Trade.Buy(trading_volume,Symbol(),ask_price,(ask_price - sl_width),(ask_price + sl_width),"Custom Deep Neural Network");
      update_state(1);
     }
  }

//+------------------------------------------------------------------+
//| This function will interpret our indicators for sell signals     |
//+------------------------------------------------------------------+
void bearish_sentiment(void)
  {
//--- For bearish entries we want strong volume readings from our MFI
//--- And confirmation from our WPR indicator
   wpr_reading.CopyIndicatorBuffer(wpr_handler,0,0,1);
   mfi_reading.CopyIndicatorBuffer(mfi_handler,0,0,1);
   if((wpr_reading[0] < -50) && (mfi_reading[0] < 50))
     {
      //--- Get the bid price
      bid_price = SymbolInfoDouble(Symbol(),SYMBOL_BID);
      if(PositionsTotal() == 0)
         Trade.Sell(trading_volume,Symbol(),bid_price,(bid_price + sl_width),(bid_price - sl_width),"Custom Deep Neural Network");
      //--- Update the state
      update_state(2);
     }
  }

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

//+------------------------------------------------------------------+
//| This function will fetch forecasts from our model                |
//+------------------------------------------------------------------+
void model_predict(void)
  {
//--- First we get the input data ready
   vector input_data = {iOpen(_Symbol,PERIOD_CURRENT,0),iHigh(_Symbol,PERIOD_CURRENT,0),iLow(_Symbol,PERIOD_CURRENT,0),iClose(_Symbol,PERIOD_CURRENT,0)};
//--- Now we need to perform inferencing
   if(!OnnxRun(model,ONNX_DATA_TYPE_FLOAT,input_data,forecast))
     {
      Comment("Failed to obtain a forecast from the model: ",GetLastError());
      forecast[0] = 0;
      return;
     }
//--- We succeded!
   Comment("Model forecast: ",forecast[0]);
  }

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Make sure user inputs are valid
   if(!valid_inputs())
     {
      Comment("Invalid inputs were passed to the application.");
      return(INIT_FAILED);
     }

//--- Create the ONNX model from the buffer
   model = OnnxCreateFromBuffer(ModelBuffer,ONNX_DEFAULT);

//--- Check if we were succesfull
   if(model == INVALID_HANDLE)
     {
      Comment("[ERROR] Failed to create the ONNX model from the buffer: ",GetLastError());
      return(INIT_FAILED);
     }

//--- Set the input shape of the model
   ulong input_shape[] = {1,4};

//--- Check if we were succesfull
   if(!OnnxSetInputShape(model,0,input_shape))
     {
      Comment("[ERROR] Failed to set the ONNX model input shape: ",GetLastError());
      return(INIT_FAILED);
     }

//--- Set the output shape of the model
   ulong output_shape[] = {1,1};

//--- Check if we were succesfull
   if(!OnnxSetOutputShape(model,0,output_shape))
     {
      Comment("[ERROR] Failed to set the ONNX model output shape: ",GetLastError());
      return(INIT_FAILED);
     }

//--- Setup the technical indicators
   wpr_handler = iWPR(Symbol(),PERIOD_CURRENT,wpr_period);
   mfi_handler = iMFI(Symbol(),PERIOD_CURRENT,mfi_period,VOLUME_TICK);

//--- Fetch market data
   minimum_volume = SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
   trading_volume = minimum_volume * lot_multiple;

//--- Set the system to state 0, indicating we have no open positions
   update_state(0);

//--- Everything went fine
   return(INIT_SUCCEEDED);
  }

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Free the onnx resources
   OnnxRelease(model);

//--- Free the indicator resources
   IndicatorRelease(wpr_handler);
   IndicatorRelease(mfi_handler);

//--- Detach the expert advisor
   ExpertRemove();
  }

Наконец, нам нужно определить обработчик событий OnTick. Принимаемые меры будут зависеть от состояния системы. Если у нас не будет открытых позиций (состояние 0), приоритетом будут получение прогноза от нашей модели и определение потенциальной точки входа. Если у нас будет открытая позиция (состояние 1 для длинной или состояние 2 для короткой), наше внимание переключится на управление позицией. Сюда входят мониторинг потенциальных разворотов и проверка уровней риска, целевых показателей прибыли и максимальных уровней прибыли.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Which state is the system in?
   if(state[0] == 1)
     {
      //--- Being in this state means we have no open positions, let's analyse the market to try find one
      model_predict();
      find_entry();
     }

   if((state[1] == 1) || (state[2] == 1))
     {
      //--- Being in this state means we have an position open, if our model forecasts a reversal move we will close
      model_predict();
      find_reversal();
      check_profit_level();
     }
  }
//+------------------------------------------------------------------+

Тестирование советника на истории

Рис. 9. Тестирование нашего советника


Заключение

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

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

HTTP и Connexus (Часть 2): Понимание архитектуры HTTP и дизайна библиотеки HTTP и Connexus (Часть 2): Понимание архитектуры HTTP и дизайна библиотеки
В настоящей статье рассматриваются основы протокола HTTP, описываются основные методы (GET, POST, PUT, DELETE), коды состояния, а также структура URL-адресов. Кроме того, в ней представлено начало создания библиотеки Connexus с классами CQueryParam и CURL, облегчающими манипулирование URL-адресами и параметрами запросов в HTTP-запросах.
Реализация торговой стратегии на основе полос Боллинджера с помощью MQL5: Пошаговое руководство Реализация торговой стратегии на основе полос Боллинджера с помощью MQL5: Пошаговое руководство
Пошаговое руководство по реализации на MQL5 алгоритма автоматической торговли, основанной на торговой стратегии «Полосы Боллинджера». Подробное учебное пособие на основе создания советника, который может быть полезен трейдерам.
Алгоритм хаотической оптимизации  — Chaos optimization algorithm (COA) Алгоритм хаотической оптимизации — Chaos optimization algorithm (COA)
Усовершенствованный алгоритм хаотической оптимизации (COA), объединяющий воздействие хаоса с адаптивными механизмами поиска. Алгоритм использует множество хаотических отображений и инерционные компоненты для исследования пространства поиска. Статья раскрывает теоретические основы хаотических методов финансовой оптимизации.
Разрабатываем мультивалютный советник (Часть 26): Информер для торговых инструментов Разрабатываем мультивалютный советник (Часть 26): Информер для торговых инструментов
Прежде, чем двигаться дальше в разработке мультивалютных советников, попробуем переключиться на создание нового проекта, использующего разработанную библиотеку. На этом примере выявим, как лучше организовать хранение исходного кода, и как нам может помочь использование нового репозитория кода от MetaQuotes.