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

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

MetaTrader 5Примеры |
84 1
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

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

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

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

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

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


Визуализация торговой стратегии

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

Рисунок 1

Рисунок 1: Выявление торговой возможности в соответствии со стратегией канала скользящих средних

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

Рисунок 2

Рисунок 2: Хотя в этой стратегии есть некоторые недостатки, в целом она выглядит обоснованной


Определение базового уровня эффективности

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

//+------------------------------------------------------------------+
//|                                                           EI.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define SYSTEM_TF PERIOD_D1
#define MA_SHIFT 0
#define MA_TYPE MODE_EMA
#define ATR_PERIOD 14
#define PADDING 2

Далее мы подключим вспомогательные библиотеки, которые понадобятся нам для выполнения этого упражнения.

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>

CTrade Trade;
TradeInfo *TradeHelper;

Глобальные переменные необходимы практически во всех приложениях; они нужны нам для отслеживания показаний технических индикаторов и времени.

//+------------------------------------------------------------------+
//| Define global variables                                          |
//+------------------------------------------------------------------+
int    ma_h_handler,ma_l_handler,atr_handler;
double ma_h[],ma_l[],atr[];
MqlDateTime tc,ts;

Мы зададим входное значение 20; если вы измените это значение, не забудьте внести соответствующие изменения в скрипт.

//+------------------------------------------------------------------+
//| Input varaibles                                                  |
//+------------------------------------------------------------------+
input group "Technical Indicators"
input int MA_PERIOD = 20;//Moving average period

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Setup our technical indicators
   ma_h_handler = iMA(Symbol(),SYSTEM_TF,MA_PERIOD,MA_SHIFT,MA_TYPE,PRICE_HIGH);
   ma_l_handler = iMA(Symbol(),SYSTEM_TF,MA_PERIOD,MA_SHIFT,MA_TYPE,PRICE_LOW);
   atr_handler = iATR(Symbol(),SYSTEM_TF,ATR_PERIOD);
   TradeHelper = new TradeInfo(Symbol(),SYSTEM_TF);

//--- Mark the time
   TimeLocal(tc);
   TimeLocal(ts);
//---
   return(INIT_SUCCEEDED);
  }

Если наше приложение больше не используется, мы освободим индикаторы, которые больше не применяем.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   delete TradeHelper;
   IndicatorRelease(ma_h_handler);
   IndicatorRelease(ma_l_handler);
   IndicatorRelease(atr_handler);
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   TimeLocal(ts);

   if(ts.hour != tc.hour)
     {
      if(PositionsTotal()==0)
        {
         //--- Update the time
         TimeLocal(tc);

         //--- Update the indicator buffer
         CopyBuffer(ma_h_handler,0,0,1,ma_h);
         CopyBuffer(ma_l_handler,0,0,1,ma_l);
         CopyBuffer(atr_handler,0,0,1,atr);

         //--- Check if the current price is above or below the channel
         double c = iClose(Symbol(),SYSTEM_TF,0);

         if(c > ma_h[0])
            Trade.Buy(TradeHelper.MinVolume(),Symbol(),TradeHelper.GetAsk(),TradeHelper.GetBid()-(atr[0]*PADDING),TradeHelper.GetBid()+(atr[0]*PADDING));

         else
            if(c < ma_l[0])
               Trade.Sell(TradeHelper.MinVolume(),Symbol(),TradeHelper.GetBid(),TradeHelper.GetAsk()+(atr[0]*PADDING),TradeHelper.GetAsk()-(atr[0]*PADDING));
        }
     }
  }
//+------------------------------------------------------------------+

Наконец, мы отменяем определения всех системных констант, которые были определены ранее.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef SYSTEM_TF
#undef MA_SHIFT
#undef MA_TYPE
#undef ATR_PERIOD
#undef PADDING

Запустите тестовое приложение и установите соответствующие даты для тестирования. Для данного задания мы отобрали данные по курсу EURUSD за более чем трёхлетний период — с января 2022 года по январь 2025 года.

Рисунок 3: Выбор дат, на которых наше приложение будет использоваться для определения базового уровня эффективности

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

