Русский 中文 Español Deutsch 日本語 Português
preview
MQL5 Cookbook — Services

MQL5 Cookbook — Services

MetaTrader 5Examples | 27 January 2023, 13:34
5 919 1
Denis Kirichenko
Denis Kirichenko

Introduction

Since recently, MetaTrader 5 features a new program type known as a service. According to the developer, services allow users to create custom price feeds for the terminal, i.e. to implement price delivery from external systems in real time, just like it is implemented on brokers' trade servers. This is not the only feature of services.

In this article, I will consider the nuances of working with services. The article is focused mostly on beginners. Based on this, I tried to make the code completely reproducible and more complicated from one example to another.



1. Daemons at work

The services in MQL5 have similarities with Windows services. Wikipedia gives the following definition of a service:

Windows service is a computer program that operates in the background. From the definition, it becomes clear that it has much in common with the concept of daemons in Unix.

In our case, the external environment for services is not the operating system itself, but the MetaTrader5 terminal shell.

A few words about daemons.

A daemon is a computer program  that runs as a background process, rather than being under the direct control of an interactive user.

The term was coined by the programmers at MIT's Project MAC. рус.According to Fernando J. Corbató, who worked on Project MAC in 1963, his team was the first to use the term daemon, inspired by Maxwell's demon, an imaginary agent in physics and thermodynamics that helped to sort molecules.UNIX systems inherited this terminology.

Maxwell's demon is consistent with Greek mythology interpretation of a daemon as a supernatural being working in the background. As stated in the Unix System Administration Handbook, the ancient Greeks' concept of a "personal daemon" was similar to the modern concept of a "guardian angel".

Although the ancient Greeks did not have computers, the relationships of entities were clear to them.



2. Services – Documentation info

Before delving into the topic, I suggest to skim through the Documentation materials and see how the developer describes the capabilities of the services.

2.1 Application types

On the first page of the Documentation, namely in the Types of MQL5 Applications section, a service is defined as a type of MQL5 program:

  • A service  is a program that, unlike indicators, Expert Advisors and scripts, does not require to be bound to a chart to work. Like scripts, services do not handle any event except for trigger. To launch a service, its code should contain the OnStart handler function. Services do not accept any other events except Start, but they are able to send custom events to charts using EventChartCustom. The services are stored in <terminal_directory>\MQL5\Services.

Note here that services are very similar to scripts. The fundamental difference is that they are not tied to any of the charts.


2.2 Program execution

The Program running section provides a summary of the programs in MQL5:

Program
Running Note
  Service
 In its own thread, there are as many threads of execution as there are services
 A looped service cannot break running of other programs
  Script
 In its own thread, there are as many threads of execution as there are scripts
 A looped script cannot break running of other programs
  Expert Advisor
 In its own thread, there are as many threads of execution as there are EAs
 A looped Expert Advisor cannot break running of other programs
 Indicator
 One thread for all indicators on a symbol. The number of threads is equal to the number of symbols with indicators
 An infinite loop in one indicator will stop all other indicators on this symbol

In other words, services do not differ from scripts and EAs in terms of the method of activating the execution flow. Services are also similar to scripts and EAs in that the presence of looped code blocks does not affect the operation of other mql5 programs.


2.3 Prohibition on the use of functions in services

The developer provides an exhaustive list of functions not allowed for use:

This is reasonable since services cannot stop the Expert Advisor and work with the timer, since they handle a single Start event. They also cannot work with the functions of custom indicators.

 
2.4 Loading and unloading services

The appropriate Documentation section features several important points. Let's consider each of them.

Services are loaded immediately after starting the terminal, if they were still running at the moment of the terminal shutdown. Services are unloaded immediately after completing their work.

This is one of the remarkable properties of a service. It should not be monitored. It automatically performs its tasks after being launched once.

Services have a single OnStart() handler, in which you can implement an endless data receiving and handling loop, for example creating and updating custom symbols using the network functions.

We can draw a simple conclusion. If the service should perform a set of one-time actions, then it is not necessary to loop any block of code. If the task involves the constant or regular operation of the service, then it is necessary to wrap the code block in a loop. We will consider examples of such tasks later.

Unlike Expert Advisors, indicators and scripts, services are not bound to a specific chart, therefore a separate mechanism is provided to launch them.

 Perhaps, this is the second remarkable feature of the service. It does not require any schedule to work.

A new service instance is created from the Navigator using the "Add Service" command. A service instance can be launched, stopped and removed using the appropriate instance menu. To manage all instances, use the service menu.

This is the third remarkable property of the service. Having only one program file, you can run several instances of it at the same time. This is usually done when you need to use different parameters (Input variables).


3. Service prototype

The terminal Help that can be opened by pressing F1 describes the mechanism for starting and managing services. Therefore, we will not dwell on this now.

In MetaEditor, create the service template and name it dEmpty.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
  }
//+------------------------------------------------------------------+

After compilation, we will be able to see the service name in the Navigator (Fig. 1).


dEmpty service

Fig. 1. dEmpty service in the Navigator subwindow

After adding and launching the dEmpty service in the Navigator subwindow, we get the following entries in the Journal:

CS      0       19:54:18.590    Services        service 'dEmpty' started
CS      0       19:54:18.592    Services        service 'dEmpty' stopped

The logs show that the service was started and stopped.  Since its code contains no commands, there will be no changes in the terminal. We will not notice anything after launching the service.

Let's fill the service template with some commands. Create the dStart.mq5 service and write the following lines:

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));   
  }
//+------------------------------------------------------------------+

After launching the service, we will see the following entry on the Experts tab:

CS      0       20:04:28.347    dStart       Service "dStart" starts at: 2022.11.30 20:04:28.

So, the dStart service has informed us of its launch and then has stopped.

Let's expand the capabilities of the previous service and name the new one dStartStop.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
   ::Sleep(1000);
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

The current service already informs not only of its start, but also of stopping its activity.

After starting the service in the journal, we will see the following entries:

2022.12.01 22:49:10.324 dStartStop   Service "dStartStop" starts at: 2022.12.01 22:49:10
2022.12.01 22:49:11.336 dStartStop   Service "dStartStop" stops at: 2022.12.01 22:49:11

It is easy to see that the first and second times differ by a second. The Sleep() native function has been triggered between the first and last commands.

