MQL5, обработка событий: Изменяем период мувинга "на лету"
Введение
Это очень короткая статья, посвященная одной из новых возможностей языка MQL5 платформы MetaTrader 5 компании MetaQuotes Software Corp. Возможно, эта статья слегка запоздала (ее нужно было выложить еще в сентябре-октябре 2009 г., и тогда она могла бы стать своевременной), но аналогичных статей на эту тему пока не видно и, кроме того, тогда еще не было таких возможностей по обработке событий в индикаторах.
Представим себе, что у нас имеется какой-нибудь несложный индикатор цены, наложенный на чарт, в данном случае мувинг (мувинг – от англ. moving average, т.е. скользящая средняя), и нам захотелось изменить его период сглаживания. В платформе МТ4 у нас были следующие варианты действий:
- в редакторе экспертов правим входной параметр (extern), отвечающий за период мувинга, и компилируем исходник;
- не заходя в редактор экспертов, прямо в терминале открываем диалог свойств индикатора и правим там соответствующий входной параметр;
- открываем библиотеку Win32API, находим функции, соответствующие перехвату сообщений, после чего переделываем код индикатора так, чтобы он реагировал на события от клавиатуры.
Как известно, стремление к экономии усилий – величайший двигатель прогресса. Теперь, благодаря новой платформе МТ5, позволяющей обрабатывать в индикаторах события, инициированные пользователем, можно считать, что можно обойтись и без этого и изменять параметры индикатора одним нажатием клавиши. Технической реализации решения этой задачи эта статья и посвящена.
Постановка задачи и
проблемы
Исходный код индикатора, предназначенного для наших экспериментов, можно найти в комплекте стандартной поставки терминала. Файл исходника в неизмененном виде (Custom Moving Average.mq5) прикреплен в конце этой статьи.
Разбирать исходный
код и особенности его изменения в сравнении с соответствующим ему оригиналом для
MQL4 мы сейчас не
будем. Да, в некоторых местах он изменился существенно и не всегда очевидным
образом. Соответствующие пояснения, связанные с изменением структуры базовой части расчета, можно найти на форуме и во встроенной помощи.
Тем не менее, основной каркас индикатора на языке MQL4 остался прежним. Не менее 80% всех изменений, внесенных в код для решения нашей задачи, были сделаны исходя из представления о расчетных функциях индикатора как о черных ящиках.
Примерный вариант того, чего мы хотим достичь, выглядит так. Предположим, что мы набросили на чарт этот индикатор, а в данный момент он отображает экспоненциальный мувинг (EMA) с нулевым сдвигом и периодом 10. Наша цель – добиться увеличения периода сглаживания простого мувинга (SMA) на 3 (до 13), да еще и сдвинув его вправо на 5 баров. Предполагаемая последовательность действий такова:
- Нажав клавишу табуляции TAB несколько раз, изменяем отображаемый мувинг с экспоненциального на простой (меняем тип мувинга).
- Нажав клавишу стрелки UP на основной части клавиатуры трижды, увеличиваем период простого мувинга на 3.
- Нажав клавишу стрелки UP (цифра 8) на цифровой части клавиатуры 5 раз, сдвигаем мувинг на 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(), чтобы он заработал «как надо», т.е. позволял бы сразу и перерисовывать мувинг?
В принципе видятся несколько способов реализации этого требования:
- «Матрешка» (вызов OnCalculate() внутри OnChartEvent()): вставить внутрь этого обработчика вызов функции OnCalculate(), предварительно заполнив все ее параметры. Так как обработчик OnChartEvent() предполагает изменение хотя бы одного параметра мувинга, то это отразится на всей его истории, т.е. его придется пересчитать заново, «с нуля», без оптимизации расчетов.
- «Искусственный тик», передающий управление в начало функции OnCalculate(), которая модифицирует графический буфер. «Законных» методов, отраженных в документации к МТ5, по-видимому, нет (хотя, возможно, я плохо искал). Желающие могут поискать что-нибудь, введя ключевые фразы типа «API», «PostMessageA» и т.п. Поэтому данный вариант мы здесь рассматривать не будем, т.к. он не гарантирует, что недокументированные средства когда-нибудь не изменятся. Не сомневаюсь в том, что его реализовать можно.
«Матрёшка» работает!
Оказывается, все самое главное мы уже сделали. Ниже приведен очень простой код функции, вызов которой достаточно вставить прямо перед оператором 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 – второй вариант (пожалуй, «на четверку»): индикатор обновляется сразу, не дожидаясь прихода тика.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Алексей, скажите а вы на М1-М15 пробовали индикатор ( который с "матрешкой") ? На этих интервалах он глючит. Это проявляется так - когда кидаешь индюк на график или меняешь ТФ, то либо при нажатии горячей клавиши, либо просто кликая в окно графика -ОН смещается влево.На часовках и выше, такого эффекта вроде не видно.
Посмотрите пожалуйста, очень нужно.
Median Price не совпадает с реальной МА.
Короче не доработан.
Короче не доработан.
Из статьи:
Если нужная цена будет «элементарной» (Open, High, Low, Close), то соответствующая функция CopyXXXX() у нас уже есть, а в случае «сложных» цен (медианная, средняя или типичная) придется вычислять этот массив другим способом.
Из статьи:
возвращаясь, кому интересен метод, второй раз онинит нельзя вызывать, буфер отображения скатывается в ноль (размер == 0).
цены поправил, реализовал через пар-ры
короче как памятка.