Рисунок 4: Выбор условий, в которых мы хотим протестировать наше приложение

Кроме того, мы дали пользователю возможность самостоятельно выбирать период и тем самым активно участвовать в анализе. В противном случае значение 20 было выбрано произвольно.

Рисунок 5: Наше текущее упражнение позволяет выбирать произвольные периоды для оценки

Как объяснялось во введении, значение 20 не было рентабельным. Однако, как мы увидим, наша статистическая стратегия может извлечь из этого уроки и помочь нам правильно отфильтровывать шумы, не прибегая при этом к постоянному перебору различных периодов для наших технических индикаторов.

Рисунок 6: Кривая капитала, построенная нашим торговым приложением, нестабильна и не вызывает у нас особого доверия

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

Рисунок 7: Подробные статистические данные, полученные с помощью нашего тестового приложения, показывают, что у нас ещё есть возможности для улучшения


Получить исторические рыночные данные

Теперь давайте создадим скрипт для извлечения исторических рыночных данных и их записи в файл CSV. Мы будем использовать эти данные для разработки статистической стратегии в отношении рынка EURUSD.

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//--- Define our moving average indicator
#define MA_PERIOD 5                 //--- Moving Average Period
#define MA_TYPE   MODE_SMA          //--- Type of moving average we have
#define HORIZON   5                 //--- Forecast horizon

//--- Our handlers for our indicators
int ma_handle,ma_o_handle,ma_h_handle,ma_l_handle;

//--- Data structures to store the readings from our indicators
double ma_reading[],ma_o_reading[],ma_h_reading[],ma_l_reading[];

//--- File name
string file_name = Symbol() + " Detailed Market Data As Series Moving Average.csv";

//--- Amount of data requested
input int size = 3000;

//+------------------------------------------------------------------+
//| Our script execution                                             |
//+------------------------------------------------------------------+
void OnStart()
  {
   int fetch = size + (HORIZON * 2);
//---Setup our technical indicators
   ma_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_CLOSE);
   ma_o_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_OPEN);
   ma_h_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_HIGH);
   ma_l_handle = iMA(_Symbol,PERIOD_CURRENT,MA_PERIOD,0,MA_TYPE,PRICE_LOW);

//---Set the values as series
   CopyBuffer(ma_handle,0,0,fetch,ma_reading);
   ArraySetAsSeries(ma_reading,true);
   CopyBuffer(ma_o_handle,0,0,fetch,ma_o_reading);
   ArraySetAsSeries(ma_o_reading,true);
   CopyBuffer(ma_h_handle,0,0,fetch,ma_h_reading);
   ArraySetAsSeries(ma_h_reading,true);
   CopyBuffer(ma_l_handle,0,0,fetch,ma_l_reading);
   ArraySetAsSeries(ma_l_reading,true);

//---Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i=size;i>=1;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time",
                   //--- OHLC
                   "True Open",
                   "True High",
                   "True Low",
                   "True Close",
                   //--- MA OHLC
                   "True MA O",
                   "True MA H",
                   "True MA L",
                   "True MA C"
                  );
        }

      else
        {
         FileWrite(file_handle,
                   iTime(_Symbol,PERIOD_CURRENT,i),
                   //--- OHLC
                   iClose(_Symbol,PERIOD_CURRENT,i),
                   iOpen(_Symbol,PERIOD_CURRENT,i),
                   iHigh(_Symbol,PERIOD_CURRENT,i),
                   iLow(_Symbol,PERIOD_CURRENT,i),
                   //--- MA OHLC
                   ma_o_reading[i],
                   ma_h_reading[i],
                   ma_l_reading[i],
                   ma_reading[i]
                  );
        }
     }
//--- Close the file
   FileClose(file_handle);
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef HORIZON
#undef MA_PERIOD
#undef MA_TYPE
//+------------------------------------------------------------------+


Анализ исторических рыночных данных на Python

Загрузите наши стандартные библиотеки Python.
#Load the libraries we need
import pandas as pd
import numpy as np

Прочитайте рыночные данные, которые мы ранее сохранили в формате CSV.

