English Deutsch 日本語
preview
Создание самооптимизирующихся советников на MQL5 (Часть 16): Идентификация линейных систем на основе обучения с учителем

Создание самооптимизирующихся советников на MQL5 (Часть 16): Идентификация линейных систем на основе обучения с учителем

MetaTrader 5Примеры |
206 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

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

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

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

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

Рисунок 2: Мы заменим фиксированную торговую стратегию статистической моделью, построенной на основе рыночных данных

Наша цель состоит в том, чтобы определить, обеспечивает ли использование более сложной торговой стратегии, основанной на данных, более насыщенную структуру для обучения контроллера обратной связи и, в конечном итоге, позволяет ли это добиться лучших результатов. Чтобы изучить этот вопрос, мы вернулись к нашей предыдущей работе по управлению с обратной связью и идентификации линейных систем, в рамках которой мы разработали простую стратегию на основе скользящего среднего и подобрали контроллер с обратной связью для установления базового уровня. Затем мы заменили компонент скользящего среднего на статистическую модель, обученную на данных по EUR/USD и оценили эффективность в идентичных условиях тестирования. Результаты исследования показали:

  1. Чистая прибыль выросла с 56 долларов в базовой системе до 170 долларов — это почти 200-процентное увеличение.
  2. Валовый убыток сократился с 333 до 143 долларов, что означает снижение риска убытков на 57 %.
  3. Точность повысилась с 52,9 % до 72 %, что соответствует росту точности на 37 %.
  4. Количество сделок сократилось с 51 до 33, что повышает эффективность на 35 % и говорит нам о том, что система отфильтровала ненужные сделки.
  5. Коэффициент прибыльности вырос с 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

Использование регрессии Ренко-баров с корректировкой ошибок Использование регрессии Ренко-баров с корректировкой ошибок
В статье показан регрессионный подход к прогнозированию Ренко-баров с помощью CatBoost: модель оценивает логарифмическую доходность следующего бара и неопределённость прогноза. Разобран каскад residual-моделей с OOF-валидацией через TimeSeriesSplit, shrinkage и общим early stopping, а также условная коррекция смещения. На EURUSD D1 получено снижение OOF-MAE и около 65% точности по направлению. Приведён рабочий скрипт для MetaTrader 5, формирующий сигнал, размер позиции, SL и TP в единицах кирпича.
Статистический арбитраж на основе коинтегрированных акций (Часть 6): Система оценки Статистический арбитраж на основе коинтегрированных акций (Часть 6): Система оценки
В данной статье мы предлагаем систему оценки стратегий возврата к среднему значению, основанную на статистическом арбитраже коинтегрированных акций. В статье предлагаются критерии, которые варьируются от ликвидности и транзакционных издержек до количества рангов коинтеграции и времени возврата к среднему значению, при этом учитываются стратегические критерии — частота данных (временной интервал) и период обратного обзора для тестов на коинтеграцию, которые оцениваются до того, как будет сформирован итоговый оценочный балл (rank_score). Предоставляются файлы, необходимые для воспроизведения бэктеста, а также приводятся комментарии к его результатам.
Разработка инструментария для анализа Price Action (Часть 32): Модуль распознавания свечных паттернов на Python (II) – Распознавание с помощью Ta-Lib Разработка инструментария для анализа Price Action (Часть 32): Модуль распознавания свечных паттернов на Python (II) – Распознавание с помощью Ta-Lib
В этой статье мы перешли от ручной реализации распознавания свечных паттернов на Python к использованию TA-Lib – библиотеки, распознающей более шестидесяти различных паттернов. Эти формации дают ценную информацию о возможных разворотах рынка и продолжении тренда. Читайте дальше, чтобы узнать больше.
Упрощение работы с базами данных в MQL5 (Часть 2): Создание сущностей с помощью метапрограммирования Упрощение работы с базами данных в MQL5 (Часть 2): Создание сущностей с помощью метапрограммирования
Мы изучили расширенное использование #define для метапрограммирования в MQL5, создания сущностей, представляющих таблицы и метаданные столбцов (тип, первичный ключ, автоинкремент, возможность обнуления и т.д.). Мы централизовали эти определения в TickORM.mqh, автоматизировав генерацию классов метаданных и проложив путь для эффективной работы с данными в ORM без необходимости писать SQL вручную.