Мультивалютный советник на MQL5. Пример простой, точной и быстрой схемы.

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

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

И есть ещё один интересный вариант, автор которого Константин Груздев (с работами этого автора можно ознакомиться на сайте mql5.com). В этом варианте используется событийная модель, когда с помощью функции OnChartEvent() экспертом принимаются события, которые воспроизводят индикаторы-агенты, размещённые на графиках символов участвующих в тесте/торговле. Индикатор-агент может воспроизводить события новых баров и тиков того символа, на котором он размещён. Такой индикатор (EventsSpy.mq5) можно скачать в конце статьи. Он понадобится для работы эксперта из этой статьи. Контакты автора указаны в коде.

Итак. Думаю, что можно начать творческий процесс...

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

//+------------------------------------------------------------------+
//| ВНЕШНИЕ ПАРАМЕТРЫ ЭКСПЕРТА                                       |
//+------------------------------------------------------------------+
sinput long   MagicNumber     = 777;      // Magic Number
sinput int    Deviation       = 10;       // Deviation (p)
//---
sinput string dlm_00          = "";       // * * * * * * * * * * * * * * * * * * * * * * * * * *
//---
sinput string Symbol_01       = "EURUSD"; // 01 _ Symbol
input  int    PeriodInd_01    = 5;        // 01 _ Period Indicator
input  double TakeProfit_01   = 100;      // 01 _ Take Profit (p)
input  double StopLoss_01     = 50;       // 01 _ Stop Loss (p)
input  double TrailingSL_01   = 10;       // 01 _ Step Trailing Stop (p)
input  bool   Reverse_01      = true;     // 01 _ On/Off Reverse
input  double Lot_01          = 0.1;      // 01 _ Lot
input  double Increase_01     = 0.1;      // 01 _ Increase Volume
input  double StepIncrease_01 = 10;       // 01 _ Step Increase (p)
//---
sinput string dlm_01          = "";       // * * * * * * * * * * * * * * * * * * * * * * * * * *
//---
sinput string Symbol_02       = "AUDUSD"; // 02 _ Symbol
input  int    PeriodInd_02    = 5;        // 02 _ Period Indicator
input  double TakeProfit_02   = 100;      // 02 _ Take Profit (p)
input  double StopLoss_02     = 50;       // 02 _ Stop Loss (p)
input  double TrailingSL_02   = 10;       // 02 _ Step Trailing Stop (p)
input  bool   Reverse_02      = true;     // 02 _ On/Off Reverse
input  double Lot_02          = 0.1;      // 02 _ Lot
input  double Increase_02     = 0.1;      // 02 _ Increase Volume
input  double StepIncrease_02 = 10;       // 02 _ Step Increase (p)
//---

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

//---
#define CNTS 2 // Кол-во торгуемых символов
#define AVAL 3 // Кол-во значений, которые нужно получить у индикаторов и баров
#define NAME_EXPERT MQL5InfoString(MQL5_PROGRAM_NAME) // Имя эксперта
//---

В файле ARRAYS.mqh теперь нужно создать массивы для внешних параметров:

//+------------------------------------------------------------------+
//| МАССИВЫ ВНЕШНИХ ПАРАМЕТРОВ                                       |
//+------------------------------------------------------------------+
string aSymbol[CNTS];       // Symbol
int    aPeriodInd[CNTS];    // Period Indicator
double aTakeProfit[CNTS];   // Take Profit (p)
double aStopLoss[CNTS];     // Stop Loss (p)
double aTrailingSL[CNTS];   // Step Trailing Stop (p)
bool   aReverse[CNTS];      // On/Off Reverse
double aLot[CNTS];          // Lot
double aIncrease[CNTS];     // Increase Volume
double aStepIncrease[CNTS]; // Step Increase (p)
//---

Функции для инициализации массивов расположим в подключаемом файле INIT_ARRAYS.mqh. Для инициализации массива aSymbol создадим функцию CheckGetSymbol(). В неё будем передавать имя символа из внешних параметров, и если такой символ есть в списке символов на сервере то, если его нет в окне "Обзор Рынка", он будет туда установлен. Если же такого символа нет, то будет возвращаться пустая строка.

Ниже представлен код функции CheckGetSymbol():