#Read in the data
data = pd.read_csv("../EURUSD Detailed Market Data As Series Moving Average.csv")
data

Определите горизонт прогнозирования.

#Define the forecast horizon
HORIZON = 20

Удалите все исторические данные, которые пересекаются с периодом бэктеста.

#Drop the dates that overlap with the back test
data = data.iloc[:-(365*3),:]
_ = data.iloc[-(365*3):,:]

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

#Label the data
data['Target H'] = data['Close'].shift(-HORIZON) - data['MA H'].shift(-HORIZON)
data['Target L'] = data['Close'].shift(-HORIZON) - data['MA L'].shift(-HORIZON)

#Drop missing rows
data = data.iloc[:-HORIZON,:]

Загрузите наши библиотеки ONNX. ONNX (сокращение от Open Neural Network Exchange) — это библиотека с открытым исходным кодом, которая помогает создавать и развертывать модели машинного обучения без переноса зависимостей из сред обучения.

from sklearn.linear_model import Ridge
import onnx
from skl2onnx import convert_sklearn 
from skl2onnx.common.data_types import FloatTensorType

Обучите модель на всех обучающих данных.

model = Ridge(alpha=1e-3)
model.fit(data.iloc[:,1:-2],data.loc[:,['Target H','Target L']])

Определите формы входных и выходных данных нашей модели ONNX.

initial_types = [('float_input',FloatTensorType([1,8]))]
final_types = [('float_output',FloatTensorType([1,2]))]

Сохраните модель ONNX в виде прототипа ONNX.

onnx_proto = convert_sklearn(model=model,initial_types=initial_types,final_types=final_types,target_opset=12)

Сохраните прототип ONNX в виде файла на диске. 

onnx.save(onnx_proto,'EURUSD MA R.onnx')


Превосходство над базовым показателем

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

//+------------------------------------------------------------------+
//|                                                           EI.mq5 |
//|                                               Gamuchirai Ndawana |
//|                    https://www.mql5.com/en/users/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define ONNX_FEATURES 8
#define ONNX_TARGETS 2

Теперь загрузим нашу модель ONNX.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD MA R.onnx" as const uchar onnx_buffer[];

Теперь мы создадим нашу модель ONNX на основе ранее определённого буфера и настроим структуру входных и выходных данных модели.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DATA_TYPE_FLOAT);

   if(onnx_model == INVALID_HANDLE)
     {
      Print("Failed to create ONNX model: ",GetLastError());
      return(INIT_FAILED);
     }

   ulong input_shape[]  = {1,ONNX_FEATURES};
   ulong output_shape[] = {1,ONNX_TARGETS};

   onnx_inputs = vectorf::Zeros(ONNX_FEATURES);
   onnx_output = vectorf::Zeros(ONNX_TARGETS);

   if(!OnnxSetInputShape(onnx_model,0,input_shape))
     {
      Print("Failed to define ONNX input shape: ",GetLastError());
      return(INIT_FAILED);
     }

   if(!OnnxSetOutputShape(onnx_model,0,output_shape))
     {
      Print("Failed to define ONNX output shape: ",GetLastError());
      return(INIT_FAILED);
     }

//--- Mark the time
   TimeLocal(tc);
   TimeLocal(ts);
//---
   return(INIT_SUCCEEDED);
  }

