Рецепты MQL5 - Мультивалютный эксперт: пример простой, точной и быстрой схемы

Anatoli Kazharski | 11 июня, 2013

Введение

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

Мультивалютную схему на MQL5 можно реализовать несколькими способами:


Процесс разработки эксперта

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

//--- Внешние параметры эксперта
sinput long   MagicNumber           = 777;      // Магический номер
sinput int    Deviation             = 10;       // Проскальзывание
//---
sinput string delimeter_00=""; // --------------------------------
sinput string Symbol_01             = "EURUSD"; // Символ 1
input  int    IndicatorPeriod_01    = 5;        // |     Период индикатора
input  double TakeProfit_01         = 100;      // |     Тейк Профит
input  double StopLoss_01           = 50;       // |     Стоп Лосс
input  double TrailingStop_01       = 10;       // |     Трейлинг Стоп
input  bool   Reverse_01            = true;     // |     Разворот позиции
input  double Lot_01                = 0.1;      // |     Лот
input  double VolumeIncrease_01     = 0.1;      // |     Приращение объема позиции
input  double VolumeIncreaseStep_01 = 10;       // |     Шаг для приращения объема
//---
sinput string delimeter_01=""; // --------------------------------
sinput string Symbol_02             = "NZDUSD"; // Символ 2
input  int    IndicatorPeriod_02    = 5;        // |     Период индикатора
input  double TakeProfit_02         = 100;      // |     Тейк Профит
input  double StopLoss_02           = 50;       // |     Стоп Лосс
input  double TrailingStop_02       = 10;       // |     Трейлинг Стоп
input  bool   Reverse_02            = true;     // |     Разворот позиции
input  double Lot_02                = 0.1;      // |     Лот
input  double VolumeIncrease_02     = 0.1;      // |     Приращение объема позиции
input  double VolumeIncreaseStep_02 = 10;       // |     Шаг для приращения объема

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

//--- Количество торгуемых символов
#define NUMBER_OF_SYMBOLS 2
//--- Имя эксперта
#define EXPERT_NAME MQL5InfoString(MQL5_PROGRAM_NAME)

Создадим массивы, которые понадобятся для хранения внешних параметров:

//--- Массивы для хранения внешних параметров
string Symbols[NUMBER_OF_SYMBOLS];            // Символ
int    IndicatorPeriod[NUMBER_OF_SYMBOLS];    // Период индикатора
double TakeProfit[NUMBER_OF_SYMBOLS];         // Тейк Профит
double StopLoss[NUMBER_OF_SYMBOLS];           // Стоп Лосс
double TrailingStop[NUMBER_OF_SYMBOLS];       // Трейлинг Стоп
bool   Reverse[NUMBER_OF_SYMBOLS];            // Разворот позиции
double Lot[NUMBER_OF_SYMBOLS];                // Лот
double VolumeIncrease[NUMBER_OF_SYMBOLS];     // Приращение объема позиции
double VolumeIncreaseStep[NUMBER_OF_SYMBOLS]; // Шаг для приращения объема

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

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

//+------------------------------------------------------------------+
//| Добавляет указанный символ в окно "Обзор рынка"                  |
//+------------------------------------------------------------------+
string GetSymbolByName(string symbol)
  {
   string symbol_name="";   // Имя символа на сервере
//--- Если передали пустую строку, вернем пустую строку
   if(symbol=="")
      return("");
//--- Пройтись по списку всех символов на сервере
   for(int s=0; s<SymbolsTotal(false); s++)
     {
      //--- Получим имя символа
      symbol_name=SymbolName(s,false);
      //--- Если искомый символ есть на сервере
      if(symbol==symbol_name)
        {
         //--- Выберем его в окне "Обзор рынка"
         SymbolSelect(symbol,true);
         //--- Вернем имя символа
         return(symbol);
        }
     }
//--- Если искомого символа нет, вернем пустую строку
   Print("Символ "+symbol+" не найден на сервере!");
   return("");
  }

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