Now let's extend the capabilities of the current service so that it runs until it is forcibly stopped.  Let's name the new service dStandBy.mq5.

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
   do
     {
      ::Sleep(1);
     }
   while(!::IsStopped());
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
//--- final goodbye
   for(ushort cnt=0; cnt<5; cnt++)
     {
      ::PrintFormat("Count: %hu", cnt+1);
      ::Sleep(10000);
     }
  }
//+------------------------------------------------------------------+

After exiting the do while loop due to the program stop, the service will still write several counter values to the log. After each such entry, Sleep() is called with a delay interval of 10 seconds.

The journal contains the following records:

CS      0       23:20:44.478    dStandBy     Service "dStandBy" starts at: 2022.12.01 23:20:44
CS      0       23:20:51.144    dStandBy     Service "dStandBy" stops at: 2022.12.01 23:20:51
CS      0       23:20:51.144    dStandBy     Count: 1
CS      0       23:20:51.159    dStandBy     Count: 2
CS      0       23:20:51.175    dStandBy     Count: 3
CS      0       23:20:51.191    dStandBy     Count: 4
CS      0       23:20:51.207    dStandBy     Count: 5

The service was started at 23:20:44 and forcibly stopped at 23:20:51. It is also easy to see that the intervals between the values of the counter do not exceed 0.02 seconds. Although a delay of 10 seconds was previously set for such intervals.

According to the Documentation regarding the Sleep() function:

Note

The Sleep() function cannot be called from custom indicators, since indicators are executed in the interface thread and should not slow it down. The function has a built-in check of the EA stop flag status every 0.1 seconds.

So, in our case, the Sleep() function quickly detected that the service was forcibly stopped and seized delaying the execution of the mql5 program.

For the sake of completeness, let's take a look at what the Documentation says about the return value of the IsStopped() status check function:

Return Value

Returns true, if the _StopFlag system variable contains a value other than 0. A nonzero value is written into _StopFlag, if a mql5 program has been commanded to complete its operation. In this case, you must immediately terminate the program, otherwise the program will be completed forcibly from the outside after 3 seconds.

Thus, after a forced stop, the service has 3 seconds to do something else before it is completely deactivated. Let's check this moment in practice. Let's add a matrix calculation to the code of the previous service after the loop. The calculation takes about a minute. We will see if the service has time to calculate everything after it was forcibly stopped. Let's name the new service srvcStandByMatrixMult.mq5.

After the loop of calculating the counter values, we need to add the following block to the previous code:

//--- Matrix mult
//--- matrix A 1000x2000
   int rows_a=1000;
   int cols_a=2000;
//--- matrix B 2000x1000
   int rows_b=cols_a;
   int cols_b=1000;
//--- matrix C 1000x1000
   int rows_c=rows_a;
   int cols_c=cols_b;
//--- matrix A: size=rows_a*cols_a
   int size_a=rows_a*cols_a;
   int size_b=rows_b*cols_b;
   int size_c=rows_c*cols_c;
//--- prepare matrix A
   double matrix_a[];
   ::ArrayResize(matrix_a, rows_a*cols_a);
   for(int i=0; i<rows_a; i++)
      for(int j=0; j<cols_a; j++)
         matrix_a[i*cols_a+j]=(double)(10*::MathRand()/32767);
//--- prepare matrix B
   double matrix_b[];
   ::ArrayResize(matrix_b, rows_b*cols_b);
   for(int i=0; i<rows_b; i++)
      for(int j=0; j<cols_b; j++)
         matrix_b[i*cols_b+j]=(double)(10*::MathRand()/32767);
//--- CPU: calculate matrix product matrix_a*matrix_b
   double matrix_c_cpu[];
   ulong time_cpu=0;
   if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu))
     {
      ::PrintFormat("Error in calculation on CPU. Error code=%d", ::GetLastError());
      return;
     }
   ::PrintFormat("time CPU=%d ms", time_cpu);

Launch the dStandByMatrixMult service and forcibly stop it after a few seconds. The following lines appear in the log:

CS      0       15:17:23.493    dStandByMatrixMult   Service "dStandByMatrixMult" starts at: 2022.12.02 15:17:23
CS      0       15:18:17.282    dStandByMatrixMult   Service "dStandByMatrixMult" stops at: 2022.12.02 15:18:17
CS      0       15:18:17.282    dStandByMatrixMult   Count: 1
CS      0       15:18:17.297    dStandByMatrixMult   Count: 2
CS      0       15:18:17.313    dStandByMatrixMult   Count: 3
CS      0       15:18:17.328    dStandByMatrixMult   Count: 4
CS      0       15:18:17.344    dStandByMatrixMult   Count: 5
CS      2       15:18:19.771    dStandByMatrixMult   Abnormal termination

As we see, the command to terminate the execution of the mql5 program arrived at 15:18:17.282. The service itself was forcibly terminated at 15:18:19.771. Indeed, 2.489 seconds passed from the moment of termination to the forced stop of the service. The fact that the service was stopped forcibly and, moreover, as a result of emergency termination, is shown by the  "Abnormal termination" entry.

Since no more than 3 seconds remain before forcibly stopping the service (_StopFlag  == true), it is not recommended to make any serious calculations or trading actions for the interrupted loop.

Here is a simple example. Suppose that the terminal features a service that closes all positions when the terminal itself is closed. The terminal closes and the service tries to liquidate all active positions. As a result, the terminal is closed, and some positions remain open, while we are unaware of that. 


4. Examples of use

Before proceeding to practical examples, I propose to discuss what trading terminal services can do. On the one hand, we can introduce almost any code into the service (except for the one that is prohibited), and on the other hand, it is probably worth delimiting powers and giving services their own niche in the trading terminal environment.

First, services should not duplicate the work of other active MQL5 programs: Expert Advisors, indicators and scripts. Let's say there is an Expert Advisor that places limit orders by a signal at the end of a trading session. Also, there is a service that places these limit orders. As a result, the system accounting limit orders in the EA itself may be disrupted, or in case of different magic numbers, the EA might lose sight of the orders placed by the service.

Second, we need to avoid the opposite situation - the conflict of serviceswith other MQL5 programs. Let's say there is an Expert Advisor that places limit orders by a signal at the end of a trading session. And there is a service that controls that at the end of the trading day all positions are closed and pending orders are removed.  There is a conflict of interest: the EA will place orders, and the service will immediately remove them. All this may end in a sort of a DDoS attack on a trading server.

