preview
Тип рисования DRAW_ARROW в мультисимвольных мультипериодных индикаторах

Тип рисования DRAW_ARROW в мультисимвольных мультипериодных индикаторах

MetaTrader 5Примеры | 24 января 2024, 14:18
644 13
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

Продолжаем тему мультисимвольных мультипериодных индикаторов. Последняя статья этой серии была о цветных буферах в мультисимвольных мультипериодных индикаторах. Сегодня доработаем класс мульти- индикаторов так, чтобы он мог работать со стрелочными индикаторами.

Стрелочные индикаторы подразумевают не постоянное наличие данных в их рисуемом буфере. Там, где отображается стрелка — там есть значение в буфере, тогда как в остальные моменты времени в буфере находится пустое значение, установленное для него. Обычно это EMPTY_VALUE, но для буфера возможно установить любое значение, которое будет "пустым", и не будет отображаться на графике. Сделать это можно при помощи функции

PlotIndexSetDouble(buffer_index,PLOT_EMPTY_VALUE,empty_value);

где buffer_index — это индекс буфера, которому устанавливается пустое значение, а empty_value — величина "пустого значения", которое будет установлено для этого буфера.

В мультипериодных индикаторах, где буфер заполняется данными с разрывами, нужно учитывать наличие пустых значений там, где нет стрелок, и не вписывать эти пустые значения в бар графика, где уже вписано непустое значение. Иначе стрелка, поставленная ранее, будет стёрта новым пустым значением. Это справедливо в случае, если на старший таймфрейм копируются данные от индикатора, рассчитанного на младшем таймфрейме.

Поясню на примере. На изображении графика М5 помечены бары графика М15:


Здесь мы видим фракталы графика М5, которые необходимо установить на бары графика М15.

  • Для первого бара М15 (I) необходимо установить верхний фрактал, расположенный на баре 3 — этот фрактал в любом случае будет виден на М15, так как он последний из трёх.
  • Для второго бара М15 (II) нужно установить фрактал, расположенный на баре 2 — этот фрактал будет виден на М15 только в том случае, если не копировать пустое значение с бара 3, так как оно заменит собою значение нижнего фрактала. Бар на М15 будет иметь два фрактала — верхний и нижний, так как на М5 есть нижний фрактал на баре 2 и верхний фрактал на баре 3.
  • Для третьего бара М15 (III) нужно установить верхний фрактал с бара 1 — этот фрактал будет виден на М15 только в том случае, если не копировать пустые значения с баров 2 и 3, так как они заменят собою значение фрактала.

Таким образом, нам необходимо отслеживать значение на баре текущего графика и, если оно пустое, то можно копировать любое значение из буфера младшего таймфрейма. Но если значение уже не пустое, то копировать нужно только тоже не пустое значение. В этом случае на баре графика старшего таймфрейма будет установлено значение последнего фрактала из тех баров младшего таймфрейма, которые входят в состав бара старшего периода графика. Почему именно последнего бара, а не первого — считаю, что это значение более актуально, так как оно находится ближе к текущему времени.


При копировании данных индикатора, рассчитанного на старшем таймфрейме, на график младшего таймфрейма, нужно учитывать то, что стрелка может ставиться не на нулевой или первый бар графика, а, например, на второй, третий, или более старший. Если же копировать данные только первого и нулевого баров, как это сделано сейчас в классе мульти- индикаторов, то никакие стрелки на график выставлены не будут — они попросту находятся на барах с более высоким индексом и, соответственно, алгоритм расчёта индикатора их просто не может увидеть:


Здесь мы видим, что фрактал образован на баре с индексом 2. Если же копировать только нулевой и первый бары, то этот фрактал для программы будет не виден.

Соответственно, так как мы не можем точно знать какой индикатор на каком баре ставит свои стрелки, то для класса мульти- индикаторов необходимо ввести переменную, в которой будем явно указывать бар, с которого необходимо начинать поиск и копирование значений. На данный момент класс копирует только первый и нулевой бары на текущем тике, соответственно, фракталы он видеть не может — для них значения начинаются с бара с индексом 2.

Если в качестве исходного индикатора использовать пользовательский, то он может ставить стрелки и на любом ином баре — не только на втором, как индикатор Fractals. Например, если это индикатор пользовательских фракталов, где ищутся фракталы иной размерности, например 3-X-3, то стрелка может ставиться на баре с индексом 3 (если фрактал перерисовывается) или 4 (если фрактал не перерисовывается).

Для стандартных индикаторов будем явно указывать индекс бара, так как он известен. Для пользовательских индикаторов сделаем возможность указания бара начала поиска в параметрах конструктора класса.


Доработка классов мульти- индикаторов

При копировании данных в методах класса мы использовали локальные временные массивы. Сделаем их глобальными чтобы каждый раз не перевыделять память под эти массивы.

В приватной секции базового класса мультисимвольных мультипериодных индикаторов CIndMSTF объявим новые массивы, а в защищённой секции — переменную для хранения количества баров для расчёта индикатора на текущем тике:

