Реализация мультивалютного режима в MetaTrader 5

Konstantin Gruzdev | 10 января, 2011


Введение

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

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


Обзор традиционного подхода

Традиционным подходом, в нашем случае, можно считать попытку реализовать мультивалютную систему на основе стандартных функций OnTick() и OnCalculate(), которые пришли на смену функции start() из MQL4. Проще говоря, при появлении нового тика или бара на текущем графике последовательно опрашиваются все валютные пары (участвующие в мультивалютной системе) с целью последующего анализа и принятия соответствующих решений.

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

  1. Зависимость всей системы от поступления тиков по торговому инструменту текущего графика.

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

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

  2. Сложность синхронизации исторических данных по всем используемым в мультивалютной системе инструментам.

    "Система построения графиков в MetaTrader 4 устроена так, что рисуются только те бары, во временных промежутках которых было хоть одно изменение цены. Если в течение минуты изменения цены не было, то на минутном графике образуется пропуск в один бар", - так начинается статья Графики без "дыр".

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

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

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

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

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

    Например, когда мы строим одновалютный индикатор - проблем нет. Если входная переменная prev_calculated функции OnCalculate() обнулилась, то мы пересчитываем индикатор. А что делать и как узнать, если произошло обновление истории по символу не на текущем графике? Это может произойти в любой момент и, возможно, требуется пересчитать мультивалютный индикатор. Ответ на этот вопрос актуален.

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


Новые надежды с функцией OnTimer()

Появление возможности в MQL-программе генерировать событие "Timer" и стандартного обработчика событий OnTimer() дало надежду на появление мультивалютных систем нового типа.  Это обусловлено, с одной стороны, тем, что теперь мультивалютный эксперт/индикатор  может быть независим от поступления тиков по инструменту текущего графика и, с другой стороны, можно контролировать работу эксперта по времени. Но...

Это частично решает проблему, обозначенную в пункте 1 предыдущего раздела и, конечно, дает некоторые преимущества. Но так же, как при поступлении события "NewTick", при поступлении события "Timer" приходится последовательно опрашивать все валютные пары в попытке отследить изменения. Иногда это приходится делать достаточно часто, что может существенно увеличивать ресурсоемкость MQL5-программ.

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

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


Новые возможности с функцией OnChartEvent() 

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

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

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

Под забавным словом "шпион" я подразумеваю вот такой индикатор:

//+------------------------------------------------------------------+
//|                                                         iSpy.mq5 |
//|                                            Copyright 2010, Lizar |
//|                                               lizar-2010@mail.ru |
//+------------------------------------------------------------------+
#define VERSION         "1.00 Build 2 (26 Dec 2010)"

#property copyright   "Copyright 2010, Lizar"
#property link        "https://www.mql5.com/ru/users/Lizar"
#property version     VERSION
#property description "Индикатор iSpy - индикатор-шпион."
#property description "Запускается на нужном торговом инструменте для получения тиков"
#property indicator_chart_window

input long            chart_id=0;        // идентификатор графика-получателя события
input ushort          custom_event_id=0; // идентификатор события

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,      // размер массива price[]
                 const int prev_calculated,// обработано баров на предыдущем вызове
                 const int begin,          // откуда начинаются значимые данные
                 const double& price[]      // массив для расчета
   )
  {
   double price_current=price[rates_total-1];

   //--- Инициализация:
   if(prev_calculated==0) 
     { // Генерируем и отправляем событие "Инициализация"
      EventChartCustom(chart_id,0,(long)_Period,price_current,_Symbol);
      return(rates_total);
     }
   
   // При появлении нового бара генерируем пользовательское событие "новый тик", 
   // которое можно будет поймать в эксперте или индикаторе
   EventChartCustom(chart_id,custom_event_id+1,(long)_Period,price_current,_Symbol);
   
   //--- return value of prev_calculated for next call
   return(rates_total);
  }

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

Для демонстрации работы нескольких "шпионов" одновременно, сделаем простой советник  exSpy.mq5 (полная версия в архиве):

//+------------------------------------------------------------------+
//|                                                        exSpy.mq5 |
//|                                            Copyright 2010, Lizar |
//|                            https://www.mql5.com/ru/users/Lizar |
//+------------------------------------------------------------------+
#define VERSION       "1.00 Build 1 (26 Dec 2010)"