//+------------------------------------------------------------------+
//| ДОБАВЛЯЕТ УКАЗАННЫЕ В ПАРАМЕТРАХ СИМВОЛЫ В ОКНО ОБЗОР РЫНКА      |
//+------------------------------------------------------------------+
string CheckGetSymbol(string symbol)
  {
   int symbol_total=0; // Количество символов
   string nm_symbol=""; // Имя символа
//---
// Если передали пустую строку, то
// ...вернуть пустую строку
   if(symbol=="") { return(""); }
//---
   symbol_total=SymbolsTotal(false); // Всего символов на сервере
//---
// Пройтись по всему списку символов
   for(int s=symbol_total-1; s>=0; s--)
     {
      nm_symbol=SymbolName(s,false); // Имя символа на сервере
      //---
      // Если есть такой символ, то...
      if(nm_symbol==symbol)
        {
         // ...установим его в окно Обзор Рынка и...
         SymbolSelect(nm_symbol,true);
         return(symbol); // ...вернём его имя
        }
     }
//---
// Если такого символа нет, то вернём пустую строку
   return("");
  }
//---

Инициализация массива aSymbol будет производиться в функции GetASymbols():

//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ СИМВОЛОВ                                        |
//+------------------------------------------------------------------+
void GetASymbols()
  {
   aSymbol[0]=CheckGetSymbol(Symbol_01);
   aSymbol[1]=CheckGetSymbol(Symbol_02);
  }
//---

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

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

//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ ПЕРИОДА ИНДИКАТОРА                              |
//+------------------------------------------------------------------+
void GetAPeriodInd()
  {
   aPeriodInd[0]=PeriodInd_01;
   aPeriodInd[1]=PeriodInd_02;
  }
//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ TAKEPROFIT                                      |
//+------------------------------------------------------------------+
void GetATakeProfit()
  {
   aTakeProfit[0]=TakeProfit_01;
   aTakeProfit[1]=TakeProfit_02;
  }
//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ STOPLOSS                                        |
//+------------------------------------------------------------------+
void GetAStopLoss()
  {
   aStopLoss[0]=StopLoss_01;
   aStopLoss[1]=StopLoss_02;
  }
//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ TRAILING STOP                                   |
//+------------------------------------------------------------------+
void GetATrailingSL()
  {
   aTrailingSL[0]=TrailingSL_01;
   aTrailingSL[1]=TrailingSL_02;
  }
//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ REVERSE                                         |
//+------------------------------------------------------------------+
void GetAReverse()
  {
   aReverse[0]=Reverse_01;
   aReverse[1]=Reverse_02;
  }
//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ LOT                                             |
//+------------------------------------------------------------------+
void GetALot()
  {
   aLot[0]=Lot_01;
   aLot[1]=Lot_02;
  }
//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ INCREASE                                        |
//+------------------------------------------------------------------+
void GetAIncrease()
  {
   aIncrease[0]=Increase_01;
   aIncrease[1]=Increase_02;
  }
//+------------------------------------------------------------------+
//| ЗАПОЛНЯЕТ МАССИВ STEP INCREASE                                   |
//+------------------------------------------------------------------+
void GetAStepIncrease()
  {
   aStepIncrease[0]=StepIncrease_01;
   aStepIncrease[1]=StepIncrease_02;
  }
//---

И теперь нужно сделать функцию InitArrInpParams(), с помощью которой можно проинициализивароть сразу все массивы внешних параметров:

//+------------------------------------------------------------------+
//| ИНИЦИАЛИЗАЦИЯ МАССИВОВ ВНЕШНИХ ПАРАМЕТРОВ                        |
//+------------------------------------------------------------------+
void InitArrInpParams()
  {
   GetASymbols();
   GetAPeriodInd();
   GetATakeProfit();
   GetAStopLoss();
   GetATrailingSL();
   GetAReverse();
   GetAIncrease();
   GetAStepIncrease();
  }
//---

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

//---
// Пройдёмся по всем символам
   for(int s=0; s<CNTS; s++)
     {
      // Если торговля по символу разрешена
      if(aSymbol[s]!="")
        {
         // Остальной код...
        }
     }
//---

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

Для хэндлов индикаторов понадобится два массива:

//+------------------------------------------------------------------+
//| МАССИВЫ ХЭНДЛОВ ИНДИКАТОРОВ                                      |
//+------------------------------------------------------------------+
int
hdlSpyInd[CNTS], // Массив хэндлов для индикаторов-агентов
hdlSgnInd[CNTS]; // Массив хэндлов сигнальных индикаторов
//---

Изначально эти два массива будут проинициализированны невалидными значениями:

//+------------------------------------------------------------------+
//| ИНИЦИАЛИЗАЦИЯ МАССИВОВ ХЭНДЛОВ ИНДИКАТОРОВ                       |
//+------------------------------------------------------------------+
void InitArrHandles()
  {
   ArrayInitialize(hdlSpyInd,INVALID_HANDLE);
   ArrayInitialize(hdlSgnInd,INVALID_HANDLE);
  }
