Создание самооптимизирующихся советников на MQL5 (Часть 16): Идентификация линейных систем на основе обучения с учителем
В ходе нашего предыдущего обсуждения регуляторов с обратной связью мы узнали, что эти системы могут стабилизировать результаты торговых стратегий, сначала наблюдая за их поведением в действии. Здесь мы разместили быструю ссылку на предыдущее обсуждение. Такая структура приложения позволила нам выявить основные корреляционные связи, которые наблюдались как в успешных, так и в неудачных сделках. По сути, регуляторы с обратной связью помогли нашему торговому приложению научиться вести себя оптимальным образом в текущих рыночных условиях — подобно трейдерам-людям, которые уделяют меньше внимания прогнозированию будущего и больше — разумному реагированию на текущую ситуацию.
Читателю следует иметь в виду, что до сих пор мы уделяли основное внимание регуляторам с обратной связью, которые корректируют простые стратегии, основанные на правилах. Такой простой подход позволял читателю сразу же увидеть, какое влияние оказывал контроллер с обратной связью, даже если он впервые сталкивался с этой темой. На рисунке 1 ниже мы представили схематическое изображение конфигурации приложения, чтобы читатель мог наглядно представить себе изменения, которые мы сегодня вносим.

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

Рисунок 2: Мы заменим фиксированную торговую стратегию статистической моделью, построенной на основе рыночных данных
Наша цель состоит в том, чтобы определить, обеспечивает ли использование более сложной торговой стратегии, основанной на данных, более насыщенную структуру для обучения контроллера обратной связи и, в конечном итоге, позволяет ли это добиться лучших результатов. Чтобы изучить этот вопрос, мы вернулись к нашей предыдущей работе по управлению с обратной связью и идентификации линейных систем, в рамках которой мы разработали простую стратегию на основе скользящего среднего и подобрали контроллер с обратной связью для установления базового уровня. Затем мы заменили компонент скользящего среднего на статистическую модель, обученную на данных по EUR/USD и оценили эффективность в идентичных условиях тестирования. Результаты исследования показали:
- Чистая прибыль выросла с 56 долларов в базовой системе до 170 долларов — это почти 200-процентное увеличение.
- Валовый убыток сократился с 333 до 143 долларов, что означает снижение риска убытков на 57 %.
- Точность повысилась с 52,9 % до 72 %, что соответствует росту точности на 37 %.
- Количество сделок сократилось с 51 до 33, что повышает эффективность на 35 % и говорит нам о том, что система отфильтровала ненужные сделки.
- Коэффициент прибыльности вырос с 1,17 до 2,18, что означает повышение рентабельности на единицу риска на 86 %.
В совокупности эти результаты показывают, что сочетание регулятора с обратной связью и подходящей статистической модели может привести к существенному повышению как эффективности, так и стабильности. Синергия между управлением с обратной связью и обучением с учителем позволяет реализовать форму интеллектуальной адаптации. Такая система может напоминать алгоритмы обучения с подкреплением, но с точки зрения обучения с учителем.
В итоге в данной статье будут рассмотрены проектные решения, лежащие в основе этой усовершенствованной системы, а также предложен структурированный подход, позволяющий повысить производительность собственных приложений MetaTrader 5 за счет использования принципов управления с обратной связью для поддержки статистических моделей.
Начало работы с нашим анализом на Python
Первым шагом в нашем анализе рыночных данных MetaTrader 5 с использованием Python является импорт необходимых библиотек.
#Import the standard python libraries import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt import MetaTrader5 as mt5
После загрузки всех необходимых компонентов мы приступаем к инициализации терминала MetaTrader 5.
#Check if we have started the terminal if(mt5.initialize()): print("Failed To Startup") else: print("Logged In")
Logged In
На этом этапе мы выбираем торговый символ, который хотим проанализировать.
if(mt5.symbol_select("EURUSD")): print("Found EURUSD Market") else: print("Failed To Find EURUSD Market")
Найден рынок EURUSD
Если вы дочитали до этого места, то теперь готовы загружать исторические рыночные данные прямо из своего терминала MetaTrader 5. Обязательно преобразуйте временные метки из секунд в удобный для восприятия формат, так как MetaTrader 5 по умолчанию возвращает данные о времени в секундах
#Read in the market data data = pd.DataFrame(mt5.copy_rates_from_pos("EURUSD",mt5.TIMEFRAME_D1,0,4000)) data['time'] = pd.to_datetime(data['time'],unit='s') data