Если наше приложение больше не используется, мы также должны освободить модель ONNX, которая нам больше не нужна.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   OnnxRelease(onnx_model);
  }

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

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(ts.hour != tc.hour)
     {
      if(PositionsTotal()==0)
        {
         onnx_inputs[0] = (float) iOpen(Symbol(),SYSTEM_TF,0);
         onnx_inputs[1] = (float) iHigh(Symbol(),SYSTEM_TF,0);
         onnx_inputs[2] = (float) iLow(Symbol(),SYSTEM_TF,0);
         onnx_inputs[3] = (float) iClose(Symbol(),SYSTEM_TF,0);
         onnx_inputs[4] = (float) ma_o[0];
         onnx_inputs[5] = (float) ma_h[0];
         onnx_inputs[6] = (float) ma_l[0];
         onnx_inputs[7] = (float) ma_c[0];

         if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_output))
           {
            //--- Check if the current price is above or below the channel
            Print("Forecast: ",onnx_output);
            double c = iClose(Symbol(),SYSTEM_TF,0);
	    
            if((c > ma_h[0]) && (onnx_output[0]>0) && (onnx_output[1]>0))
               Trade.Buy(TradeHelper.MinVolume(),Symbol(),TradeHelper.GetAsk(),TradeHelper.GetBid()-(atr[0]*PADDING),TradeHelper.GetBid()+(atr[0]*PADDING));

            else
               if((c < ma_l[0]) && (onnx_output[0]<0) && (onnx_output[1]<0))
                  Trade.Sell(TradeHelper.MinVolume(),Symbol(),TradeHelper.GetBid(),TradeHelper.GetAsk()+(atr[0]*PADDING),TradeHelper.GetAsk()-(atr[0]*PADDING));
           }

         else
           {
            Print("Failed to obtain a prediction from our ONNX model: ",GetLastError());
           }
        }
     }
  }
//+------------------------------------------------------------------+

Затем отменяем новые определения, которые мы создали для адаптации нашей модели ONNX.

//+------------------------------------------------------------------+
//| Undefine system constants                                        |
//+------------------------------------------------------------------+
#undef ONNX_FEATURES
#undef ONNX_TARGETS
//+------------------------------------------------------------------+

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

Рисунок 8: Затем мы выбираем те же даты тестирования, что и в первом тесте, для обеспечения согласованности

Как видно, приложение по-прежнему убыточно. Однако давайте более подробно рассмотрим статистику.

Рисунок 9: Наше приложение пока не вышло на прибыльность

Общая чистая прибыль увеличилась с -96 до -62 долларов, но у нас ещё есть большой потенциал для улучшения.

Рисунок 10: Подробные результаты анализа, который мы только что провели, по-прежнему не вселяют в нас уверенности в правильности нашей стратегии на данный момент


Дополнительные усовершенствования

Опираясь на наше предыдущее обсуждение свечных моделей, предложим для нашей статистической стратегии еще один источник сигналов. Эти данные о максимальных и минимальных ценах являются исходными данными для наших индикаторов скользящей средней. Ранее мы подробно рассмотрели свечу «бычье поглощение» и выяснили, как добиться с её помощью хороших результатов независимо от наших статистических моделей.

if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_output))
  {
   //--- Check if the current price is above or below the channel
   Print("Forecast: ",onnx_output);
   double c = iClose(Symbol(),SYSTEM_TF,0);

   //--- Check for any bullish engulfing candle sticks
   if((onnx_output[0]>0) && (onnx_output[1]>0) && (iHigh(Symbol(),PERIOD_CURRENT,1) > iHigh(Symbol(),PERIOD_CURRENT,2)) && (iLow(Symbol(),PERIOD_CURRENT,1) < iLow(Symbol(),PERIOD_CURRENT,2)))
      Trade.Buy(TradeHelper.MinVolume(),Symbol(),TradeHelper.GetAsk(),TradeHelper.GetBid()-(atr[0]*PADDING),TradeHelper.GetBid()+(atr[0]*PADDING));
            
   //--- Check for any bearish engulfing candle sticks
   else
      if((onnx_output[0]<0) && (onnx_output[1]<0) && (iHigh(Symbol(),PERIOD_CURRENT,1) > iHigh(Symbol(),PERIOD_CURRENT,2)) && (iLow(Symbol(),PERIOD_CURRENT,1) < iLow(Symbol(),PERIOD_CURRENT,2)))
         Trade.Sell(TradeHelper.MinVolume(),Symbol(),TradeHelper.GetBid(),TradeHelper.GetAsk()+(atr[0]*PADDING),TradeHelper.GetAsk()-(atr[0]*PADDING));
  }    

Давайте теперь повторим тест за тот же период, используя эту обновленную версию нашего приложения.

Рисунок 11: Запуск новой версии нашей торговой стратегии на том же трехлетнем тестовом периоде

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

Рисунок 12: Кривая капитала, построенная нашей новой обновленной версией торгового приложения, вселяет в нас уверенность

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