//+------------------------------------------------------------------+
//| Заполняет массив символов                                        |
//+------------------------------------------------------------------+
void GetSymbols()
  {
   Symbols[0]=GetSymbolByName(Symbol_01);
   Symbols[1]=GetSymbolByName(Symbol_02);
  }

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

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

//+------------------------------------------------------------------+
//| Заполняет массив периода индикатора                              |
//+------------------------------------------------------------------+
void GetIndicatorPeriod()
  {
   IndicatorPeriod[0]=IndicatorPeriod_01;
   IndicatorPeriod[1]=IndicatorPeriod_02;
  }
//+------------------------------------------------------------------+
//| Заполняет массив Take Profit                                     |
//+------------------------------------------------------------------+
void GetTakeProfit()
  {
   TakeProfit[0]=TakeProfit_01;
   TakeProfit[1]=TakeProfit_02;
  }
//+------------------------------------------------------------------+
//| Заполняет массив Stop Loss                                       |
//+------------------------------------------------------------------+
void GetStopLoss()
  {
   StopLoss[0]=StopLoss_01;
   StopLoss[1]=StopLoss_02;
  }
//+------------------------------------------------------------------+
//| Заполняет массив Trailing Stop                                   |
//+------------------------------------------------------------------+
void GetTrailingStop()
  {
   TrailingStop[0]=TrailingStop_01;
   TrailingStop[1]=TrailingStop_02;
  }
//+------------------------------------------------------------------+
//| Заполняет массив Reverse                                         |
//+------------------------------------------------------------------+
void GetReverse()
  {
   Reverse[0]=Reverse_01;
   Reverse[1]=Reverse_02;
  }
//+------------------------------------------------------------------+
//| Заполняет массив Lot                                             |
//+------------------------------------------------------------------+
void GetLot()
  {
   Lot[0]=Lot_01;
   Lot[1]=Lot_02;
  }
//+------------------------------------------------------------------+
//| Заполняет массив VolumeIncrease                                  |
//+------------------------------------------------------------------+
void GetVolumeIncrease()
  {
   VolumeIncrease[0]=VolumeIncrease_01;
   VolumeIncrease[1]=VolumeIncrease_02;
  }
//+------------------------------------------------------------------+
//| Заполняет массив VolumeIncreaseStep                              |
//+------------------------------------------------------------------+
void GetVolumeIncreaseStep()
  {
   VolumeIncreaseStep[0]=VolumeIncreaseStep_01;
   VolumeIncreaseStep[1]=VolumeIncreaseStep_02;
  }

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

//+------------------------------------------------------------------+
//| Инициализация массивов внешних параметров                        |
//+------------------------------------------------------------------+
void InitializeInputParameters()
  {
   GetSymbols();
   GetIndicatorPeriod();
   GetTakeProfit();
   GetStopLoss();
   GetTrailingStop();
   GetReverse();
   GetLot();
   GetVolumeIncrease();
   GetVolumeIncreaseStep();
  }

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

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

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

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

//--- Массив хэндлов для индикаторов-агентов
int spy_indicator_handles[NUMBER_OF_SYMBOLS];
//--- Массив хэндлов сигнальных индикаторов
int signal_indicator_handles[NUMBER_OF_SYMBOLS];

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

//+------------------------------------------------------------------+
//| Инициализация массивов хэндлов индикаторов                       |
//+------------------------------------------------------------------+
void InitializeArrayHandles()
  {
   ArrayInitialize(spy_indicator_handles,INVALID_HANDLE);
   ArrayInitialize(signal_indicator_handles,INVALID_HANDLE);
  }

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

//--- Массивы данных для проверки торговых условий
struct PriceData
  {
   double            value[];
  };
PriceData open[NUMBER_OF_SYMBOLS];      // Цена открытия бара
PriceData high[NUMBER_OF_SYMBOLS];      // Цена максимума бара
PriceData low[NUMBER_OF_SYMBOLS];       // Цена минимума бара
PriceData close[NUMBER_OF_SYMBOLS];     // Цена закрытия бара
PriceData indicator[NUMBER_OF_SYMBOLS]; // Массив значений индикаторов

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