Рисунок 3: Рыночные данные, полученные нами из терминала MetaTrader 5
Терминал предоставляет подробный набор данных, содержащий множество рыночных показателей. Однако в этой статье мы сосредоточимся только на четырёх ключевых ценовых уровнях — цене открытия, максимальной, минимальной и цене закрытия. Поэтому мы удаляем из набора данных все остальные столбцы.
#Focus on the major price levels data = data.iloc[:,:5] data

Рисунок 4: В рамках данного упражнения мы сосредоточимся на четырёх основных ценовых уровнях
Далее мы удаляем все наблюдения, которые пересекаются с периодом бэктеста, который мы планируем использовать. В ходе нашего предыдущего обсуждения идентификации линейных систем мы провели бэктест за период с 1 января 2023 года по октябрь 2025 года (текущий период на момент написания статьи). Для обеспечения единообразия мы сохраним здесь то же окно бэктеста. Рекомендуется удалять из обучающего набора все данные, которые могут привести к утечке данных из тестовой выборки
#Drop off the test period data = data.iloc[:-(370*2),:] data

Рисунок 5: Рекомендуется исключить все наблюдения, которые пересекаются с результатами бэк-теста, который мы намерены провести
После очистки набора данных мы определяем горизонт прогнозирования — то есть, на какой период в будущем наша модель будет пытаться сделать прогноз — и соответствующим образом маркируем набор данных целевыми значениями.
#Define the new horizon HORIZON = 10
В заключение мы удаляем все отсутствующие строки, чтобы обеспечить целостность данных. Как только набор данных будет готов и правильно отформатирован, мы сможем загрузить библиотеки машинного обучения и приступить к обучению модели.
#Label the data data['Target 1'] = data['close'].shift(-HORIZON) data['Target 2'] = data['high'].shift(-HORIZON) data['Target 3'] = data['low'].shift(-HORIZON)
Затем мы удаляем все строки, в которых отсутствуют данные.
#Drop missing rows data.dropna(inplace=True)
Давайте теперь приготовимся к настройке наших моделей машинного обучения. Поскольку мы не знаем, какая модель подойдет лучше всего, для начала мы импортируем несколько различных моделей.
#Import cross validation tools from sklearn.linear_model import Ridge,LinearRegression from sklearn.model_selection import TimeSeriesSplit,cross_val_score from sklearn.metrics import root_mean_squared_error from sklearn.neural_network import MLPRegressor from sklearn.ensemble import RandomForestRegressor,GradientBoostingRegressor from sklearn.neighbors import KNeighborsRegressor,RadiusNeighborsRegressor from sklearn.svm import LinearSVR
Создадим новые экземпляры каждой модели.
models = [LinearRegression(), Ridge(alpha=10e-3), RandomForestRegressor(random_state=0), GradientBoostingRegressor(random_state=0), KNeighborsRegressor(n_jobs=-1,n_neighbors=5), RadiusNeighborsRegressor(n_jobs=-1), LinearSVR(random_state=0), MLPRegressor(random_state=0,hidden_layer_sizes=(4,10,40,10),solver='lbfgs')]
Разделим данные на две равные части: одну для обучения, другую — для тестирования.
#The big picture of what we want to test train , test = data.iloc[:data.shape[0]//2,:] , data.iloc[data.shape[0]//2:,:]
Теперь, когда наш набор данных готов, мы можем определить входные данные и целевые значения для нашей модели машинного обучения.
#Define inputs and target X = data.columns[1:-3] y = data.columns[-3:]
Для начала создадим специальную функцию, которая при каждом вызове возвращает новый экземпляр модели.
#Fetch a new copy of the model def get_model(): return(LinearRegression())
Как мы выяснили в ходе предыдущего обсуждения, не все исторические данные всегда полезны для составления прогнозов на настоящее время. Читатели, которые еще не знакомы с нашим предыдущим материалом о «памяти рынка», могут воспользоваться приведенной здесь ссылкой. Чтобы определить, какой объем исторических данных нам на самом деле нужен, мы вновь проводим перекрестную валидацию, на этот раз проверяя, насколько эффективно первая половина наших обучающих данных позволяет предсказать результаты второй половины. Наши результаты показывают, что для точного прогнозирования оставшейся половины достаточно лишь около 60 % данных первой половины. Это означает, что мы можем смело сократить наш обучающий набор, чтобы сосредоточиться исключительно на наиболее когерентном разделе — той части данных, которая выглядит внутренне согласованной.
#Store our performance error = [] #Define the total number of iterations we wish to perform ITERATIONS = 10 #Let us perform the line search for i in np.arange(ITERATIONS): #Training fraction fraction =((i+1)/10) #Partition the data to select the most recent information partition_index = train.shape[0] - int(train.shape[0]*fraction) train_X_partition = train.loc[partition_index:,X] train_y_partition = train.loc[partition_index:,y[0]] #Fit a model model = get_model() #Fit the model model.fit(train_X_partition,train_y_partition) #Cross validate the model out of sample score = root_mean_squared_error(test.loc[:,y[0]],model.predict(test.loc[:,X])) #Append the error levels error.append(score) #Plot the results plt.title('Improvements Made By Historical Data') plt.plot(error,color='black') plt.grid() plt.ylabel('Out of Sample RMSE') plt.xlabel('Progressivley Fitting On All Historical Data') plt.scatter(np.argmin(error),np.min(error),color='red')