//+------------------------------------------------------------------+
//| Базовый класс мультисимвольного мультипериодного индикатора      |
//+------------------------------------------------------------------+
class CIndMSTF : public CObject
  {
private:
   ENUM_PROGRAM_TYPE m_program;           // Тип программы
   ENUM_INDICATOR    m_type;              // Тип индикатора
   ENUM_TIMEFRAMES   m_timeframe;         // Период графика
   string            m_symbol;            // Символ графика
   int               m_handle;            // Хэндл индикатора
   int               m_id;                // Идентификатор
   bool              m_success;           // Флаг успешного расчёта
   ENUM_ERR_TYPE     m_type_err;          // Тип ошибки при расчёте
   string            m_description;       // Пользовательское описание индикатора
   string            m_name;              // Наименование индикатора
   string            m_parameters;        // Описание параметров индикатора
   double            m_array_data[];      // Временный массив для копирования данных
   datetime          m_array_time[];      // Временный массив для копирования времени
   
protected:
   ENUM_IND_CATEGORY m_category;          // Категория индикатора
   MqlParam          m_param[];           // Массив параметров индикатора
   string            m_title;             // Заголовок (наименование индикатора + описание параметров)
   SBuffer           m_buffers[];         // Буферы индикатора
   int               m_digits;            // Digits значений индикатора
   int               m_limit;             // Количество баров, необходимое для просчёта индикатора на текущем тике
   int               m_rates_total;       // Количество доступных баров для просчёта индикатора
   int               m_prev_calculated;   // Количество просчитанных баров на прошлом вызове индикатора
   uint              m_bars_to_calculate; // Количество баров, необходимое для расчёта индикатора на текущем тике


В публичной секции класса напишем метод, устанавливающий количество баров, необходимое для расчёта индикатора на текущем тике:

//--- Возвращает количество рассчитанных данных
   int               Calculated(void)                          const { return ::BarsCalculated(this.m_handle); }
//--- Устанавливает количество баров, необходимое для расчёта индикатора на текущем тике
   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);
                          }
                       }
   
//--- Виртуальный метод, возвращающий тип объекта (индикатора)

В метод передаётся требуемое количество баров, в новую переменную записывается это значение и размеры массивов устанавливаются равными значению этой переменной.

Таким образом, при успешном изменении размеров массивов, их далее уже не придётся заново и каждый раз устанавливать в методах копирования данных. Если же здесь не удастся изменить размеры массивов, то функции копирования данных внутри методов копирования в этом случае будут пытаться изменить размер массива под требуемое количество. Если же и там это сделать не удастся, то из методов будет выдано сообщение об ошибке копирования.

Таким образом, мы один раз устанавливаем требуемый размер массивам — либо в конструкторе класса, либо (при неудаче) в методах копирования, а далее уже используем массивы с установленным требуемым размером, что избавляет нас от постоянного перевыделения памяти под массивы, в том случае, если бы они были локальными.

В конструкторе класса вызываем этот метод со значением по умолчанию для подавляющего большинства индикаторов, равным два бара, для копирования данных (первый и нулевой):

//+------------------------------------------------------------------+
//| Конструктор                                                      |
//+------------------------------------------------------------------+
CIndMSTF::CIndMSTF(const ENUM_INDICATOR type,const uint buffers,const string symbol,const ENUM_TIMEFRAMES timeframe)
  {
//--- Запускаем таймер
   ::ResetLastError();
   if(!::EventSetTimer(1))
      ::PrintFormat("%s: EventSetTimer failed. Error %lu",__FUNCTION__,::GetLastError());
//--- Устанавливаем свойствам переданные в конструктор значения, или значения по умолчанию
   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;
//--- Устанавливаем массиву структуре буферов размер, равный количеству буферов индикатора
   ::ResetLastError();
   if(::ArrayResize(this.m_buffers,buffers)!=buffers)
      ::PrintFormat("%s: Buffers ArrayResize failed. Error  %lu"__FUNCTION__,::GetLastError());
//--- Устанавливаем "пустое" значение для каждого буфера по умолчанию (потом можно изменить)
   for(int i=0;i<(int)this.m_buffers.Size();i++)
      this.SetBufferInitValue(i,EMPTY_VALUE);
//--- Устанавливаем начальные значения переменным, участвующим в экономном расчёте индикатора
   this.m_prev_calculated=0;
   this.m_limit=0;
   this.m_rates_total=::Bars(this.m_symbol,this.m_timeframe);
   this.SetBarsToCalculate(2);
//--- Если индикатор рассчитывается не на текущем графике - делаем запрос данных с нужного графика
//--- (первое обращение к данным запускает подкачку этих данных)
   datetime array[];
   if(symbol!=::Symbol() || timeframe!=::Period())
      ::CopyTime(this.m_symbol,this.m_timeframe,0,this.m_rates_total,array);
  }


В деструкторе класса освобождаем память, выделенную под временные массивы:

//+------------------------------------------------------------------+
//| Деструктор                                                       |
//+------------------------------------------------------------------+
CIndMSTF::~CIndMSTF()
  {
//--- Удаляем таймер
   ::EventKillTimer();
//--- Освобождаем хэндл индикатора
   ::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());
//--- Освобождаем память массивов буферов
   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);
     }
//--- Освобождаем память временных массивов
   ::ArrayFree(this.m_array_data);
   ::ArrayFree(this.m_array_time);
  }


В методе копирования данных указанного массива указанного буфера теперь проверяем количество копируемых данных по значению, установленному в новой переменной. Ранее там было жёстко задано значение в два бара:

//+------------------------------------------------------------------+
//| Копирует данные указанного массива указанного буфера             |
//+------------------------------------------------------------------+
bool CIndMSTF::CopyArray(const uint buff_num,const uint array_num,const int to_copy,double &array[])
  {
   ::ResetLastError();
//--- Копируем либо два последних бара в массив array, либо все имеющиеся исторические данные из массива расчётной части индикатора в массив-буфер объекта индикатора
   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>0)
      return true;
//--- Если скопированы не все данные
//--- Если CopyBuffer вернула -1, то это означает начало загрузки исторических данных
//--- выведем сообщение об этом в журнал
   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());
//--- В любом ином случае - ещё не все данные удалось скопировать
//--- выведем сообщение об этом в журнал
   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;
  }