In general, services should be harmoniously integrated into the operation of the trading terminal, without interfering with mql5 programs, while instead interacting with them for more efficient use of trading algorithms.


4.1 Clearing logs

Suppose that the service is tasked to clear the folder of logs (journals) generated by one or more Expert Advisors in the past (yesterday, the day before yesterday, etc.) at the beginning of a new trading day.

What tools do we need here? We will need file operations and the definition of a new bar. Find out more about the new bar detection class in the "New Bar" event handler article.

Now let's deal with file operations. Native file operations will not work here, since we will run into the limitations of the file sandbox. According to the Documentation:

For reasons of security, working with files is strictly controlled in MQL5 language. Files used in file operations by means of MQL5 language cannot be located outside the file sandbox.

Log files written to disk by MQL5 programs are located in %MQL5\Logs. Luckily, we can use WinAPI featuring file operations.

WinAPI is included using the following directive:

#include <WinAPI\winapi.mqh>

We will use eight functions in the file WinAPI:

  1. FindFirstFileW(),
  2. FindNextFileW(),
  3. CopyFileW(),
  4. GetFileAttributesW(),
  5. SetFileAttributesW(),
  6. DeleteFileW(),
  7. FindClose(),
  8. GetLastError().

The first function searches the specified folder for the first file with the given name. It is possible to substitute a mask as a name. So, to find the log files in a folder, it is enough to specify the ".log" string as a name.

The second function continues the search initiated by the first function.

The third function copies an existing file into a new one.

The fourth function gets the file system attributes for the specified file or directory.

The fifth function sets such attributes.

The sixth function deletes the file with the given name.

The seventh function closes the file search handle.

The eighth function retrieves the last error code value.

Let's have a look at the dClearTradeLogs.mq5 service code.

//--- include
#include <WinAPI\winapi.mqh>
#include "Include\CisNewBar.mqh"
//--- defines
#define ERROR_FILE_NOT_FOUND 0x2
#define ERROR_NO_MORE_FILES 0x12
#define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF
#define FILE_ATTRIBUTE_READONLY 0x1
#define FILE_ATTRIBUTE_DIRECTORY 0x10
#define FILE_ATTRIBUTE_ARCHIVE 0x20
//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input string InpDstPath="G:" ; // Destination drive
//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
//--- new bar
   CisNewBar daily_new_bar;
   daily_new_bar.SetPeriod(PERIOD_D1);
   daily_new_bar.SetLastBarTime(1);
//--- logs path
   string logs_path=::TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Logs\\";
   string mask_path=logs_path+"*.log";
//--- destination folder (if to copy files)
   string new_folder_name=NULL;
   uint file_attributes=0;
   if(::StringLen(InpDstPath)>0)
     {
      new_folder_name=InpDstPath+"\\Logs";
      //--- check whether a folder exists
      file_attributes=kernel32::GetFileAttributesW(new_folder_name);
      bool does_folder_exist=(file_attributes != INVALID_FILE_ATTRIBUTES) &&
                             ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0);
      if(!does_folder_exist)
        {
         //--- create a folder
         int create_res=kernel32::CreateDirectoryW(new_folder_name, 0);
         if(create_res<1)
           {
            ::PrintFormat("Failed CreateDirectoryW() with error: %x", kernel32::GetLastError());
            return;
           }
        }
     }
//--- main processing loop
   do
     {
      MqlDateTime sToday;
      ::TimeTradeServer(sToday);
      sToday.hour=sToday.min=sToday.sec=0;
      datetime dtToday=::StructToTime(sToday);
      if(daily_new_bar.isNewBar(dtToday))
        {
         ::PrintFormat("\nToday is: %s", ::TimeToString(dtToday, TIME_DATE));
         string todays_log_file_name=::TimeToString(dtToday, TIME_DATE);
         int replaced=::StringReplace(todays_log_file_name, ".", "");
         if(replaced>0)
           {
            todays_log_file_name+=".log";
            //--- log files
            FIND_DATAW find_file_data;
            ::ZeroMemory(find_file_data);
            HANDLE hFind=kernel32::FindFirstFileW(mask_path, find_file_data);
            if(hFind==INVALID_HANDLE)
              {
               ::PrintFormat("Failed FindFirstFile (hFind) with error: %x", kernel32::GetLastError());
               continue;
              }
            // List all the files in the directory with some info about them
            int result=0;
            uint files_cnt=0;
            do
              {
               string name="";
               for(int i=0; i<MAX_PATH; i++)
                  name+=::ShortToString(find_file_data.cFileName[i]);
               //--- delete any file except today's
               if(::StringCompare(name, todays_log_file_name))
                 {
                  string file_name=logs_path+name;
                  //--- if to copy a file before deletion
                  if(::StringLen(new_folder_name)>0)
                    {                     
                     string new_file_name=new_folder_name+"\\"+name;
                     if(kernel32::CopyFileW(file_name, new_file_name, 0)==0)
                       {
                        ::PrintFormat("Failed CopyFileW() with error: %x", kernel32::GetLastError());
                       }
                     //--- set READONLY attribute
                     file_attributes=kernel32::GetFileAttributesW(new_file_name);
                     if(file_attributes!=INVALID_FILE_ATTRIBUTES)
                        if(!(file_attributes & FILE_ATTRIBUTE_READONLY))
                          {
                           file_attributes=kernel32::SetFileAttributesW(new_file_name, file_attributes|FILE_ATTRIBUTE_READONLY);
                           if(!(file_attributes & FILE_ATTRIBUTE_READONLY))
                              ::PrintFormat("Failed SetFileAttributesW() with error: %x", kernel32::GetLastError());
                          }
                    }
                  int del_ret=kernel32::DeleteFileW(file_name);
                  if(del_ret>0)
                     files_cnt++;
                 }
               //--- next file
               ::ZeroMemory(find_file_data);
               result= kernel32::FindNextFileW(hFind, find_file_data);
              }
            while(result!=0);
            uint kernel32_last_error=kernel32::GetLastError();
            if(kernel32_last_error>0)
               if(kernel32_last_error!=ERROR_NO_MORE_FILES)
                  ::PrintFormat("Failed FindNextFileW (hFind) with error: %x", kernel32_last_error);
            ::PrintFormat("Deleted log files: %I32u", files_cnt);
            int file_close=kernel32::FindClose(hFind);
           }
        }
      ::Sleep(15000);
     }
   while(!::IsStopped());
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

