Download MetaTrader 5

Event handling in MQL5: Changing MA period on-the-fly

16 March 2010, 09:31
Sceptic Philozoff
1
4 553

Introduction

This short article is devoted to one of the new MQL5 features of MetaTrader 5 platform, developed by MetaQuotes Software Corp. Perhaps this article is slightly late (it should have been issued in September-October 2009 - then it would be timely), but there were no similar articles on this topic. Moreover, in that time there were no such possibilities for handling events in indicators.

Imagine that we have some simple price indicator applied to a chart (in this case it is Moving Average, i.e. MA), and we want to change its smoothing period. In the MT4 platform we had the following options:

  • In MetaEditor you can edit expert input parameter (extern), responsible for the MA period, and then compile the source file.
  • Without switching to MetaEditor, right in the terminal window you can open the indicator's Properties dialog box and there edit corresponding input parameter.
  • You can open Win32 API library, find functions of messages capture, then tweak the indicator code so that it responds to events from the keyboard.

As we know, the desire for minimum of efforts is the greatest engine of progress. Now, thanks to a new MT5 platform, that allows indicators events handling initiated by the user, we can skip the above possibilities and change indicator parameters with single press of a key. This article covers the technical implementation of this problem solution.

Task assignment and problems

The indicator source code, used in our experiments, ships with Client Terminal. The unchanged source code file (Custom Moving Average.mq5) is attached in the end of this article.

For now we will not analyze the source code and especially the changes compared to its MQL4 original. Yes, in some places it has changed significantly, and that is not always obvious. Corresponding explanations, related to the restructuring of the base part of the calculation, can be found on forum and in online help.

Nevertheless, the main part of indicator in MQL4 remained unchanged. At least 80% of all changes in code, intended to resolve our problem, have been made based on the idea of computational functions of indicator as the "black boxes".

An example of what we want to achieve looks like this. Suppose that we've applied this indicator to a chart, and at a given moment it displays an exponential MA (EMA) with zero shift and period of 10. Our goal is to increase the smoothing period of simple MA (SMA) by 3 (up to 13), and to shift it by 5 bars to the right. The assumed action sequence is as follows:

  • Pressing the TAB key a few times to change the displayed MA from exponential to simple (changing MA type).
  • Pressing the UP arrow key on the main part of the keyboard three times to increase the period of simple MA by 3.
  • Pressing the UP (8) arrow key on the numeric keypad 5 times to offset MA by 5 bars to the right.

The first and the most obvious solution is to insert the OnChartEvent() function into indicator code and to write the keystroke event handler. According to the List of changes in MetaTrader 4 Client Terminal builds 245  and 246 https://www.mql5.com/en/forum/53/page1/#comment_2655,

MetaTrader 5 Client Terminal builds 245 and 246

MQL5: Added the possibility of event handling by custom indicators, similar to that by Expert Advisors.

So, now we have no problems with adding new event handlers in the indicator. But for this we still have to slightly modify its code.

First, in MQL5 the status of indicator's external parameters has changed: you can't modify them in code. The only way to change them is via Properties dialog box in Client Terminal. Generally, in urgent need to change them this restriction is easily bypassed: just copy the values of external parameters into new global variables of indicator, and all calculations are done as if these new variables are actually the indicator's external parameters. On the other hand, in this case goes away the feasibility of external parameters, whose values can only mislead users. Now these parameters are simply not needed.

Thus, there are no external (input) parameters in indicator. The variables playing the role of external parameters, now will be the Terminal Global Variables or TGV, for short. If you want to see the TGV, responsible for the former external parameters of indicator, you can simply press F3 key in terminal. I can't find another simple way to control the indicator parameters.

Second (and this is important), on any change in external parameters of indicator we have to recalculate all its values throughout the history, again and from scratch. In other words, we'll have to perform calculations, that are usually made only on very first start of indicator. Indicator's calculations optimization remains, but now it becomes more subtle.

Several code parts of the first version of modified indicator are below. Full code is attached in the end of this article.

 

"Standard" version: the description of changes in standard indicator source code

External parameters are no longer external, but simply global variables

All external parameters of indicator have lost their input modifier. Generally, I could not even make them global, but I decided to do so by tradition:

int              MA_Period   = 13;
int              MA_Shift    =  0;
ENUM_MA_METHOD   MA_Method   =  0;
int              Updated     =  0;     /// Indicates whether the indicator has updated after changing it's values

The first three options - are the period, the offset and the type of MA, and the fourth - Updated - is responsible for the calculations optimization, when changing the MA parameters. Explanations are few lines below. 

