Создание шаблона советника средствами MQL5

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

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

sinput string          Model = "our_model.net";     
sinput int             BarsToPattern = 40;      
sinput bool            Common = true;
input ENUM_TIMEFRAMES  TimeFrame = PERIOD_M5;
input double           TradeLevel=0.9;
input double           Lot = 0.01;
input int              MaxTP500;
input double           ProfitMultiply = 0.8;
input int              MinTarget=100;
input int              StopLoss=300;

Также я выбрал упрощенный подход к использованию модели. Я не стал создавать и обучать модель внутри советника, а зашел с другой стороны. Во всех создаваемых нами скриптах для тестирования архитектурных решений нейронных слоев мы сохраняли обученные модели. Так почему бы нам просто не загрузить одну из обученных моделей? Вы можете создать и обучить свою модель, а потом просто указать имя файла с обученной моделью во внешнем параметре Model и использовать ее. Остается лишь указать место хранения файла Common, количество баров описания одного паттерна BarsToPattern и используемый таймфрейм TimeFrame. Также для принятия решения мы укажем минимальную прогнозируемую вероятность получения прибыли TradeLevel.

Чтобы повысить вероятность закрытия сделки по тейк-профиту, добавим параметр ProfitMultiply, в котором укажем коэффициент доверия прогнозируемой силы движения. Иными словами, при указании тейк-профита открываемой позиции мы будем корректировать размер ожидаемого движения на данный коэффициент.

Использование параметра Common для указания места хранения обученной модели является довольно важным показателем, как бы это ни показалось странным. Дело в том, что доступ к файлам в MetaTrader 5 ограничен песочницей. Каждый терминал, установленный на компьютере, имеет свою песочницу. Таким образом, каждый из двух терминалов, установленных на одном компьютере, работает в своей песочнице и не мешает второму. Для случаев, когда необходим обмен данными между терминалами на одном компьютере, используется отдельная общая папка. Именно на ее использование и указывает значение true в параметре Common.

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

После объявления внешних параметров нашего советника мы в глобальном пространстве подключаем нашу библиотеку для работы с моделями нейронных сетей neuronnet.mqh и стандартную библиотеку для совершения торговых операций Trade\Trade.mqh.

#include "..\..\Include\NeuroNetworksBook\realization\neuronnet.mqh"
#include <Trade\Trade.mqh>
 
CNet *net;
CTrade *trade;
datetime lastbar = 0;
int h_RSI;
int h_MACD;

Следом мы объявляем глобальные переменные:

  • net — указатель на объект модели;
  • trade — указатель на объект совершения торговых операций;
  • lastbar — время последнего анализируемого бара, используется для проверки события открытия новой свечи;
  • h_RSI — хендл индикатора RSI;
  • h_MACD — хендл индикатора MACD.

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

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

