Synchronizing several same-symbol charts on different timeframes

6 June 2018, 13:30
Dmitriy Gizlyk
4
15 621

Introduction

From Elder to the present day, traders make trading decisions analyzing charts at different timeframes. I think, many of you are familiar with the situation when objects that display global trends are applied to higher timeframe charts. After that, a price behavior is analyzed near objects on lower timeframes. During such an analysis, previously created objects may change. Existing MetaTrader 5 tools allow performing this work on a single chart by changing a timeframe while preserving applied objects. But what if you need to keep track of a price on several charts simultaneously?

You can use templates for that. However, changing even a single object requires that you re-save the template and re-apply it to all charts. In this article, I propose to automate this process and assign the function of synchronizing charts to an indicator.


1. Setting a task

The main task of our indicator is synchronization of charts open in MetaTrader 5. The program should define necessary charts by a symbol. At the same time, the program should constantly monitor the state of all graphical objects on all the charts of interest. Each change of an object on one of the charts should be repeated by the program on the remaining ones.

The status of graphical objects can be tracked in two ways. The first approach: defining periodicity, as well as periodically checking and synchronizing all applied objects. The advantage of this approach is that we need only one instance of the program on one of the chart. But there are two issues:

  • synchronization delay caused by update periodicity;
  • it is difficult to determine what object status should be considered to be the last one.

At first glance, both issues are easily solved by increasing the synchronization frequency and storing the last synchronized data about objects in the program variables or in a disk file. But as the number of objects on the charts increases, so do the time spent for executing each loop and a volume of stored data. Unlike EAs and scripts, indicators are launched in the general flow of MetaTrader 5. Therefore, an excessive load on the indicator can lead to delays in the performance of other indicators and the terminal as a whole.

The second approach is to assign tracking of object changes to the terminal and start synchronization of objects by terminal events processing them in the OnChartEvent function. This approach allows the program to react immediately after creating or modifying an object and thus minimizes the delay. Therefore, we do not need to save data about all synchronized objects, nor periodically check their status on all charts. This seriously reduces the load of the program.

It seems that the second option suits us perfectly. However, it also comes with a small drawback: the OnChartEvent function can only be called by the events of a chart the program is launched at. This would not stop us in case we were able to define the Master chart to conduct all calculations. A single indicator instance would be enough in that case. But we do not want to be limited to one chart for changing objects. We need to run the indicator instances on each chart. This work can be done manually or it can be automated thanks to the ChartIndicatorAdd function.

Taking into account all the above, I chose the second option for the program implementation. Thus, the indicator operation can be divided into two blocks.

  1. When the indicator is launched, the open charts are sorted by the symbol. The presence of the indicator on the open charts of the corresponding symbol is checked. All objects on the current chart are cloned to the selected charts.
  2. Initialization block diagram

  3. Processing chart events. When creating or modifying a graphical object, the program reads a data on a modified object and passes it to all charts from the previously formed list.

Events handling

While the program is running, users are able to open and close charts. Therefore, in order to maintain the relevance of the chart list, I would suggest launching the first process by the timer with a certain periodicity.


2. Arranging work with charts

The first process is cloning an indicator to charts. To solve this problem, we create the CCloneIndy class. We save a symbol and indicator names, as well as the path for calling the indicator in its variables. The class has one public function (SearchCharts) for selecting necessary charts. The function receives an ID of a source chart and returns an array of selected charts.

class CCloneIndy
  {
private:
   string            s_Symbol;
   string            s_IndyName;
   string            s_IndyPath;

public:
                     CCloneIndy();
                    ~CCloneIndy();
   bool              SearchCharts(long chart,long &charts[]);

protected:
   bool              AddChartToArray(const long chart,long &charts[]);
   bool              AddIndicator(const long master_chart,const long slave_chart);
  };

When initializing the class, save the source data to the variables and set a short name for the indicator. We will need it to get parameters necessary for launching the indicator copies.