#property copyright   "Copyright 2010, Lizar"
#property link        "https://www.mql5.com/ru/users/Lizar"
#property version     VERSION
#property description "Эксперт демонстрирует работу индикатора-шпиона iSPY"

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {   
   if(iCustom("GBPUSD",PERIOD_M1,"iSpy",ChartID(),0)==INVALID_HANDLE) 
      { Print("Ошибка установки шпиона на GBPUSD"); return(true);}
   if(iCustom("EURUSD",PERIOD_M1,"iSpy",ChartID(),1)==INVALID_HANDLE) 
      { Print("Ошибка установки шпиона на EURUSD"); return(true);}
   if(iCustom("USDJPY",PERIOD_M1,"iSpy",ChartID(),2)==INVALID_HANDLE) 
      { Print("Ошибка установки шпиона на USDJPY"); return(true);}
      
   Print("Шпионы засланы, ждем шифровок...");
   //---
   return(0);
  }
  
//+------------------------------------------------------------------+
//| Стандартная функция обработки событий. Она может использоваться  |
//| по своему прямому назначению, как описано в документации.        |
//|                                                                  |
//| В данном примере демонстрирует декодирвание сообщений индикатора-|
//| шпиона iSPY.                                                     |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,  // идентификатор события:
                              // если id-CHARTEVENT_CUSTOM=0 - событие "инициализация"
                  const long&   lparam, // период графика
                  const double& dparam, // цена
                  const string& sparam  // инструмент 
                 )
  {
   if(id>=CHARTEVENT_CUSTOM)      
     {
      Print(TimeToString(TimeCurrent(),TIME_SECONDS)," -> id=",
           id-CHARTEVENT_CUSTOM,":  ",sparam," ",
           EnumToString((ENUM_TIMEFRAMES)lparam)," price=",dparam);
     }
  }

Запускаем советник на любом графике. Вот результаты работы:

 

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

Подведем промежуточный итог. Итак:

В общем-то, уже на этом месте можно было бы поставить точку в статье, т.к. нам удалось реализовать мультивалютный режим при помощи средств терминала MetaTrader 5 и языка программирования MQL5. Но это слишком "сырая" реализация. Поэтому идем дальше.


Реализация мультивалютного режима

Если использовать вышеизложенную идею реализации мультивалютного режима в чистом виде, то в какой-то момент вы начнете сталкиваться с трудностями. Дело в том, что такой подход позволяет получать ВСЕ тики по КАЖДОМУ торговому инструменту, на котором запущен "шпион".

В секунду, при быстром рынке, будет приходить по несколько тиков по каждому инструменту. Это может привести к "загромождению" очереди событий. Вот предупреждение из справки:

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

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

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

Давайте сделаем его универсальным, чтобы больше никогда не возвращаться к вопросу о том, как получить то или иное событие по символу в мультивалютном эксперте или индикаторе. Для этой цели возьмем за образец перечисление событий ENUM_CHART_EVENT_SYMBOL из описания Панель управления мультивалютным режимом "Control panel MCM" :