If the input variable specifies the disk the files will be copied to,create a folder to store the log files after checking the existence of this folder.

In the main processing loop, check if a new day has arrived. Then we search and delete the log files in the same loop, skipping today's files. If we need to copy a file, check this possibility and after copying set the "Read only" attribute for the new file

In the loop, set a pause of 15 seconds. This is probably a relatively optimal frequency for determining a new day.

So, before launching the service, the %MQL5\Logs folder looked like this in Explorer (Fig. 2).

"%MQL5\Logs" Explorer folder before deleting files

Fig. 2. "%MQL5\Logs" Explorer folder before deleting files


After launching the service, the following messages will appear in the log:

2022.12.05 23:26:59.960 dClearTradeLogs Service "dClearTradeLogs" starts at: 2022.12.05 23:26:59
2022.12.05 23:26:59.960 dClearTradeLogs 
2022.12.05 23:26:59.960 dClearTradeLogs Today is: 2022.12.05
2022.12.05 23:26:59.985 dClearTradeLogs Deleted log files: 6

It is easy to see that the service did not write anything to the log regarding the end of its work. The reason is that the service operation is not over yet. It just loops and runs until it is interrupted.

"%MQL5\Logs" Explorer folder after deleting files

Fig. 3. "%MQL5\Logs" Explorer folder after deleting files

So, after deleting the logs, only one file remains in the specified folder (Fig. 3). Naturally, the task of deleting files can be improved and made more flexible. For example, before deleting files, you can copy them to another disk so as not to lose the necessary information for good. In general, the implementation already depends on the specific requirements for the algorithm. In the current example, the files were copied to the G:\Logs folder (Fig. 4).

"G:\Logs" Explorer folder after copying files

Fig. 4. "G:\Logs" Explorer folder after copying files

This concludes the work with the logs. In the following example, let's assign the task of displaying charts to the service.


4.2 Managing charts

Let's imagine that we are faced with the following task. The terminal should feature the charts of the currently traded symbols, i.e. the ones featuring open positions.

The rules for open charts are very simple. If there is an open position for one of the symbols, then open the chart of this symbol. If there is no position, there will be no chart. If there are several positions for one symbol, then only one chart will be opened.

Also, let's add some colors. If the position is in profit, then the background color of the chart will be light blue, and if it is in the red, it will be light pink. Zero profit uses the lavender color.


So, to perform this task, we first need a loop in the service code, in which we will monitor the status of positions and charts. The loop has turned out big enough. So let's analyze its code block by block.

The cycle is divided into two blocks.

The first block is handling the situation when there are no positions:

int positions_num=::PositionsTotal();
//--- if there are no positions
if(positions_num<1)
  {
   // close all the charts
   CChart temp_chart_obj;
   temp_chart_obj.FirstChart();
   long temp_ch_id=temp_chart_obj.ChartId();
   for(int ch_idx=0; ch_idx<MAX_CHARTS && temp_ch_id>-1; ch_idx++)
     {
      long ch_id_to_close=temp_ch_id;
      temp_chart_obj.NextChart();
      temp_ch_id=temp_chart_obj.ChartId();
      ::ChartClose(ch_id_to_close);
     }
  }

In the block, we go through open charts (if any) and close them. Here and below I will use the CChart class to handle the properties of the price chart.

The second block is more complex:

//--- if there are some positions
else
   {
   //--- collect unique position symbols
   CHashSet<string> pos_symbols_set;
   for(int pos_idx=0; pos_idx<positions_num; pos_idx++)
      {
      string curr_pos_symbol=::PositionGetSymbol(pos_idx);
      if(!pos_symbols_set.Contains(curr_pos_symbol))
         {
         if(!pos_symbols_set.Add(curr_pos_symbol))
            ::PrintFormat("Failed to add a symbol \"%s\" to the positions set!", curr_pos_symbol);
         }
      }
   string pos_symbols_arr[];
   int unique_pos_symbols_num=pos_symbols_set.Count();
   if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num)
      continue;
   //--- collect unique chart symbols and close duplicates
   CHashMap<string, long> ch_symbols_map;
   CChart map_chart_obj;
   map_chart_obj.FirstChart();
   long map_ch_id=map_chart_obj.ChartId();
   for(int ch_idx=0; ch_idx<MAX_CHARTS && map_ch_id>-1; ch_idx++)
      {
      string curr_ch_symbol=map_chart_obj.Symbol();
      long ch_id_to_close=0;
      if(!ch_symbols_map.ContainsKey(curr_ch_symbol))
         {
         if(!ch_symbols_map.Add(curr_ch_symbol, map_ch_id))
            ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_ch_symbol);
         }
      else
         {
         //--- if there's a duplicate
         ch_id_to_close=map_chart_obj.ChartId();
         }
      //--- move to the next chart
      map_chart_obj.NextChart();
      map_ch_id=map_chart_obj.ChartId();
      if(ch_id_to_close>0)
         {
         ::ChartClose(ch_id_to_close);
         }
      }
   map_chart_obj.Detach();
   //--- looking for a chart if there's a position
   for(int s_pos_idx=0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++)
      {
      string curr_pos_symbol=pos_symbols_arr[s_pos_idx];
      //--- if there's no chart of the symbol
      if(!ch_symbols_map.ContainsKey(curr_pos_symbol))
         if(::SymbolSelect(curr_pos_symbol, true))
            {
            //--- open a chart of the symbol
            CChart temp_chart_obj;
            long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1);
            if(temp_ch_id<1)
               ::PrintFormat("Failed to open a chart of the symbol \"%s\"!", curr_pos_symbol);
            else
               {
               if(!ch_symbols_map.Add(curr_pos_symbol, temp_ch_id))
                  ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_pos_symbol);
               temp_chart_obj.Detach();
               }
            }
      }
   string ch_symbols_arr[];
   long ch_ids_arr[];
   int unique_ch_symbols_num=ch_symbols_map.Count();
   if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num)
      continue;
   //--- looking for a position if there's a chart
   for(int s_ch_idx=0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++)
      {
      string curr_ch_symbol=ch_symbols_arr[s_ch_idx];
      long ch_id_to_close=ch_ids_arr[s_ch_idx];
      CChart temp_chart_obj;
      temp_chart_obj.Attach(ch_id_to_close);
      //--- if there's no position of the symbol
      if(!pos_symbols_set.Contains(curr_ch_symbol))
         {
         temp_chart_obj.Close();
         }
      else
         {
         CPositionInfo curr_pos_info;
         //--- calculate  a position profit
         double curr_pos_profit=0.;
         int pos_num=::PositionsTotal();
         for(int pos_idx=0; pos_idx<pos_num; pos_idx++)
            if(curr_pos_info.SelectByIndex(pos_idx))
               {
               string curr_pos_symbol=curr_pos_info.Symbol();
               if(!::StringCompare(curr_ch_symbol, curr_pos_symbol))
                  curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap();
               }
         //--- apply a color
         color profit_clr=clrLavender;
         if(curr_pos_profit>0.)
            {
            profit_clr=clrLightSkyBlue;
            }
         else if(curr_pos_profit<0.)
            {
            profit_clr=clrLightPink;
            }
         if(!temp_chart_obj.ColorBackground(profit_clr))
            ::PrintFormat("Failed to apply a profit color for the symbol \"%s\"!", curr_ch_symbol);
         temp_chart_obj.Redraw();
         }
      temp_chart_obj.Detach();
      }
   //--- tile windows (Alt+R)
   uchar vk=VK_MENU;
   uchar scan=0;
   uint flags[]= {0, KEYEVENTF_KEYUP};
   ulong extra_info=0;
   uchar Key='R';
   for(int r_idx=0; r_idx<2; r_idx++)
      {
      user32::keybd_event(vk, scan, flags[r_idx], extra_info);
      ::Sleep(10);
      user32::keybd_event(Key, scan, flags[r_idx], extra_info);
      }
   }

