Synchronizing several same-symbol charts on different timeframes

Dmitriy Gizlyk | 6 June, 2018

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:

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:

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:

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