
DRAW_ARROW drawing type in multi-symbol multi-period indicators
Contents
Introduction
We continue the topic of multi-symbol multi-period indicators. The previous article of the series covered the topic of multi-symbol multi-period indicators. In the current article, we will modify the multi-indicator class so that it can work with arrow indicators.
Arrow indicators imply that data is not constantly available in their drawing buffer. The value is present in the buffer where the arrow is displayed, while at other times the buffer contains the empty value set for it. Usually this is EMPTY_VALUE, but it is possible to set the buffer to any value that will be "empty" and will not be displayed on the chart. This can be done using the function
PlotIndexSetDouble(buffer_index,PLOT_EMPTY_VALUE,empty_value);
where buffer_index is a buffer index set to empty, while empty_value is the "empty value" that will be set for that buffer.
In multi-period indicators, where the buffer is filled with data with gaps, you need to take into account the presence of empty values where there are no arrows, and not enter these empty values into a chart bar where a non-empty value is already entered. Otherwise, the arrow placed earlier will be erased by a new empty value. This is true if data from an indicator calculated on a lower timeframe is copied to a higher timeframe.
Let me give an example. М15 bars are superimposed on M5 chart:
Here we see the fractals of the M5 chart, which need to be installed on the bars of the M15 chart.
- For the first bar M15 (I), we need to set the upper fractal located on bar 3 - this fractal will be visible on M15 in any case, since it is the last of the three.
- For the second bar M15 (II), you need to set a fractal located on bar 2 - this fractal will be visible on M15 only if you do not copy the empty value from bar 3, since it will replace the value of the lower fractal. The M15 bar will have two fractals - an upper and a lower one, since on M5 there is a lower fractal on bar 2 and an upper fractal on bar 3.
- For the third M15 bar (III), we need to set the top fractal from bar 1 - this fractal will be visible on M15 only if you do not copy empty values from bars 2 and 3, since they will replace the fractal value.
Thus, we need to track the value on the bar of the current chart and, if it is empty, then we can copy any value from the buffer of the lower timeframe. But if the value is no longer empty, then we only need to copy the non-empty value as well. In this case, the chart bar of the higher timeframe contains the value of the last fractal from those bars of the lower timeframe that are part of the bar of the higher chart period. Why exactly the last bar, and not the first? I think that the last value is more relevant, since it is closer to the current time.
When copying indicator data calculated on a higher timeframe to a chart of a lower timeframe, we need to take into account that the arrow can be placed not on the zero or first bar of the chart, but, for example, on the second, third, or higher one. If you copy the data of only the first and zero bars, as is now done in the multi-indicator class, then no arrows are displayed on the chart - they are simply located on bars with a higher index and, accordingly, the indicator calculation algorithm simply cannot see them:
Here we see that the fractal is formed on the bar with index 2. If we copy only the zero and first bars, then this fractal will not be visible to the program.
Accordingly, since we cannot know exactly which indicator places its arrows on which bar, for the class of multi-indicators it is necessary to introduce a variable, in which we will explicitly indicate the bar searching and copying values starts from. At the moment, the class copies only the first and zero bars on the current tick. Accordingly, it cannot see fractals - the values start from the bar with index 2 for them.
If you use a custom indicator as the initial indicator, then it can place arrows on any other bar - not only on the second one, like the Fractals indicator. For example, if this is an indicator of custom fractals, where fractals of a different dimension are searched, for example 3-X-3, then the arrow can be placed on a bar with index 3 (if the fractal is redrawn) or 4 (if the fractal is not redrawn).
For standard indicators, we will explicitly indicate the bar index, since it is known. For custom indicators, we will make it possible to specify the search start bar in the class constructor parameters.
Refinement of multi-indicator classes
When copying data in class methods, we used local temporary arrays. Let's make them global so that we do not have to re-allocate memory for these arrays every time.
In the private section of the CIndMSTF base class of multi-symbol multi-period indicators, declare new arrays, while in the protected section, declare a variable for storing the number of bars for calculating the indicator on the current tick:
//+------------------------------------------------------------------+ //| Base class of the multi-symbol multi-period indicator | //+------------------------------------------------------------------+ class CIndMSTF : public CObject { private: ENUM_PROGRAM_TYPE m_program; // Program type ENUM_INDICATOR m_type; // Indicator type ENUM_TIMEFRAMES m_timeframe; // Chart timeframe string m_symbol; // Chart symbol int m_handle; // Indicator handle int m_id; // Identifier bool m_success; // Successful calculation flag ENUM_ERR_TYPE m_type_err; // Calculation error type string m_description; // Custom description of the indicator string m_name; // Indicator name string m_parameters; // Description of indicator parameters double m_array_data[]; // Temporary array for copying data datetime m_array_time[]; // Temporary array for copying time protected: ENUM_IND_CATEGORY m_category; // Indicator category MqlParam m_param[]; // Array of indicator parameters string m_title; // Title (indicator name + description of parameters) SBuffer m_buffers[]; // Indicator buffers int m_digits; // Digits in indicator values int m_limit; // Number of bars required to calculate the indicator on the current tick int m_rates_total; // Number of available bars for indicator calculation int m_prev_calculated; // Number of calculated bars on the previous indicator call uint m_bars_to_calculate; // Number of bars required to calculate the indicator on the current tick
In the public section of the class, write the method that sets the number of bars required to calculate the indicator on the current tick :
//--- Returns amount of calculated data int Calculated(void) const { return ::BarsCalculated(this.m_handle); } //--- Set the number of bars required to calculate the indicator on the current tick void SetBarsToCalculate(const int bars) { this.m_bars_to_calculate=bars; if(this.m_array_data.Size()!=this.m_bars_to_calculate) { ::ArrayResize(this.m_array_data,this.m_bars_to_calculate); ::ArrayResize(this.m_array_time,this.m_bars_to_calculate); } } //--- Virtual method returning the type of object (indicator)
The method receives the required number of bars. The value is set for the new variable, while the sizes of the arrays are set equal to the value of this variable.
Thus, if the sizes of the arrays are successfully changed, they will no longer have to be re-installed each time in the data copying methods. If it is not possible to change the sizes of the arrays here, then, in this case, the functions for copying data inside the copy methods try to change the size of the array to the required amount. If this also fails, then the methods display a copy error message.
Thus, we set the required size to the arrays once - either in the class constructor, or (if unsuccessful) in the copy methods. Then we use arrays with the required size set, which saves us from constantly re-allocating memory for the arrays.
In the class constructor, call the method with the default value to copy data (first and zero). For the vast majority of indicators, it is equal to two bars:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CIndMSTF::CIndMSTF(const ENUM_INDICATOR type,const uint buffers,const string symbol,const ENUM_TIMEFRAMES timeframe) { //--- Start the timer ::ResetLastError(); if(!::EventSetTimer(1)) ::PrintFormat("%s: EventSetTimer failed. Error %lu",__FUNCTION__,::GetLastError()); //--- Set properties to the values passed to the constructor or to default values this.m_program=(ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE); this.m_type=type; this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol); this.m_timeframe=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe); this.m_handle=INVALID_HANDLE; this.m_digits=::Digits(); this.m_success=true; this.m_type_err=ERR_TYPE_NO_ERROR; //--- Set the size of the buffer structure array to the number of indicator buffers ::ResetLastError(); if(::ArrayResize(this.m_buffers,buffers)!=buffers) ::PrintFormat("%s: Buffers ArrayResize failed. Error %lu"__FUNCTION__,::GetLastError()); //--- Set the "empty" value for each buffer by default (can be changed it later) for(int i=0;i<(int)this.m_buffers.Size();i++) this.SetBufferInitValue(i,EMPTY_VALUE); //--- Set initial values of variables involved in resource-efficient calculation of the indicator this.m_prev_calculated=0; this.m_limit=0; this.m_rates_total=::Bars(this.m_symbol,this.m_timeframe); this.SetBarsToCalculate(2); //--- If the indicator is calculated on non-current chart, request data from the required chart //--- (the first access to data starts data pumping) datetime array[]; if(symbol!=::Symbol() || timeframe!=::Period()) ::CopyTime(this.m_symbol,this.m_timeframe,0,this.m_rates_total,array); }
In the class destructor, release the memory allocated for temporary arrays:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CIndMSTF::~CIndMSTF() { //--- Delete timer ::EventKillTimer(); //--- Release handle of the indicator ::ResetLastError(); if(this.m_handle!=INVALID_HANDLE && !::IndicatorRelease(this.m_handle)) ::PrintFormat("%s: %s, handle %ld IndicatorRelease failed. Error %ld",__FUNCTION__,this.Title(),m_handle,::GetLastError()); //--- Free up the memory of buffer arrays for(int i=0;i<(int)this.BuffersTotal();i++) { ::ArrayFree(this.m_buffers[i].array0); ::ArrayFree(this.m_buffers[i].array1); ::ArrayFree(this.m_buffers[i].array2); ::ArrayFree(this.m_buffers[i].array3); ::ArrayFree(this.m_buffers[i].color_indexes); } //--- Free up the memory of temporary arrays ::ArrayFree(this.m_array_data); ::ArrayFree(this.m_array_time); }
Now, check the amount of data being copied using the value set in the new variable in the method for copying the data of the specified buffer's array. Previously, there was a hard-coded value of two bars:
//+------------------------------------------------------------------+ //| Copy data of the specified array of the specified buffer | //+------------------------------------------------------------------+ bool CIndMSTF::CopyArray(const uint buff_num,const uint array_num,const int to_copy,double &array[]) { ::ResetLastError(); //--- Copy either the last two bars to 'array' or all available historical data from indicator's calculation part array to buffer array of indicator object int copied=0; if(to_copy==this.m_bars_to_calculate) { switch(array_num) { case 0 : case 1 : case 2 : case 3 : copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(), -this.m_buffers[buff_num].Shift(),to_copy,array); break; case 4 : copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom()+1,-this.m_buffers[buff_num].Shift(),to_copy,array); break; default : break; } } else { switch(array_num) { case 0 : copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(), -this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].array0); break; case 1 : copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(), -this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].array1); break; case 2 : copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(), -this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].array2); break; case 3 : copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom(), -this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].array3); break; case 4 : copied=::CopyBuffer(this.m_handle,this.m_buffers[buff_num].BufferFrom()+1,-this.m_buffers[buff_num].Shift(),to_copy,this.m_buffers[buff_num].color_indexes); break; default : break; } } //--- If copied successfully if(copied>0) return true; //--- If not all data is copied //--- If CopyBuffer returned -1, this means the start of historical data downloading //--- print a message about this to the log if(copied==WRONG_VALUE) ::PrintFormat("%s::%s: Start downloading data by %s/%s. Waiting for the next tick...",__FUNCTION__,this.Title(),this.m_symbol,this.TimeframeDescription()); //--- In any other case, not all data has been copied yet //--- print a message about this to the log else ::PrintFormat("%s::%s: Not all data was copied. Data available: %lu, total copied: %ld",__FUNCTION__,this.Title(),this.m_rates_total,copied); return false; }
In the method of copying the data of all arrays of the specified buffer, copying of two bars was hardcoded previously. Besides, the source and target indices of the buffer array were explicitly set as well :
//+------------------------------------------------------------------+ //| Copy data of all arrays of the specified buffer | //+------------------------------------------------------------------+ bool CIndMSTF::CopyArrays(const uint buff_num,const int to_copy) { bool res=true; double array[2]; if(to_copy==2) { switch(this.BufferDrawType(buff_num)) { //--- One buffer case DRAW_LINE : case DRAW_HISTOGRAM : case DRAW_ARROW : case DRAW_SECTION : res=this.CopyArray(buff_num,0,to_copy,array); if(res) { this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-1]=array[1]; this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-2]=array[0]; } return res; //--- Two buffers case DRAW_HISTOGRAM2 : case DRAW_ZIGZAG : case DRAW_FILLING :
Now the amount of copied data is not known in advance. It is set in a new variable. Therefore, we will check the variable value, while the data will be copied in a loop by the amount of copied data.
We will calculate the indices of the source and target arrays from the loop index . At the same time, we have now announced the global temporary array, so the local one is no longer needed:
//+------------------------------------------------------------------+ //| Copy data of all arrays of the specified buffer | //+------------------------------------------------------------------+ bool CIndMSTF::CopyArrays(const uint buff_num,const int to_copy) { bool res=true; if(to_copy==this.m_bars_to_calculate) { int total=(int)this.m_array_data.Size(); switch(this.BufferDrawType(buff_num)) { //--- One buffer case DRAW_LINE : case DRAW_HISTOGRAM : case DRAW_ARROW : case DRAW_SECTION : res=this.CopyArray(buff_num,0,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-(total-i)]=this.m_array_data[i]; } return res; //--- Two buffers case DRAW_HISTOGRAM2 : case DRAW_ZIGZAG : case DRAW_FILLING : res=this.CopyArray(buff_num,0,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array1[this.DataTotal(buff_num,1)-(total-i)]=this.m_array_data[i]; } return res; //--- Four buffers case DRAW_BARS : case DRAW_CANDLES : res=this.CopyArray(buff_num,0,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array1[this.DataTotal(buff_num,1)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,2,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array2[this.DataTotal(buff_num,2)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,3,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array3[this.DataTotal(buff_num,3)-(total-i)]=this.m_array_data[i]; } return res; //--- One buffer + color buffer case DRAW_COLOR_LINE : case DRAW_COLOR_HISTOGRAM : case DRAW_COLOR_ARROW : case DRAW_COLOR_SECTION : res=this.CopyArray(buff_num,0,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,4,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].color_indexes[this.DataTotal(buff_num,4)-(total-i)]=this.m_array_data[i]; } return res; //--- Two buffers + color buffer case DRAW_COLOR_HISTOGRAM2 : case DRAW_COLOR_ZIGZAG : res=this.CopyArray(buff_num,0,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array1[this.DataTotal(buff_num,1)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,4,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].color_indexes[this.DataTotal(buff_num,4)-(total-i)]=this.m_array_data[i]; } return res; //--- Four buffers + color buffer case DRAW_COLOR_BARS : case DRAW_COLOR_CANDLES : res=this.CopyArray(buff_num,0,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array1[this.DataTotal(buff_num,1)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,2,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array2[this.DataTotal(buff_num,2)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,3,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array3[this.DataTotal(buff_num,3)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,4,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].color_indexes[this.DataTotal(buff_num,4)-(total-i)]=this.m_array_data[i]; } return res; //---DRAW_NONE default: break; } } else { switch(this.BufferDrawType(buff_num)) { //--- One buffer case DRAW_LINE : case DRAW_HISTOGRAM : case DRAW_ARROW : case DRAW_SECTION : return this.CopyArray(buff_num,0,to_copy,this.m_array_data); //--- Two buffers case DRAW_HISTOGRAM2 : case DRAW_ZIGZAG : case DRAW_FILLING : res =this.CopyArray(buff_num,0,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); return res; //--- Four buffers case DRAW_BARS : case DRAW_CANDLES : res =this.CopyArray(buff_num,0,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,2,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,3,to_copy,this.m_array_data); return res; //--- One buffer + color buffer case DRAW_COLOR_LINE : case DRAW_COLOR_HISTOGRAM : case DRAW_COLOR_ARROW : case DRAW_COLOR_SECTION : res =this.CopyArray(buff_num,0,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,4,to_copy,this.m_array_data); return res; //--- Two buffers + color buffer case DRAW_COLOR_HISTOGRAM2 : case DRAW_COLOR_ZIGZAG : res =this.CopyArray(buff_num,0,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,4,to_copy,this.m_array_data); return res; //--- Four buffers + color buffer case DRAW_COLOR_BARS : case DRAW_COLOR_CANDLES : res =this.CopyArray(buff_num,0,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,2,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,3,to_copy,this.m_array_data); res &=this.CopyArray(buff_num,4,to_copy,this.m_array_data); return res; //---DRAW_NONE default: break; } } return false; }
In the method of filling object buffers with data from the calculation part buffer in the code block responsible for operation on the current tick, copy the number of bars set in the m_bars_to_calculate variable instead of two bars:
//--- If calculated m_limit is less than or equal to 1, this means either opening of a new bar (m_limit==1) or current tick (m_limit==0) //--- In this case, it is necessary to calculate the number of bars specified in the m_bars_to_calculate variable - the current one and the rest if(this.m_limit<=1) { //--- In a loop over the number of indicator buffers for(int i=0;i<total;i++) { //--- If this is the opening of a new bar and resizing the indicator buffer failed, if(this.m_limit==1 && !this.BufferResize(i,this.m_rates_total)) { //--- add 'false' to the m_success variable and return 'false' //--- Here, an error message will be printed to log from the BufferResize method this.m_success=false; return false; } //--- If failed to copy bars in the amount of m_bars_to_calculate from the indicator's calculation part buffer, ::ResetLastError(); if(!this.CopyArrays(i,this.m_bars_to_calculate)) { //--- report this via the log, add 'false' to the m_success variable and return 'false' ::PrintFormat("%s::%s: CopyBuffer(%lu) failed. Error %lu",__FUNCTION__,this.Title(),i,::GetLastError()); this.m_success &=false; } } //--- If the loop is followed by errors, return 'false' if(!this.m_success) { this.m_type_err=ERR_TYPE_NO_DATA; return false; } //--- Success this.m_type_err=ERR_TYPE_NO_ERROR; this.m_success=true; return true; } //--- Undefined 'limit' option - return 'false' return false; }
The method that fills the passed array with data from the class buffer is now completely redesigned. It takes into account the nuances of copying data from a lower timeframe to a higher one, from a higher one to a lower one, and the amount of data copied on the current tick, which were discussed at the very beginning of the article. The logic of this method has been completely rewritten and commented:
//+------------------------------------------------------------------+ //| Fills the passed plot array with data from the class buffer | //+------------------------------------------------------------------+ bool CIndMSTF::DataToBuffer(const string symbol_to,const ENUM_TIMEFRAMES timeframe_to,const uint buffer_num,const uint array_num,const int limit,double &buffer[]) { //--- Set the success flag this.m_success=true; //--- Get the indexing direction of the buffer array passed to the method and, //--- if non-timeseries indexing, set timeseries indexing bool as_series=::ArrayGetAsSeries(buffer); if(!as_series) ::ArraySetAsSeries(buffer,true); //--- Set the symbol name and timeframe value passed to the method string symbol=(symbol_to=="" || symbol_to==NULL ? ::Symbol() : symbol_to); ENUM_TIMEFRAMES timeframe=(timeframe_to==PERIOD_CURRENT ? ::Period() : timeframe_to); //--- If this is the first launch or history changes, initialize the buffer array passed to the method if(limit>1 && this.m_limit>1) { ::PrintFormat("%s::%s First start, or historical data has been changed. Initialize Buffer(%lu)",__FUNCTION__,this.Title(),buffer_num); ::ArrayInitialize(buffer,this.BufferInitValue(buffer_num)); } //--- Set the value of the loop counter (no more than the maximum number of bars in the terminal on the chart) int count=(limit<=1 ? (int)this.m_bars_to_calculate : ::fmin(::TerminalInfoInteger(TERMINAL_MAXBARS),limit)); //--- In a loop from the zero bar to the value of the loop counter for(int i=0;i<count;i++) { //--- If the chart timeframe matches the class object timeframe, fill the buffer directly from the class object array if(timeframe==::Period() && this.m_timeframe==::Period()) buffer[i]=this.GetData(buffer_num,array_num,i); //--- Otherwise, if the chart timeframe is not equal to the timeframe of the class object else { //--- If the chart timeframe is lower than the timeframe of the class object, if(timeframe<this.m_timeframe) { //--- If this is historical data (a bar with an index greater than m_bars_to_calculate-1) if(i>(int)this.m_bars_to_calculate-1) { //--- Find out which time of this class the bar of the current chart timeframe, corresponding to the loop index, belongs to ::ResetLastError(); if(::CopyTime(symbol,timeframe,i,1,this.m_array_time)!=1) { //--- If there is no data in the terminal, move on if(::GetLastError()==4401) continue; //--- Error in obtaining existing data - return false this.m_success &=false; return false; } //--- Using time of bar of current chart timeframe, find the corresponding bar index of the chart period in the class object buffer array int bar=::iBarShift(this.m_symbol,this.m_timeframe,this.m_array_time[0]); if(bar==WRONG_VALUE) { this.m_success &=false; continue; } //--- in the indicator buffer at the loop index, write the value obtained from the calculation part buffer buffer[i]=this.GetData(buffer_num,array_num,bar); } //--- If this is the current (zero) bar or a bar with an index less than or equal to m_bars_to_calculate-1 else { //--- Get the time of bars by symbol/timeframe of the class object in the amount of m_bars_to_calculate if(::CopyTime(this.m_symbol,this.m_timeframe,0,this.m_bars_to_calculate,this.m_array_time)!=this.m_bars_to_calculate) { this.m_success=false; return false; } //--- Get the number of bars of the current chart period contained in the chart period of the class object int num_bars=::PeriodSeconds(this.m_timeframe)/::PeriodSeconds(timeframe); if(num_bars<1) num_bars=1; //--- In a loop through the array of received bar times for(int j=0;j<(int)this.m_array_time.Size();j++) { //--- Get the corresponding bar on the chart by the time of the bar in the buffer array of the class object int barN=::iBarShift(symbol,timeframe,this.m_array_time[j]); if(barN==WRONG_VALUE) { this.m_success &=false; return false; } //--- Calculate the final bar on the chart to copy data from the buffer of the calculation part of the indicator int end=barN-num_bars; if(end<0) end=-1; //--- In a loop from barN to end, copy data from the buffer array of the calculation part to the indicator buffer being drawn on the chart for(int n=barN;n>end;n--) buffer[n]=this.GetData(buffer_num,array_num,this.m_bars_to_calculate-1-j); } } } //--- If the chart timeframe is higher than the timeframe of the class object, else if(timeframe>this.m_timeframe) { //--- Get the number of bars of the class object chart period contained in the bar of the current chart period int num_bars=::PeriodSeconds(timeframe)/::PeriodSeconds(this.m_timeframe); //--- Find out which time of this class the bar of the current chart timeframe, corresponding to the loop index, belongs to ::ResetLastError(); if(::CopyTime(symbol,timeframe,i,1,this.m_array_time)!=1) { //--- If there is no data in the terminal, move on if(::GetLastError()==4401) continue; //--- Error in obtaining existing data - return false this.m_success &=false; return false; } //--- Using time of bar of current chart timeframe, find the corresponding bar index of the chart period in the class object buffer array //--- This bar will be the first of several bars of the class object in the amount of num_bars included in the chart bar int bar=::iBarShift(this.m_symbol,this.m_timeframe,this.m_array_time[0]); if(bar==WRONG_VALUE) { this.m_success &=false; continue; } //--- Calculate the index of the last bar to copy in the loop int end=bar-num_bars; if(end<0) end=-1; //--- In a loop from the first to the last bar included in the bar of the older chart period for(int j=bar;j>end;j--) { //--- Find out which time of this class the bar of the current chart timeframe, corresponding to the loop index, belongs to ::ResetLastError(); if(::CopyTime(this.m_symbol,this.m_timeframe,j,1,this.m_array_time)!=1) { //--- If there is no data in the terminal, move on if(::GetLastError()==4401) continue; //--- Error in obtaining existing data - return false this.m_success &=false; return false; } //--- Get the bar number on the chart corresponding to the time of the buffer array of the calculation part by j loop index int barN=::iBarShift(symbol,timeframe,this.m_array_time[0]); //--- Get the data of the j bar in the buffer array of the calculation part and the data of barN contained in the chart indicator buffer double value_data=this.GetData(buffer_num,array_num,j); double value_buff=buffer[barN]; //--- If the bar on the chart has an empty value, then write data from the buffer array of the calculation part into it. Or //--- if there is already data on the chart, then we write new data to this bar only if the data contains a non-empty value. if(value_buff==this.BufferInitValue(buffer_num) || (value_buff!=this.BufferInitValue(buffer_num) && value_data!=this.BufferInitValue(buffer_num))) buffer[barN]=value_data; } } } } //--- Set initial indexing of the buffer array passed to the method ::ArraySetAsSeries(buffer,as_series); //--- Successful return true; }
The base object class of the multi-symbol multi-period indicator is ready.
Now let's finalize the successor classes.
Of all the standard indicators, only Bill Williams' fractla indicator has a drawing in the form of arrows with breaks (Parabolic SAR is also drawn using arrows, but without breaks on the chart).
And the drawing of the Fractals indicator begins on the bar with index 2. In other words, to calculate the indicator, we need to update three bars on each tick: 2, 1 and 0.
In the indicator class constructor, set the number of calculation bars to three:
//+------------------------------------------------------------------+ //| Fractals indicator class | //+------------------------------------------------------------------+ class CIndFractals : public CIndMSTF { public: //--- Constructor CIndFractals(const string symbol,const ENUM_TIMEFRAMES timeframe) : CIndMSTF(IND_FRACTALS,2,symbol,timeframe) { // Buffer indexes: 0 - UPPER_LINE, 1 - LOWER_LINE //--- Create description of parameters //--- If non-current chart symbol or period, their descriptions are added to parameters bool current=(this.Symbol()==::Symbol() && this.Timeframe()==::Period()); string symbol_period=(current ? "" : ::StringFormat("%s,%s",this.Symbol(),this.TimeframeDescription())); string param=(current ? "" : StringFormat("(%s)",symbol_period)); //--- Write description of parameters, indicator name, its description, title and category this.SetParameters(param); this.SetName("Fractals"); this.SetDescription("Fractals"); this.m_title=this.Name()+this.Parameters(); this.m_category=IND_CATEGORY_WILLIAMS; //--- Description of UPPER_LINE and LOWER_LINE line buffers this.SetBufferDescription(UPPER_LINE,this.m_title+" Up"); this.SetBufferDescription(LOWER_LINE,this.m_title+" Down"); //--- Set drawing style for buffers 0 and 1 and the numbers of calculation part data buffers this.SetBufferDrawType(0,DRAW_ARROW,UPPER_LINE); this.SetBufferDrawType(1,DRAW_ARROW,LOWER_LINE); //--- Set default colors for buffers 0 and 1 this.SetBufferColorToIndex(UPPER_LINE,0,clrGray); this.SetBufferColorToIndex(LOWER_LINE,0,clrGray); //--- Set the number of bars required to calculate the indicator on the current tick this.SetBarsToCalculate(3); } };
In the base class, this amount is set to two bars by default. Here we redefine the quantity to three bars and, accordingly, increase the dimensions of the temporary arrays to three as well.
In the custom indicator class, it is not known in advance how many bars are required to calculate it on the current tick. This means that we cannot specify the required number of bars in advance, as was done in the constructor of the fractal indicator class. Here we will have to set this number in the class constructor parameters passing and setting it via the input:
//+------------------------------------------------------------------+ //| Custom indicator class | //+------------------------------------------------------------------+ class CIndCustom : public CIndMSTF { public: //--- Constructor CIndCustom(const string symbol,const ENUM_TIMEFRAMES timeframe, const string path, // path to the indicator (for example, "Examples\\MACD.ex5") const string name, // name of the custom indicator const uint buffers, // number of indicator buffers const uint bars_to_calculate, // number of bars required to calculate the indicator on the current tick const MqlParam ¶m[] // array of custom indicator parameters ) : CIndMSTF(IND_CUSTOM,buffers,symbol,timeframe) { //--- If an empty array of parameters is passed, print this to log int total=(int)param.Size(); if(total==0) ::PrintFormat("%s Error. Passed an empty array",__FUNCTION__); //--- If the array is not empty and its size is increased by 1 (the first string parameter must contain the indicator name) ResetLastError(); if(total>0 && ::ArrayResize(this.m_param,total+1)==total+1) { //--- Reset data in the array and enter name (path to file and name of .ex5 file) ::ZeroMemory(this.m_param); //--- name of the custom indicator this.m_param[0].type=TYPE_STRING; this.m_param[0].string_value=path; //--- fill the array of indicator parameters for(int i=0;i<total;i++) { this.m_param[i+1].type=param[i].type; this.m_param[i+1].double_value=param[i].double_value; this.m_param[i+1].integer_value=param[i].integer_value; this.m_param[i+1].string_value=param[i].string_value; } //--- Create description of parameters //--- If non-current chart symbol or period, their descriptions are added to parameters bool current=(this.Symbol()==::Symbol() && this.Timeframe()==::Period()); string symbol_period=(current ? "" : ::StringFormat("%s,%s",this.Symbol(),this.TimeframeDescription())); string param=(current ? "" : StringFormat("(%s)",symbol_period)); //--- Write description of parameters, indicator name, its description, title and category this.SetParameters(param); this.SetName(name); this.SetDescription(name); this.m_title=this.Name()+this.Parameters(); this.m_category=IND_CATEGORY_CUSTOM; //--- Write a description of the first line buffer this.SetBufferDescription(0,this.m_title); //--- Set the number of bars required to calculate the indicator on the current tick this.SetBarsToCalculate(bars_to_calculate); } } };
We also need to specify the number of bars to calculate the indicator in the indicator collection class in the method for creating a new custom indicator object:
int AddNewVolumes(const string symbol,const ENUM_TIMEFRAMES timeframe,const ENUM_APPLIED_VOLUME applied_volume=VOLUME_TICK); int AddNewCustom(const string symbol,const ENUM_TIMEFRAMES timeframe,const string path, // path to the indicator (for example, "Examples\\MACD.ex5") const string name, // name of custom indicator (for example, "Custom MACD") const uint buffers, // number of buffers const uint bars_to_calculate,// number of bars required to calculate the indicator on the current tick const MqlParam ¶m[]); // Array of parameters //--- Timer
while in the method implementation code, we need to pass the value of this variable to the constructor of the custom indicator class:
//+------------------------------------------------------------------+ //| Add a custom indicator to the collection | //+------------------------------------------------------------------+ int CMSTFIndicators::AddNewCustom(const string symbol,const ENUM_TIMEFRAMES timeframe,const string path,const string name,const uint buffers,const uint bars_to_calculate,const MqlParam ¶m[]) { //--- Create a new indicator object. If there is an error, add a journal message and return INVALID_HANDLE CIndCustom *ind_obj=new CIndCustom(symbol,timeframe,path,name,buffers,bars_to_calculate,param); if(ind_obj==NULL) { ::PrintFormat("%s: Error. Failed to create %s custom indicator object",__FUNCTION__,name); return INVALID_HANDLE; } //--- Return the result of adding the created indicator object to the collection list return this.AddNewIndicator(ind_obj,__FUNCTION__); }
Now classes of multi-symbol multi-period indicators should work with arrow indicators and the ones containing data with gaps in the buffer — indicators of DRAW_ARROW, DRAW_COLOR_ARROW, DRAW_SECTION, DRAW_ZIGZAG, DRAW_COLOR_SECTION and DRAW_COLOR_ZIGZAG drawing styles.
Test
To perform the test, I can use any indicator from the previously created ones, for example Parabolis SAR since it also applies arrows. It has one drawn buffer, while the fractal indicator has two. One buffer is used to draw the upper fractals, while the second is used to draw the lower ones. Let's modify the indicator code so that it draws arrows in two buffers and save it under the name TestMSTFFractals.mq5.
The indicator will display data from two Fractals indicators on the chart at once - one will draw fractals from the current chart period, and the second will draw fractals from the Fractals indicator, calculated on the symbol and chart period specified in the settings. Also, to display data from fractal indicators, we will use the data panel in all indicators in the articles of this series:
//+------------------------------------------------------------------+ //| TestMSTFFractals.mq5 | //| Copyright 2023, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2023, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window #property indicator_buffers 4 #property indicator_plots 4 //--- enums //--- plot FractalsUp1 #property indicator_label1 "FractalsUp1" #property indicator_type1 DRAW_ARROW #property indicator_color1 clrGray #property indicator_width1 1 //--- plot FractalsDown1 #property indicator_label2 "FractalsDown1" #property indicator_type2 DRAW_ARROW #property indicator_color2 clrGray #property indicator_width2 1 //--- plot FractalsUp2 #property indicator_label3 "FractalsUp2" #property indicator_type3 DRAW_ARROW #property indicator_color3 clrDodgerBlue #property indicator_width3 1 //--- plot FractalsDown2 #property indicator_label4 "FractalsDown2" #property indicator_type4 DRAW_ARROW #property indicator_color4 clrDodgerBlue #property indicator_width4 1 //--- includes #include <IndMSTF\IndMSTF.mqh> #include <Dashboard\Dashboard.mqh> //--- input parameters input string InpSymbol = NULL; /* Symbol */ // Moving average symbol input ENUM_TIMEFRAMES InpTimeframe = PERIOD_CURRENT; /* Timeframe */ // Moving average timeframe input uchar InpArrowShift1 = 10; /* Senior period Arrow VShift */ // Shift the arrows vertically for the higher period input uchar InpArrowShift2 = 10; /* Junior period Arrow VShift */ // Shift the arrows vertically for the lower period input bool InpAsSeries = true; /* As Series flag */ // Timeseries flag of indicator buffer arrays //--- indicator buffers double BufferFractalsUp1[]; double BufferFractalsDn1[]; double BufferFractalsUp2[]; double BufferFractalsDn2[]; //--- global variables int handle_fractals1; int handle_fractals2; CMSTFIndicators indicators; // An instance of the indicator collection object //--- variables for the panel CDashboard *panel=NULL; // Pointer to the panel object int mouse_bar_index; // Index of the bar the data is taken from //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set a timer with an interval of 1 second EventSetTimer(1); //--- Assign the BufferFractalsUp1 and BufferFractalsDn1 arrays to the plot buffers 0 and 1, respectively SetIndexBuffer(0,BufferFractalsUp1,INDICATOR_DATA); SetIndexBuffer(1,BufferFractalsDn1,INDICATOR_DATA); //--- Assign the BufferFractalsUp2 and BufferFractalsDn2 arrays to the plot buffers 2 and 3, respectively SetIndexBuffer(2,BufferFractalsUp2,INDICATOR_DATA); SetIndexBuffer(3,BufferFractalsDn2,INDICATOR_DATA); //--- Define the symbol code from the Wingdings font to draw in PLOT_ARROW PlotIndexSetInteger(0,PLOT_ARROW,217); PlotIndexSetInteger(1,PLOT_ARROW,218); PlotIndexSetInteger(2,PLOT_ARROW,217); PlotIndexSetInteger(3,PLOT_ARROW,218); //--- Set the vertical shift of the arrows PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,-InpArrowShift1); PlotIndexSetInteger(1,PLOT_ARROW_SHIFT, InpArrowShift1); PlotIndexSetInteger(2,PLOT_ARROW_SHIFT,-InpArrowShift2); PlotIndexSetInteger(3,PLOT_ARROW_SHIFT, InpArrowShift2); //--- Set the timeseries flags for the indicator buffer arrays (for testing, to see that there is no difference) ArraySetAsSeries(BufferFractalsUp1,InpAsSeries); ArraySetAsSeries(BufferFractalsDn1,InpAsSeries); ArraySetAsSeries(BufferFractalsUp2,InpAsSeries); ArraySetAsSeries(BufferFractalsDn2,InpAsSeries); //--- Create two indicators of the same type //--- The first one is calculated on the current chart symbol/period, the second - on those specified in the settings handle_fractals1=indicators.AddNewFractals(NULL,PERIOD_CURRENT); handle_fractals2=indicators.AddNewFractals(InpSymbol,InpTimeframe); //--- If failed to create indicator handles, return initialization error if(handle_fractals1==INVALID_HANDLE || handle_fractals2==INVALID_HANDLE) return INIT_FAILED; //--- Set descriptions for indicator lines from buffer descriptions of calculation part of created indicators indicators.SetPlotLabelFromBuffer(0,handle_fractals1,0); indicators.SetPlotLabelFromBuffer(1,handle_fractals1,1); indicators.SetPlotLabelFromBuffer(2,handle_fractals2,0); indicators.SetPlotLabelFromBuffer(3,handle_fractals2,1); //--- Dashboard //--- Create the panel int width=311; panel=new CDashboard(1,20,20,width,264); if(panel==NULL) { Print("Error. Failed to create panel object"); return INIT_FAILED; } //--- Set font parameters panel.SetFontParams("Calibri",9); //--- Display the panel with the "Symbol, Timeframe description" header text panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),7)); //--- Create a table with ID 0 to display bar data in it panel.CreateNewTable(0); //--- Draw a table with ID 0 on the panel background panel.DrawGrid(0,2,20,6,2,18,width/2-2); //--- Create a table with ID 1 to display the data of indicator 1 panel.CreateNewTable(1); //--- Get the Y2 table coordinate with ID 0 and //--- set the Y1 coordinate for the table with ID 1 int y1=panel.TableY2(0)+22; //--- Draw a table with ID 1 on the panel background panel.DrawGrid(1,2,y1,2,2,18,width/2-2); //--- Create a table with ID 2 to display the data of indicator 2 panel.CreateNewTable(2); //--- Get the Y2 coordinate of the table with ID 1 and //--- set the Y1 coordinate for the table with ID 2 int y2=panel.TableY2(1)+3; //--- Draw a table with ID 2 on the background of the dashboard panel.DrawGrid(2,2,y2,3,2,18,width/2-2); //--- Initialize the variable with the index of the mouse cursor bar mouse_bar_index=0; //--- Display the data of the current bar on the panel DrawData(mouse_bar_index,TimeCurrent()); //--- Successful initialization return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Delete the timer EventKillTimer(); //--- If the panel object exists, delete it if(panel!=NULL) delete panel; //--- Delete all comments Comment(""); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- Number of bars for calculation int limit=rates_total-prev_calculated; //--- If limit > 1, then this is the first calculation or change in the history if(limit>1) { //--- specify all the available history for calculation limit=rates_total-1; /* // If the indicator has any buffers that display other calculations (not multi-indicators), // initialize them here with the "empty" value set for these buffers */ } //--- Calculate all created multi-symbol multi-period indicators if(!indicators.Calculate()) return 0; //--- Display the bar data under cursor (or current bar if cursor is outside the chart) on the dashboard DrawData(mouse_bar_index,time[mouse_bar_index]); //--- From buffers of calculated indicators, output data to indicator buffers if(!indicators.DataToBuffer(NULL,PERIOD_CURRENT,handle_fractals1,0,0,limit,BufferFractalsUp1)) return 0; if(!indicators.DataToBuffer(NULL,PERIOD_CURRENT,handle_fractals1,1,0,limit,BufferFractalsDn1)) return 0; if(!indicators.DataToBuffer(NULL,PERIOD_CURRENT,handle_fractals2,0,0,limit,BufferFractalsUp2)) return 0; if(!indicators.DataToBuffer(NULL,PERIOD_CURRENT,handle_fractals2,1,0,limit,BufferFractalsDn2)) return 0; //--- return value of prev_calculated for the next call return(rates_total); } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- Call the indicator collection timer indicators.OnTimer(); } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Handling the panel //--- Call the panel event handler panel.OnChartEvent(id,lparam,dparam,sparam); //--- If the cursor moves or a click is made on the chart if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK) { //--- Declare the variables to record time and price coordinates in them datetime time=0; double price=0; int wnd=0; //--- If the cursor coordinates are converted to date and time if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price)) { //--- write the bar index where the cursor is located to a global variable mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time); //--- Display the bar data under the cursor on the panel DrawData(mouse_bar_index,time); } } //--- If we received a custom event, display the appropriate message in the journal if(id>CHARTEVENT_CUSTOM) { //--- Here we can implement handling a click on the close button on the panel PrintFormat("%s: Event id=%ld, object id (lparam): %lu, event message (sparam): %s",__FUNCTION__,id,lparam,sparam); } } //+------------------------------------------------------------------+ //| Display data from the specified timeseries index to the panel | //+------------------------------------------------------------------+ void DrawData(const int index,const datetime time) { //--- Declare the variables to receive data in them MqlRates rates[1]; //--- Exit if unable to get the bar data by the specified index if(CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates)!=1) return; //--- Set font parameters for bar and indicator data headers int size=0; uint flags=0; uint angle=0; string name=panel.FontParams(size,flags,angle); panel.SetFontParams(name,9,FW_BOLD); panel.DrawText("Bar data ["+(string)index+"]",3,panel.TableY1(0)-16,clrMaroon,panel.Width()-6); panel.DrawText("Indicators data ["+(string)index+"]",3,panel.TableY1(1)-16,clrGreen,panel.Width()-6); //--- Set font parameters for bar and indicator data panel.SetFontParams(name,9); //--- Display the data of the specified bar in table 0 on the panel panel.DrawText("Date", panel.CellX(0,0,0)+2, panel.CellY(0,0,0)+2); panel.DrawText(TimeToString( rates[0].time,TIME_DATE), panel.CellX(0,0,1)+2, panel.CellY(0,0,1)+2,clrNONE,90); panel.DrawText("Time", panel.CellX(0,1,0)+2, panel.CellY(0,1,0)+2); panel.DrawText(TimeToString( rates[0].time,TIME_MINUTES), panel.CellX(0,1,1)+2, panel.CellY(0,1,1)+2,clrNONE,90); panel.DrawText("Open", panel.CellX(0,2,0)+2, panel.CellY(0,2,0)+2); panel.DrawText(DoubleToString(rates[0].open,Digits()), panel.CellX(0,2,1)+2, panel.CellY(0,2,1)+2,clrNONE,90); panel.DrawText("High", panel.CellX(0,3,0)+2, panel.CellY(0,3,0)+2); panel.DrawText(DoubleToString(rates[0].high,Digits()), panel.CellX(0,3,1)+2, panel.CellY(0,3,1)+2,clrNONE,90); panel.DrawText("Low", panel.CellX(0,4,0)+2, panel.CellY(0,4,0)+2); panel.DrawText(DoubleToString(rates[0].low,Digits()), panel.CellX(0,4,1)+2, panel.CellY(0,4,1)+2,clrNONE,90); panel.DrawText("Close", panel.CellX(0,5,0)+2, panel.CellY(0,5,0)+2); panel.DrawText(DoubleToString(rates[0].close,Digits()), panel.CellX(0,5,1)+2, panel.CellY(0,5,1)+2,clrNONE,90); //--- Output the data of buffer 0 of indicator 1 from the specified bar into table 1 panel.DrawText(indicators.Title(handle_fractals1)+" Up", panel.CellX(1,0,0)+2, panel.CellY(1,0,0)+2); double value10=indicators.GetData(handle_fractals1,0,0,index); string value_str10=(value10!=EMPTY_VALUE ? DoubleToString(value10,indicators.Digits(handle_fractals1)) : " "); panel.DrawText(value_str10,panel.CellX(1,0,1)+2,panel.CellY(1,0,1)+2,clrNONE,110); //--- Output the data of buffer 1 of indicator 1 from the specified bar into table 1 panel.DrawText(indicators.Title(handle_fractals1)+" Down", panel.CellX(1,1,0)+2, panel.CellY(1,1,0)+2); double value11=indicators.GetData(handle_fractals1,1,0,index); string value_str11=(value11!=EMPTY_VALUE ? DoubleToString(value11,indicators.Digits(handle_fractals1)) : " "); panel.DrawText(value_str11,panel.CellX(1,1,1)+2,panel.CellY(1,1,1)+2,clrNONE,110); //--- Output the data of buffer 0 of indicator 2 from the specified bar into table 2 panel.DrawText(indicators.Title(handle_fractals2)+" Up", panel.CellX(2,0,0)+2, panel.CellY(2,0,0)+2); double value20=indicators.GetDataTo(Symbol(),PERIOD_CURRENT,handle_fractals2,0,0,index); string value_str20=(value20!=EMPTY_VALUE ? DoubleToString(value20,indicators.Digits(handle_fractals2)) : " "); panel.DrawText(value_str20,panel.CellX(2,0,1)+2,panel.CellY(2,0,1)+2,clrNONE,110); //--- Output the data of buffer 1 of indicator 2 from the specified bar into table 2 panel.DrawText(indicators.Title(handle_fractals2)+" Down", panel.CellX(2,1,0)+2, panel.CellY(2,1,0)+2); double value21=indicators.GetDataTo(Symbol(),PERIOD_CURRENT,handle_fractals2,1,0,index); string value_str21=(value21!=EMPTY_VALUE ? DoubleToString(value21,indicators.Digits(handle_fractals2)) : " "); panel.DrawText(value_str21,panel.CellX(2,1,1)+2,panel.CellY(2,1,1)+2,clrNONE,110); //--- Redraw the chart to immediately display all changes on the panel ChartRedraw(ChartID()); } //+------------------------------------------------------------------+
Compile the indicator and launch it on the M1 chart, then switch the chart period to M5 and M15:
We see how the indicator buffers on the M1 chart are filled with data from the indicator designed for M5.
At the same time, when switching to M15, we see how all the fractals of the indicator designed for M5 are drawn on M15 bars. At the same time, they seemingly should not be present on every bar of the M15 chart. But this is not the case. In each bar of the M15 chart there are three bars from M5 chart containing the fractals. It is these fractals that are displayed on the bars of the M15 chart, since this is what the indicator was created for - to be a multi-symbol multi-period indicator.
Conclusion
In this article, we created and tested the operation of classes of multi-symbol multi-period indicators with the DRAW_ARROW drawing style. In subsequent articles, we will focus on the remaining drawing styles that involve data breaks on the chart. Besides, we will complete the remaining standard indicators that have not yet been considered in order to close the topic of multi-symbol multi-period indicators.
All files used in the article (the multi-indicator classes, the panel class and the indicator file) are attached to the article for independent tests and use.
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/14105





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Good. (chuckles) Glad you figured it out on your own
I apologise, maybe off-topic, I have been following your topics for a long time, you wrote that when the time comes, you will create an example robot on your library that on its basis it would be clear what and where to insert, to fully use and collect algorithms.
The time has not come yet? I just see the topics on the library are over.
I apologise, maybe off topic, I have been following your threads for a long time, you wrote that when the time comes, you will create an example robot on your library that on its basis it would be clear what and where to insert, to fully use and collect algorithms.
The time has not come yet? I just see the topics on the library are over.
On the library - on the continuation with graphics, while suspended development because of an unpleasant bug that manifests itself in periodic blinking of hidden parts of objects. Until I find the cause, I should not make graphics, so as not to accumulate bugs. But the rest of the library's features will be continued soon. And then we will get to examples.
due to an unpleasant bug that manifests itself in periodic blinking of hidden parts of objects.
You should check the conditions of its appearance just in case. For example, whether this blinking is related to the quotes reloading. If yes, the problem is not in your code.
On the library - on the continuation with graphics, so far suspended development because of an unpleasant bug that manifests itself in periodic blinking of hidden parts of objects. Until I find the cause, I should not make graphics, so as not to accumulate bugs. But the rest of the library's features will be continued soon. And then we will get to examples.
Thank you, we are very much looking forward to it.
It is worth checking the conditions of its appearance just in case. For example, whether this blinking is related to the quotes reloading. If yes, the problem is not in your code.