First, collect unique values of the symbols, positions for which are open. The CHashSet<T> class features are suitable for the task. The class is an implementation of the unordered dynamic data set of type T, with the required uniqueness of each value. Copy the obtained unique values into a string array in order to have simplified access to them later.

At the next stage, collect unqiue symbol values charts are opened for. Close duplicate charts along the way if any. Suppose we have two EURUSD charts. This means we leave one chart and close another. The instance of the CHashMap<TKey,TValue> class is applied here. The class is an implementation of a dynamic hash table whose data is stored as unordered key/value pairs, with the required uniqueness of a key.

Only two loops remain. In the first one, move along the array of open position symbols and check if there is a chart for it. If not, open it. In the second loop, move along the array of open chart symbols and check if the open position corresponds to each symbol. Suppose that there is an open USDJPY chart but it features no position. Then USDJPY is closed. In the same loop, calculate the profit of the position in order to set the background color, as determined at the beginning of the task. To access the position properties and get their values, the CPositionInfo class of the Standard Library was used.

Finally, let's add some visual appeal by placing chart windows as a tile. To achieve this, use WinAPI, namely the keybd_event() function simulating  keystrokes.

That's it. It remains only to launch the dActivePositionsCharts service.


4.3 Custom symbol, quotes

One of the advantages of the service is the ability to work in the background without using the price chart. As an example, in this section, I will show how the service can be used to create a custom symbol and its tick history, as well as generate new ticks.

I will use the US dollar index act as a custom symbol.

4.3.1 USD index, composition

The US dollar index is a synthetic that reflects the value of USD relative to a basket of six other currencies:

  1. EUR (57.6%);
  2. JPY (13.6%);
  3. GBP (11.9%);
  4. CAD (9.1%);
  5. SEK (4.2%);
  6. CHF (3.6%).

The Index equation is the geometric weighted average of the USD exchange rates against these currencies with a correction factor:

USDX = 50.14348112 * EURUSD-0.576 * USDJPY0.136 * GBPUSD-0.119 * USDCAD0.091 * USDSEK0.042 * USDCHF0.036

Based on the equation, suppose that the quote of the pair is raised to a negative power when USD in the quote is a quoted currency, andthe quote of the pair is raised to a positive power when the USD in the quote is a base currency.

The basket of currencies can be schematically displayed as follows (Fig. 5).



USD index currency basket (DXY)

Fig. 5. USD index (DXY) currency basket


USD index is the underlying asset for futures traded on Intercontinental Exchange (ICE). Index futures are calculated approximately every 15 seconds. Prices for calculation are taken at the highest bid price and the lowest ask price in the market depth of the currency pair included in the index.


4.3.2 USD index, service

We have everything we need for calculations, so it is time to start the service code. But first, I will note that the service will work in stages. At the first stage, it will form the history of ticks and bars for synthetics, and at the second stage, it will process new ticks. Obviously, the first stage is connected with the past, and the second - with the present.

Let's create an MQL5 program (service) template named dDxySymbol.mq5.

Define the following as input variables:

input datetime InpStartDay=D'01.10.2022'; // Start date
input int InpDxyDigits=3;                 // Index digits

The first one defines the beginning of the history of quotes, which we will try to get to create our symbol. In other words, we will be downloading the history of quotes from October 1, 2022.

The second one sets the symbol quote accuracy.

So, to start working with the index, we need to create a custom symbol - the basis for displaying a synthetic quote. DXY is a name for the index symbol. The resource has a lot of material on custom symbols. I will address the CiCustomSymbol class defined in the article MQL5 Cookbook – Trading strategy stress testing using custom symbols.

Here is the block of code where the work on creating the DXY synthetics is in progress:

//--- create a custom symbol
string custom_symbol="DXY",
       custom_group="Dollar Index";
CiCustomSymbol custom_symbol_obj;
const uint batch_size = 1e6;
const bool is_selected = true;
int code = custom_symbol_obj.Create(custom_symbol, custom_group, NULL, batch_size, is_selected);
::PrintFormat("Custom symbol \"%s\", code: %d", custom_symbol, code);
if(code < 0)
   return;

If the DXY symbol has not been created before and is not in the list of custom terminal symbols, then the CiCustomSymbol::Create() method returns code 1. If the DXY symbol is already among the symbols, then we get the code 0. If it is not possible to create a symbol, then we get an error - code -1. In case of an error while creating a custom symbol, the service will stop working.

After creating the synthetic, we will set several properties for it.