CCloneIndy::CCloneIndy()
  {
   s_Symbol=_Symbol;
   s_IndyName=MQLInfoString(MQL_PROGRAM_NAME);
   s_IndyPath=MQLInfoString(MQL_PROGRAM_PATH);
   int pos=StringFind(s_IndyPath,"\\Indicators\\",0);
   if(pos>=0)
     {
      pos+=12;
      s_IndyPath=StringSubstr(s_IndyPath,pos);
     }
   IndicatorSetString(INDICATOR_SHORTNAME,s_IndyName);
  }

2.1. The function for selecting charts by a symbol

Let's consider in detail how the function for selecting necessary charts works. First, we will check the ID of a Master chart passed in the function parameters. If it is not valid, the function immediately returns 'false'. If the ID is not set, assign the current chart's ID to the parameter. Then, let's check if the name of the saved symbol corresponds to the Master chart's one. If it does not, re-write the symbol name for sorting out charts.

bool CCloneIndy::SearchCharts(long chart,long &charts[])
  {
   switch((int)chart)
     {
      case -1:
        return false;
        break;
      case 0:
        chart=ChartID();
        break;
      default:
        if(s_Symbol!=ChartSymbol(chart))
           s_Symbol=ChartSymbol(chart);
        break;
     }

Next, iterate over all open charts. If the ID of the verified chart is equal to the Master chart ID, move to the next one.

   long check_chart=ChartFirst();
   while(check_chart!=-1)
     {
      if(check_chart==chart)
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

Then check if the verified chart's symbol corresponds to the one we are looking for. If a symbol does not satisfy the search condition, proceed to the next chart.

      if(ChartSymbol(check_chart)!=s_Symbol)
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

After that, check if the indicator is present on the verified chart. If yes, save the chart ID to the array and proceed to the next one.

      int handl=ChartIndicatorGet(check_chart,0,s_IndyName);
      if(handl!=INVALID_HANDLE)
        {
         AddChartToArray(check_chart,charts);
         check_chart=ChartNext(check_chart);
         continue;
        }

Otherwise, launch the indicator call function specifying Master and verified charts' IDs. If unsuccessful, move on to the next chart. An attempt will be made to attach the indicator to the previous chart during the next function call.

      if(!AddIndicator(chart,check_chart))
        {
         check_chart=ChartNext(check_chart);
         continue;
        }

If the indicator is successfully attached to the chart, add the chart ID to the array. The function of cloning all objects from the Master chart to the verified one is launched.

      AddChartToArray(check_chart, charts);
      check_chart=ChartNext(check_chart);
     }
//---
   return true;
  }

Upon the loop completion, exit the function and return 'true'.

2.2. The indicator call function

Let's dwell on the function of binding the indicator to the chart. The function receives IDs of the Master and recipient charts in the parameters. Their validity is checked at the beginning of the function: the IDs should be valid and not the same.

bool CCloneIndy::AddIndicator(const long master_chart,const long slave_chart)
  {
   if(master_chart<0 || slave_chart<=0 || master_chart==slave_chart)
      return false;

Return the indicator handle on the Master chart. If it is invalid, exit the function with the 'false' result.

   int master_handle=ChartIndicatorGet(master_chart,0,s_IndyName);
   if(master_handle==INVALID_HANDLE)
      return false;

If the handle is valid, we get the parameters for calling a similar indicator on a new chart. If the parameters are not received, exit the function with the 'false' result.

   MqlParam params[];
   ENUM_INDICATOR type;
   if(IndicatorParameters(master_handle,type,params)<0)
      return false;

Next, write the indicator call path, saved during the initialization, to the first array slot, while the ID of the chart to be processed by the indicator is written to the second one. Define the timeframe of the recipient chart and call the indicator. If the indicator call fails, exit the function with the 'false' result.

   params[0].string_value=s_IndyPath;
   params[1].integer_value=slave_chart;
   ENUM_TIMEFRAMES Timeframe=ChartPeriod(slave_chart);
   int slave_handle=IndicatorCreate(s_Symbol,Timeframe,type,ArraySize(params),params);
   if(slave_handle<0)
      return false;

To complete the function, add the indicator to the recipient chart by a received handle.

   return ChartIndicatorAdd(slave_chart,0,slave_handle);
  }