double indicator_value=indicator[0].value[1];

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

//--- Массивы для получения времени открытия текущего бара
struct Datetime
  {
   datetime          time[];
  };
Datetime lastbar_time[NUMBER_OF_SYMBOLS];
//--- Массив для проверки нового бара на каждом символе
datetime new_bar[NUMBER_OF_SYMBOLS];

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

//+------------------------------------------------------------------+
//| Получает хэндлы индикаторов                                      |
//+------------------------------------------------------------------+
void GetIndicatorHandles()
  {
//--- Пройдемся по всем символам
   for(int s=0; s<NUMBER_OF_SYMBOLS; s++)
     {
      //--- Если торговля по этому символу разрешена
      if(Symbols[s]!="")
        {
         //--- Если хэндл еще не получен
         if(signal_indicator_handles[s]==INVALID_HANDLE)
           {
            //--- Получим хэндл индикатора
            signal_indicator_handles[s]=iMA(Symbols[s],_Period,IndicatorPeriod[s],0,MODE_SMA,PRICE_CLOSE);
            //--- Если не удалось получить хэндл индикатора
            if(signal_indicator_handles[s]==INVALID_HANDLE)
               Print("Не удалось получить хэндл индикатора для символа "+Symbols[s]+"!");
           }
        }
     }
  }

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

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

//+------------------------------------------------------------------+
//| События новых баров и тиков со всех символов и таймфреймов       |
//+------------------------------------------------------------------+
enum ENUM_CHART_EVENT_SYMBOL
  {
   CHARTEVENT_NO         = 0,          // События отключены - 0
   CHARTEVENT_INIT       = 0,          // Событие "инициализация" - 0
   //---
   CHARTEVENT_NEWBAR_M1  = 0x00000001, // Событие "новый бар" на минутном графике (1)
   CHARTEVENT_NEWBAR_M2  = 0x00000002, // Событие "новый бар" на 2-минутном графике (2)
   CHARTEVENT_NEWBAR_M3  = 0x00000004, // Событие "новый бар" на 3-минутном графике (4)
   CHARTEVENT_NEWBAR_M4  = 0x00000008, // Событие "новый бар" на 4-минутном графике (8)
   //---
   CHARTEVENT_NEWBAR_M5  = 0x00000010, // Событие "новый бар" на 5-минутном графике (16)
   CHARTEVENT_NEWBAR_M6  = 0x00000020, // Событие "новый бар" на 6-минутном графике (32)
   CHARTEVENT_NEWBAR_M10 = 0x00000040, // Событие "новый бар" на 10-минутном графике (64)
   CHARTEVENT_NEWBAR_M12 = 0x00000080, // Событие "новый бар" на 12-минутном графике (128)
   //---
   CHARTEVENT_NEWBAR_M15 = 0x00000100, // Событие "новый бар" на 15-минутном графике (256)
   CHARTEVENT_NEWBAR_M20 = 0x00000200, // Событие "новый бар" на 20-минутном графике (512)
   CHARTEVENT_NEWBAR_M30 = 0x00000400, // Событие "новый бар" на 30-минутном графике (1024)
   CHARTEVENT_NEWBAR_H1  = 0x00000800, // Событие "новый бар" на часовом графике (2048)
   //---
   CHARTEVENT_NEWBAR_H2  = 0x00001000, // Событие "новый бар" на 2-часовом графике (4096)
   CHARTEVENT_NEWBAR_H3  = 0x00002000, // Событие "новый бар" на 3-часовом графике (8192)
   CHARTEVENT_NEWBAR_H4  = 0x00004000, // Событие "новый бар" на 4-часовом графике (16384)
   CHARTEVENT_NEWBAR_H6  = 0x00008000, // Событие "новый бар" на 6-часовом графике (32768)
   //---
   CHARTEVENT_NEWBAR_H8  = 0x00010000, // Событие "новый бар" на 8-часовом графике (65536)
   CHARTEVENT_NEWBAR_H12 = 0x00020000, // Событие "новый бар" на 12-часовом графике (131072)
   CHARTEVENT_NEWBAR_D1  = 0x00040000, // Событие "новый бар" на дневном графике (262144)
   CHARTEVENT_NEWBAR_W1  = 0x00080000, // Событие "новый бар" на недельном графике (524288)
   //---
   CHARTEVENT_NEWBAR_MN1 = 0x00100000, // Событие "новый бар" на месячном графике (1048576)
   CHARTEVENT_TICK       = 0x00200000, // Событие "новый тик" (2097152)
   //---
   CHARTEVENT_ALL        = 0xFFFFFFFF  // Все события включены (-1)
  };