//--- Integer properties
//--- sector
ENUM_SYMBOL_SECTOR symbol_sector = SECTOR_CURRENCY;
if(!custom_symbol_obj.SetProperty(SYMBOL_SECTOR, symbol_sector))
   {
   ::PrintFormat("Failed to set a sector for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- background color
color symbol_background_clr = clrKhaki;
if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr))
   {
   ::PrintFormat("Failed to set a background color for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- chart mode
ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID;
if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode))
   {
   ::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- digits
if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits))
   {
   ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- trade mode
ENUM_SYMBOL_TRADE_MODE symbol_trade_mode = SYMBOL_TRADE_MODE_DISABLED;
if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_MODE, symbol_trade_mode))
   {
   ::PrintFormat("Failed to disable trade for the custom symbol \"%s\"", custom_symbol);
   return;
   }

The following properties are of ENUM_SYMBOL_INFO_INTEGER type:

  • SYMBOL_SECTOR,
  • SYMBOL_BACKGROUND_COLOR,
  • SYMBOL_CHART_MODE,
  • SYMBOL_DIGITS,
  • SYMBOL_TRADE_MODE.

The last property is responsible for the trading mode. The synthetic will not be allowed to trade, so the property will be set to SYMBOL_TRADE_MODE_DISABLED. If we need to check some strategy by symbol in the Tester, then the property should be enabled (SYMBOL_TRADE_MODE_FULL).

//--- Double properties
//--- point
double symbol_point = 1./::MathPow(10, InpDxyDigits);
if(!custom_symbol_obj.SetProperty(SYMBOL_POINT, symbol_point))
   {
   ::PrintFormat("Failed to to set a point value for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- tick size
double symbol_tick_size = symbol_point;
if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_TICK_SIZE, symbol_tick_size))
   {
   ::PrintFormat("Failed to to set a tick size for the custom symbol \"%s\"", custom_symbol);
   return;
   }

The following properties are of ENUM_SYMBOL_INFO_DOUBLE type:

  • SYMBOL_POINT,
  • SYMBOL_TRADE_TICK_SIZE.
Since it was previously determined that the symbol will be non-trading, therefore there are few double properties.