Рисунок 13: Анализ подробных результатов, полученных с помощью нашей усовершенствованной торговой стратегии, показывает, что распределение сделок требует дальнейшей доработки


Последний шанс

Давайте теперь предпримем последнюю попытку усовершенствовать модель. Для начала мы внесем важные изменения в нашу модель. Мы будем прогнозировать наши технические индикаторы на несколько шагов вперед, при этом каждый индикатор прогнозируется на 1 и на 20 шагов вперёд, что даст нам в общей сложности четыре целевых значения.

#Label the data
data['Target H'] = data['MA H'].shift(-1)
data['Target L'] = data['MA L'].shift(-1)

data['Target H 2'] = data['MA H'].shift(-HORIZON)
data['Target L 2'] = data['MA L'].shift(-HORIZON)

#Drop missing rows
data = data.iloc[:-HORIZON,:]

Затем мы обучим модель Ridge Regression с параметром alpha = 0.001. Это определяет, насколько быстро незначимые коэффициенты должны сходиться к нулю, позволяя нашей модели сосредоточиться на важных параметрах. Затем мы подбираем нашу модель.

model = Ridge(alpha=1e-3)
model.fit(data.iloc[:,1:-4],data.loc[:,['Target H','Target L','Target H 2','Target L 2']])

Теперь определены формы входных и выходных данных модели.

initial_types = [('float_input',FloatTensorType([1,8]))]
final_types = [('float_output',FloatTensorType([1,4]))]

В заключение сохраним нашу модель ONNX в файл.

onnx.save(onnx_proto,'EURUSD MA MFH R.onnx')


Внедрение наших улучшений в MQL5

Теперь мы готовы реализовать наши улучшения в MQL5. Для начала мы изменим размер выходных данных нашей модели ONNX.

//+------------------------------------------------------------------+
//| System constants                                                 |
//+------------------------------------------------------------------+
#define ONNX_TARGETS 4

Затем мы загружаем нашу недавно обновленную многоэтапную модель прогнозирования.

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#resource "\\Files\\EURUSD MA MFH R.onnx" as const uchar onnx_buffer[];

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

 if(OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,onnx_inputs,onnx_output))
           {
            //--- Check if the current price is above or below the channel
            Print("Forecast: ",onnx_output);
            double c = iClose(Symbol(),SYSTEM_TF,0);

            if((ma_o[0]<ma_c[0]) && (onnx_output[0]<onnx_output[2]) && (onnx_output[1]<onnx_output[3]))
               Trade.Buy(TradeHelper.MinVolume(),Symbol(),TradeHelper.GetAsk(),TradeHelper.GetBid()-(atr[0]*PADDING),TradeHelper.GetBid()+(atr[0]*PADDING));

            else
               if((ma_o[0]>ma_c[0]) && (onnx_output[0]>onnx_output[2]) && (onnx_output[1]>onnx_output[3]))
                  Trade.Sell(TradeHelper.MinVolume(),Symbol(),TradeHelper.GetBid(),TradeHelper.GetAsk()+(atr[0]*PADDING),TradeHelper.GetAsk()-(atr[0]*PADDING));
           }

Давайте теперь протестируем усовершенствованную версию нашего торгового приложения за тот же тестовый период.

Рисунок 14: Тестирование окончательной конфигурации нашего торгового приложения за тот же трехлетний период, который мы использовали в данном упражнении

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

Рисунок 15: Кривая капитала, построенная на основе нашей окончательной версии торгового приложения, достигает новых максимумов, которых нам ранее не удавалось достичь

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

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



Заключение

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

Название файла Описание файла
Загрузить данные MA.mq5Скрипт MQL5, который мы использовали для получения исторических рыночных данных из терминала MetaTrader 5.
EI Baseline.mq5 Данное приложение послужило эталоном для оценки прибыльности классической стратегии «канал скользящей средней».
EI.mq5Наша первая попытка превзойти контрольный показатель — обратите внимание, что эта версия приложения не была прибыльной.
EI 2.mq5 Наша первая удачная попытка превзойти эталонный индекс; обратите внимание, что данная версия приложения была ориентирована на длинные позиции.
EI 3.mq5Лучшая версия стратегии «канал скользящих средних», разработанная нами, которая не только превзошла по результатам классическую стратегию, но и обеспечивала относительно объективные сделки.
MA Channel AI 3.ipynb Jupyter Notebook, который мы создали вместе для анализа исторических рыночных данных, полученных с помощью нашего скрипта MQL5.

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