//---

Доступ к массивам ценовых данных и значений индикаторов будет теперь осуществляться через структуры:

//+------------------------------------------------------------------+
//| МАССИВЫ ЦЕН И ЗНАЧЕНИЙ ИНДИКАТОРОВ                               |
//+------------------------------------------------------------------+
struct Buffer { double dBuf[]; };
//---
Buffer prc_c[CNTS]; // Close (цена закрытия бара)
Buffer prc_o[CNTS]; // Open (цена открытия бара)
Buffer prc_h[CNTS]; // High (цена максимума бара)
Buffer prc_l[CNTS]; // Low (цена минимума бара)
//---
Buffer bfr_ind[CNTS]; // Массив для значений индикаторов
//---

Теперь, если нужно получить значение индикатора на последнем сформировавшемся баре первого в списке символа, нужно сделать примерно такую запись:

//---
double var=bfr_ind[0].dBuf[1];
//---

Ещё нужно создать массивы вместо тех переменных, которые до этого использовались в функции NewBar():

//+------------------------------------------------------------------+
//| МАССИВЫ ДАТЫ И ВРЕМЕНИ                                           |
//+------------------------------------------------------------------+
struct Buffer2 { datetime dtBuf[]; };
//---
Buffer2 lastbar_time[CNTS]; // Массив для получения времени открытия текущего бара
//----
datetime aNewBar[CNTS]; // Массив для проверки нового бара
//---

С массивами разобрались. Теперь в соответствии с изменениями выше нужно исправить многие функции. Начнём с функции GetHandlesIndicators():

//+------------------------------------------------------------------+
//| ПОЛУЧАЕТ ХЭНДЛЫ ИНДИКАТОРОВ                                      |
//+------------------------------------------------------------------+
void GetHandlesIndicators()
  {
// Пройдёмся по всем символам
   for(int s=0; s<CNTS; s++)
     {
      // Если торговля по этому символу разрешена
      if(aSymbol[s]!="")
        {
         // Если хэндл ещё не получен...
         if(hdlSgnInd[s]==INVALID_HANDLE)
           {
            hdlSgnInd[s]=iMA(aSymbol[s],_Period,aPeriodInd[s],0,MODE_SMA,PRICE_CLOSE);
            //---
            // Если неудалось получить хендл индикатора
            if(hdlSgnInd[s]==INVALID_HANDLE)
              { Print("Не удалось получить хэндл индикатора для символа "+aSymbol[s]+"!"); }
           }
        }
     }
  }
//---

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

Подобной выше функции нужно создать ещё одну для получения хэндлов индикаторов-агентов, которые будут транслировать тики с других символов. Перед этим нужно добавить ещё одно перечисление ENUM_CHART_EVENT_SYMBOL в файл ENUMS.mqh (не привожу здесь с целью сокращения объёма статьи):

//+------------------------------------------------------------------+
//| ПОЛУЧАЕТ ХЭНДЛЫ АГЕНТОВ ПО УКАЗАННЫМ СИМВОЛАМ                    |
//+------------------------------------------------------------------+
void GetHandlesSpy()
  {
// Пройдёмся по всем символам
   for(int s=0; s<CNTS; s++)
     {
      // Если торговля по символу разрешена
      if(aSymbol[s]!="")
        {
         // Если хэндл ещё не получен...
         if(hdlSpyInd[s]==INVALID_HANDLE)
           {
            hdlSpyInd[s]=iCustom(aSymbol[s],_Period,"EventsSpy.ex5",ChartID(),0,CHARTEVENT_TICK);
            //---
            if(hdlSpyInd[s]==INVALID_HANDLE)
              { Print("Не удалось установить агента на "+aSymbol[s]+""); }
           }
        }
     }
  }
//---

Обратите внимание на последний параметр в функции iCustom() в выделенной строке в коде выше. В данном случае для получения событий тиков нужно было передать идентификатор CHARTEVENT_TICK. Но при необходимости можно указать, чтобы агент сообщал о событии новых баров. Например, если написать такую строку, как показано ниже, то эксперт будет получать события новых баров по минутному (M1) и часовому (H1) таймфрейму:

//---
hdlSpyInd[s]=iCustom(aSymbol[s],_Period,"EventsSpy.ex5",ChartID(),0,CHARTEVENT_NEWBAR_M1|CHARTEVENT_NEWBAR_H1);
//---

А для получения всех событий (тиков и новых баров со всех таймфреймов) нужно указать идентификатор CHARTEVENT_ALL.

Инициализация всех массивов производится в функции OnInit():