int OnInit()
  {
//---
   if(!(net = new CNet()))
     {
      PrintFormat("Error creating Net: %d"GetLastError());
      return INIT_FAILED;
     }
   if(!net.Load(ModelCommon))
     {
      PrintFormat("Error loading mode %s: %d"ModelGetLastError());
      return INIT_FAILED;
     }
   net.UseOpenCL(UseOpenCL);

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

   h_RSI = iRSI(_SymbolTimeFrame12PRICE_TYPICAL);
   if(h_RSI == INVALID_HANDLE)
     {
      PrintFormat("Error loading indicator %s""RSI");
      return INIT_FAILED;
     }
   h_MACD = iMACD(_SymbolTimeFrame124812PRICE_TYPICAL);
   if(h_MACD == INVALID_HANDLE)
     {
      PrintFormat("Error loading indicator %s""MACD");
      return INIT_FAILED;
     }

Следующим шагом мы создаем экземпляр объекта для совершения торговых операций. Проверяем результат создания объекта и устанавливаем тип исполнения ордеров.

void OnDeinit(const int reason)
  {
   if(!!net)
      delete net;
   if(!!trade)
      delete trade;
   IndicatorRelease(h_RSI);
   IndicatorRelease(h_MACD);
  }

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

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

void OnDeinit(const int reason)
  {
   if(CheckPointer(net) == POINTER_DYNAMIC)
      delete net;
   if(CheckPointer(trade) == POINTER_DYNAMIC)
      delete trade;
   IndicatorRelease(h_RSI);
   IndicatorRelease(h_MACD);
  }

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

void OnTick()
  {
   if(lastbar >= iTime(_SymbolTimeFrame0))
      return;
   lastbar = iTime(_SymbolTimeFrame0);

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

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

   double macd_main[], macd_signal[], rsi[];
   if(h_RSI == INVALID_HANDLE || CopyBuffer(h_RSI01BarsToPatternrsi) <= 0)
     {
      PrintFormat("Error loading indicator %s data""RSI");
      return;
     }
   if(h_MACD == INVALID_HANDLE || CopyBuffer(h_MACDMAIN_LINE1BarsToPatternmacd_main) <= 0 ||
      CopyBuffer(h_MACDSIGNAL_LINE1BarsToPatternmacd_signal) <= 0)
     {
      PrintFormat("Error loading indicator %s data""MACD");
      return;
     }

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

   CBufferType *input_data = new CBufferType();
   if(!input_data)
     {
      PrintFormat("Error creating Input data array: %d"GetLastError());
      return;
     }
   if(!input_data.BufferInit(BarsToPattern40))
      return;

   for(int i = 0i < BarsToPatterni++)
     {
      if(!input_data.Update(i0, (TYPE)rsi[i]))
        {
         PrintFormat("Error adding Input data to array: %d"GetLastError());
         delete input_data;
         return;
        }

      if(!input_data.Update(i1, (TYPE)macd_main[i]))
        {
         PrintFormat("Error adding Input data to array: %d"GetLastError());
         delete input_data;
         return;
        }

      if(!input_data.Update(i2, (TYPE)macd_signal[i]))
        {
         PrintFormat("Error adding Input data to array: %d"GetLastError());
         delete input_data;
         return;
        }

      if(!input_data.Update(i3, (TYPE)(macd_main[i] - macd_signal[i])))
        {
         PrintFormat("Error adding Input data to array: %d"GetLastError());
         delete input_data;
         return;
        }
     }
   if(!input_data.Reshape(1,input_data.Total())
     return;

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

   if(!net)
     {
      delete input_data;
      return;
     }
   if(!net.FeedForward(input_data))
     {
      PrintFormat("Error of Feed Forward: %d"GetLastError());
      delete input_data;
      return;
     }

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

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

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

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

   if(!net.GetResults(input_data))
     {
      PrintFormat("Error of Get Result: %d"GetLastError());
      delete input_data;
      return;
     }
   if(input_data.At(0) > 0.0)
     {
      bool opened = false;
      for(int i = 0i < PositionsTotal(); i++)
        {
         if(PositionGetSymbol(i) != _Symbol)
            continue;
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            opened = true;
        }

      if(opened)
        {
         delete input_data;
         return;
        }

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

      if(input_data.At(0) < TradeLevel ||
         input_data.At(1) < (MinTarget * SymbolInfoDouble(_SymbolSYMBOL_POINT)))
        {
         delete input_data;
         return;
        }

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

      double tp = SymbolInfoDouble(_SymbolSYMBOL_BID) + MathMin(input_data.At(1) * 
                    ProfitMultiplyMaxTP * SymbolInfoDouble(_SymbolSYMBOL_POINT));
      double sl = SymbolInfoDouble(_SymbolSYMBOL_BID) - 
                  StopLoss * SymbolInfoDouble(_SymbolSYMBOL_POINT);
      trade.Buy(Lot_Symbol0sltp);
     }

Аналогично организован алгоритм принятия решения на продажу.

   if(input_data.At(0) < 0)
     {
      bool opened = false;
      for(int i = 0i < PositionsTotal(); i++)
        {
         if(PositionGetSymbol(i) != _Symbol)
            continue;
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
            opened = true;
        }

      if(opened)
        {
         delete input_data;
         return;
        }

      if(input_data.At(0) > -TradeLevel ||
         input_data.At(1) > -(MinTarget * SymbolInfoDouble(_SymbolSYMBOL_POINT)))
        {
         delete input_data;
         return;
        }

      double tp = SymbolInfoDouble(_SymbolSYMBOL_BID) + MathMax(input_data.At(1) * 
                   ProfitMultiply, -MaxTP * SymbolInfoDouble(_SymbolSYMBOL_POINT));
      double sl = SymbolInfoDouble(_SymbolSYMBOL_BID) + 
                  StopLoss * SymbolInfoDouble(_SymbolSYMBOL_POINT);
      trade.Sell(Lot_Symbol0sltp);
     }
   delete input_data;
  }

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

Советник сделан весьма упрощенным, но и он позволит проверить работу нашей модели в тестере стратегий MetaTrader 5.