You may ask why we cannot attach the indicator to the recipient chart by the Master chart's handle immediately. The answer is simple: the indicator and the chart should match by a symbol and a timeframe. Based on our tasks, the timeframes of the charts will be different.

You can analyze the source code of the class in the attachment.

3. Classes for working with graphical objects

The next process of our program is handling events and passing data about graphical objects to other charts. Before writing the code, we should define how data is to be passed between charts.

MQL5 tools allow programs on one chart to create and modify objects on another one by specifying the chart ID in the functions for working with graphical objects. This is suitable for working with a small number of charts and graphical objects.

But there is another option. Earlier, we decided to use events to track changes of objects on the chart. We even wrote the code for adding the indicator instances to all charts of interest. We can use the event model to pass data on changed objects to other indicators on different charts. The task of working with objects is assigned to the indicator applied to the chart. This allows us to distribute work with objects among all the indicators creating a sort of an asynchronous model.

The plan looks good, but we know that the OnChartEvent function gets only four parameters:

  • even ID;
  • long-type event parameter;
  • double-type event parameter;
  • string-type event parameter.

How to fit all data about an object into these four parameters? We will simply pass an event ID, while all the information about the object is written into a string-type parameter. We will use the results of the article "Using cloud storage services for data exchange between terminals" to gather data about the object to a single string-type variable.

Let's create the CCloneObjects class to gather data on graphical objects and then display them on the chart.

class CCloneObjects
  {
private:
   string            HLineToString(long chart, string name, int part);
   string            VLineToString(long chart, string name, int part);
   string            TrendToString(long chart, string name, int part);
   string            RectangleToString(long chart, string name, int part);
   bool              CopySettingsToObject(long chart,string name,string &settings[]);

public:
                     CCloneObjects();
                    ~CCloneObjects();
//---
   string            CreateMessage(long chart, string name, int part);
   bool              DrawObjects(long chart, string message);
  };

Operation of the functions of this class is described in details: I think, there is no need to repeat the description here. However, pay attention to this: when generating a custom event using the EventChartCustom function, the 'sparam' parameter length is limited by 63 characters. Therefore, when you pass data about an object to other charts, we split the message into two parts. To achieve this, the message creation function features the parameter for setting the necessary portion of data. The code of the function for gathering data about a trend line is displayed below as an example.

string CCloneObjects::TrendToString(long chart,string name, int part)
  {
   string result = NULL;
   if(ObjectFind(chart,name)!=0)
      return result;
   
   switch(part)
     {
      case 0:
        result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=0="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,0),5)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_TIME)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_TIME,0))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=1="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,1),5)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_TIME)+"=1="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_TIME,1))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_RAY_LEFT)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_RAY_LEFT))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_RAY_RIGHT)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_RAY_RIGHT))+"|";
        break;
      default:
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_COLOR)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_COLOR))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_WIDTH)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_WIDTH))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_STYLE)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_STYLE))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_BACK)+"="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_BACK))+"|";
        result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TEXT)+"="+ObjectGetString(chart,name,OBJPROP_TEXT)+"|";
        result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TOOLTIP)+"="+ObjectGetString(chart,name,OBJPROP_TOOLTIP);
        break;
     }
   return result;
  }

The code of all the functions is attached below.


4. Assembling the indicator

Everything is ready. Now, it is time to assemble our indicator to track and copy graphical objects. The indicator will have a single parameter — chart ID.

sinput long    Chart =  0;

When launching the indicator, the parameter value is always equal to zero. You may ask, why include a parameter that never changes.

Its value changes when calling the indicator from the program for attaching to other charts.