В методе копирования данных всех массивов указанного буфера ранее было жёстко задано копирование двух баров, и было прямо прописано в коде с какого индекса временного массива в какой индекс массива-буфера копировать данные:

//+------------------------------------------------------------------+
//| Копирует данные всех массивов указанного буфера                  |
//+------------------------------------------------------------------+
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))
        {
         //--- Один буфер
         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;
         //--- Два буфера
         case DRAW_HISTOGRAM2       :
         case DRAW_ZIGZAG           :
         case DRAW_FILLING          :

Сейчас же заранее не известно количество копируемых данных — оно прописано в новой переменной. Поэтому и проверять будем значение этой переменной, а копировать данные будем в цикле по количеству копируемых данных.
Индексы исходного и приёмного массивов будем рассчитывать из индекса цикла. При этом теперь у нас объявлен глобальный временный массив, поэтому локальный, как был ранее, уже не нужен:

//+------------------------------------------------------------------+
//| Копирует данные всех массивов указанного буфера                  |
//+------------------------------------------------------------------+
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))
        {
         //--- Один буфер
         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;
         //--- Два буфера
         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;
         //--- Четыре буфера
         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;
         //--- Один буфер + буфер цвета
         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;
         //--- Два буфера + буфер цвета
         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;
         //--- Четыре буфера + буфер цвета
         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))
        {
         //--- Один буфер
         case DRAW_LINE             :
         case DRAW_HISTOGRAM        :
         case DRAW_ARROW            :
         case DRAW_SECTION          :
            return this.CopyArray(buff_num,0,to_copy,this.m_array_data);
         //--- Два буфера
         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;
         //--- Четыре буфера
         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;
         //--- Один буфер + буфер цвета
         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;
         //--- Два буфера + буфер цвета
         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;
         //--- Четыре буфера + буфер цвета
         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;
  }


В методе заполнения буферов объекта данными из буфера расчётной части в блоке кода, отвечающем за работу на текущем тике, теперь будем копировать не два бара, а количество баров, установленное в переменной m_bars_to_calculate:

//--- Если рассчитанное значение m_limit меньше, либо равно 1 - значит это либо открытие нового бара (m_limit==1), либо текущий тик (m_limit==0)
//--- В этом случае необходимо рассчитать указанное в переменной m_bars_to_calculate количество баров - текущий и остальные
   if(this.m_limit<=1)
     {
      //--- В цикле по количеству буферов индикатора
      for(int i=0;i<total;i++)
        {
         //--- Если это открытие нового бара и не удалось изменить размер буфера индикатора,
         if(this.m_limit==1 && !this.BufferResize(i,this.m_rates_total))
           {
            //--- добавим к переменной m_success значение false и вернём false
            //--- При этом сообщение об ошибке будет выведено в журнал из метода BufferResize
            this.m_success=false;
            return false;
           }
         //--- Если не удалось скопировать бары в количестве m_bars_to_calculate из буфера расчётной части индикатора,
         ::ResetLastError();
         if(!this.CopyArrays(i,this.m_bars_to_calculate))
           {
            //--- сообщим об этом в журнал, добавим к переменной m_success значение false и вернём false
            ::PrintFormat("%s::%s: CopyBuffer(%lu) failed. Error %lu",__FUNCTION__,this.Title(),i,::GetLastError());
            this.m_success &=false;
           }
        }
      //--- Если после цикла есть ошибки - возвращаем false
      if(!this.m_success)
        {
         this.m_type_err=ERR_TYPE_NO_DATA;
         return false;
        }
      
      //--- Успешно
      this.m_type_err=ERR_TYPE_NO_ERROR;
      this.m_success=true;
      return true;
     }
//--- Неопределённый вариант limit - возвращаем false
   return false;
  }


Метод, заполняющий переданный рисуемый массив данными из буфера класса, теперь полностью переработан. В нём учтены нюансы копирования данных с младшего таймфрейма на старший, со старшего на младший и количество копируемых данных на текущем тике, которые обсуждались в самом начале этой статьи. Логика этого метода полностью переписана и прокомментирована:

//+------------------------------------------------------------------+
//| Заполняет переданный рисуемый массив данными из буфера класса    |
//+------------------------------------------------------------------+
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[])
  {
//--- Устанавливаем флаг успешности
   this.m_success=true;
//--- Получаем направление индексации переданного в метод массива буфера и,
//--- если индексация не как у таймсерии, - устанавливаем индексацию как у таймсерии
   bool as_series=::ArrayGetAsSeries(buffer);
   if(!as_series)
      ::ArraySetAsSeries(buffer,true);
//--- Устанавливаем наименование символа и значение таймфрейма, переданные в метод
   string symbol=(symbol_to=="" || symbol_to==NULL ? ::Symbol() : symbol_to);
   ENUM_TIMEFRAMES timeframe=(timeframe_to==PERIOD_CURRENT ? ::Period() : timeframe_to);

//--- Если это первый запуск, или изменения в истории - инициальзируем массив буфера, переданный в метод
   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));
     }
      
//--- Устанавливаем значение счётчика цикла (не более максимального количества баров в терминале на графике)
   int count=(limit<=1 ? (int)this.m_bars_to_calculate : ::fmin(::TerminalInfoInteger(TERMINAL_MAXBARS),limit));