Рисунок 6: Как мы узнали из нашего предыдущего обсуждения эффективной перекрестной валидации на исторических данных, не все имеющиеся исторические данные оказываются полезными
Определим соответствующий индекс
#Let us select the partition of interest partition_index = train.shape[0] - int(train.shape[0]*(0.6))
Преобразуйте обучающие данные и удалите старые, менее значимые наблюдения.
train = train.loc[partition_index:,:] train.reset_index(inplace=True,drop=True) train

Рисунок 7: Мы сократили наш набор данных, оставив только те наблюдения, которые, по нашему мнению, наилучшим образом соответствуют текущей ситуации
На ранних этапах процесса проектирования мы составили список возможных типов моделей. Теперь мы пройдемся по каждому из них и оценим их эффективность на тестовом наборе. Обратите внимание: хотя мы оцениваем модели на тестовых данных, мы никогда не подбираем их на этом тестовом наборе, поскольку он предназначен для нашего окончательного бэктеста.
#Store each model's error levels error = [] #Fit each model for m in models: m.fit(train.loc[:,X],train.loc[:,y[0]]) #Store our error levels error.append(root_mean_squared_error(test.loc[:,y[0]],m.predict(test.loc[:,X])))
Далее мы визуализируем результаты работы каждой модели с помощью гистограммы. Как видно, модель регрессии Риджа демонстрирует наилучшие результаты, хотя глубокая нейронная сеть (DNN) не сильно отстает от неё. Это позволяет предположить, что настройка параметров может улучшить работу нейронной сети.
sns.barplot(error,color='black') plt.axhline(np.min(error),color='red',linestyle=':') plt.scatter(np.argmin(error),np.min(error),color='red') plt.ylabel('Out of Sample RMSE') plt.title('Model Selection For EURUSD Market') plt.xticks([0,1,2,3,4,5,6,7],['OLS','Ridge','RF','GBR','KNR','RNR','LSVR','DNN'])