Virtual keys codes

Enter the codes for virtual keys:

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

These are codes for "up arrow" and "down arrow" keys, similar arrows on the numeric keyboard (key "8" and "2"), as well as the TAB key. The same codes (with other names of VK_XXX constants) actually exist in the <MT5dir>\MQL5\Include\VirtualKeys.mqh file, but in this case I decided to leave it as it is.


Small code correction in function, calculating linear weighted moving average (LWMA)

I've made a little tweak in CalculateLWMA() function: in original version the weightsum variable was declared using the static modifier. Apparently, the only reason why developers did that was the need to pre-calculate it on the first call of this function. Furthermore in code this variable remains unchanged. Here's the original code of this function, in which the parts related to the calculation and weightsum use are marked with comments:

void CalculateLWMA(int rates_total,int prev_calculated,int begin,const double &price[])
  {
   int              i,limit;
   static int     weightsum;                       // <-- using weightsum
   double               sum;
//--- first calculation or number of bars was changed
   if(prev_calculated==0)                          // <-- using weightsum
     {
      weightsum=0;                                 // <-- using  weightsum
      limit=InpMAPeriod+begin;                     // <-- using 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++)                     // <-- using weightsum
        {
         int k=i-begin+1;                          // <-- using weightsum
         weightsum+=k;                             // <-- using 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;              // <-- using weightsum
      }
//---
  }

Previously, this variant worked quite fine, but when I ran the "indicator + advisor" tandem (this is mentioned in the end of this article), in this very type of MA came troubles. The main one was caused by the circumstances described above, i.e. weightsum was the static variable: this variable was constantly increasing since on each change of MA parameter on-the-fly it was necessary to recalculate it from scratch.

The easiest way to directly and immediately calculate the weightsum value (it is equal to the sum of integers from 1 to MA period - for this purpose there is a simple formula for the sum of arithmetic progression), and at the same time deny its status as a static, which I did. Now, instead of previous weightsum declaration using the static modifier, we declare it without him, just initialize with "correct" value and thus remove the initial loop of "variable accumulation".

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

Now, all works correctly.


OnCalculate() function as a handler

I had to make a lot of changes in the OnCalculate() function, and therefore I quote here its code completely.

int OnCalculate(const int rates_total,
                const int prev_calculated,            /// Mathemat: full recalculation!
                const int begin,                      /// Mathemat: full recalculation!
                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 - optimized by Mathemat)

   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);
  }
//+------------------------------------------------------------------+

The main change relates to the perceived need for complete calculation of indicator "from scratch": it is obvious, that if a user's keyboard manipulation changed MA period from 13 to 14, all previous optimizations of its calculations are already useless, and we have to calculate MA again. This occurs when the Updated variable has 0 value (TGV has changed after pressing the hot-key, but the tick, that redraws the indicator, has not came yet).

However, in addition, previously we have to explicitly call the OnInit() function, because we needed to change the indicator short name, which will be displayed when a cursor hovers over the line. After initial MA calculation the Updated TGV is set to 1, which opens the way to the optimized indicator calculation - until you again are not willing to change some indicator's parameter on-the-fly.


OnChartEvent() handler

Below is a simple code of the OnChartEvent() handler:

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;
}//+------------------------------------------------------------------+      

The handler works as follows: you press the hot-key, its virtual code is defined, and then the changeTerminalGlobalVar() auxiliary function is started and is correctly modifying the desired TGV. After this the Updated flag is reset to zero, waiting for tick, which will launch OnCalculate() and redraw the indicator "from scratch".


Auxiliary function that "correctly" changes the TGV

And, finally, the code of changeTerminalGlobalVar() function, used in the OnChartEvent() handler:

void changeTerminalGlobalVar( string name, int dir = 0 )
{
   int var = GlobalVariableGet( name );
   int newparam = var + dir;
   if( name == "MA_Period" )
   {
      if( newparam > 0 )       /// Possible period is valid for MA
      {
         GlobalVariableSet( name, newparam ); 
         MA_Period = newparam;     /// Don't forget to change the global variable    
      }   
      else                       /// we do not change the period, because MA period is equal to 1 minimum
      {   
         GlobalVariableSet( name, 1 );     
         MA_Period = 1;     /// Don't forget to change the global variable 
      }   
   }   
      
   if( name == "MA_Method" )    /// Here when you call the 'dir' it is always equal to 1, the dir value is not important    
   {
      newparam = ( var + 1 ) % 4;
      GlobalVariableSet( name, newparam );  
      MA_Method = newparam;
   }      

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

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

The main purpose of this function - the correct calculation of new MA parameters with taking into account "physical limitations". Obviously, we can't make the MA period less than 1, the MA shift can be random, but the MA type is the number from 0 to 3, corresponding to a conditional member number in the ENUM_MA_METHOD enumeration.

 

Checking. It works, but "at C grade". What we can do?

OK, let's apply our indicator to the chart and start to sporadically press the hot-keys, that change the MA parameters. Yes, everything works correctly, but there is one unpleasant fact: the TGVs are changing immediately (you can check this by calling the TGV using F3 key), but MA are redrawing not always immediately, but only on the new tick arrival. If we have an American session with the active ticks flow, then we can hardly notice the delay. But if it happens at night, during the calm, we can wait the redrawing for a several minutes. What's up?

Well, as they say, what ye write, so shall ye got. Prior to build 245 in indicators there was only one "entry point" - the OnCalculate() function. Of course, I'm not talking about OnInit() and OnDeinit() functions, that provide indicator's initial calculations, initialization and completion. Now there are several entry points and they are connected with  the new Timer and ChartEvent events.

However, the new handlers are doing only what they include and are not formally associated with the OnCalculate() handler. So, what can we do with our "alien" OnChartEvent() handler to make it work "right", i.e. it would allow to immediately redraw the MA?

In general, there are several ways to implement this requirement:

  • "Matryoshka" (OnCalculate() call inside of OnChartEvent()): insert the OnCalculate() function call into this handler, pre-filling all of its parameters. As the OnChartEvent() handler implies changing at least one MA parameter, then it will affect all of its history, i.e. we must recalculate again it "from scratch', without the optimization of calculations.
  • "Artificial tick" that transfers control to the beginning of OnCalculate() function, which modifies the graphical buffer. Apparently, there are no "legitimate" methods, as reflected in the MT5 documentation (although, perhaps I've searched not so thoroughly). If you are interested you can search for something like «API», «PostMessageA», etc. Therefore, we will not consider this variant here, because it doesn't guarantee that undocumented features someday will not change. I have no doubt that it can be realized.

 

"Matryoshka" works!

It turns out, that we have already done the most important thing. Below is a very simple code of function. You can simply insert its call directly in front of the return operator of OnChartEvent() handler.

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 );
}//+------------------------------------------------------------------+

After compiling indicator and applying it to the chart, we see that in general the code works quickly and independently of ticks arrival.

The disadvantage of this implementation is that exactly the Close prices are copied into the price[] array. If desired, the CopyClose() function can be replaced with what we want by setting the field "Apply to" on "Settings" tab of the indicator properties dialog box. If the current price will be basic (Open, High, Low, Close), then we already have the corresponding CopyXXXX() function. In the case of more complicated prices (Median, Typical or Weighted) we have to calculate the array in different way.

I'm not sure if we don't need the CopyClose() function, that copies the entire history of array. On the other hand, this function is fast enough when the history is not too deeply loaded. Checking the indicator on EURUSD H1 with history before 1999 (about 700 thousand bars) showed that the indicator deals with calculations and does not show any slowdown. On such history the possible slowdowns may be caused not by the CopyXXXX() function, but by the need of more complicated indicator recalculation from the beginning of history (this is obligatory).


Several findings and conclusion

What is better - one indicator file or "indicator + advisor" tandem?

In fact, this question is not so simple. On the one hand, it is good if we have one indicator file, because all functions, including event handlers, are concentrated in one place.

On the other hand, let's imagine that there are 3 or 4 indicators applied to chart along with an Expert Advisor - this situation is not uncommon. Furthermore, assume that each indicator is equipped with its own event handler, in addition to standard OnCalculate(). To avoid confusion with events processing in this "motley crue" it's more reasonable to concentrate all event handlers, now allowed in the indicators, in one place - in Expert Advisor.

For long time software developers have been deciding to give us the ability to process events in the indicator: from the non-public beta-release 09.09.09 (when the indicator is considered as "pure calculation & mathematical entity" and must not be contaminated by any features that hinder the speed of calculation) passed exactly 5 months. Likely, the "purity of idea" has to suffer - and now some real chaos of programmers fantasies will be unleashed. But the balance is always somewhere in the middle between pure, but limited idea, and not so clean, but more powerful capability.