enum ENUM_CHART_EVENT_SYMBOL
  {
   CHARTEVENT_INIT      =0,          // Событие "Инициализация"
   CHARTEVENT_NO        =0,          // События отключены

   CHARTEVENT_NEWBAR_M1 =0x00000001, // Событие "новый бар" на 1 -минутном графике
   CHARTEVENT_NEWBAR_M2 =0x00000002, // Событие "новый бар" на 2 -минутном графике
   CHARTEVENT_NEWBAR_M3 =0x00000004, // Событие "новый бар" на 3 -минутном графике
   CHARTEVENT_NEWBAR_M4 =0x00000008, // Событие "новый бар" на 4 -минутном графике
   
   CHARTEVENT_NEWBAR_M5 =0x00000010, // Событие "новый бар" на 5 -минутном графике
   CHARTEVENT_NEWBAR_M6 =0x00000020, // Событие "новый бар" на 6 -минутном графике
   CHARTEVENT_NEWBAR_M10=0x00000040, // Событие "новый бар" на 10-минутном графике
   CHARTEVENT_NEWBAR_M12=0x00000080, // Событие "новый бар" на 12-минутном графике
   
   CHARTEVENT_NEWBAR_M15=0x00000100, // Событие "новый бар" на 15-минутном графике
   CHARTEVENT_NEWBAR_M20=0x00000200, // Событие "новый бар" на 20-минутном графике
   CHARTEVENT_NEWBAR_M30=0x00000400, // Событие "новый бар" на 30-минутном графике
   CHARTEVENT_NEWBAR_H1 =0x00000800, // Событие "новый бар" на 1 -часовом графике
   
   CHARTEVENT_NEWBAR_H2 =0x00001000, // Событие "новый бар" на 2 -часовом графике
   CHARTEVENT_NEWBAR_H3 =0x00002000, // Событие "новый бар" на 3 -часовом графике
   CHARTEVENT_NEWBAR_H4 =0x00004000, // Событие "новый бар" на 4 -часовом графике
   CHARTEVENT_NEWBAR_H6 =0x00008000, // Событие "новый бар" на 6 -часовом графике
   
   CHARTEVENT_NEWBAR_H8 =0x00010000, // Событие "новый бар" на 8 -часовом графике
   CHARTEVENT_NEWBAR_H12=0x00020000, // Событие "новый бар" на 12-часовом графике
   CHARTEVENT_NEWBAR_D1 =0x00040000, // Событие "новый бар" на дневном графике
   CHARTEVENT_NEWBAR_W1 =0x00080000, // Событие "новый бар" на недельном графике
     
   CHARTEVENT_NEWBAR_MN1=0x00100000, // Событие "новый бар" на месячном графике   
   CHARTEVENT_TICK      =0x00200000, // Событие "новый тик"
   
   CHARTEVENT_ALL       =0xFFFFFFFF, // Все события включены
  };

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

Комбинировать флаги можно при помощи побитовой операции "ИЛИ". Например, комбинация CHARTEVENT_NEWBAR_M1 | CHARTEVENT_NEWBAR_H1 будет означать, что мы собираемся посылать при помощи "шпиона" события "новый бар" с минутного и часового таймфрейма. Данные флаги будут являться входным параметром для нашего индикатора-шпиона. Теперь будем его называть солиднее: "индикатор - агент".

Сам индикатор, в соответствии с новыми задумками, теперь выглядит так:

//+------------------------------------------------------------------+
//|                                        Spy Control panel MCM.mq5 |
//|                                            Copyright 2010, Lizar |
//|                            https://www.mql5.com/ru/users/Lizar |
//+------------------------------------------------------------------+
#define VERSION       "1.00 Build 3 (26 Dec 2010)"

#property copyright   "Copyright 2010, Lizar"
#property link        "https://www.mql5.com/ru/users/Lizar"
#property version     VERSION
#property description "Это индикатор-агент Control panel MCM."
#property description "Запускается на нужном инструменте на любом таймфрейме"
#property description "и генерирует пользовательское событие NewBar"
#property description "и/или NewTick для графика-получателя события"

#property indicator_chart_window
  
input long                    chart_id;                 // идентификатор графика-получателя события
input ushort                  custom_event_id;          // идентификатор события  
input ENUM_CHART_EVENT_SYMBOL flag_event=CHARTEVENT_NO;// флаг, определяющий тип события.