Прикрепленные файлы |
Fetch_Data_MA.mq5 (3.69 KB)
EI_Baseline.mq5 (4.24 KB)
EI.mq5 (6.6 KB)
EI_2.mq5 (6.98 KB)
EI_3.mq5 (6.74 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Konstantin Mescheryakov
Konstantin Mescheryakov | 24 апр. 2026 в 14:46
How to address missing ONNX resources?
Since a .onnx file is an externally compiled model that cannot be recovered without the original training data, the best solution is to replace it with a built-in MQL5 mathematical regression.
The result is the integration of a "Statistical Signal Confirmation Block based on Dynamic Linear Regression" into the system.

Mathematical Forecasting (Replacing the Neural Network)
The robot no longer just reacts to what is happening now; it attempts to look into the future.

Linear Regression: On every tick, the Expert Advisor (EA) analyzes the last 50 days (daily bars) and calculates the mathematical trajectory of the price.

Level Projection: It calculates probable High and Low values for 1 step ahead (tomorrow) and 20 steps ahead (in one month).

"Ensemble" Trade Filtering
This is the most critical intellectual enhancement. The decision to open a trade is made by two independent "voices":

The Voice of Indicators (Intuition): Checks the current candle direction and Moving Average (MA) positions.

The Voice of Mathematics (Forecast): Compares short-term forecasts with long-term trends.

Result: A trade opens only when both voices agree. For example, if an indicator says "Buy," but the mathematical forecast shows the trend is fading (Tomorrow's projected High is lower than the projected High in 20 days), the robot will ignore the entry, saving you from buying at the market top.

Dynamic Market Adaptation
Unlike the static ONNX file used in the original article (which was trained only once), my current version features:

On-the-fly Re-learning: It constantly updates regression coefficients. If market volatility changes, the calculations adapt automatically.

Autonomy: It no longer requires external files or libraries. The entire logic is contained within the code, making it highly reliable for testing and optimization.

Combining Aggressive Grid Strategy with Cautious Entry
When applying this method to a grid strategy, you get the best of both worlds:

The core Grid Scalping algorithm remains aggressive—the EA is capable of building a grid of orders and exiting based on total profit.

However, the initial order in a series is now opened with much higher precision, as it is confirmed by the mathematical expectation of a rise or fall.
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Повторное использование нарушенных ордер-блоков в качестве блоков смягчения (SMC) Повторное использование нарушенных ордер-блоков в качестве блоков смягчения (SMC)
В этой статье мы рассмотрим, как ранее ставшие недействительными ордер-блоки можно повторно использовать в качестве блоков смягчения последствий в рамках «Концепции умных денег» (Smart Money Concepts, SMC). Эти зоны показывают, где институциональные трейдеры повторно входят на рынок после неудачного ордер-блока, предоставляя зоны высокой вероятности продолжения торговли в рамках доминирующего тренда.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Разработка инструментария для анализа Price Action (Часть 34): Построение прогнозных моделей на основе необработанных рыночных данных с помощью усовершенствованного пайплайна загрузки данных Разработка инструментария для анализа Price Action (Часть 34): Построение прогнозных моделей на основе необработанных рыночных данных с помощью усовершенствованного пайплайна загрузки данных
Случалось ли вам пропустить внезапный рыночный всплеск или оказаться застигнутым врасплох, когда такой всплеск происходил? Лучший способ заранее распознавать события в реальном времени – учиться на исторических паттернах. Если вы хотите обучить модель машинного обучения, в этой статье сначала показано, как создать скрипт для MetaTrader 5, который собирает исторические данные и отправляет их в Python для хранения, закладывая основу системы обнаружения всплесков. Читайте дальше, чтобы увидеть каждый шаг на практике.