In September-October of 2009, when the MT5 beta version build number has not even reached 200, I've wrote and debugged code of "Expert Adviser + Indicator" tandem, that allowed to manage MA parameters on-the-fly, but "at C grade": it was updated only after tick arrival, but not immediately. At that time this tandem was the only possible solution, and now unlikely it's interesting to anyone.

I could not then think of how to bring indicator functionality to "B grade", i.e. as it is presented in the latest version. Now I am pleased to provide more convenient solution to all whom it may be interesting.


Attached is my short video, demonstrating the work of our creation. Smooth change of the MA curve (only the period is changing - at first it increases, then decreases) is even dazzling in some way. This is Matryoshka (similarly to famous Russian set of nesting dolls).


Of course, such tricks are useful only when the calculation of indicator itself from scratch doesn't takes too much time. The simple MA, contained in this indicator, meet this requirement.


One slippery aspect

Remember, that the former external parameters of the indicator are now the terminal global variables (TGV), which you can see by pressing F3 key. Suppose, that you've opened the Global Variables dialog box and changed one of TGV's (for example, the MA period). You expect that this change is immediately reflected in the indicator on chart.

Currently in terminal there is no event, corresponding to TGV editing made by user (for example, CHARTEVENT_GLOBALVAR_ENDEDIT). Also, I think, we can't disable TGV modification in Global Variables dialog box. Therefore, here we can't reckon on any event except for the tick. What will happen in real?

If you don't touch the keyboard, then even on the next tick the update will be "wrong": the Updated variable wasn't set to zero, and therefore only the "optimized" calculation of indicator (corresponding to previous value of changed TGV) will be made. In this case, to restore justice, we can advise only one thing: after editing TGV you should at least once press the hot-key, that modifies the TGV, sets the Updated = 0 and causes full recalculation of indicator.

All possible users and developers should bear this fact in mind.


Attached files of source code and video

Finally, I'm attaching the source code files. Explanation:

  1. Custom Moving Average.mq5 - MA source code file, that ships with MT5.
  2. MyMA_ind_with_ChartEvent.mq5 - initial ("C grade") implementation : indicator updates only after tick arrival.
  3. MyMA_ind_with_ChartEvent_Matryoshka.mq5 - the second (perhaps, "B grade") variant : the indicator updates immediately, without waiting for tick arrival.

Translated from Russian by MetaQuotes Software Corp.
Original article: https://www.mql5.com/ru/articles/39

Last comments | Go to discussion (1)
onewithzachy
onewithzachy | 29 Jun 2012 at 16:10

Though I'd love to see that MA line's sneaky move at my command, anyone who knows MQL4 will be sad that in MQL5 we really can't call and change any indicator parameter on the-the-fly.

In MQL5 once it's handle initialized, the indicator is fixed - dead to it's parameter. I can't no longer scan price movement with different Period, because indicator's Period is already fixed. 

In MQL4, we can call indicator right inside start(), and change it's parameter as many as we like. 

No wonder, Integer wrote so many ...OnArray libraries in code base.

:( 

Step on New Rails: Custom Indicators in MQL5 Step on New Rails: Custom Indicators in MQL5

I will not list all of the new possibilities and features of the new terminal and language. They are numerous, and some novelties are worth the discussion in a separate article. Also there is no code here, written with object-oriented programming, it is a too serous topic to be simply mentioned in a context as additional advantages for developers. In this article we will consider the indicators, their structure, drawing, types and their programming details, as compared to MQL4. I hope that this article will be useful both for beginners and experienced developers, maybe some of them will find something new.

Here Comes the New MetaTrader 5 and MQL5 Here Comes the New MetaTrader 5 and MQL5

This is just a brief review of MetaTrader 5. I can't describe all the system's new features for such a short time period - the testing started on 2009.09.09. This is a symbolical date, and I am sure it will be a lucky number. A few days have passed since I got the beta version of the MetaTrader 5 terminal and MQL5. I haven't managed to try all its features, but I am already impressed.

False trigger protection for Trading Robot False trigger protection for Trading Robot

Profitability of trading systems is defined not only by logic and precision of analyzing the financial instrument dynamics, but also by the quality of the performance algorithm of this logic. False trigger is typical for low quality performance of the main logic of a trading robot. Ways of solving the specified problem are considered in this article.

Using text files for storing input parameters of Expert Advisors, indicators and scripts Using text files for storing input parameters of Expert Advisors, indicators and scripts

The article describes the application of text files for storing dynamic objects, arrays and other variables used as properties of Expert Advisors, indicators and scripts. The files serve as a convenient addition to the functionality of standard tools offered by MQL languages.