//+------------------------------------------------------------------+
//| ИНИЦИАЛИЗАЦИЯ                                                    |
//+------------------------------------------------------------------+
void OnInit()
  {
   InitArrInpParams(); // Инициализация массивов внешних параметров
   InitArrHandles(); // Инициализация массивов хэндлов индикаторов
   GetHandlesSpy(); // Получаем хэндлы агентов
   GetHandlesIndicators(); // Получим хэндлы индикаторов
   InitArrNewBar(); // Инициализируем новый бар
  }
//---

Как уже упоминалось в начале статьи, события от индикаторов-агентов принимаются в функции OnChartEvent(). Ниже представлен код, который будет использоваться в этой функции:

//+------------------------------------------------------------------+
//| ПОЛЬЗОВАТЕЛЬСКИЕ СОБЫТИЯ                                         |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam
                 )
  {
   // Если это пользовательское событие
   if(id>=CHARTEVENT_CUSTOM)
     {
      // Выйти, если запрещено торговать
      if(CheckReasonAllowedTrade()>0) { return; }
      //---
      // Если было событие "тик"
      if(lparam==CHARTEVENT_TICK)
        {
         CheckSignalAndTrade(); // Проверяет сигналы и торгует по ним
         //---
         return;
        }
     }
  }
//---

В функцию CheckSignalAndTrade() (выделенная строка в коде выше) поместим цикл, в котором поочерёдно будут проверятся все символы на событие нового бара и торговых сигналов также, как это работало раньше в функции OnTick().

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

Например, ниже показан код обновлённой функции NewBar():

//+------------------------------------------------------------------+
//| ПРОВЕРКА НОВОГО БАРА                                             |
//+------------------------------------------------------------------+
bool NewBar(int s)
  {
// Получим время открытия текущего бара
// Если возникла ошибка при получении, сообщим об этом
   if(CopyTime(aSymbol[s],_Period,0,1,lastbar_time[s].dtBuf)==-1)
     { Print(__FUNCTION__,": Ошибка при копировании времени открытия бара: "+IS(GetLastError())+""); }
//---
// Если это первый вызов функции
   if(aNewBar[s]==0)
     {
      // Установим время
      aNewBar[s]=lastbar_time[s].dtBuf[0];
      Print(__FUNCTION__,
            ": Инициализация ["+aSymbol[s]+"][TF: "+TFtoS(_Period)+"]["+TSdms(lastbar_time[s].dtBuf[0])+"]");
      return(false); // Вернём false и выйдем 
     }
//---
// Если время отличается
   if(aNewBar[s]!=lastbar_time[s].dtBuf[0])
     {
      aNewBar[s]=lastbar_time[s].dtBuf[0]; // Установим время и выйдем 
      return(true); // Запомним время и вернем true
     }
//---
   return(false); // Дошли до этого места - значит бар не новый, вернем false
  }
//---

Код функции CheckSignalAndTrade() представлен ниже:

//+------------------------------------------------------------------+
//| ПРОВЕРЯЕТ СИГНАЛЫ И ТОРГУЕТ ПО СОБЫТИЮ НОВЫЙ БАР                 |
//+------------------------------------------------------------------+
void CheckSignalAndTrade()
  {
   // Пройдёмся по всем указанным символам
   for(int s=0; s<CNTS; s++)
     {
      if(aSymbol[s]!="") // Если торговля по этому символу разрешена
        {
         // Если бар не новый, перейти к следующему символу
         if(!NewBar(s)) { continue; }
         else // Если есть новый бар
           {
            // Получим данные индикаторов
            // Если данных нет, перейти к следующему символу
            if(!GetDataIndicators(s)) { continue; }
            //---
            GetDataBars(s); // Получим данные баров
            TradingBlock(s); // Проверим условия и торгуем
            TrailingStopLoss(s); // Трейлинг
           }
        }
     }
  }
//---

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

Сначала произведём оптимизацию для одного символа, а потом для второго. Первый символ установим EURUSD.

Ниже показаны настройки для тестера:

Настройки тестера

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

Настройки эксперта

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

Результат теста на символе EURUSD по максимальному значению фактора восстановления

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

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

Результат для NZDUSD получился таким, как показано ниже:

Результат теста на символе NZDUSD по максимальному значению фактора восстановления

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

Ниже показан результат по двум символам сразу:

Результат теста по двум символам

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

Успехов!




Скачать архив с файлами эксперта expMultiSymb.zip
Скачать индикатор-агент EventsSpy.mq5
Скачать сет с настройками эксперта


Комментариев нет :

Отправить комментарий