Рисунок 8: Мы определили подходящую эталонную модель, которую будем пытаться превзойти.
Для поиска оптимальных параметров нейронной сети мы используем перекрестную валидацию временных рядов с помощью библиотеки scikit-learn.
from sklearn.model_selection import RandomizedSearchCV,TimeSeriesSplit
Мы определяем количество разбиений и временной интервал между каждым разбиением, а затем задаем сетку параметров, охватывающую все значения, которые необходимо изучить. Затем мы определяем базовую конфигурацию нейронной сети с фиксированными параметрами, чтобы обеспечить воспроизводимость результатов. Например, мы отключаем параметр shuffle=True (поскольку при работе с временными рядами необходимо сохранять порядок данных) и фиксируем значение random на 0, чтобы инициализация весов оставалась неизменной при повторном запуске. Кроме того, мы отключаем функцию досрочного прекращения и устанавливаем максимальное количество итераций равным 1000.
#Define the time series cross validation tool tscv = TimeSeriesSplit(n_splits=5,gap=HORIZON) #Define the parameter values we want to search over dist = dict( loss=['squared_error','poisson'], activation = ['identity','relu','tanh','logistic'], solver=['adam','lbfgs','sgd'], learning_rate=['constant','invscaling','adaptive'], learning_rate_init=[1,0,10e-1,10e-2,10e-3], hidden_layer_sizes=[(4,10,4),(4,4,4,4),(4,1,8,2),(4,2,6,3),(4,2,1,4),(4,2,8,16,2)], alpha=[1,0,10e-1,10e-2,10e-3] ) #Define basic model parameters we want to keep fixed model = MLPRegressor(shuffle=False,random_state=0,early_stopping=False,max_iter=1000) #Define the randomized search object rscv = RandomizedSearchCV(model,cv=tscv,param_distributions=dist,random_state=0,n_iter=50) #Perform the search rscv.fit(train.loc[:,X],train.loc[:,y[0]]) #Retreive the best parameters we found rscv.best_params_
{'solver': 'lbfgs',
'loss': 'squared_error',
'learning_rate_init': 0.1,
'learning_rate': 'adaptive',
'hidden_layer_sizes': (4, 2, 1, 4),
'alpha': 0.01,
'activation': 'identity'}
После выполнения поиска по сетке модель возвращает наиболее эффективную комбинацию параметров, которую мы сравниваем с предыдущими результатами. Интересно, что наша оптимизированная нейронная сеть — которую можно увидеть в крайнем правом столбце диаграммы производительности — по-прежнему не превосходит по результатам эталонную модель регрессии Риджа.
sns.barplot(error,color='black') plt.scatter(x=np.argmin(error),y=np.min(error),color='red') plt.axhline(np.min(error),color='red',linestyle=':') plt.xticks([0,1,2,3,4,5,6,7,8],['OLS','Ridge','RF','GBR','KNR','RNR','LSVR','DNN','ODNN']) plt.ylabel('Out of Sample RMSE') plt.title('Final Model Selection For EURUSD 2023-2025 Backtest')