Это перечисление необходимо для работы с пользовательским индикатором EventsSpy.mq5 (файл находится в приложении к статье) в функции GetSpyHandles(), код которой представлен ниже:

//+------------------------------------------------------------------+
//| Получает хэндлы агентов по указанным символам                    |
//+------------------------------------------------------------------+
void GetSpyHandles()
  {
//--- Пройдемся по всем символам
   for(int s=0; s<NUMBER_OF_SYMBOLS; s++)
     {
      //--- Если торговля по символу разрешена
      if(Symbols[s]!="")
        {
         //--- Если хэндл еще не получен
         if(spy_indicator_handles[s]==INVALID_HANDLE)
           {
            //--- Получим хэндл индикатора
            spy_indicator_handles[s]=iCustom(Symbols[s],_Period,"EventsSpy.ex5",ChartID(),0,CHARTEVENT_TICK);
            //--- Если не удалось получить хэндл индикатора
            if(spy_indicator_handles[s]==INVALID_HANDLE)
               Print("Не удалось установить агента на "+Symbols[s]+"");
           }
        }
     }
  }

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

handle_event_indicator[s]=iCustom(Symbols[s],_Period,"EventsSpy.ex5",ChartID(),0,CHARTEVENT_NEWBAR_M1|CHARTEVENT_NEWBAR_H1);

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

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
void OnInit()
  {
//--- Инициализация массивов внешних параметров
   InitializeInputParameters();
//--- Инициализация массивов хэндлов индикаторов
   InitializeArrayHandles();
//--- Получаем хэндлы агентов
   GetSpyHandles();
//--- Получим хэндлы индикаторов
   GetIndicatorHandles();
//--- Инициализируем новый бар
   InitializeArrayNewBar();
  }

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

//+------------------------------------------------------------------+
//| Обработчик событий на графиках                                   |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,         // Идентификатор события
                  const long &lparam,   // Параметр события типа long
                  const double &dparam, // Параметр события типа double
                  const string &sparam) // Параметр события типа string
  {
//--- Если это пользовательское событие
   if(id>=CHARTEVENT_CUSTOM)
     {
      //--- Выйти, если запрещено торговать
      if(CheckTradingPermission()>0)
         return;
      //--- Если было событие "тик"
      if(lparam==CHARTEVENT_TICK)
        {
         //--- Проверяет сигналы и торгует по ним
         CheckSignalsAndTrade();
         return;
        }
     }
  }

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

//+------------------------------------------------------------------+
//| Проверяет сигналы и торгует по событию новый бар                 |
//+------------------------------------------------------------------+
void CheckSignalsAndTrade()
  {
//--- Пройдемся по всем указанным символам
   for(int s=0; s<NUMBER_OF_SYMBOLS; s++)
     {
      //--- Если торговля по этому символу разрешена
      if(Symbols[s]!="")
        {
         //--- Если бар не новый, перейдем к следующему символу
         if(!CheckNewBar(s))
            continue;
         //--- Если есть новый бар
         else
           {
            //--- Получим данные индикаторов. Если данных нет, перейдем к следующему символу
            if(!GetIndicatorsData(s))
               continue;
            //--- Получим данные баров               
            GetBarsData(s);
            //--- Проверим условия и торгуем
            TradingBlock(s);
            //--- Трейлинг
            ModifyTrailingStop(s);
           }
        }
     }
  }

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

Например, ниже показан код обновленных функций CheckNewBar(), TradingBlock() и OpenPosition().

Код функции CheckNewBar():

//+------------------------------------------------------------------+
//| Проверка нового бара                                             |
//+------------------------------------------------------------------+
bool CheckNewBar(int number_symbol)
  {
//--- Получим время открытия текущего бара
//    Если возникла ошибка при получении, сообщим об этом
   if(CopyTime(Symbols[number_symbol],Period(),0,1,lastbar_time[number_symbol].time)==-1)
      Print(__FUNCTION__,": Ошибка копирования времени открытия бара: "+IntegerToString(GetLastError()));
//--- Если это первый вызов функции
   if(new_bar[number_symbol]==NULL)
     {
      //--- Установим время
      new_bar[number_symbol]=lastbar_time[number_symbol].time[0];
      Print(__FUNCTION__,": Инициализация ["+Symbols[number_symbol]+"][TF: "+TimeframeToString(Period())+"]["
            +TimeToString(lastbar_time[number_symbol].time[0],TIME_DATE|TIME_MINUTES|TIME_SECONDS)+"]");
      return(false);
     }
//--- Если время отличается
   if(new_bar[number_symbol]!=lastbar_time[number_symbol].time[0])
     {
      //--- Установим время и выйдем
      new_bar[number_symbol]=lastbar_time[number_symbol].time[0];
      return(true);
     }
//--- Дошли до этого места - значит бар не новый, вернем false
   return(false);
  }

Код функции TradingBlock():

//+------------------------------------------------------------------+
//| Торговый блок                                                    |
//+------------------------------------------------------------------+
void TradingBlock(int symbol_number)
  {
   ENUM_ORDER_TYPE      signal=WRONG_VALUE;                 // Переменная для приема сигнала
   string               comment="hello :)";                 // Комментарий для позиции
   double               tp=0.0;                             // Take Profit
   double               sl=0.0;                             // Stop Loss
   double               lot=0.0;                            // Объем для расчета позиции в случае переворота позиции
   double               position_open_price=0.0;            // Цена открытия позиции
   ENUM_ORDER_TYPE      order_type=WRONG_VALUE;             // Тип ордера для открытия позиции
   ENUM_POSITION_TYPE   opposite_position_type=WRONG_VALUE; // Противоположный тип позиции
//--- Узнаем, есть ли позиция
   pos.exists=PositionSelect(Symbols[symbol_number]);
//--- Получим сигнал
   signal=GetTradingSignal(symbol_number);
//--- Если сигнала нет, выходим
   if(signal==WRONG_VALUE)
      return;
//--- Получим свойства символа
   GetSymbolProperties(symbol_number,S_ALL);
//--- Определим значения торговым переменным
   switch(signal)
     {
      //--- Присвоим переменным значения для BUY
      case ORDER_TYPE_BUY  :
         position_open_price=symb.ask;
         order_type=ORDER_TYPE_BUY;
         opposite_position_type=POSITION_TYPE_SELL;
         break;
         //--- Присвоим переменным значения для SELL
      case ORDER_TYPE_SELL :
         position_open_price=symb.bid;
         order_type=ORDER_TYPE_SELL;
         opposite_position_type=POSITION_TYPE_BUY;
         break;
     }
//--- Получим уровни Take Profit и Stop Loss
   sl=CalculateStopLoss(symbol_number,order_type);
   tp=CalculateTakeProfit(symbol_number,order_type);
//--- Если позиции нет
   if(!pos.exists)
     {
      //--- Скорректируем объем
      lot=CalculateLot(symbol_number,Lot[symbol_number]);
      //--- Откроем позицию
      OpenPosition(symbol_number,lot,order_type,position_open_price,sl,tp,comment);
     }
//--- Если позиция есть
   else
     {
      //--- Получим тип позиции
      GetPositionProperties(symbol_number,P_TYPE);
      //--- Если позиция противоположна сигналу и включен переворот позиции
      if(pos.type==opposite_position_type && Reverse[symbol_number])
        {
         //--- Получим объем позиции
         GetPositionProperties(symbol_number,P_VOLUME);
         //--- Скорректируем объем
         lot=pos.volume+CalculateLot(symbol_number,Lot[symbol_number]);
         //--- Перевернем позицию
         OpenPosition(symbol_number,lot,order_type,position_open_price,sl,tp,comment);
         return;
        }
      //--- Если сигнал по направлению позиции и включено наращивание объема, то увеличим объем позиции
      if(!(pos.type==opposite_position_type) && VolumeIncrease[symbol_number]>0)
        {
         //--- Получим Stop Loss текущей позиции
         GetPositionProperties(symbol_number,P_SL);
         //--- Получим Take Profit текущей позиции
         GetPositionProperties(symbol_number,P_TP);
         //--- Скорректируем объем
         lot=CalculateLot(symbol_number,VolumeIncrease[symbol_number]);
         //--- Увеличим объем позиции
         OpenPosition(symbol_number,lot,order_type,position_open_price,pos.sl,pos.tp,comment);
         return;
        }
     }
  }

Код функции OpenPosition():

//+------------------------------------------------------------------+
//| Открывает позицию                                                |
//+------------------------------------------------------------------+
void OpenPosition(int symbol_number,
                  double lot,
                  ENUM_ORDER_TYPE order_type,
                  double price,
                  double sl,
                  double tp,
                  string comment)
  {
//--- Установим номер мэджика в торговую структуру
   trade.SetExpertMagicNumber(MagicNumber);
//--- Установим размер проскальзывания в пунктах
   trade.SetDeviationInPoints(CorrectValueBySymbolDigits(Deviation));
//--- Режим Instant Execution и Market Execution
//    *** Начиная с 803 билда, уровни Stop Loss и Take Profit ***
//    *** можно устанавливать при открытии позиции в режиме SYMBOL_TRADE_EXECUTION_MARKET ***
   if(symb.execution_mode==SYMBOL_TRADE_EXECUTION_INSTANT ||
      symb.execution_mode==SYMBOL_TRADE_EXECUTION_MARKET)
     {
      //--- Если позиция не открылась, вывести сообщение об этом
      if(!trade.PositionOpen(Symbols[symbol_number],order_type,lot,price,sl,tp,comment))
         Print("Ошибка при открытии позиции: ",GetLastError()," - ",ErrorDescription(GetLastError()));
     }
  }

То есть, в каждую функцию теперь передается номер символа (symbol_number). Обратите также внимание на изменение, которое было произведено с 803 билда:

Начиная с 803 билда, уровни Stop Loss и Take Profit можно устанавливать при открытии позиции в режиме SYMBOL_TRADE_EXECUTION_MARKET.

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


Оптимизация параметров и тестирование эксперта

Сначала произведем оптимизацию для одного символа, а потом для второго. Начнем с EURUSD.

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

Рис. 1. Настройки Тестера стратегий

Рис. 1. Настройки Тестера стратегий.

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

Рис. 2. Настройки эксперта для оптимизации параметров: EURUSD

Рис. 2. Настройки эксперта для оптимизации параметров: EURUSD.

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

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

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

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

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

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

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

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

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

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

Рис. 5. Результат теста по двум символам: EURUSD и NZDUSD

Рис. 5. Результат теста по двум символам: EURUSD и NZDUSD.


Заключение

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

После распаковки архива поместите папку MultiSymbolExpert в директорию MetaTrader 5\MQL5\Experts. Индикатор EventsSpy.mq5 нужно поместить в директорию MetaTrader 5\MQL5\Indicators.