MqlDateTime time, prev_time;

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,       // размер массива price[]
                 const int prev_calculated, // обработано баров на предыдущем вызове
                 const int begin,           // откуда начинаются значимые данные
                 const double& price[]      // массив для расчета
   )
  {  
   double price_current=price[rates_total-1];

   TimeCurrent(time);
   
   if(prev_calculated==0)
     {
      EventCustom(CHARTEVENT_INIT,price_current);
      prev_time=time; 
      return(rates_total);
     }
   
//--- new tick
   if((flag_event & CHARTEVENT_TICK)!=0) EventCustom(CHARTEVENT_TICK,price_current);       

//--- check change time
   if(time.min==prev_time.min && 
      time.hour==prev_time.hour && 
      time.day==prev_time.day &&
      time.mon==prev_time.mon) return(rates_total);

//--- new minute
   if((flag_event & CHARTEVENT_NEWBAR_M1)!=0) EventCustom(CHARTEVENT_NEWBAR_M1,price_current);     
   if(time.min%2 ==0 && (flag_event & CHARTEVENT_NEWBAR_M2)!=0)  EventCustom(CHARTEVENT_NEWBAR_M2,price_current);
   if(time.min%3 ==0 && (flag_event & CHARTEVENT_NEWBAR_M3)!=0)  EventCustom(CHARTEVENT_NEWBAR_M3,price_current); 
   if(time.min%4 ==0 && (flag_event & CHARTEVENT_NEWBAR_M4)!=0)  EventCustom(CHARTEVENT_NEWBAR_M4,price_current);      
   if(time.min%5 ==0 && (flag_event & CHARTEVENT_NEWBAR_M5)!=0)  EventCustom(CHARTEVENT_NEWBAR_M5,price_current);     
   if(time.min%6 ==0 && (flag_event & CHARTEVENT_NEWBAR_M6)!=0)  EventCustom(CHARTEVENT_NEWBAR_M6,price_current);     
   if(time.min%10==0 && (flag_event & CHARTEVENT_NEWBAR_M10)!=0) EventCustom(CHARTEVENT_NEWBAR_M10,price_current);      
   if(time.min%12==0 && (flag_event & CHARTEVENT_NEWBAR_M12)!=0) EventCustom(CHARTEVENT_NEWBAR_M12,price_current);      
   if(time.min%15==0 && (flag_event & CHARTEVENT_NEWBAR_M15)!=0) EventCustom(CHARTEVENT_NEWBAR_M15,price_current);      
   if(time.min%20==0 && (flag_event & CHARTEVENT_NEWBAR_M20)!=0) EventCustom(CHARTEVENT_NEWBAR_M20,price_current);      
   if(time.min%30==0 && (flag_event & CHARTEVENT_NEWBAR_M30)!=0) EventCustom(CHARTEVENT_NEWBAR_M30,price_current);      
   if(time.min!=0) {prev_time=time; return(rates_total);}
//--- new hour
   if((flag_event & CHARTEVENT_NEWBAR_H1)!=0) EventCustom(CHARTEVENT_NEWBAR_H1,price_current);
   if(time.hour%2 ==0 && (flag_event & CHARTEVENT_NEWBAR_H2)!=0)  EventCustom(CHARTEVENT_NEWBAR_H2,price_current);
   if(time.hour%3 ==0 && (flag_event & CHARTEVENT_NEWBAR_H3)!=0)  EventCustom(CHARTEVENT_NEWBAR_H3,price_current);      
   if(time.hour%4 ==0 && (flag_event & CHARTEVENT_NEWBAR_H4)!=0)  EventCustom(CHARTEVENT_NEWBAR_H4,price_current);      
   if(time.hour%6 ==0 && (flag_event & CHARTEVENT_NEWBAR_H6)!=0)  EventCustom(CHARTEVENT_NEWBAR_H6,price_current);      
   if(time.hour%8 ==0 && (flag_event & CHARTEVENT_NEWBAR_H8)!=0)  EventCustom(CHARTEVENT_NEWBAR_H8,price_current);      
   if(time.hour%12==0 && (flag_event & CHARTEVENT_NEWBAR_H12)!=0) EventCustom(CHARTEVENT_NEWBAR_H12,price_current);      
   if(time.hour!=0) {prev_time=time; return(rates_total);}
//--- new day
   if((flag_event & CHARTEVENT_NEWBAR_D1)!=0) EventCustom(CHARTEVENT_NEWBAR_D1,price_current);      
//--- new week
   if(time.day_of_week==1 && (flag_event & CHARTEVENT_NEWBAR_W1)!=0) EventCustom(CHARTEVENT_NEWBAR_W1,price_current);      
//--- new month
   if(time.day==1 && (flag_event & CHARTEVENT_NEWBAR_MN1)!=0) EventCustom(CHARTEVENT_NEWBAR_MN1,price_current);      
   prev_time=time;
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

void EventCustom(ENUM_CHART_EVENT_SYMBOL event,double price)
  {
   EventChartCustom(chart_id,custom_event_id,(long)event,price,_Symbol);
   return;
  } 

Так как данный индикатор является составной частью "Control panel MCM", я не стал его переименовывать и в приложении выложил просто обновленную версию (см. "Spy Control panel MCM.mq5”). Но это не означает, что его нельзя использовать отдельно от панели.

Этот индикатор-агент генерирует пользовательские события  и транслирует их на график-получатель с целью дальнейшей обработки этих событий в эксперте или индикаторе с помощью функции  OnChartEvent(). Теперь входные параметры этой функции необходимо интерпретировать следующим образом:

 Демонстрационный эксперт выглядит не сложнее предыдущего (полная версия в архиве):

//+------------------------------------------------------------------+
//|                                      exSpy Control panel MCM.mq5 |
//|                                            Copyright 2010, Lizar |
//|                            https://www.mql5.com/ru/users/Lizar |
//+------------------------------------------------------------------+
#define VERSION       "1.00 Build 1 (28 Dec 2010)"

#property copyright   "Copyright 2010, Lizar"
#property link        "https://www.mql5.com/ru/users/Lizar"
#property version     VERSION
#property description "Эксперт демонстрирует работу индикатора-агента Spy Control panel MCM"

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {   
   if(iCustom("GBPUSD",PERIOD_M1,"Spy Control panel MCM",ChartID(),0,
             CHARTEVENT_NEWBAR_M1|CHARTEVENT_NEWBAR_M5)==INVALID_HANDLE) 
      { Print("Ошибка установки шпиона на GBPUSD"); return(true);}
   if(iCustom("EURUSD",PERIOD_M1,"Spy Control panel MCM",ChartID(),1,
             CHARTEVENT_NEWBAR_M2)==INVALID_HANDLE) 
      { Print("Ошибка установки шпиона на EURUSD"); return(true);}
   if(iCustom("USDJPY",PERIOD_M1,"Spy Control panel MCM",ChartID(),2,
             CHARTEVENT_NEWBAR_M6)==INVALID_HANDLE) 
      { Print("Ошибка установки шпиона на USDJPY"); return(true);}
      
   Print("Агенты засланы, ждем сообщений...");
   //---
   return(0);
  }
  
//+------------------------------------------------------------------+
//| Стандартная функция обработки событий. Она может использоваться  |
//| по своему прямому назначению, как описано в документации.        |
//|                                                                  |
//| В данном примере демонстрируется декодирование сообщений         |
//| индикатора-агента iSPY Control panel MCM и реализацию            |
//| мультивалютного режима                                           |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,          // идентификатор события
                  const long&   lparam, // флаг события поступившего от агента панели.
                                       // Флаги соответствуют перечислению ENUM_CHART_EVENT_SYMBOL.
                  const double& dparam, // цена
                  const string& sparam  // инструмент 
                 )
  {
   if(id>=CHARTEVENT_CUSTOM)      
     {
      Print(TimeToString(TimeCurrent(),TIME_SECONDS)," -> id=",id-CHARTEVENT_CUSTOM,
           ":  ",sparam," ",EnumToString((ENUM_CHART_EVENT_SYMBOL)lparam)," price=",dparam);
     }
  }

Результат работы exSpy Control panel MCM:

 

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

Давайте еще раз подведем небольшой промежуточный итог:

Что ж, как оказалось, в MetaTrader 5 реализовать полноценный мультивалютный режим не так сложно.

В дополнение к этому хочу отметить пару нюансов.

Первый. Все события, которые генерируют "агенты" для нашего мультивалютного советника/индикатора являются внешними. В связи с этим возникает вопрос: "А обязательно ли запускать агентов непосредственно из эксперта/индикатора?". Ответ: "нет".

Второй. В функции OnChartEvent() идентификатор события id, как бы, является лишним, т.к. понять, по какому символу пришло событие, мы можем по параметру sparam - имя торгового инструмента. Поэтому, возможно, мы еще как-то можем его использовать? Ответ: "можем". 

Подобные рассуждения привели к тому, что появилась Панель управления мультивалютным режимом "Control panel MCM". Она является, своего рода, прослойкой между терминалом и экспертом/индикатором. Это дало еще больше преимуществ и гибкости в настройке мультивалютной среды:


Мультивалютный индикатор RSI для индекса доллара USDx

Чтобы прочувствовать все достоинства вышеизложенного метода, предлагаю реализовать мультивалютный вариант индикатора RSI для индекса доллара USDx с использованием "Control panel MCM".

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

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

Кто читал, вникая в суть статьи, мог заметить, что толком не был решен вопрос синхронизации друг с другом исторических данных по всем используемым в мультивалютной системе инструментам  (смотрите пункт 2 из раздела "Обзор традиционного подхода").

Этот вопрос решен в индикаторе при помощи класса функций для построения синхронизированных буферов RSI (файл CSynchronizedBufferRSI.mqh).  Нет смысла приводить весь код класса, выделю только особые моменты. 

Первое, индикаторный буфер определен внутри класса со спецификатором доступа public:

public:
   double   buffer[];           // индикаторный буфер

Второе, инициализация индикатора происходит с помощью метода класса:

//--- Методы инициализации:
bool Init(int n,string symbol,int rsi_count,int rsi_period);

И, третье, значение индикаторного буфера на каждом баре синхронизируется с текущим таймфреймом с помощью refresh-метода класса:

//+------------------------------------------------------------------+
//| Метод получения/обновления данных индикатора для одного бара     |
//| индикаторного буфера.                                            |
//| INPUT:  bar   - номер бара                                       |
//| OUTPUT: no.                                                      |
//| REMARK: no.                                                      |
//+------------------------------------------------------------------+
void CSynchronizedBufferRSI::Refresh(int bar=0)
  {
   buffer[bar]=EMPTY_VALUE; // Инициализация бара индикаторного буфера.
     
   //--- Запрашиваем время бара на текущем графике:
   datetime time[1];      
   if(CopyTime(_Symbol,_Period,bar,1,time)!=1) return; // При ошибке ждем следующего тика/бара...

   //--- Запрашиваем значение индикатора для символа по времени совпадающим с баром текущего графика:
   double value[1];
   if(CopyBuffer(m_handle,0,time[0],time[0],value)!=1) return; // При ошибке ждем следующего тика/бара...

   buffer[bar]=value[0];
   return;
  }

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

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

На графике индикатора очень хорошо видно, зачем это стоит делать:

 

Для больших таймфреймов обычно такое не наблюдается.

И последнее. Привожу код стандартного обработчика пользовательских событий, который использован в индикаторе:

//+------------------------------------------------------------------+
//| Стандартная функция обработки событий. Она может использоваться  |
//| по своему прямому назначению, как описано в документации.        |
//|                                                                  |
//| Вместе с Control panel MCM является удобным средством для        |
//| мультивалютной торговли.                                         |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,         // идентификатор события или позиция символа в "Обзор рынка"+CHARTEVENT_CUSTOM  
                const long& lparam,   // флаг события
                const double& dparam, // цена
                const string& sparam  // инструмент 
                )
  {
   int custom_id=id-CHARTEVENT_CUSTOM-1;
   
   if(custom_id>=0)      
     {
      if(lparam!=CHARTEVENT_NEWBAR_NO)
        { 
         //--- Пересчет последнего незавершенного бара:
         if(EventToPeriod(lparam)==_Period && sparam==_Symbol)
           { // Пересчет индикатора при появлении нового бара на текущем графике
            iRSIUSDx_Ind[0]=EMPTY_VALUE;
            //--- Обновление значения RSI по всем валютным парам для нового бара
            for(int i=0;i<symbol_total;i++) buffer[i].Refresh();
            iRSIUSDx(symbol_total);    // Расчет текущего незавершенного бара RSI для индекса
            return;
           }
         
         buffer[custom_id].Refresh();  // Значение RSI для custom_id валютной пары для текущего бара
         iRSIUSDx(symbol_total);       // Расчет текущего незавершенного бара RSI для индекса
         return;
        }
      else 
        { 
         //--- Пересчет индикатора при событии "Инициализация" 
         buffer[custom_id].RefreshBuffer();     // Обновление всего буфера RSI для custom_id валютной пары
         Init_iRSIUSDx(symbol_total,calculate); // Обновление всего буфера RSI для индекса
         return;
        }
     }
  }

Особенности кода:

Изучив полный код индикатора RSI для индекса доллара USDx, станет более понятно, как это все работает.

Особенности установки:


Заключение

Рассмотренные вопросы реализации полноценного мультивалютного режима в MetaTrader 5 в полной мере показали преимущества как самой платформы, так и языка программирования MQL5 для решения поставленной задачи. То, что раньше вызывало больше всего затруднений, теперь решается доступными средствами.

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