The ChartID function returns the ID of the chart the indicator has been called from (rather than the one it has been attached to). This happens due to some nuances of indicator processing in MetaTrader 5. If one indicator is called for the same symbol and timeframe several times, it is launched only once — during the first call. All subsequent calls to it (even from other charts) are directed to the already launched indicator. In turn, the indicator works on its chart and returns information about it. Thus, when you call the indicator instances in the CCloneIndy class, new indicator copies will work and return data on the chart its first copy has been launched from. To avoid this, we should assign a specific chart for each indicator instance.

Let's have a detailed look at the indicator code. The following items are declared in the global variables block:

  • copies of previously created classes,
  • variable for storing the working chart ID
  • and the array for storing IDs of the chart the objects are to be cloned to.
CCloneIndy    *CloneIndy;
CCloneObjects *CloneObjects;
long           l_Chart;
long           ar_Charts[];

In the OnInit function, initialize class instances for working with charts and objects.

int OnInit()
  {
//--- indicator Create classes
   CloneIndy   =  new   CCloneIndy();
   if(CheckPointer(CloneIndy)==POINTER_INVALID)
      return INIT_FAILED;
   CloneObjects=  new CCloneObjects();
   if(CheckPointer(CloneObjects)==POINTER_INVALID)
      return INIT_FAILED;

Initialize working chart ID.

   l_Chart=(Chart>0 ? Chart : ChartID());

Let's search for open charts by symbol. If necessary, the indicator copies are added to the detected charts.

   CloneIndy.SearchCharts(l_Chart,ar_Charts);

At the end of the function, initialize the timer with an interval of 10 seconds. Its only objective is to update the list of charts for cloning objects.

   EventSetTimer(10);
//---
   return(INIT_SUCCEEDED);
  }

The OnCalculate function does not perform any operations. As mentioned above, our indicator is based on the event model. This means the entire functionality of our indicator is concentrated in the OnChartEvent function. Declare auxiliary local variables at the beginning of the function.

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   string message1=NULL;
   string message2=NULL;
   int total=0;

Next, build operation branching depending on an incoming event using the 'switch' operator.

The first block of operations gathers and passes data on creating and modifying an object to other charts. It is called by events of creating, modifying or moving an object on a chart. If these events occur, the indicator creates two messages with the state of the object, and then runs the loop to send them to all the charts with the IDs from our array.

   switch(id)
     {
      case CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_OBJECT_DRAG:
        message1=CloneObjects.CreateMessage(l_Chart,sparam,0);
        message2=CloneObjects.CreateMessage(l_Chart,sparam,1);
        total=ArraySize(ar_Charts);
        for(int i=0;i<total;i++)
          {
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,message1);
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,message2);
          }
        break;

The next block is launched when the object is removed from the chart. In this case, there is no need for a separate message, since only its name is enough to remove the object, and we already have it in the 'sparam' variable. Therefore, the loop of sending messages to other charts is launched at once.

      case CHARTEVENT_OBJECT_DELETE:
        total=ArraySize(ar_Charts);
        for(int i=0;i<total;i++)
           EventChartCustom(ar_Charts[i],(ushort)id,0,0,sparam);
        break;

The following two blocks process the messages received from other charts. When the information about creating or modifying the object arrives, we call the function for displaying an object on the chart. Pass the working chart ID and the incoming message in the function parameters.

      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_CHANGE:
      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_CREATE:
      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_DRAG:
        CloneObjects.DrawObjects(l_Chart,sparam);
        ChartRedraw(l_Chart);
        break;

When receiving data on removing an object, call the function for removing a similar object on a working chart.

      case CHARTEVENT_CUSTOM + CHARTEVENT_OBJECT_DELETE:
        ObjectDelete(l_Chart,sparam);
        ChartRedraw(l_Chart);
        break;
     }
  }

The indicator code and applied classes are provided in the attachment.


Conclusion

In this article, we have proposed the method for developing the indicator, which automatically copies graphical objects between terminal charts in real time. This method implements the mechanism of the two-way data exchange between charts opened in the terminal. It does not limit a user in the number of synchronized charts. At the same time, users are able to create, modify and remove graphical objects on any of the synchronized charts. The indicator operation is displayed in the video:



References

  1. Using cloud storage services for data exchange between terminals

Programs used in the article

#
 Name
Type 
Description 
1 ChartObjectsClone.mq5  Indicator  Indicator of data exchange between charts
2 CloneIndy.mqh  Class library  Class for selecting charts by a symbol
3 CloneObjects.mqh  Class library  Class for working with graphical objects
4 ChartObjectsClone.mqproj    Project description file

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

Attached files |
MQL5.zip (56.08 KB)
Last comments | Go to discussion (4)
Tapani
Tapani | 16 Aug 2018 at 14:05
I am looking to synchronize support and resistance levels on different timeframes, i.e. support added on one time frame appears on all the other timeframe charts
Dmitriy Gizlyk
Dmitriy Gizlyk | 16 Aug 2018 at 15:30
Tapani:
I am looking to synchronize support and resistance levels on different timeframes, i.e. support added on one time frame appears on all the other timeframe charts

Hello,
How you have added resistance levels on chart? Like horizontal line or rectangle box?

yousurfer
yousurfer | 24 Sep 2018 at 09:40

Is there a way to choose to clone only certain types of objects and ignore the rest? Also, how would I clone the objects visibility properties as well because it seems the the cloned objects visibility setting is not copied over and must be manually set again on the other charts.

Many thanks

Dmitriy Gizlyk
Dmitriy Gizlyk | 24 Sep 2018 at 22:00
yousurfer:

Is there a way to choose to clone only certain types of objects and ignore the rest? Also, how would I clone the objects visibility properties as well because it seems the the cloned objects visibility setting is not copied over and must be manually set again on the other charts.

Many thanks

Hello, yousurfer.
If you want select objects by some of property you can add it in this function

string CCopyObject::CreateMessage(long chart)
  {
   string result = NULL;
   int total = ObjectsTotal(chart, 0);
   for(int i=0;i<total;i++)
     {
      string name = ObjectName(chart, i, 0);
//
//    if(!( check object property to copy) )    // if you don't want to copy this object
//       continue;                              // go to next object
//
      switch((ENUM_OBJECT)ObjectGetInteger(chart,name,OBJPROP_TYPE))
        {
         case OBJ_HLINE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_HLINE)+"|"+HLineToString(chart, name)+"}";
           break;
         case OBJ_VLINE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_VLINE)+"|"+VLineToString(chart, name)+"}";
           break;
         case OBJ_TREND:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_TREND)+"|"+TrendToString(chart, name)+"}";
           break;
         case OBJ_RECTANGLE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_RECTANGLE)+"|"+RectangleToString(chart, name)+"}";
           break;
        }
     }
   return result;
  }

Regards,
Dmitriy.

Multi-symbol balance graph in MetaTrader 5 Multi-symbol balance graph in MetaTrader 5

The article provides an example of an MQL application with its graphical interface featuring multi-symbol balance and deposit drawdown graphs based on the last test results.

Deep Neural Networks (Part V). Bayesian optimization of DNN hyperparameters Deep Neural Networks (Part V). Bayesian optimization of DNN hyperparameters

The article considers the possibility to apply Bayesian optimization to hyperparameters of deep neural networks, obtained by various training variants. The classification quality of a DNN with the optimal hyperparameters in different training variants is compared. Depth of effectiveness of the DNN optimal hyperparameters has been checked in forward tests. The possible directions for improving the classification quality have been determined.

ZUP - Universal ZigZag with Pesavento patterns. Search for patterns ZUP - Universal ZigZag with Pesavento patterns. Search for patterns

The ZUP indicator platform allows searching for multiple known patterns, parameters for which have already been set. These parameters can be edited to suit your requirements. You can also create new patterns using the ZUP graphical interfaces and save their parameters to a file. After that you can quickly check, whether these new patterns can be found on charts.

Developing multi-module Expert Advisors Developing multi-module Expert Advisors

MQL programming language allows implementing the concept of modular development of trading strategies. The article shows an example of developing a multi-module Expert Advisor consisting of separately compiled file modules.