Рисунок 9: Нам удалось превзойти эталонную модель, который мы определили ранее
Экспорт в формат ONNX
После завершения оптимизации мы экспортируем окончательную модель в формат Open Neural Network Exchange (ONNX). ONNX предоставляет независимый от конкретной платформы интерфейс, который позволяет обмениваться обученными моделями и развертывать их в различных программных средах без переноса исходных зависимостей, связанных с обучением.
#Fit the baseline model
model = rscv.best_estimator_Чтобы начать экспорт, мы определяем модель и импортируем необходимые библиотеки ONNX.
#Prepare to export to ONNX import onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType
Укажите формат входных данных (1x4, что соответствует четырём основным ценовым уровням) и формат выходных данных (1x1, представляющий прогнозируемое значение).
#Define ONNX model input and output dimensions initial_types = [("FLOAT_INPUT",FloatTensorType([1,4]))] final_types = [("FLOAT_OUTPUT",FloatTensorType([1,1]))]
Затем мы генерируем прототип ONNX — промежуточное представление модели.
#Convert the model to its ONNX prototype onnx_proto = convert_sklearn(model,initial_types=initial_types,final_types=final_types,target_opset=12)
В заключение сохраняем файл на диск в формате ONNX-буфера, который мы затем импортируем в наше приложение MetaTrader 5.
#Save the ONNX model onnx.save(onnx_proto,"EURUSD Improved Baseline LR.onnx")
Создание нашего приложения MQL5
Теперь, когда наша модель ONNX определена и готова, приступим к созданию приложения для MetaTrader 5. Первым делом необходимо определить системные константы — фиксированные параметры, которые определяют нашу стратегию на протяжении всего приложения. К ним относятся периоды скользящих средних, количество наблюдений, необходимое для активации регулятора с обратной связью, а также количество входных и выходных переменных для модели ONNX.//+------------------------------------------------------------------+ //| Feedback Control Benchmark .mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define SYMBOL Symbol() #define MA_PERIOD 42 #define MA_SHIFT 0 #define MA_MODE MODE_EMA #define MA_APPLIED_PRICE PRICE_CLOSE #define SYSTEM_TIME_FRAME PERIOD_D1 #define MIN_VOLUME SymbolInfoDouble(SYMBOL,SYMBOL_VOLUME_MIN) #define OBSERVATIONS 90 #define FEATURES 7 #define MODEL_INPUTS 8 #define TOTAL_MODEL_INPUTS 4 #define TOTAL_MODEL_OUTPUTS 1
После определения этих констант мы загружаем созданную ранее модель ONNX.
//+------------------------------------------------------------------+ //| System resources we need | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD Improved Baseline LR.onnx" as const uchar onnx_buffer[];
Наше приложение также импортирует несколько вспомогательных библиотек, чтобы упростить выполнение типичных торговых операций, таких как открытие, закрытие и изменение позиций.
//+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> CTrade Trade;
Далее мы объявляем глобальные переменные для сохранения общего состояния между функциями — это гарантирует, что одни и те же значения ключей будут доступны везде, где это необходимо.
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ma_handler,atr_handler,scenes; bool forecast; long onnx_model; double ma[],atr[]; double ask,bid,open,high,low,close,padding; matrix snapshots,b,X,y,U,S,VT,current_forecast; vector s; vectorf onnx_inputs,onnx_output;
Во время инициализации мы создаём экземпляр модели ONNX из экспортированного буфера и выполняем проверку целостности, чтобы убедиться, что она не повреждена. В случае успеха мы определяем формы входных и выходных данных модели, которые должны совпадать с формами, заданными в Python. Затем мы загружаем наши технические индикаторы и инициализируем глобальные переменные значениями по умолчанию.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create the ONNX model from its buffer onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT); //--- Check for errors if(onnx_model == INVALID_HANDLE) { //--- User feedback Print("An error occured loading the ONNX model:\n",GetLastError()); //--- Abort return(INIT_FAILED); } //--- Setup the ONNX handler input shape else { //--- Define the I/O shapes ulong input_shape[] = {1,4}; ulong output_shape[] = {1,1}; //--- Attempt to set input shape if(!OnnxSetInputShape(onnx_model,0,input_shape)) { //--- User feedback Print("Failed to specify the correct ONNX model input shape:\n",GetLastError()); //--- Abort return(INIT_FAILED); } //--- Attempt to set output shape if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { //--- User feedback Print("Failed to specify the correct ONNX model output shape:\n",GetLastError()); //--- Abort return(INIT_FAILED); } } //--- Initialize the indicator ma_handler = iMA(SYMBOL,SYSTEM_TIME_FRAME,MA_PERIOD,MA_SHIFT,MA_MODE,MA_APPLIED_PRICE); atr_handler = iATR(SYMBOL,SYSTEM_TIME_FRAME,14); //--- Prepare global variables forecast = false; snapshots = matrix::Zeros(FEATURES,OBSERVATIONS); scenes = -1; return(INIT_SUCCEEDED); }
По завершении работы программы все выделенные ресурсы освобождаются для обеспечения эффективного использования памяти.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release the ONNX model OnnxRelease(onnx_model); //--- Release the indicator IndicatorRelease(ma_handler); IndicatorRelease(atr_handler); }
При поступлении новых данных о ценах система проверяет, сформировалась ли новая свеча. Если сформировалась новая свеча, обновляется как счетчик свечей, так и общее количество «сцен» (эпизодов), зарегистрированных регулятором с обратной связью. Как только контроллер с обратной связью соберет необходимое количество данных, он активируется — с этого момента перед открытием новых позиций учитываются его прогнозы.
Если открытых позиций нет, приложение обновляет свои индикаторы и запрашивает прогноз — либо у контроллера обратной связи (если он активен), либо у модели ONNX. Модель принимает в качестве входных данных четыре основных ценовых уровня и выдает прогнозное значение. Затем система фиксирует текущие значения ключевых показателей, таких как уровень цен, баланс счета, капитал и показатели индикаторов.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Check if a new candle has formed datetime current_time = iTime(Symbol(),SYSTEM_TIME_FRAME,0); static datetime time_stamp; if(current_time != time_stamp) { //--- Update the time time_stamp = current_time; scenes = scenes+1; //--- Check how many scenes have elapsed if(scenes == (OBSERVATIONS-1)) { forecast = true; } //--- If we have no open positions if(PositionsTotal()==0) { //--- Update indicator buffers CopyBuffer(ma_handler,0,1,1,ma); CopyBuffer(atr_handler,0,0,1,atr); padding = atr[0] * 2; //--- Prepare a prediction from our model onnx_inputs = vectorf::Zeros(TOTAL_MODEL_INPUTS); onnx_inputs[0] = (float) iOpen(Symbol(),SYSTEM_TIME_FRAME,0); onnx_inputs[1] = (float) iHigh(Symbol(),SYSTEM_TIME_FRAME,0); onnx_inputs[2] = (float) iLow(Symbol(),SYSTEM_TIME_FRAME,0); onnx_inputs[3] = (float) iClose(Symbol(),SYSTEM_TIME_FRAME,0); //--- Also prepare the outputs onnx_output = vectorf::Zeros(TOTAL_MODEL_OUTPUTS); //--- Fetch current market prices ask = SymbolInfoDouble(SYMBOL,SYMBOL_ASK); bid = SymbolInfoDouble(SYMBOL,SYMBOL_BID); close = iClose(SYMBOL,SYSTEM_TIME_FRAME,1); //--- Do we need to forecast? if(!forecast) { //--- Check trading signal check_signal(); } //--- We need a forecast else if(forecast) { model_forecast(); } } //--- Take a snapshot if(!forecast) take_snapshot(); //--- Otherwise, we have positions open else { //--- Let the model decide if we should close or hold our position if(forecast) model_forecast(); //--- Otherwise record all observations on the performance of the application else if(!forecast) take_snapshot(); } } } //+------------------------------------------------------------------+
Торговые сигналы генерируются только в том случае, если открытых позиций нет. Если модель ONNX прогнозирует рост цен, регистрируется сигнал на покупку — но только в том случае, если цена уже находится выше своей скользящей средней. Напротив, сигнал на продажу фиксируется только в том случае, если цена закрытия находится ниже скользящей средней, и модель прогнозирует снижение курса.
//+------------------------------------------------------------------+ //| Check for our trading signal | //+------------------------------------------------------------------+ void check_signal(void) { if(PositionsTotal() == 0) { //--- Fetch a prediction from our model if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_output)) { if((close > ma[0]) && (onnx_output[0] > iClose(Symbol(),SYSTEM_TIME_FRAME,0))) { Trade.Buy(MIN_VOLUME,SYMBOL,ask,ask-padding,ask+padding); } if((close < ma[0]) && (onnx_output[0] < iClose(Symbol(),SYSTEM_TIME_FRAME,0))) { Trade.Sell(MIN_VOLUME,SYMBOL,bid,ask+padding,ask-padding); } } } }
Метод прогнозирования контроллера с обратной связью начинается с копирования всех ранее записанных значений и добавления к ним текущего значения. Затем он формирует два сдвинутых разбиения: одно представляет текущие входные данные, а другое — целевые значения следующего шага (будущие наблюдения). Целевой переменной в данной модели является будущий остаток на счете.
С помощью сингулярного разложения (SVD) контроллер с обратной связью разлагает матрицу наблюдений на три унитарные матрицы. Поскольку две из них ортогональны, их обратные матрицы можно получить, просто взяв их транспонированные формы — в результате останется обратить только диагональную матрицу S. Такой подход значительно снижает вычислительную нагрузку.
После вычисления оптимальных коэффициентов контроллер с обратной связью умножает их на текущий вектор входных данных, чтобы получить будущий баланс счета. Если прогнозируемый баланс превышает текущий, разрешение на торговлю выдается; в противном случае оно не выдается. В редких случаях, когда оценка коэффициентов заканчивается неудачей — как правило, из-за сингулярной диагональной матрицы (S, содержащей нули) — контроллер с обратной связью прерывает процесс прогнозирования.
//+------------------------------------------------------------------+ //| Obtain a forecast from our model | //+------------------------------------------------------------------+ void model_forecast(void) { Print(scenes); Print(snapshots); //--- Create a copy of the current snapshots matrix temp; temp.Copy(snapshots); snapshots = matrix::Zeros(FEATURES,scenes+1); for(int i=0;i<FEATURES;i++) { snapshots.Row(temp.Row(i),i); } //--- Attach the latest readings to the end take_snapshot(); //--- Obtain a forecast for our trading signal //--- Define the model inputs and outputs //--- Implement the inputs and outputs X = matrix::Zeros(FEATURES+1,scenes); y = matrix::Zeros(1,scenes); //--- The first row is the intercept. X.Row(vector::Ones(scenes),0); //--- Filling in the remaining rows for(int i =0; i<scenes;i++) { //--- Filling in the inputs X[1,i] = snapshots[0,i]; //Open X[2,i] = snapshots[1,i]; //High X[3,i] = snapshots[2,i]; //Low X[4,i] = snapshots[3,i]; //Close X[5,i] = snapshots[4,i]; //Moving average X[6,i] = snapshots[5,i]; //Account equity X[7,i] = snapshots[6,i]; //Account balance //--- Filling in the target y[0,i] = snapshots[6,i+1];//Future account balance } Print("Finished implementing the inputs and target: "); Print("Snapshots:\n",snapshots); Print("X:\n",X); Print("y:\n",y); //--- Singular value decomposition X.SingularValueDecompositionDC(SVDZ_S,s,U,VT); //--- Transform s to S, that is the vector to a diagonal matrix S = matrix::Zeros(s.Size(),s.Size()); S.Diag(s,0); //--- Done Print("U"); Print(U); Print("S"); Print(s); Print(S); Print("VT"); Print(VT); //--- Learn the system's coefficients //--- Check if S is invertible if(S.Rank() != 0) { //--- Invert S matrix S_Inv = S.Inv(); Print("S Inverse: ",S_Inv); //--- Obtain psuedo inverse solution b = VT.Transpose().MatMul(S_Inv); b = b.MatMul(U.Transpose()); b = y.MatMul(b); //--- Prepare the current inputs matrix inputs = matrix::Ones(MODEL_INPUTS,1); for(int i=1;i<MODEL_INPUTS;i++) { inputs[i,0] = snapshots[i-1,scenes]; } //--- Done Print("Coefficients:\n",b); Print("Inputs:\n",inputs); current_forecast = b.MatMul(inputs); Print("Forecast:\n",current_forecast[0,0]); //--- The next trade may be expected to be profitable if(current_forecast[0,0] > AccountInfoDouble(ACCOUNT_BALANCE)) { //--- Feedback Print("Next trade expected to be profitable. Checking for trading singals."); //--- Check for our trading signal check_signal(); } //--- Next trade may be expected to be unprofitable else { Print("Next trade expected to be unprofitable. Waiting for better market conditions"); } } //--- S is not invertible! else { //--- Error Print("[Critical Error] Singular values are not invertible."); } }
Система непрерывно фиксирует моментальные снимки своего состояния на протяжении всего периода торговых сессий. Именно с помощью таких записей мы создаем приложения, способные учиться на собственном опыте работы на рынке.
//+------------------------------------------------------------------+ //| Take a snapshot of the market | //+------------------------------------------------------------------+ void take_snapshot(void) { //--- Record system state snapshots[0,scenes]=iOpen(SYMBOL,SYSTEM_TIME_FRAME,1); //Open snapshots[1,scenes]=iHigh(SYMBOL,SYSTEM_TIME_FRAME,1); //High snapshots[2,scenes]=iLow(SYMBOL,SYSTEM_TIME_FRAME,1); //Low snapshots[3,scenes]=iClose(SYMBOL,SYSTEM_TIME_FRAME,1);//Close snapshots[4,scenes]=ma[0]; //Moving average snapshots[5,scenes]=AccountInfoDouble(ACCOUNT_EQUITY); //Equity snapshots[6,scenes]=AccountInfoDouble(ACCOUNT_BALANCE);//Balance Print("Scene: ",scenes); Print(snapshots); } //+------------------------------------------------------------------+
При завершении работы советник очищает все ранее заданные константы и переменные.
//+------------------------------------------------------------------+ //| Undefine system constants | //+------------------------------------------------------------------+ #undef SYMBOL #undef SYSTEM_TIME_FRAME #undef MA_APPLIED_PRICE #undef MA_MODE #undef MA_SHIFT #undef MIN_VOLUME #undef MODEL_INPUTS #undef FEATURES #undef OBSERVATIONS //+------------------------------------------------------------------+
Затем мы выбираем программу и даты тестирования.

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

Рисунок 11: Выберите условия бэк-тестирования, которые имитируют реальные рыночные условия
Показатели производительности нашего усовершенствованного приложения говорят сами за себя. Общая чистая прибыль выросла более чем в два раза, а точность торговли повысилась до 72%, приблизившись к отметке в 80%. Основные показатели эффективности — в том числе ожидаемая доходность, коэффициент Шарпа и коэффициент восстановления — улучшились по сравнению с базовой моделью.

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

Рисунок 13: Кривая капитала, построенная нашим торговым приложением, демонстрирует устойчивый рост, что именно мы и хотим наблюдать
Наконец, одним из особенно впечатляющих результатов стала точность прогнозирования контроллера с обратной связью. По итогам бэктеста прогнозировался итоговый баланс в размере 270,28 долларов, при этом фактический результат отличался от этой оценки не более чем на 10 центов. Как мы уже отмечали в предыдущих статьях, это незначительное расхождение, вероятно, обусловлено неотъемлемым различием между математическим пространством прогнозов модели и пространством реальных результатов — то есть идеальное совпадение теоретически невозможно. Тем не менее, близость этого результата к ожидаемому подтверждает, что наша структура управления с обратной связью дает достоверные прогнозы.

Рисунок 14: Похоже, что контроллер с обратной связью также имеет обоснованные ожидания относительно того, как данная стратегия повлияет на баланс счета
Заключение
Прочитав эту статью, читатель познакомится с новым фреймворком для создания самоадаптирующихся торговых приложений, которые регулируют своё поведение в зависимости от результатов своих действий. Алгоритмы линейного управления с обратной связью позволяют эффективно выявлять нежелательные явления даже в сложных нелинейных системах. Их полезность для алгоритмической торговли невозможно исчерпать. Эти алгоритмы, по-видимому, хорошо подходят для усовершенствования наших классических моделей рынка. Кроме того, в этой статье читатель узнал, как создать ансамбль интеллектуальных систем, которые взаимодействуют друг с другом для построения торговых приложений, призванных осваивать эффективные модели поведения на рынке. Похоже, что прогнозирование временных рядов само по себе является лишь одним из компонентов более комплексного решения.
| Название файла | Описание файла |
|---|---|
| Feedback_Control_Benchmark_3.mq5 | Торговое приложение MetaTrader 5, которое мы разработали, основано на сочетании обучения с учителем и идентификации систем. |
| Идентификация линейных систем на основе обучения с учителем.ipynb | Мы создали интерактивную веб-среду разработки Jupyter для анализа рыночных данных по паре EURUSD, которые были получены из торгового терминала с помощью библиотеки интеграции Python. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/20023
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Использование регрессии Ренко-баров с корректировкой ошибок
Статистический арбитраж на основе коинтегрированных акций (Часть 6): Система оценки
Разработка инструментария для анализа Price Action (Часть 32): Модуль распознавания свечных паттернов на Python (II) – Распознавание с помощью Ta-Lib
Упрощение работы с базами данных в MQL5 (Часть 2): Создание сущностей с помощью метапрограммирования
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования