MQL5, обработка событий: Изменяем период мувинга "на лету"

Sceptic Philozoff | 11 марта, 2010

Введение

Это очень короткая статья, посвященная одной из новых возможностей языка MQL5 платформы MetaTrader 5 компании MetaQuotes Software Corp. Возможно, эта статья слегка запоздала (ее нужно было выложить еще в сентябре-октябре 2009 г., и тогда она могла бы стать своевременной), но аналогичных статей на эту тему пока не видно и, кроме того, тогда еще не было таких возможностей по обработке событий в индикаторах.

Представим себе, что у нас имеется какой-нибудь несложный индикатор цены, наложенный на чарт, в данном случае­­ мувинг (мувинг от англ. moving average, т.е. скользящая средняя), и нам захотелось изменить его период сглаживания. В платформе МТ4 у нас были следующие варианты действий:

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

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

Исходный код индикатора, предназначенного для наших экспериментов, можно найти в комплекте стандартной поставки терминала. Файл исходника в неизмененном виде (Custom Moving Average.mq5) прикреплен в конце этой статьи.

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

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

Примерный вариант того, чего мы хотим достичь, выглядит так. Предположим, что мы набросили на чарт этот индикатор, а в данный момент он отображает экспоненциальный  мувинг (EMA) с нулевым сдвигом и периодом 10. Наша цель – добиться увеличения периода сглаживания простого мувинга (SMA) на 3 (до 13), да еще и сдвинув его вправо на 5 баров. Предполагаемая последовательность действий такова:

Первое и самое очевидное решение напрашивается даже без размышлений: вставляем в код индикатора функцию OnChartEvent() и пишем обработчик события нажатия клавиш.  Согласно описанию нововведений в 245-м билде терминала, https://www.mql5.com/ru/forum?utm_campaign=MQL4.404 ,

MetaTrader 5 Client Terminal build 245

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

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

Во-первых, в «пятерке» статус внешних параметров индикатора изменился: их нельзя изменять программным способом. Единственный способ их изменения – через диалог свойств в терминале. В принципе, при острой необходимости их изменения это ограничение несложно обходится: достаточно скопировать значения внешних параметров в новые глобальные переменные индикатора, а все вычисления производить так, как будто эти новые переменные и есть внешние параметры индикатора. С другой стороны, в этом случае исчезает сама целесообразность внешних параметров, значения которых могут только ввести в заблуждение пользователя индикатора в терминале. Они теперь просто не нужны.

Таким образом, внешних параметров (input) у индикатора не будет вообще. При этом переменные, играющие роль внешних параметров, теперь будут глобальными переменными терминала (ГПТ). Пользователь этого индикатора, если захочет увидеть ГПТ, отвечающие за бывшие внешние параметры индикатора, может просто нажать F3. Другого простого способа контроля параметров индикатора мне придумать не удалось.

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

Несколько частей кода первой версии модифицированного индикатора приведены ниже. Полный код прикреплен в конце статьи.

 

"Стандартная" версия: описание изменений в коде стандартного индикатора

Внешние параметры – уже не внешние, а просто глобальные переменные

Все внешние параметры индикатора лишились модификатора input. В принципе их можно было бы даже не делать глобальными, но я решил оставить их глобальность – по традиции:

int              MA_Period   = 13;
int              MA_Shift    =  0;
ENUM_MA_METHOD   MA_Method    =  0;
int              Updated     =  0;     /// показывает, обновился ли индикатор после изменения параметров

Первые три параметра – это период, сдвиг и тип мувинга, а четвертый Updated отвечает за оптимизацию расчетов при изменении параметров мувингов. Пояснения – немного ниже.

Коды виртуальных клавиш

Вводим коды виртуальных клавиш:

#define KEY_UP             38
#define KEY_DOWN           40
#define KEY_NUMLOCK_DOWN   98
#define KEY_NUMLOCK_UP    104
#define KEY_TAB             9

Это коды клавиш «стрелка вверх», «стрелка вниз», аналогичных стрелок на цифровой клавиатуре (клавиши "8" и "2"), а также клавиши табуляции. Те же коды (с другими названиями констант, VK_XXX) на самом деле имеются в файле <Каталог MT5>\MQL5\Include\VirtualKeys.mqh, но в данном случае я решил оставить так, как есть.


Небольшая коррекция кода функции вычисления линейно сглаженного среднего (LWMA)

Функцию CalculateLWMA() пришлось чуть-чуть подправить: в первоначальном варианте переменная weightsum была объявлена с модификатором static. По-видимому, единственной причиной, сподвигнувшей разработчиков терминала на этот шаг, являлась необходимость ее предварительного вычисления при первом вызове этой функции. Далее эта переменная не меняется. Вот прежний код функции, в котором к части, связанной с вычислением и дальнейшим использованием weightsum, добавлены соответствующие комментарии:

void CalculateLWMA(int rates_total,int prev_calculated,int begin,const double &price[])
  {
   int              i,limit;
   static int     weightsum;                       // <-- использование weightsum
   double               sum;
//--- first calculation or number of bars was changed
   if(prev_calculated==0)                          // <-- использование weightsum
     {
      weightsum=0;                                 // <-- использование weightsum
      limit=InpMAPeriod+begin;                     // <-- использование weightsum
      //--- set empty value for first limit bars
      for(i=0;i<limit;i++) ExtLineBuffer[i]=0.0;
      //--- calculate first visible value
      double firstValue=0;
      for(i=begin;i<limit;i++)                     // <-- использование weightsum
        {
         int k=i-begin+1;                          // <-- использование weightsum
         weightsum+=k;                             // <-- использование weightsum
         firstValue+=k*price[i];
        }
      firstValue/=(double)weightsum;
      ExtLineBuffer[limit-1]=firstValue;
     }
   else limit=prev_calculated-1;
//--- main loop
   for(i=limit;i<rates_total;i++)
     {
      sum=0;
      for(int j=0;j<InpMAPeriod;j++) sum+=(InpMAPeriod-j)*price[i-j];
      ExtLineBuffer[i]=sum/weightsum;              // <-- использование weightsum
      }
//---
  }

Раньше этот вариант работал вполне удовлетворительно, но, когда я запустил тандем «индикатор + советник» (об этом упомянуто в конце статьи), именно на этом типе мувинга появились проблемы. И главная из них была связана именно с описанным выше обстоятельством, т.е. статичностью переменной weightsum: переменная постоянно увеличивалась, т.к. при каждом изменении параметра мувинга на лету нужно было пересчитывать его "с нуля".

Проще всего было напрямую и сразу вычислить значение weightsum (оно равно сумме целых чисел от 1 до периода мувинга – для этого существует простая формула суммы арифметической прогрессии) и одновременно лишить ее статуса статической, что я и сделал. Теперь вместо прежнего объявления weightsum с модификатором static мы объявляем ее без него, сразу инициализируем "правильным" значением и соответственно удаляем первоначальный цикл "накопления переменной".

int weightsum = MA_Period *( MA_Period + 1 ) / 2;

Теперь все заработало корректно.


Функция обработчика OnCalculate()

Пришлось внести существенные изменения в функцию OnCalculate(), и поэтому я привожу здесь ее код полностью.

int OnCalculate(const int rates_total,
                const int prev_calculated,            /// Mathemat: пересчет полный!
                const int begin,                      /// Mathemat: пересчет полный!
                const double &price[])
  {
//--- check for bars count
   if(rates_total<MA_Period-1+begin)
      return(0);// not enough bars for calculation
//--- first calculation or number of bars was changed
   if(prev_calculated==0)
      ArrayInitialize(LineBuffer,0);
//--- sets first bar from what index will be draw
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,MA_Period-1+begin);

//--- calculation (оптимизированный - Mthmt)

   if( GlobalVariableGet( "Updated" ) == 1 )
   {
      if(MA_Method==MODE_EMA)  CalculateEMA(       rates_total,prev_calculated,begin,price);
      if(MA_Method==MODE_LWMA) CalculateLWMA_Mthmt(rates_total,prev_calculated,begin,price);
      if(MA_Method==MODE_SMMA) CalculateSmoothedMA(rates_total,prev_calculated,begin,price);
      if(MA_Method==MODE_SMA)  CalculateSimpleMA(  rates_total,prev_calculated,begin,price);
   }
   else
   {
      OnInit( );                 /// Mthmt
      if(MA_Method==MODE_EMA)  CalculateEMA(       rates_total,0,0,price);
      if(MA_Method==MODE_LWMA) CalculateLWMA_Mthmt(rates_total,0,0,price);
      if(MA_Method==MODE_SMMA) CalculateSmoothedMA(rates_total,0,0,price);
      if(MA_Method==MODE_SMA)  CalculateSimpleMA(  rates_total,0,0,price);
      GlobalVariableSet( "Updated", 1 );
      Updated = 1;
   }
   
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

Главное изменение связано с возникшей необходимостью в полном расчете индикатора "с нуля": очевидно, что если в результате клавиатурных манипуляций пользователя период мувинга изменился с 13 до 14, то все предшествующие оптимизации его расчетов уже бесполезны, и его придется вычислить заново. Это происходит тогда, когда значение переменной Updated равно 0 (ГПТ уже изменились после нажатия пользователем "горячей" клавиши, но тик, перерисовывающий индикатор, еще не пришел).

Однако, кроме того, предварительно мы должны явно вызвать функцию OnInit(), т.к. нужно изменить короткое имя индикатора, которое при подведении мыши к линии будет видеть пользователь. После первоначального расчета мувинга ГПТ Updated устанавливается в 1, что открывает нам дорогу теперь уже к оптимизированному расчету индикатора - до тех пор, пока пользователь снова не захочет изменить какой-нибудь параметр индикатора "на лету".


Обработчик OnChartEvent()

Ниже приведен несложный код обработчика OnChartEvent():

void OnChartEvent( const int          id,
                   const long    &lparam,
                   const double  &dparam,
                   const string  &sparam )
{
   if( id == CHARTEVENT_KEYDOWN )
      switch( lparam )
      {
         case( KEY_TAB          ):  changeTerminalGlobalVar( "MA_Method",  1 ); 
                                    GlobalVariableSet( "Updated",  0 );
                                    Updated = 0;
                                    break;
         
         case( KEY_UP           ):  changeTerminalGlobalVar( "MA_Period",  1 ); 
                                    GlobalVariableSet( "Updated",  0 );
                                    Updated = 0;
                                    break;
         case( KEY_DOWN         ):  changeTerminalGlobalVar( "MA_Period", -1 ); 
                                    GlobalVariableSet( "Updated",  0 );
                                    Updated = 0;
                                    break;
         
         case( KEY_NUMLOCK_UP   ):  changeTerminalGlobalVar( "MA_Shift",   1 ); 
                                    GlobalVariableSet( "Updated",  0 );
                                    Updated = 0;
                                    break;
         case( KEY_NUMLOCK_DOWN ):  changeTerminalGlobalVar( "MA_Shift",  -1 ); 
                                    GlobalVariableSet( "Updated",  0 );
                                    Updated = 0;
                                    break;
      }
      
      return;
}//+------------------------------------------------------------------+      

Обработчик действует так: при нажатии пользователем "горячей" клавиши определяем ее виртуальный код и затем запускаем вспомогательную функцию changeTerminalGlobalVar(), правильно изменяющую нужную ГПТ. После этого флаг Updated сбрасываем в нуль, ожидая прихода тика, который запустит OnCalculate() и перерисует индикатор «с нуля».


Вспомогательная функция «правильного» изменения ГПТ

И, наконец, код функции changeTerminalGlobalVar(), используемой в обработчике OnChartEvent():

void changeTerminalGlobalVar( string name, int dir = 0 )
{
   int var = GlobalVariableGet( name );
   int newparam = var + dir;
   if( name == "MA_Period" )
   {
      if( newparam > 0 )       /// возможный период валиден для мувинга
      {
         GlobalVariableSet( name, newparam ); 
         MA_Period = newparam;     /// не забываем сменить глобальную переменную    
      }   
      else                       /// период не меняем, т.к. период МА равен 1 минимум
      {   
         GlobalVariableSet( name, 1 );     
         MA_Period = 1;     /// не забываем сменить глобальную переменную 
      }   
   }   
      
   if( name == "MA_Method" )    /// Тут dir при вызове всегда равен 1, значение dir не важно    
   {
      newparam = ( var + 1 ) % 4;
      GlobalVariableSet( name, newparam );  
      MA_Method = newparam;
   }      

   if( name == "MA_Shift" )
   {
      GlobalVariableSet( name, newparam );     
      MA_Shift = newparam;
   }      

   ChartRedraw();
   return;
}//+------------------------------------------------------------------+

Главное предназначение этой функции – правильное вычисление новых параметров мувингов с учетом «физических ограничений». Очевидно, период мувинга нельзя делать меньше 1, сдвиг мувинга может быть любым, а тип мувинга – это числа от 0 до 3, соответствующие условным номерам членов в перечислении ENUM_MA_METHOD.

 

Проверка работы. Работает, но «на троечку». Что делать?

ОК, набрасываем индикатор на чарт и начинаем спорадически нажимать на «горячие клавиши», меняющие параметры мувингов. Да, все работает исправно, но есть одно неприятное обстоятельство: ГПТ изменяются мгновенно (проверить можно, вызвав ГПТ по F3), однако мувинги перерисовываются далеко не всегда сразу, а только по приходе нового тика. Если у нас американская сессия с активным потоком тиков, то задержку мы можем почти не замечать. Но если дело происходит ночью, во время затишья, ждать перерисовки приходится и по несколько минут. В чем дело?

Ну, как говорится, что писали, то и получили. До build 245 в индикаторах была предусмотрена единственная «точка входа» – функция OnCalculate().  (Я, конечно, не говорю о функциях OnInit() и OnDeinit(), обеспечивающих первоначальные расчеты и инициализации и завершение работы индикатора.) Теперь таких точек входа несколько, и они связаны с новыми событиями – Timer и ChartEvent.  

Однако новые обработчики делают только то, что к ним относится, и формально не связаны с обработчиком OnCalculate(). Что же нам нужно сделать с нашим "чужим" обработчиком OnChartEvent(), чтобы он заработал «как надо», т.е. позволял бы сразу и перерисовывать мувинг?

В принципе видятся несколько способов реализации этого требования:

 

«Матрёшка» работает!

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

int OnCalculate_Void()
{
   const int rates_total = Bars( _Symbol, PERIOD_CURRENT );
   CopyClose( _Symbol, PERIOD_CURRENT, 0, rates_total, _price );
   OnCalculate( rates_total, 0, 0, _price );
   return( 1 );
}//+------------------------------------------------------------------+

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

Недостаток этой реализации в том, что в массив price[] копируются именно цены закрытия. При желании, функцию CopyClose() можно заменить нужной нам, заглянув в окно свойств индикатора, в поле «Применить к» закладки «Параметры».  Если нужная цена будет «элементарной» (Open, High, Low, Close), то соответствующая функция CopyXXXX() у нас уже есть, а в случае «сложных» цен (медианная, средняя или типичная) придется вычислять этот массив другим способом.

Я не уверен в том, что без функции CopyClose(), копирующей всю историю в массив, не обойтись. С другой стороны, эта функция при не слишком глубокой загруженной истории достаточно быстра, так что «тормозов» она не создаст. Проверка работы индикатора на EURUSD Н1 с историей до начала 1999 года (порядка 700 тысяч баров) показала, что индикатор справляется с вычислениями и не демонстрирует каких-то серьезных "тормозов". Вероятно, если при такой истории они и будут создаваться, то скорее не из-за функции CopyXXXX(), а благодаря необходимости "более тяжелого" пересчета индикатора с самого начала истории (от которого никуда не убежать).


Несколько выводов и заключение

Что лучше – один файл индикатора или тандем «индикатор+советник»?

Вопрос на самом деле не так прост. С одной стороны, один файл индикатора – это хорошо, т.к. все функции, в том числе и обработчики событий, сосредоточены в одном месте.

С другой стороны, давайте представим, что на график наброшены 3-4 индикатора вместе с советником – не такая уж и редкость. Кроме того, пусть каждый из индикаторов оборудован собственными обработчиками событий, помимо стандартных OnCalculate().  Чтобы не запутаться с обработкой событий в этом «зоопарке», разумнее сосредоточить все обработчики событий, теперь разрешенные в индикаторах, в едином месте – в советнике.

Разработчики долго решались на то, чтобы дать нам возможность обработки событий в индикаторе: начиная с непубличного выхода «беты» 09.09.09 (когда индикатор считался «чистой расчетно-математической сущностью» и не должен был быть загрязнен никакими возможностями, мешающими скорости вычислений) прошло ровно 5 месяцев, прежде чем был выпущен билд с возможностью обработки событий чарта в индикаторах. Вероятно, пострадала «чистота идеи», но золотая середина всегда где-то посередине – между чистой, но ограниченной идеей, и не очень чистой, но более мощной возможностью.


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


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


Один скользкий момент

Вспомним, что бывшие внешние параметры индикатора стали глобальными переменными терминала (ГПТ), которые пользователь может посмотреть, нажав клавишу F3. Допустим, наш пользователь вызвал диалог ГПТ и изменил одну из них (например, период мувинга). Он рассчитывает, что это изменение сразу отразится на графике индикатора.

События, соответствующего пользовательскому редактированию ГПТ (например, CHARTEVENT_GLOBALVAR_ENDEDIT), в терминале пока нет. Запретить изменение ГПТ из диалога, вызываемого F3, мы, кажется, тоже не можем. Поэтому ни на какое обрабатываемое событие, кроме тика, здесь мы рассчитывать не можем. Что получится на самом деле?

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

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


Приложенные файлы исходных кодов и видео

Наконец, прикладываю несколько файлов-исходников. Расшифровка:

1. Custom Moving Average.mq5 – файл исходника мувингов, идущий в стандартной поставке терминала МТ5.

2. MyMA_ind_with_ChartEvent.mq5 – первоначальная реализация («на троечку»): обновление индикатора происходит не ранее, чем придет тик.

3. MyMA_ind_with_ChartEvent_Matryoshka.mq5 – второй вариант (пожалуй, «на четверку»): индикатор обновляется сразу, не дожидаясь прихода тика.