//--- String properties
//--- category
string symbol_category="Currency indices";
if(!custom_symbol_obj.SetProperty(SYMBOL_CATEGORY, symbol_category))
   {
   ::PrintFormat("Failed to to set a category for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- country
string symbol_country= "US";
if(!custom_symbol_obj.SetProperty(SYMBOL_COUNTRY, symbol_country))
   {
   ::PrintFormat("Failed to to set a country for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- description
string symbol_description= "Synthetic US Dollar Index";
if(!custom_symbol_obj.SetProperty(SYMBOL_DESCRIPTION, symbol_description))
   {
   ::PrintFormat("Failed to to set a description for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- exchange
string symbol_exchange= "ICE";
if(!custom_symbol_obj.SetProperty(SYMBOL_EXCHANGE, symbol_exchange))
   {
   ::PrintFormat("Failed to to set an exchange for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- page
string symbol_page = "https://www.ice.com/forex/usdx";
if(!custom_symbol_obj.SetProperty(SYMBOL_PAGE, symbol_page))
   {
   ::PrintFormat("Failed to to set a page for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- path
string symbol_path="Custom\\"+custom_group+"\\"+custom_symbol;
if(!custom_symbol_obj.SetProperty(SYMBOL_PATH, symbol_path))
   {
   ::PrintFormat("Failed to to set a path for the custom symbol \"%s\"", custom_symbol);
   return;
   }

The following properties are of ENUM_SYMBOL_INFO_STRING type:

  • SYMBOL_CATEGORY,
  • SYMBOL_COUNTRY,
  • SYMBOL_DESCRIPTION,
  • SYMBOL_EXCHANGE,
  • SYMBOL_PAGE,
  • SYMBOL_PATH.

The last property is responsible for the path in the symbol tree. While creating a synthetic, a group of characters and a symbol name were specified. Therefore, this property may be skipped as it will be identical.

Of course, I could set the equation for the synthetic directly without collecting ticks. But then the meaning of the example would be lost. Besides, the price of the index is calculated periodically. In the current example, the counting period is 10 seconds.

Now let's move on to the next block - this is a check for the trading history. Here we will solve two tasks: check the history of bars and load ticks. Let's check the bars the following way:

//--- check quotes history
CBaseSymbol base_symbols[BASE_SYMBOLS_NUM];
const string symbol_names[]=
  {
   "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF"
  };
ENUM_TIMEFRAMES curr_tf=PERIOD_M1;
::Print("\nChecking of quotes history is running...");
for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
  {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   string curr_symbol_name=symbol_names[s_idx];
   if(ptr_base_symbol.Init(curr_symbol_name, curr_tf, InpStartDay))
     {
      ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name);
      ulong start_cnt=::GetTickCount64();
      int check_load_code=ptr_base_symbol.CheckLoadHistory();
      ::PrintFormat("   Checking code: %I32d", check_load_code);
      ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
      ::PrintFormat("   Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC);
      if(check_load_code<0)
        {
         ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name);
         return;
        }
     }
  }

We will have 6 symbols we need to loop through and handle their quotes. The CBaseSymbol class has been created for convenience.

//+------------------------------------------------------------------+
//| Class CBaseSymbol                                                |
//+------------------------------------------------------------------+
class CBaseSymbol : public CObject
  {
      //--- === Data members === ---
   private:
      CSymbolInfo    m_symbol;
      ENUM_TIMEFRAMES m_tf;
      matrix         m_ticks_mx;
      datetime       m_start_date;
      ulong          m_last_idx;
      //--- === Methods === ---
   public:
      //--- constructor/destructor
      void           CBaseSymbol(void);
      void          ~CBaseSymbol(void) {};
      //---
      bool           Init(const string _symbol, const ENUM_TIMEFRAMES _tf, datetime start_date);
      int            CheckLoadHistory(void);
      bool           LoadTicks(const datetime _stop_date, const uint _flags);
      matrix         GetTicks(void) const
        {
         return m_ticks_mx;
        };
      bool           SearchTickLessOrEqual(const double _dbl_time, vector &_res_row);
      bool           CopyLastTick(vector &_res_row);
  };

The class deals with the history of bars and ticks, which is an extremely important task, otherwise there will be no data to create synthetics. 

Let's load the ticks:

//--- try to load ticks
::Print("\nLoading of ticks is running...");
now=::TimeCurrent();
uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK;
double first_tick_dbl_time=0.;
for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
   {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   string curr_symbol_name=symbol_names[s_idx];
   ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name);
   ulong start_cnt=::GetTickCount64();
   ::ResetLastError();
   if(!ptr_base_symbol.LoadTicks(now, flags))
      {
      ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError());
      return;
      }
   ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
   ::PrintFormat("   Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC);
   //--- looking for the 1st tick
   matrix ticks_mx=ptr_base_symbol.GetTicks();
   double tick_dbl_time=ticks_mx[0][0];
   if(tick_dbl_time>first_tick_dbl_time)
      first_tick_dbl_time=tick_dbl_time;
   }

The matrix::CopyTicksRange() native function was used to load ticks. The convenience is that you can only load those columns in the tick structure defined by flags. The issue of saving resources is extremely relevant when we request millions of ticks.

COPY_TICKS_INFO    = 1,       // ticks caused by Bid and/or Ask changes
COPY_TICKS_TRADE   = 2,       // ticks caused by Last and Volume changes
COPY_TICKS_ALL     = 3,       // all ticks that have changes
COPY_TICKS_TIME_MS = 1<<8,    // time in milliseconds
COPY_TICKS_BID     = 1<<9,    // Bid price
COPY_TICKS_ASK     = 1<<10,   // Ask price
COPY_TICKS_LAST    = 1<<11,   // Last price
COPY_TICKS_VOLUME  = 1<<12,   // volume
COPY_TICKS_FLAGS   = 1<<13,   // tick flags

The stages of checking the history and loading ticks in the log will be described in terms of time costs.

CS      0       12:01:11.802    dDxySymbol      Checking of quotes history is running...
CS      0       12:01:11.802    dDxySymbol      
CS      0       12:01:11.802    dDxySymbol         Symbol #1: "EURUSD"
CS      0       12:01:14.476    dDxySymbol         Checking code: 1
CS      0       12:01:14.476    dDxySymbol         Time elapsed: 2.688 sec
CS      0       12:01:14.476    dDxySymbol      
CS      0       12:01:14.476    dDxySymbol         Symbol #2: "USDJPY"
CS      0       12:01:17.148    dDxySymbol         Checking code: 1
CS      0       12:01:17.148    dDxySymbol         Time elapsed: 2.672 sec
CS      0       12:01:17.148    dDxySymbol      
CS      0       12:01:17.148    dDxySymbol         Symbol #3: "GBPUSD"
CS      0       12:01:19.068    dDxySymbol         Checking code: 1
CS      0       12:01:19.068    dDxySymbol         Time elapsed: 1.922 sec
CS      0       12:01:19.068    dDxySymbol      
CS      0       12:01:19.068    dDxySymbol         Symbol #4: "USDCAD"
CS      0       12:01:21.209    dDxySymbol         Checking code: 1
CS      0       12:01:21.209    dDxySymbol         Time elapsed: 2.140 sec
CS      0       12:01:21.209    dDxySymbol      
CS      0       12:01:21.209    dDxySymbol         Symbol #5: "USDSEK"
CS      0       12:01:22.631    dDxySymbol         Checking code: 1
CS      0       12:01:22.631    dDxySymbol         Time elapsed: 1.422 sec
CS      0       12:01:22.631    dDxySymbol      
CS      0       12:01:22.631    dDxySymbol         Symbol #6: "USDCHF"
CS      0       12:01:24.162    dDxySymbol         Checking code: 1
CS      0       12:01:24.162    dDxySymbol         Time elapsed: 1.531 sec
CS      0       12:01:24.162    dDxySymbol      
CS      0       12:01:24.162    dDxySymbol      Loading of ticks is running...
CS      0       12:01:24.162    dDxySymbol      
CS      0       12:01:24.162    dDxySymbol         Symbol #1: "EURUSD"
CS      0       12:02:27.204    dDxySymbol         Time elapsed: 63.032 sec
CS      0       12:02:27.492    dDxySymbol      
CS      0       12:02:27.492    dDxySymbol         Symbol #2: "USDJPY"
CS      0       12:02:32.587    dDxySymbol         Time elapsed: 5.094 sec
CS      0       12:02:32.938    dDxySymbol      
CS      0       12:02:32.938    dDxySymbol         Symbol #3: "GBPUSD"
CS      0       12:02:37.675    dDxySymbol         Time elapsed: 4.734 sec
CS      0       12:02:38.285    dDxySymbol      
CS      0       12:02:38.285    dDxySymbol         Symbol #4: "USDCAD"
CS      0       12:02:43.223    dDxySymbol         Time elapsed: 4.937 sec
CS      0       12:02:43.624    dDxySymbol      
CS      0       12:02:43.624    dDxySymbol         Symbol #5: "USDSEK"
CS      0       12:03:18.484    dDxySymbol         Time elapsed: 34.860 sec
CS      0       12:03:19.596    dDxySymbol      
CS      0       12:03:19.596    dDxySymbol         Symbol #6: "USDCHF"
CS      0       12:03:24.317    dDxySymbol         Time elapsed: 4.719 sec

After the ticks are received, form the tick history for the DXY synthetic. This process will take place in the following block:

//--- create a custom symbol ticks history
::Print("\nCustom symbol ticks history is being formed...");
long first_tick_time_sec=(long)(first_tick_dbl_time/MS_IN_SEC);
long first_tick_time_ms=(long)first_tick_dbl_time%(long)MS_IN_SEC;
::PrintFormat("   First tick time: %s.%d", ::TimeToString((datetime)first_tick_time_sec,
              TIME_DATE|TIME_SECONDS), first_tick_time_ms);
double active_tick_dbl_time=first_tick_dbl_time;
double now_dbl_time=MS_IN_SEC*now;
uint ticks_cnt=0;
uint arr_size=0.5e8;
MqlTick ticks_arr[];
::ArrayResize(ticks_arr, arr_size);
::ZeroMemory(ticks_arr);
matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, 2);
do
   {
   //--- collect base symbols ticks
   bool all_ticks_ok=true;
   for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
      {
      CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
      vector tick_prices_vc;
      bool to_break_loop=false;
      if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc))
         to_break_loop=true;
      else
         {
         if(!base_prices_mx.Row(tick_prices_vc, s_idx))
            to_break_loop=true;
         }
      if(to_break_loop)
         {
         all_ticks_ok=false;
         break;
         }
      }
   //--- calculate index prices
   if(all_ticks_ok)
      {
      MqlTick last_ind_tick;
      CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick);
      arr_size=ticks_arr.Size();
      if(ticks_cnt>=arr_size)
         {
         uint new_size=(uint)(arr_size+0.1*arr_size);
         if(::ArrayResize(ticks_arr, new_size)!=new_size)
            continue;
         }
      ticks_arr[ticks_cnt]=last_ind_tick;
      ticks_cnt++;
      }
   active_tick_dbl_time+=TICK_PAUSE;
   }
while(active_tick_dbl_time<now_dbl_time);
::ArrayResize(ticks_arr, ticks_cnt);
int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true);

Set a temporary point (active_tick_dbl_time) and add 10 seconds to it at the end of the loop. This is a kind of 'timestamp' for getting ticks for all the symbols that make up the Index.

So, the search for the desired tick on each symbol is based on a specific point in time in the past. The CBaseSymbol::SearchTickLessOrEqual() method provides a tick that arrived no later than the active_tick_dbl_time value.

When ticks from each component of the Index are received, the tick prices are already in the base_prices_mx matrix. 

The CalcIndexPrices() function returns the already prepared value of the index tick at the moment of time. 

When ticks are created, the tick database is updated using the CiCustomSymbol::TicksReplace() method.

The service then only deals with the present in the next block:

//--- main processing loop
::Print("\nA new tick processing is active...");
do
   {
   ::ZeroMemory(base_prices_mx);
   //--- collect base symbols ticks
   bool all_ticks_ok=true;
   for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
      {
      CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
      vector tick_prices_vc;
      bool to_break_loop=false;
      if(!ptr_base_symbol.CopyLastTick(tick_prices_vc))
         to_break_loop=true;
      else
         {
         if(!base_prices_mx.Row(tick_prices_vc, s_idx))
            to_break_loop=true;
         }
      if(to_break_loop)
         {
         all_ticks_ok=false;
         break;
         }
      }
   //--- calculate index prices
   if(all_ticks_ok)
      {
      MqlTick last_ind_tick, ticks_to_add[1];
      now=::TimeCurrent();
      now_dbl_time=MS_IN_SEC*now;
      CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick);
      ticks_to_add[0]=last_ind_tick;
      int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true);
      }
   ::Sleep(TICK_PAUSE);
   }
while(!::IsStopped());

The task of the block is similar to that of the previous block, albeit a bit easier. Every 10 seconds, we need to get tick data on symbols and calculate the index prices. In the Index, the bid price is calculated based on the bids of all symbols, and the ask price is calculated based on all the asks, respectively.

After launching the dDxySymbol service, the custom DXY symbol chart can be opened after some time (Fig. 6). 

Fig. 6. DXY custom symbol chart with holidays

Fig. 6. DXY custom symbol chart with holidays 


On the chart, Saturdays are highlighted with red vertical segments. It turns out that on Saturdays and Sundays, the service continues to calculate ticks on the history, which is probably not entirely correct. It is necessary to supplement the service code with a time limit (days of the week). Let's assign this task to the CheckDayOfWeek() function.

Now the synthetic chart looks like this (Fig. 7). It looks like the bug has been fixed.

DXY custom symbol chart without holidays

Fig. 7. DXY custom symbol chart without holidays 

This completes the work with the dDxySymbol service.


Conclusion

The article has highlighted some features of an MQL5 program type known as a service. This type of an MQL5 program differs in that it does not have a binding chart but works independently. The nature of the services is such that they can conflict with other EAs, scripts and probably, to a lesser extent, with indicators. Therefore, the task of defining the rights and obligations of service programs in the MetaTrader 5 environment falls on the shoulders of the developer.

The archive contains sources that can be placed in the %MQL5\\Services folder.

Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/11826

Attached files |
code.zip (23.7 KB)
Last comments | Go to discussion (1)
Guilherme Mendonca
Guilherme Mendonca | 5 May 2023 at 04:03

Very nice. Thank you for sharing this article and the code examples.

I created a very simple service that identifies when it is a new day and runs a task just once a day to save the trade history in a CSV file. Afterward, this file is updated only with the new trades from the history.

The advantage is that I don't need a chart window to do this, however it raised a question about whether the service will use more or less processing power and memory from my computer compared to if I used this task inside an empty indicator for example, running an 'OnTimer' function.

If you have the answer to my question about the processing power and memory usage, could you please let me know? Thanks again for sharing this article and the code examples.
Develop a Proof-of-Concept DLL with C++ multi-threading support for MetaTrader 5 on Linux Develop a Proof-of-Concept DLL with C++ multi-threading support for MetaTrader 5 on Linux
We will begin the journey to explore the steps and workflow on how to base development for MetaTrader 5 platform solely on Linux system in which the final product works seamlessly on both Windows and Linux system. We will get to know Wine, and Mingw; both are the essential tools to make cross-platform development works. Especially Mingw for its threading implementations (POSIX, and Win32) that we need to consider in choosing which one to go with. We then build a proof-of-concept DLL and consume it in MQL5 code, finally compare the performance of both threading implementations. All for your foundation to expand further on your own. You should be comfortable building MT related tools on Linux after reading this article.
Population optimization algorithms: Cuckoo Optimization Algorithm (COA) Population optimization algorithms: Cuckoo Optimization Algorithm (COA)
The next algorithm I will consider is cuckoo search optimization using Levy flights. This is one of the latest optimization algorithms and a new leader in the leaderboard.
Category Theory in MQL5 (Part 2) Category Theory in MQL5 (Part 2)
Category Theory is a diverse and expanding branch of Mathematics which as of yet is relatively uncovered in the MQL5 community. These series of articles look to introduce and examine some of its concepts with the overall goal of establishing an open library that attracts comments and discussion while hopefully furthering the use of this remarkable field in Traders' strategy development.
DoEasy. Controls (Part 28): Bar styles in the ProgressBar control DoEasy. Controls (Part 28): Bar styles in the ProgressBar control
In this article, I will develop display styles and description text for the progress bar of the ProgressBar control.