//--- В цикле от нулевого бара до значения счётчика цикла
   for(int i=0;i<count;i++)
     {
      //--- Если таймфрейм графика совпадает с таймфреймом объекта класса - заполняем буфер напрямую из массива объекта класса
      if(timeframe==::Period() && this.m_timeframe==::Period())
         buffer[i]=this.GetData(buffer_num,array_num,i);
      //--- Иначе, если таймфрейм графика не равен таймфрейму объекта класса
      else
        {
         //--- Если таймфрейм графика младше таймфрейма объекта класса,
         if(timeframe<this.m_timeframe)
           {
            //--- Если это исторические данные (бар с индексом большим, чем m_bars_to_calculate-1)
            if(i>(int)this.m_bars_to_calculate-1)
              {
               //--- Узнаём какому времени данного класса принадлежит бар текущего таймфрейма графика, соответствующий индексу цикла
               ::ResetLastError();
               if(::CopyTime(symbol,timeframe,i,1,this.m_array_time)!=1)
                 {
                  //--- Если данных нет в терминале - идём дальше
                  if(::GetLastError()==4401)
                     continue;
                  //--- Ошибка получения существующих данных - возвращаем false
                  this.m_success &=false;
                  return false;
                 }
               //--- По времени бара текущего таймфрейма графика находим соответствующий индекс бара периода графика в массиве-буфере объекта класса
               int bar=::iBarShift(this.m_symbol,this.m_timeframe,this.m_array_time[0]);
               if(bar==WRONG_VALUE)
                 {
                  this.m_success &=false;
                  continue;
                 }
               //--- записываем в буфер индикатора по индексу цикла значение, полученное из буфера расчётной части
               buffer[i]=this.GetData(buffer_num,array_num,bar);
              }
            //--- Если это текущий (нулевой) бар или бар, с индексом меньшим, либо равным m_bars_to_calculate-1
            else
              {
               //--- Получаем время баров по символу/таймфрейму объекта класса в количестве 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;
                 }
               
               //--- Получаем количество баров текущего периода графика, содержащихся в периоде графика объекта класса
               int num_bars=::PeriodSeconds(this.m_timeframe)/::PeriodSeconds(timeframe);
               if(num_bars<1)
                  num_bars=1;
               
               //--- В цикле по массиву полученных времён баров
               for(int j=0;j<(int)this.m_array_time.Size();j++)
                 {
                  //--- По времени бара в массиве-буфере объекта класса получаем соответствующий бар на графике 
                  int barN=::iBarShift(symbol,timeframe,this.m_array_time[j]);
                  if(barN==WRONG_VALUE)
                    {
                     this.m_success &=false;
                     return false;
                    }
                  //--- Рассчитываем конечный бар на графике для копирования данных из буфера расчётной части индикатора
                  int end=barN-num_bars;
                  if(end<0)
                     end=-1;
                  //--- В цикле от бара barN до бара end копируем данные из массива-буфера расчётной части в рисуемый буфер индикатора на графике
                  for(int n=barN;n>end;n--)
                     buffer[n]=this.GetData(buffer_num,array_num,this.m_bars_to_calculate-1-j);
                 }
              }
           }
         
         //--- Если таймфрейм графика старше таймфрейма объекта класса,
         else if(timeframe>this.m_timeframe)
           {
            //--- Получаем количество баров периода графика объекта класса, содержащихся в баре текущего периода графика
            int num_bars=::PeriodSeconds(timeframe)/::PeriodSeconds(this.m_timeframe);
            //--- Узнаём какому времени данного класса принадлежит бар текущего таймфрейма графика, соответствующий индексу цикла
            ::ResetLastError();
            if(::CopyTime(symbol,timeframe,i,1,this.m_array_time)!=1)
              {
               //--- Если данных нет в терминале - идём дальше
               if(::GetLastError()==4401)
                  continue;
               //--- Ошибка получения существующих данных - возвращаем false
               this.m_success &=false;
               return false;
              }
            //--- По времени бара текущего таймфрейма графика находим соответствующий индекс бара периода графика в массиве-буфере объекта класса
            //--- Этот бар будет первым из нескольких баров объекта класса в количестве num_bars, входящих в состав бара на графике
            int bar=::iBarShift(this.m_symbol,this.m_timeframe,this.m_array_time[0]);
            if(bar==WRONG_VALUE)
              {
               this.m_success &=false;
               continue;
              }
            //--- Рассчитываем индекс последнего бара для копирования в цикле
            int end=bar-num_bars;
            if(end<0)
               end=-1;
            //--- В цикле от первого до последнего бара, входящих в состав бара старшего периода графика
            for(int j=bar;j>end;j--)
              {
               //--- Узнаём какому времени данного класса принадлежит бар текущего таймфрейма графика, соответствующий индексу цикла
               ::ResetLastError();
               if(::CopyTime(this.m_symbol,this.m_timeframe,j,1,this.m_array_time)!=1)
                 {
                  //--- Если данных нет в терминале - идём дальше
                  if(::GetLastError()==4401)
                     continue;
                  //--- Ошибка получения существующих данных - возвращаем false
                  this.m_success &=false;
                  return false;
                 }
               //--- Получаем номер бара на графике, соответствующий времени массива-буфера расчётной части по индексу цикла j
               int barN=::iBarShift(symbol,timeframe,this.m_array_time[0]);
               //--- Получаем данные бара j в массиве-буфере расчётной части и данные бара barN, содержащиеся в буфере индикатора на графике
               double value_data=this.GetData(buffer_num,array_num,j);
               double value_buff=buffer[barN];
               //--- Если бар на графике имеет пустое значение, то записываем в него данные из массива-буфера расчётной части. Либо,
               //--- если на графике уже есть данные, то новые данные в этот бар записываем только, если в данных содержится не пустое значение.
               if(value_buff==this.BufferInitValue(buffer_num) || (value_buff!=this.BufferInitValue(buffer_num) && value_data!=this.BufferInitValue(buffer_num)))
                  buffer[barN]=value_data;
              }
           }
        }
     }
//--- Устанавливаем изначальную индексацию переданного в метод массива буфера
   ::ArraySetAsSeries(buffer,as_series);
//--- Успешно
   return true;
  }

Класс базового объекта мультисимвольного мультипериодного индикатора готов.

Теперь доработаем классы-наследники.

Из всех стандартных индикаторов только индикатор фракталов Билла Вильямса имеет отрисовку в виде стрелок с разрывами (Parabolic SAR тоже рисуется стрелками, но без разрывов на графике).

И отрисовка индикатора Fractals начинается на баре с индексом 2, т.е. для расчёта индикатора нужно обновлять на каждом тике три бара: 2, 1 и 0.

В конструкторе класса этого индикатора укажем количество баров расчёта равное трём:

//+------------------------------------------------------------------+
//| Класс индикатора Fractals                                        |
//+------------------------------------------------------------------+
class CIndFractals : public CIndMSTF
  {
public:
//--- Конструктор
   CIndFractals(const string symbol,const ENUM_TIMEFRAMES timeframe) : CIndMSTF(IND_FRACTALS,2,symbol,timeframe)
     {
      // Номера буферов: 0 - UPPER_LINE, 1 - LOWER_LINE
      //--- Создаём описание параметров.
      //--- Если символ или период графика не текущие, то к параметрам добавляются их описания
      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));
      //--- Записываем описание параметров, наименование индикатора, его описание, заголовок и категорию
      this.SetParameters(param);
      this.SetName("Fractals");
      this.SetDescription("Fractals");
      this.m_title=this.Name()+this.Parameters();
      this.m_category=IND_CATEGORY_WILLIAMS;
      //--- Описание буферов линий UPPER_LINE и LOWER_LINE
      this.SetBufferDescription(UPPER_LINE,this.m_title+" Up");
      this.SetBufferDescription(LOWER_LINE,this.m_title+" Down");
      
      //--- Установим стиль рисования для буферов 0, 1 и номера буферов данных расчётной части
      this.SetBufferDrawType(0,DRAW_ARROW,UPPER_LINE);
      this.SetBufferDrawType(1,DRAW_ARROW,LOWER_LINE);
      
      //--- Зададим для буферов 0 и 1 цвета по умолчанию
      this.SetBufferColorToIndex(UPPER_LINE,0,clrGray);
      this.SetBufferColorToIndex(LOWER_LINE,0,clrGray);
      
      //--- Устанавливаем количество баров, необходимое для расчёта индикатора на текущем тике
      this.SetBarsToCalculate(3);
     }
  };

В базовом классе это количество по умолчанию устанавливается равное двум барам. Здесь мы переопределяем количество на три бара и, соответственно, увеличиваем и размерности временных массивов тоже до трёх.


В классе пользовательского индикатора заранее не известно сколько требуется баров для его расчёта на текущем тике. А значит, мы не можем заранее указать нужное количество баров, как это сделано в конструкторе класса индикатора фракталов. Здесь придётся это количество указывать в параметрах конструктора класса, передавая и устанавливая его через входную переменную:

//+------------------------------------------------------------------+
//| Класс пользовательского индикатора                               |
//+------------------------------------------------------------------+
class CIndCustom : public CIndMSTF
  {
public:
//--- Конструктор
   CIndCustom(const string symbol,const ENUM_TIMEFRAMES timeframe,
              const string path,                      // путь к индикатору (например, "Examples\\MACD.ex5")
              const string name,                      // имя пользовательского индикатора
              const uint   buffers,                   // количество буферов индикатора
              const uint   bars_to_calculate,         // количество баров, необходимое для расчёта индикатора на текущем тике
              const MqlParam &param[]                 // массив параметров пользовательского индикатора
             ) : CIndMSTF(IND_CUSTOM,buffers,symbol,timeframe)
     {
      //--- Если передан пустой массив параметров - сообщаем об этом в журнал
      int total=(int)param.Size();
      if(total==0)
         ::PrintFormat("%s Error. Passed an empty array",__FUNCTION__);
      //--- Если массив не пустой и его размер увеличен на 1 (в первый параметр типа string должно быть записано имя индикатора)
      ResetLastError();
      if(total>0 && ::ArrayResize(this.m_param,total+1)==total+1)
        {
         //--- Обнуляем данные в массиве и вписываем имя (путь к файлу и имя .ex5 файла)
         ::ZeroMemory(this.m_param);
         //--- имя пользовательского индикатора
         this.m_param[0].type=TYPE_STRING;
         this.m_param[0].string_value=path;
         //--- заполняем массив параметров индикатора
         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;
           }
         //--- Создаём описание параметров.
         //--- Если символ или период графика не текущие, то к параметрам добавляются их описания
         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));
         //--- Записываем описание параметров, наименование индикатора, его описание, заголовок и категорию
         this.SetParameters(param);
         this.SetName(name);
         this.SetDescription(name);
         this.m_title=this.Name()+this.Parameters();
         this.m_category=IND_CATEGORY_CUSTOM;
         //--- Записываем описание первого буфера линии
         this.SetBufferDescription(0,this.m_title);
         //--- Устанавливаем количество баров, необходимое для расчёта индикатора на текущем тике
         this.SetBarsToCalculate(bars_to_calculate);
        }
     }
  };


В классе-коллекции индикаторов в методе создания нового объекта пользовательского индикатора необходимо также указать количество баров для расчёта индикатора:

   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,             // путь к индикатору (например, "Examples\\MACD.ex5")
                                                                                      const string name,             // имя пользовательского индикатора (например, "Custom MACD")
                                                                                      const uint   buffers,          // количество буферов
                                                                                      const uint   bars_to_calculate,// количество баров, необходимое для расчёта индикатора на текущем тике
                                                                                      const MqlParam &param[]);      // Массив параметров
//--- Таймер

а в коде реализации этого метода передать значение этой переменной в конструктор класса пользовательского индикатора:

//+------------------------------------------------------------------+
//| Добавляет в коллекцию пользовательский индикатор                 |
//+------------------------------------------------------------------+
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 &param[])
  {
//--- Создаём новый объект индикатора. При ошибке сообщаем об этом в журнал и возвращаем 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 this.AddNewIndicator(ind_obj,__FUNCTION__);
  }

Теперь классы мультисимвольных мультипериодных индикаторов должны работать со стрелочными индикаторами и теми индикаторами, в которых в буфере содержатся дынные с разрывами — индикаторы со стилями рисования DRAW_ARROW, DRAW_COLOR_ARROW, DRAW_SECTION, DRAW_ZIGZAG, DRAW_COLOR_SECTION и DRAW_COLOR_ZIGZAG.


Тестирование

Для тестирования возьмём любой индикатор из ранее созданных, например Parabolis SAR, так как он тоже рисуется стрелками. Он имеет один рисуемый буфер, а индикатор фракталов — два. Один буфер используется для отрисовки верхних фракталов, а второй — для отрисовки нижних. Доработаем код индикатора так, чтобы он рисовал стрелки в двух буферах и сохраним его под именем TestMSTFFractals.mq5.

Индикатор будет выводить на график данные сразу двух индикаторов Fractals — один будет рисовать фракталы с текущего периода графика, а второй — фракталы с индикатора Fractals, рассчитанного на указанных в настройках символе и периоде графика. Также, для отображения данных индикаторов фракталов будет использоваться информационная панель, которую используем во всех индикаторах в статьях этой серии:

//+------------------------------------------------------------------+
//|                                             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                     */ // Символ скользящей средней
input ENUM_TIMEFRAMES      InpTimeframe   =  PERIOD_CURRENT;   /* Timeframe                  */ // Таймфрейм скользящей средней
input uchar                InpArrowShift1 =  10;               /* Senior period Arrow VShift */ // Смещение стрелок по вертикали для старшего периода
input uchar                InpArrowShift2 =  10;               /* Junior period Arrow VShift */ // Смещение стрелок по вертикали для младшего периода
input bool                 InpAsSeries    =  true;             /* As Series flag             */ // Флаг серийности массивов буферов индикатора

//--- indicator buffers
double         BufferFractalsUp1[];
double         BufferFractalsDn1[];
double         BufferFractalsUp2[];
double         BufferFractalsDn2[];
//--- global variables
int handle_fractals1;
int handle_fractals2;
CMSTFIndicators indicators;      // Экземпляр объекта коллекции индикаторов
//--- переменные для панели
CDashboard *panel=NULL;          // Указатель на объект панели
int         mouse_bar_index;     // Индекс бара, с которого берутся данные
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Устанавливаем таймер с периодичностью в 1 секунду
   EventSetTimer(1);
//--- Назначаем рисуемым буферам 0 и 1 массивы BufferFractalsUp1 и BufferFractalsDn1 соответственно
   SetIndexBuffer(0,BufferFractalsUp1,INDICATOR_DATA);
   SetIndexBuffer(1,BufferFractalsDn1,INDICATOR_DATA);
//--- Назначаем рисуемым буферам 2 и 3 массивы BufferFractalsUp2 и BufferFractalsDn2 соответственно
   SetIndexBuffer(2,BufferFractalsUp2,INDICATOR_DATA);
   SetIndexBuffer(3,BufferFractalsDn2,INDICATOR_DATA);
//--- Зададём код символа из шрифта Wingdings для отрисовки в PLOT_ARROW
   PlotIndexSetInteger(0,PLOT_ARROW,217);
   PlotIndexSetInteger(1,PLOT_ARROW,218);
   PlotIndexSetInteger(2,PLOT_ARROW,217);
   PlotIndexSetInteger(3,PLOT_ARROW,218);
//--- Устанавливаем смещение стрелок по вертикали 
   PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,-InpArrowShift1);
   PlotIndexSetInteger(1,PLOT_ARROW_SHIFT, InpArrowShift1);
   PlotIndexSetInteger(2,PLOT_ARROW_SHIFT,-InpArrowShift2);
   PlotIndexSetInteger(3,PLOT_ARROW_SHIFT, InpArrowShift2);
//--- Устанавливаем флаги серийности массивам буферов индикатора (для теста, чтобы видно было отсутствие разницы)
   ArraySetAsSeries(BufferFractalsUp1,InpAsSeries);
   ArraySetAsSeries(BufferFractalsDn1,InpAsSeries);
   ArraySetAsSeries(BufferFractalsUp2,InpAsSeries);
   ArraySetAsSeries(BufferFractalsDn2,InpAsSeries);
   
//--- Создаём два индикатора одного типа
//--- Первый рассчитывается на текущих символе/периоде графика, а второй - на тех, что заданы в настройках
   handle_fractals1=indicators.AddNewFractals(NULL,PERIOD_CURRENT);
   handle_fractals2=indicators.AddNewFractals(InpSymbol,InpTimeframe);

//--- Если не удалось создать хэндлы индикаторов - возвращаем ошибку инициализации
   if(handle_fractals1==INVALID_HANDLE || handle_fractals2==INVALID_HANDLE)
      return INIT_FAILED;
//--- Устанавливаем описания линий индикатора из описаний буферов расчётной части созданных индикаторов
   indicators.SetPlotLabelFromBuffer(0,handle_fractals1,0);
   indicators.SetPlotLabelFromBuffer(1,handle_fractals1,1);
   indicators.SetPlotLabelFromBuffer(2,handle_fractals2,0);
   indicators.SetPlotLabelFromBuffer(3,handle_fractals2,1);
      
//--- Панель
//--- Создаём панель
   int width=311;
   panel=new CDashboard(1,20,20,width,264);
   if(panel==NULL)
     {
      Print("Error. Failed to create panel object");
      return INIT_FAILED;
     }
//--- Устанавливаем параметры шрифта
   panel.SetFontParams("Calibri",9);
//--- Отображаем панель с текстом в заголовке "Символ, Описание таймфрейма"
   panel.View(Symbol()+", "+StringSubstr(EnumToString(Period()),7));

//--- Создаём таблицу с идентификатором 0 для отображения в ней данных бара
   panel.CreateNewTable(0);
//--- Рисуем таблицу с идентификатором 0 на фоне панели
   panel.DrawGrid(0,2,20,6,2,18,width/2-2);

//--- Создаём таблицу с идентификатором 1 для отображения в ней данных индикатора 1
   panel.CreateNewTable(1);
//--- Получаем координату Y2 таблицы с идентификатором 0 и
//--- устанавливаем координату Y1 для таблицы с идентификатором 1
   int y1=panel.TableY2(0)+22;
//--- Рисуем таблицу с идентификатором 1 на фоне панели
   panel.DrawGrid(1,2,y1,2,2,18,width/2-2);

//--- Создаём таблицу с идентификатором 2 для отображения в ней данных индикатора 2
   panel.CreateNewTable(2);
//--- Получаем координату Y2 таблицы с идентификатором 1 и
//--- устанавливаем координату Y1 для таблицы с идентификатором 2
   int y2=panel.TableY2(1)+3;
//--- Рисуем таблицу с идентификатором 2 на фоне панели
   panel.DrawGrid(2,2,y2,3,2,18,width/2-2);
   
//--- Инициализируем переменную с индексом бара указателя мышки
   mouse_bar_index=0;
//--- Выводим на панель данные текущего бара
   DrawData(mouse_bar_index,TimeCurrent());

//--- Успешная инициализация
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Уничтожаем таймер
   EventKillTimer();
//--- Если объект панели существует - удаляем его
   if(panel!=NULL)
      delete panel;
//--- Стираем все комментарии
   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[])
  {
//--- Количество баров для расчёта
   int limit=rates_total-prev_calculated;
//--- Если limit > 1, значит это первый расчёт, либо изменение в истории
   if(limit>1)
     {
      //--- указываем для просчёта всю доступную историю
      limit=rates_total-1;
      /*
      // Если в индикаторе есть какие-либо буферы, в которых отображены другие расчёты (не мульти- индикаторы),
      // то здесь их нужно инициализировать "пустым" значением, установленным для этих буферов
      */
     }
//--- Рассчитываем все созданные мультисимвольные мультипериодные индикаторы
   if(!indicators.Calculate())
      return 0;

//--- Выводим на панель данные бара под курсором (либо текущий бар, если курсор за пределами графика)
   DrawData(mouse_bar_index,time[mouse_bar_index]);

//--- Из буферов рассчитанных индикаторов выводим данные в индикаторные буферы
   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 next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Timer function                                                   | 
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- Вызываем таймер коллекции индикаторов
   indicators.OnTimer();
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Работа с панелью
//--- Вызываем обработчик событий панели
   panel.OnChartEvent(id,lparam,dparam,sparam);

//--- Если курсор перемещается или щелчок по графику
   if(id==CHARTEVENT_MOUSE_MOVE || id==CHARTEVENT_CLICK)
     {
      //--- Объявляем переменные для записи в них координат времени и цены
      datetime time=0;
      double price=0;
      int wnd=0;
      //--- Если координаты курсора преобразованы в дату и время
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,wnd,time,price))
        {
         //--- записываем индекс бара, где расположен курсор в глобальную переменную
         mouse_bar_index=iBarShift(Symbol(),PERIOD_CURRENT,time);
         //--- Выводим данные бара под курсором на панель
         DrawData(mouse_bar_index,time);
        }
     }

//--- Если получили пользовательское событие - выводим об этом сообщение в журнал
   if(id>CHARTEVENT_CUSTOM)
     {
      //--- Здесь может быть обработка щелчка по кнопке закрытия на панели
      PrintFormat("%s: Event id=%ld, object id (lparam): %lu, event message (sparam): %s",__FUNCTION__,id,lparam,sparam);
     }
  }
//+------------------------------------------------------------------+
//| Выводит данные с указанного индекса таймсерии на панель          |
//+------------------------------------------------------------------+
void DrawData(const int index,const datetime time)
  {
//--- Объявляем переменные для получения в них данных
   MqlRates rates[1];

//--- Если данные бара по указанному индексу получить не удалось - уходим
   if(CopyRates(Symbol(),PERIOD_CURRENT,index,1,rates)!=1)
      return;

//--- Устанавливаем параметры шрифта для заголовков данных бара и индикатора
   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);
//--- Устанавливаем параметры шрифта для данных бара и индикатора
   panel.SetFontParams(name,9);

//--- Выводим на панель данные указанного бара в таблицу 0
   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);

//--- Выводим в таблицу 1 данные буфера 0 индикатора 1 с указанного бара в таблицу 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);
   
//--- Выводим в таблицу 1 данные буфера 1 индикатора 1 с указанного бара в таблицу 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);
   
//--- Выводим в таблицу 2 данные буфера 0 индикатора 2 с указанного бара в таблицу 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);
   
//--- Выводим в таблицу 2 данные буфера 1 индикатора 2 с указанного бара в таблицу 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);
   
//--- Перерисовываем график для немедленного отображения всех изменений на панели
   ChartRedraw(ChartID());
  }
//+------------------------------------------------------------------+


Скомпилируем индикатор и запустим его на графике М1, затем переключим период графика на М5 и М15:


Видим, как буферы индикатора на графике М1 заполняются данными с индикатора, рассчитанного на М5.

В то же время, при переключении графика на М15, мы видим, как все фракталы индикатора, рассчитанного на М5 рисуются на барах М15. При этом видно, что на каждом баре графика М15, где присутствуют фракталы, их, судя по графику, быть там не должно. Но это не так — просто в каждом баре графика М15 присутствует три бара с периода графика М5, а там эти фракталы есть, и вот они-то и отображаются на барах графика М15, так как индикатор для того и создан — быть мультисимвольным мультипериодным индикатором.


Заключение

Сегодня создали и протестировали работу классов мультисимвольных мультипериодных индикаторов со стилем рисования DRAW_ARROW. В последующих статьях попробуем разобраться с остальными стилями рисования, предполагающих разрывы данных на графике, и доделаем остальные, ещё не рассмотренные стандартные индикаторы — чтобы закрыть тему мультисимвольных мультипериодных индикаторов.

Все используемые в статье файлы — классы мульти- индикаторов, класс панели и файл индикатора прикреплены к статье для самостоятельного тестирования и использования.



Прикрепленные файлы |
IndMSTF.mqh (646.62 KB)
Dashboard.mqh (219.04 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (13)
Konstantin Seredkin
Konstantin Seredkin | 31 янв. 2024 в 01:39
Artyom Trishkin #:

Хорошо. Рад, что сами разобрались

Извиняюсь, может не по теме, слежу за вашими темами очень давно, вы как то писали что как придет время, вы создадите на своей библиотеке примерного робота что бы на его основе было понятно что и где вставить, что бы полноценно пользоваться и собирать алгоритмы.

Время еще не пришло ? просто смотрю темы по библиотеке закончились.

Artyom Trishkin
Artyom Trishkin | 31 янв. 2024 в 02:49
Konstantin Seredkin #:

Извиняюсь, может не по теме, слежу за вашими темами очень давно, вы как то писали что как придет время, вы создадите на своей библиотеке примерного робота что бы на его основе было понятно что и где вставить, что бы полноценно пользоваться и собирать алгоритмы.

Время еще не пришло ? просто смотрю темы по библиотеке закончились.

По библиотеке - по продолжению с графикой, пока приостановил разработку из-за неприятного бага, проявляющегося в периодичестком моргании скрытых частей объектов. Пока не выявлю причину, графику делать не стоит, чтобы не накопить ошибок. Но по остальным возможностям библиотеки скоро будет продолжение. А там и до примеров дойдёт.

Vitaliy Kuznetsov
Vitaliy Kuznetsov | 31 янв. 2024 в 07:37
Artyom Trishkin #:
из-за неприятного бага, проявляющегося в периодическом моргании скрытых частей объектов.

На всякий случай стоит проверить условия его появления. Например, связано ли это моргание из-за дозагрузки котировок. Если да, то проблема не в вашем коде.

Konstantin Seredkin
Konstantin Seredkin | 31 янв. 2024 в 08:23
Artyom Trishkin #:

По библиотеке - по продолжению с графикой, пока приостановил разработку из-за неприятного бага, проявляющегося в периодичестком моргании скрытых частей объектов. Пока не выявлю причину, графику делать не стоит, чтобы не накопить ошибок. Но по остальным возможностям библиотеки скоро будет продолжение. А там и до примеров дойдёт.

Спасибо, очень ждем. 

Artyom Trishkin
Artyom Trishkin | 31 янв. 2024 в 09:40
Vitaliy Kuznetsov #:

На всякий случай стоит проверить условия его появления. Например, связано ли это моргание из-за дозагрузки котировок. Если да, то проблема не в вашем коде.

Нет. Именно и только при взаимодействии с графическим объектами. 
Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 2) Разработка MQTT-клиента для MetaTrader 5: методология TDD (Часть 2)
Статья является частью серии, описывающей этапы разработки нативного MQL5-клиента для протокола MQTT. В этой части мы описываем организацию нашего кода, первые заголовочные файлы и классы, а также написание тестов. В эту статью также включены краткие заметки о разработке через тестирование (Test-Driven-Development) и о ее применении в этом проекте.
Разрабатываем мультивалютный советник (Часть 1): Совместная работа нескольких торговых стратегий Разрабатываем мультивалютный советник (Часть 1): Совместная работа нескольких торговых стратегий
Различных торговых стратегий существует довольно много. С точки зрения диверсификации рисков и повышения устойчивости торговых результатов может оказаться полезным использовать несколько параллельно работающих стратегий. Но если каждая стратегия будет реализована в виде отдельного советника, то управлять их совместной работой на одном торговом счёте становится гораздо сложнее. Для решения этой проблемы желательно реализовать работу разных торговых стратегий в одном советнике.
Популяционные алгоритмы оптимизации: Бинарный генетический алгоритм (Binary Genetic Algorithm, BGA). Часть II Популяционные алгоритмы оптимизации: Бинарный генетический алгоритм (Binary Genetic Algorithm, BGA). Часть II
В этой статье мы рассмотрим бинарный генетический алгоритм (BGA), который моделирует естественные процессы, происходящие в генетическом материале у живых существ в природе.
Теория категорий в MQL5 (Часть 20): Самовнимание и трансформер Теория категорий в MQL5 (Часть 20): Самовнимание и трансформер
Немного отвлечемся от наших постоянных тем и рассмотрим часть алгоритма ChatGPT. Есть ли у него какие-то сходства или понятия, заимствованные из естественных преобразований? Попытаемся ответить на эти и другие вопросы, используя наш код в формате класса сигнала.