English 中文 Español Deutsch 日本語 Português
preview
Индикатор рыночного профиля — Market Profile (Часть 2): Оптимизация и отрисовка на канвасе

Индикатор рыночного профиля — Market Profile (Часть 2): Оптимизация и отрисовка на канвасе

MetaTrader 5Примеры |
1 078 3
Artyom Trishkin
Artyom Trishkin

Содержание


Введение

Возвращаясь к теме индикатора Профиля Рынка, начатой в прошлой статье, хочется сказать, что реализация построения диаграммы профиля рынка обычными графическими объектами потребляет достаточно много ресурсов. Ведь каждый пункт цены от Low до High дневного бара заполняется графическими объектами-прямоугольниками в количестве баров, которые на протяжении всего дня достигали этого ценового уровня. И так для каждого пункта — все они содержат множество графических объектов, и все эти объекты создаются и рисуются для каждого дня, где рисуется диаграмма профиля. Когда индикатор создаёт тысячи графических объектов это может вызывать значительные замедления при работе с иными графическими объектами и перерисовке графика. 

Запуск индикатора на графике M30 и построение Профиля Рынка для всего трёх дней:

приводит к созданию 4697 графических объектов-прямоугольников:

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

Но ведь здесь просто рисуются диаграммы при помощи графических объектов-прямоугольников. Один короткий отрезок линии гистограммы профиля — это один графический объект. Значит, можно рисовать не непосредственно на графике, а на всего одном графическом объекте-холсте, расположенном в свою очередь на графике в нужных координатах. Тогда у нас будет всего по одному (!) графическому объекту для одного дня. А для трёх дней будет три объекта вместо 4697! Это существенная разница! И это нам позволяет сделать класс для упрощенного создания пользовательских рисунковCCanvas, поставляемый в составе Стандартной Библиотеки клиентского терминала.

Версия индикатора Профиля Рынка, рисующая гистограмму профиля на холсте, представлена в терминале в папке \MQL5\Indicators\Free Indicators\ в файле MarketProfile Canvas.mq5. После изучения кода, понимаем, что здесь, в отличие от первой версии (MarketProfile.mq5), вывод графики сделан на объекты класса CCanvas. Т.е. логика работы индикатора остаётся прежней, и уже нами рассмотренной в первой статье в разделе Устройство и принципы, а вот рисование осуществляется при помощи специального класса CMarketProfile, использующего рисование на CCanvas.

Логика работы предельна проста:

  • в цикле по указанному количеству дней,
    • создаём или получаем объект класса CMarketProfile для текущего в цикле дня,
      • рисуем или перерисовываем профиль дня на холсте, соответствующему текущему в цикле дню.

Т.е. основная работа по рисованию диаграммы профиля осуществляется внутри класса CMarketProfile. Давайте рассмотрим устройство и работу этого класса.


Класс Профиля Рынка CMarketProfile

Откроем файл \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 и найдём в нём код класса CMarketProfile. Рассмотрим, что там есть, и обсудим для чего всё это:

//+------------------------------------------------------------------+
//| Class to store and draw Market Profile for the daily bar         |
//+------------------------------------------------------------------+
class CMarketProfile
  {
public:
                     CMarketProfile() {};
                     CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]);
                    ~CMarketProfile(void);

   //--- проверяет, создан ли объект для указанной даты
   bool              Check(string prefix, datetime time);
   //--- устанавливает максимум/минимум и массив внутридневных баров
   void              SetHiLoBars(double high, double low, MqlRates &bars[]);
   //--- устанавливает размеры холста и параметры рисования
   void              UpdateSizes(void);
   //--- находится ли профиль в видимой части графика?
   bool              isVisibleOnChart(void);
   //--- изменился ли масштаб графика?
   bool              isChartScaleChanged(void);
   //--- рассчитывает профиль по сессиям
   bool              CalculateSessions(void);
   //--- рисует профиль
   void              Draw(double multiplier=1.0);
   //---
protected:
   CCanvas           m_canvas;      // объект класса CCanvas для рисования профиля
   uchar             m_alpha;       // значение альфа-канала, устанавливающее прозрачность
   string            m_prefix;      // уникальный префикс объекта OBJ_BITMAP
   string            m_name;        // имя объекта OBJ_BITMAP, используемого в m_canvas
   double            m_high;        // High дня
   double            m_low;         // Low дня
   datetime          m_time1;       // время начала дня
   datetime          m_time2;       // время окончания дня
   int               m_day_size_pt; // высота дневного бара в пунктах
   int               m_height;      // высота дневного бара в пикселях на графике
   int               m_width;       // ширина дневного бара в пикселях на графике
   MqlRates          m_bars[];      // массив баров текущего таймфрейма между m_time1 и m_time2
   vector            m_asia;        // массив счётчиков баров для азиатской сессии
   vector            m_europe;      // массив счётчиков баров для европейской сессии
   vector            m_america;     // массив счётчиков баров для американской сессии
   double            m_vert_scale;  // вертикальный коэффициент масштабирования
   double            m_hor_scale;   // горизонтальный коэффициент масштабирования
  };
Публичные методы, объявленные в классе:
  • Метод Check() используется для проверки существования объекта профиля рынка, созданного для определённого дня;
  • Метод SetHiLoBars() используется для установки в объект профиля рынка значений цен High и Low дня и для передачи в объект массива внутридневных баров;
  • Метод UpdateSizes() устанавливает в объект профиля рынка размеры холста и коэффициенты масштабирования для рисования прямоугольников;
  • Метод isVisibleOnChart() возвращает флаг того, что профиль рынка находится в пределах видимости на графике;
  • Метод isChartScaleChanged() объявлен в классе, но не реализован;
  • Метод CalculateSessions() рассчитывает параметры и заполняет массивы торговых сессий;
  • Метод Draw() рисует на холсте гистограмму профиля рынка по данным всех торговых сессий.

Назначение переменных, объявленных в защищённой секции класса, достаточно понятно. Хочется остановиться на массивах счётчиков баров сессий.
Все они объявлены как переменные-векторы, что позволит работать с ними как с массивами данных, но немного проще:

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

Рассмотрим реализацию объявленных методов класса.

Конструктор:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
void CMarketProfile::CMarketProfile(string prefix, datetime time1, datetime time2, double high, double low, MqlRates &bars[]):
   m_prefix(prefix),
   m_time1(time1),
   m_time2(time2),
   m_high(high),
   m_low(low),
   m_vert_scale(NULL),
   m_hor_scale(NULL)
  {
//--- копируем массив внутридневных баров в массив структур MqlRates,
//--- создаём имя графического объекта и определяем размер дневной свечи
   ArrayCopy(m_bars, bars);
   m_name=ExtPrefixUniq+"_MP_"+TimeToString(time1, TIME_DATE);
   m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT));
//--- устанавливаем размеры векторов для торговых сессий
   m_asia=vector::Zeros(m_day_size_pt);
   m_europe=vector::Zeros(m_day_size_pt);
   m_america=vector::Zeros(m_day_size_pt);
//--- устанавливаем ширину и высоту холста
   UpdateSizes();
//--- если это первый тик в начале дня, то размеры холста будут нулевыми - установим размеры в 1 пиксель по высоте и ширине
   m_height=m_height?m_height:1;
   m_width=m_width?m_width:1;
//--- создаём графический объект
   if(m_canvas.CreateBitmap(m_name, m_time1, m_high, m_width, m_height, COLOR_FORMAT_ARGB_NORMALIZE))
      ObjectSetInteger(0, m_name, OBJPROP_BACK, true);
   else
     {
      Print("Error creating canvas: ", GetLastError());
      Print("time1=", m_time1, "  high=", m_high, "  width=", m_width, "  height=", m_height);
     }
  }

В параметрический конструктор класса передаются префикс имени создаваемого объекта-холста, на котором будет производиться рисование профиля дня, время начала и окончания дня, максимальная и минимальная цены дня и массив внутридневных баров. Значения этих переменных записываются в соответствующие переменные класса в строке инициализации. Далее

  • переданный по ссылке массив копируется в массив класса, создаётся уникальное имя графического объекта из переданного во входных параметрах конструктора префикса, аббревиатуры "_MP_" и времени открытия дня, и рассчитывается размер дневной свечи в пунктах;
  • каждый из массивов торговых сессий получает размер, равный размеру дневного бара в пунктах и одновременно заполняется нулями — инициализируется;
  • устанавливаются размеры холста для рисования профиля, при этом, если это первый тик дня, то размер будет нулевым, и ширине и высоте устанавливаются минимально-разрешённые размеры в один пиксель по обоим измерениям;
  • создаётся холст для рисования по заданным размерам.

Метод для проверки существования объекта профиля рынка, созданного для определённого дня:

//+------------------------------------------------------------------+
//| Checks if CMarketProfile object is for the specified 'time' date |
//+------------------------------------------------------------------+
bool CMarketProfile::Check(string prefix, datetime time)
  {
   string calculated= prefix+"_MP_"+TimeToString(time, TIME_DATE);
   return (m_name==(calculated));
  };

Так как имя каждого объекта-холста для рисования профиля устанавливается в конструкторе класса, и в имени используется строковое представление времени начала дня, то, чтобы проверить, что объект создан для определённого времени, в метод передаётся время начала дня, создаётся строка, идентичная строке имени объекта, и созданная строка сравнивается с реальным именем объекта. Результат проверки возвращается из метода.

Метод для установки в объект профиля рынка значений цен High и Low дня и для передачи в объект массива внутридневных баров:

//+------------------------------------------------------------------+
//| Sets High/Low and a set of current-timeframe bars                |
//+------------------------------------------------------------------+
void CMarketProfile::SetHiLoBars(double high, double low, MqlRates &bars[])
  {
//--- если максимум дня изменился, переместим объект OBJ_BITMAP на новую координату Y
   if(high>m_high)
     {
      m_high=high;
      if(!ObjectSetDouble(0, m_name, OBJPROP_PRICE, m_high))
         PrintFormat("Failed to update canvas for %s, error %d", TimeToString(m_time1, TIME_DATE), GetLastError());
     }
   ArrayCopy(m_bars, bars);
   m_high=high;
   m_low=low;
//--- дневной диапазон в пунктах
   m_day_size_pt=(int)((m_high-m_low)/SymbolInfoDouble(Symbol(), SYMBOL_POINT));
//--- переустанавливаем размеры векторов для торговых сессий
   m_asia=vector::Zeros(m_day_size_pt);
   m_europe=vector::Zeros(m_day_size_pt);
   m_america=vector::Zeros(m_day_size_pt);
  }

В метод передаются значения High и Low дневной свечи, и по ссылке массив внутридневных баров в формате структуры MqlRates.

  • цена High записывается в переменную объекта и холст смещается на новую координату;
  • из переданного массива баров копируются внутридневные бары во внутренний массив;
  • устанавливается цена Low дня в переменную класса;
  • рассчитывается новый размер дневного бара в пунктах
  • массивы торговых сессий увеличиваются на рассчитанное значение размера дневного бара в пунктах и заполняются нулями — инициализируются.

Хочется отметить, что для инициализации векторов используется метод матриц и векторов Zeros(), который одновременно устанавливает размер вектора и заполняет весь массив нулями.
Для простого массива пришлось бы делать две операции над массивом: ArrayResize() и ArrayInitialize().

Метод, устанавливающий в объект профиля рынка размеры холста и коэффициенты масштабирования для рисования прямоугольников:

//+------------------------------------------------------------------+
//|  Sets drawing parameters                                         |
//+------------------------------------------------------------------+
void CMarketProfile::UpdateSizes(void)
  {
//--- преобразуем время/цену в координаты x/y
   int x1, y1, x2, y2;
   ChartTimePriceToXY(0, 0, m_time1, m_high, x1, y1);
   ChartTimePriceToXY(0, 0, m_time2, m_low,  x2, y2);
//--- рассчитываем размеры холста
   m_height=y2-y1;
   m_width =x2-x1;
//--- рассчитываем коэффициенты для преобразования вертикальных уровней цен
//--- и горизонтальных счетчиков баров в пиксели графика
   m_vert_scale=double(m_height)/(m_day_size_pt);
   m_hor_scale =double(m_width*PeriodSeconds(PERIOD_CURRENT))/PeriodSeconds(PERIOD_D1);
   
//--- изменяем размер холста
   m_canvas.Resize(m_width, m_height);
  }

Логика метода прокомментирована в коде. Коэффициенты масштабирования используются для задания размеров рисуемых на холсте прямоугольников в зависимости от соотношения размера холста к размеру окна графика.
Рассчитанные коэффициенты добавляются к расчёту высоты и ширины рисуемых прямоугольников.

Метод, возвращающий флаг того, что профиль рынка находится в пределах видимости на графике:

//+------------------------------------------------------------------+
//|  Checks that the profile is in the visible part of the chart     |
//+------------------------------------------------------------------+
bool CMarketProfile::isVisibleOnChart(void)
  {
   long last_bar=ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);        // последний видимый бар на графике слева
   long first_bar=last_bar+-ChartGetInteger(0, CHART_VISIBLE_BARS);  // первый видимый бар на графике справа
   first_bar=first_bar>0?first_bar:0;
   datetime left =iTime(Symbol(), Period(), (int)last_bar);          // время левого видимого бара на графике
   datetime right=iTime(Symbol(), Period(), (int)first_bar);         // время правого видимого бара на графике
   
//--- возвращаем флаг того, что холст расположен внутри левого и правого видимых баров графика
   return((m_time1>= left && m_time1 <=right) || (m_time2>= left && m_time2 <=right));
  }

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

Метод, рассчитывающий параметры и заполняющий массивы торговых сессий:

//+------------------------------------------------------------------+
//| Prepares profile arrays by sessions                              |
//+------------------------------------------------------------------+
bool CMarketProfile::CalculateSessions(void)
  {
   double point=SymbolInfoDouble(Symbol(), SYMBOL_POINT);   // значение одного пункта
//--- если массив внутридневных баров не заполнен - уходим
   if(ArraySize(m_bars)==0)
      return(false);
//---- перебираем все бары текущего дня и отмечаем ячейки массивов (векторов), в которые попадают перебираемые в цикле бары
   int size=ArraySize(m_bars);
   for(int i=0; i<size; i++)
     {
      //--- получаем час бара
      MqlDateTime bar_time;
      TimeToStruct(m_bars[i].time, bar_time);
      uint        hour     =bar_time.hour;
      //--- рассчитываем уровни цены в пунктах от Low дня, которые были достигнуты ценой на каждом баре цикла
      int         start_box=(int)((m_bars[i].low-m_low)/point);   // индекс начала ценовых уровней, которые были достигнуты ценой на баре
      int         stop_box =(int)((m_bars[i].high-m_low)/point);  // индекс конца ценовых уровней, которые были достигнуты ценой на баре

      //--- американская сессия
      if(hour>=InpAmericaStartHour)
        {
         //--- в цикле от начала до конца ценовых уровней заполняем счётчики баров, где была цена на этом уровне
         for(int ind=start_box; ind<stop_box; ind++)
            m_america[ind]++;
        }
      else
        {
         //--- европейская сессия
         if(hour>=InpEuropeStartHour && hour<InpAmericaStartHour)
            //--- в цикле от начала до конца ценовых уровней заполняем счётчики баров, где была цена на этом уровне
            for(int ind=start_box; ind<stop_box; ind++)
               m_europe[ind]++;
         //--- азиатская сессия
         else
            //--- в цикле от начала до конца ценовых уровней заполняем счётчики баров, где была цена на этом уровне
            for(int ind=start_box; ind<stop_box; ind++)
               m_asia[ind]++;
        }
     }
//--- векторы всех сессий готовы
   return(true);
  }

В прошлой статье подробно рассматривалась логика определения количества баров торговой сессии, цена которых достигала уровней в пунктах от Low до High дня. Если в прошлой версии индикатора всё это выполнялось в основном цикле индикатора, то здесь весь этот расчёт вынесен в отдельный метод объекта профиля дня. Смысл здесь в том, чтобы подсчитать и записать в ячейки массива (вектора) количество баров, которые пересекают каждый ценовой уровень, рассчитываемый в пунктах от Low до High дня. После работы метода все векторы будут заполнены в соответствии с движением цены по ценовым уровням. Сколько баров пересекало каждый уровень, такое количество и будет записано в соответствующих ячейках массива (вектора).

Метод, рисующий на холсте гистограмму профиля рынка по данным всех торговых сессий:

//+------------------------------------------------------------------+
//|  Draw Market Profile on the canvas                               |
//+------------------------------------------------------------------+
void CMarketProfile::Draw(double multiplier=1.0)
  {
//--- суммируем все сессии для отрисовки
   vector total_profile=m_asia+m_europe+m_america;   // профиль, объединяющий все сессии
   vector europe_asia=m_asia+m_europe;               // профиль, объединяющий только европейскую и азиатскую сессии

//--- устанавливаем полностью прозрачный фон для холста
   m_canvas.Erase(ColorToARGB(clrBlack, 0));

//--- переменные для рисования прямоугольников
   int x1=0;                           // X-координата левого угла прямоугольника всегда начинается с нуля
   int y1, x2, y2;                     // координаты прямоугольников
   int size=(int)total_profile.Size(); // размер всех сессий
   
//--- рисуем американскую сессию закрашенными прямоугольниками
   for(int i=0; i<size; i++)
     {
      //--- нулевые значения векторов не рисуем - пропускаем
      if(total_profile[i]==0)
         continue;
      //--- рассчитываем две точки для рисования прямоугольника, x1 всегда равен 0 (X левого нижнего угла прямоугольника)
      y1=m_height-int(i*m_vert_scale);                    // координата Y нижнего левого угла прямоугольника
      y2=(int)(y1+m_vert_scale);                          // координата Y верхнего правого угла прямоугольника
      x2=(int)(total_profile[i]*m_hor_scale*multiplier);  // координата X верхнего правого угла прямоугольника
      //--- рисуем прямоугольник по рассчитанным координатам с установленным для американской сессии цветом и прозрачностью
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAmericaSession, InpTransparency));
     }

//--- рисуем европейскую сессию закрашенными прямоугольниками
   for(int i=0; i<size; i++)
     {
      //--- нулевые значения векторов не рисуем - пропускаем
      if(total_profile[i]==0)
         continue;
      //--- рассчитываем две точки для рисования прямоугольника
      y1=m_height-int(i*m_vert_scale);
      y2=(int)(y1+m_vert_scale);
      x2=(int)(europe_asia[i]*m_hor_scale*multiplier);
      //--- поверх нарисованной американской сессии рисуем прямоугольник по рассчитанным координатам
      //--- с установленным для европейской сессии цветом и прозрачностью
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpEuropeSession, InpTransparency));
     }

//--- рисуем азиатскую сессию закрашенными прямоугольниками
   for(int i=0; i<size; i++)
     {
      //--- нулевые значения векторов не рисуем - пропускаем
      if(total_profile[i]==0)
         continue;
      //--- рассчитываем две точки для рисования прямоугольника
      y1=m_height-int(i*m_vert_scale);
      y2=(int)(y1+m_vert_scale);
      x2=(int)(m_asia[i]*m_hor_scale*multiplier);
      //--- поверх нарисованной европейской сессии рисуем прямоугольник по рассчитанным координатам
      //--- с установленным для азиатской сессии цветом и прозрачностью
      m_canvas.FillRectangle(x1, y1, x2, y2, ColorToARGB(InpAsiaSession, InpTransparency));
     }
//--- обновляем объект OBJ_BITMAP без перерисовки графика
   m_canvas.Update(false);
  }

Логика метода подробно расписана в комментариях к коду. Вкратце: у нас есть рассчитанные и заполненные массивы (векторы) трёх сессий — азиатской, европейской и американской. Необходимо для каждой из сессий нарисовать свою гистограмму профиля. Сначала рисуется американская сессия, поверх неё рисуется европейская, и в конце, поверх нарисованных двух сессий рисуется азиатская.
Почему рисуем сессии в обратном порядке времени их работы?

  • Американская сессия, вернее её гистограмма, включает в себя как уже проторгованное время двух предыдущих сессий, так и время американской сессии, т.е. это наиболее полная гистограмма профиля всего дня. Поэтому она рисуется первой.
  • Затем рисуется европейская сессия, которая включает в себя и время проторгованной уже азиатской сессии. Соответственно, раз здесь есть всего две сессии — азиатская и европейская, то гистограмма будет короче по оси X американской сессии, а значит, её необходимо рисовать поверх американской. 
  • И далее рисуется самая короткая по оси X гистограмма азиатской сессии. 
Таким образом все гистограммы каждой сессии в правильном порядке накладываются друг на друга, являя собой полную картину всего профиля рынка дня.

Хочется отметить, как удобно объединять данные массивов при использовании векторов:

//--- суммируем все сессии для отрисовки
   vector total_profile=m_asia+m_europe+m_america;   // профиль, объединяющий все сессии
   vector europe_asia=m_asia+m_europe;               // профиль, объединяющий только европейскую и азиатскую сессии

По сути, это поэлементное объединение нескольких массивов с одинаковым размером в одном результирующем, которое можно представить таким кодом:

#define SIZE   3

double array_1[SIZE]={0,1,2};
double array_2[SIZE]={3,4,5};
double array_3[SIZE]={6,7,8};

Print("Contents of three arrays:");
ArrayPrint(array_1);
ArrayPrint(array_2);
ArrayPrint(array_3);

for(int i=0; i<SIZE; i++)
  {
   array_1[i]+=array_2[i]+=array_3[i];
  }
  
Print("\nResult of the merge:");
ArrayPrint(array_1);
/*
Contents of three arrays:
0.00000 1.00000 2.00000
3.00000 4.00000 5.00000
6.00000 7.00000 8.00000

Result of the merge:
 9.00000 12.00000 15.00000
*/

Приведённый код делает то же самое, что делает строка кода рассмотренного выше метода:

vector total_profile=m_asia+m_europe+m_america;   // профиль, объединяющий все сессии

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

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

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CMarketProfile::~CMarketProfile(void)
  {
//--- удаляем все графические объекты после использования
   ObjectsDeleteAll(0, m_prefix, 0, OBJ_BITMAP);
   ChartRedraw();
  }

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


Оптимизируем индикатор

Давайте теперь посмотрим, как сделан индикатор с использованием класса профиля рынка. Откроем с самого начала файл индикатора \MQL5\Indicators\Free Indicators\MarketProfile Canvas.mq5 и изучим его.

В первую очередь к файлу индикатора подключены файлы класса для упрощенного создания пользовательских рисунков CCanvas и файл класса для создания строго типизированных списков CArrayList<T>:

//+------------------------------------------------------------------+
//|                                         MarketProfile Canvas.mq5 |
//|                              Copyright 2009-2024, MetaQuotes Ltd |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2022, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_plots 0

#include <Canvas\Canvas.mqh>
#include <Generic\ArrayList.mqh>

//--- input parameters

Далее вписан список входных параметров индикатора, уникальный префикс графических объектов, задекларирован класс профиля рынка и объявлен список объектов этого класса:

//--- input parameters
input uint  InpStartDate       =0;           /* day number to start calculation */  // номер дня, с которого начнём расчёт (0 - текущий, 1 - предыдущий, и т.д.)
input uint  InpShowDays        =7;           /* number of days to display */        // количество отображаемых дней, начиная и включая день в InpStartDate
input int   InpMultiplier      =1;           /* histogram length multiplier */      // множитель длины гистограммы
input color InpAsiaSession     =clrGold;     /* Asian session */                    // цвет гистограммы азиатской сессии
input color InpEuropeSession   =clrBlue;     /* European session */                 // цвет гистограммы европейской сессии
input color InpAmericaSession  =clrViolet;   /* American session */                 // цвет гистограммы американской сессии
input uchar InpTransparency    =150;         /* Transparency, 0 = invisible */      // прозрачность профиля рынка, 0 = полностью прозрачный
input uint  InpEuropeStartHour =8;           /* European session opening hour */    // час открытия европейской сессии
input uint  InpAmericaStartHour=14;          /* American session opening hour */    // час открытия американской сессии

//--- уникальный префикс для идентификации графических объектов, принадлежащих индикатору
string ExtPrefixUniq;

//--- декларируем класс CMarketProfile
class CMarketProfile;
//--- объявляем список указателей на объекты класса CMarketProfile
CArrayList<CMarketProfile*> mp_list;

Так как класс профиля рынка написан ниже кода индикатора, то необходима форвард-декларация класса, чтобы при компиляции не было ошибки неизвестного типа переменной

'CMarketProfile' - unexpected token

Строго типизированный список будет содержать указатели на объекты с типом класса CMarketProfile, который написан ниже по коду.

В обработчике OnInit() создаётся префикс графических объектов как 4 последние цифры из количества миллисекунд, прошедших с момента старта системы:

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- создаём префикс для имён объектов
   string number=StringFormat("%I64d", GetTickCount64());
   ExtPrefixUniq=StringSubstr(number, StringLen(number)-4);
   Print("Indicator \"Market Profile Canvas\" started, prefix=", ExtPrefixUniq);

   return(INIT_SUCCEEDED);
  }

Рассмотрим полный код обработчика OnCalculate():

//+------------------------------------------------------------------+
//| 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[])
  {
//--- время открытия текущего дневного бара
   datetime static open_time=0;

//--- номер последнего дня для расчетов
//--- (при InpStartDate = 0 и InpShowDays = 3, lastday = 3)
//--- (при InpStartDate = 1 и InpShowDays = 3, lastday = 4) etc ...
   uint lastday=InpStartDate+InpShowDays;

//--- если первый расчет уже был
   if(prev_calculated!=0)
     {
      //--- получаем время открытия текущего дневного бара
      datetime current_open=iTime(Symbol(), PERIOD_D1, 0);
      
      //--- если текущий день не рассчитываем
      if(InpStartDate!=0)
        {
         //--- если время открытия не было получено - уходим
         if(open_time==current_open)
            return(rates_total);
        }
      //--- обновляем время открытия
      open_time=current_open;
      //--- далее будем рассчитывать только один день, так как все остальные дни уже посчитаны при первом запуске
      lastday=InpStartDate+1;
     }

//--- в цикле по указанному количеству дней (либо InpStartDate+InpShowDays при первом запуске, либо InpStartDate+1 на каждом тике)
   for(uint day=InpStartDate; day<lastday; day++)
     {
      //--- получаем в структуру данные дня с индексом day
      MqlRates day_rate[];
      //--- если индикатор запускается в выходные или праздничные дни, когда нет тиков, сначала нужно открыть дневной график символа
      //--- если не получили данные бара по индексу day дневного периода - уходим до следующего вызова OnCalculate()
      if(CopyRates(Symbol(), PERIOD_D1, day, 1, day_rate)==-1)
         return(prev_calculated);

      //--- получаем время начала и окончания дня
      datetime start_time=day_rate[0].time;
      datetime stop_time=start_time+PeriodSeconds(PERIOD_D1)-1;

      //--- получаем все внутредневные бары текущего дня
      MqlRates bars_in_day[];
      if(CopyRates(Symbol(), PERIOD_CURRENT, start_time, stop_time, bars_in_day)==-1)
         return(prev_calculated);

      CMarketProfile *market_profile;
      //--- если Профиль рынка уже создавался и его рисование ранее было выполнено
      if(prev_calculated>0)
        {
         //--- найдём объект Профиля рынка (класса CMarketProfile) в списке по времени открытия дня с индексом day
         market_profile=GetMarketProfileByDate(ExtPrefixUniq, start_time);
         //--- если объект не найден возвращаем ноль для полного перерасчёта индикатора
         if(market_profile==NULL)
           {
            PrintFormat("Market Profile not found for %s. Indicator will be recalculated for all specified days",
                        TimeToString(start_time, TIME_DATE));
            return(0);
           }
         //--- объект CMarketProfile найден в списке; устанавливаем в него значения High и Low дня и передаём массив внутридневных баров
         //--- при этом объект смещается на новую координату, соответствующую High дневной свечи, и все массивы (векторы) переинициализируются
         market_profile.SetHiLoBars(day_rate[0].high, day_rate[0].low, bars_in_day);
        }
      //--- если это первый расчёт
      else
        {
         //--- создаём новый объект класса CMarketProfile для хранения Профиля рынка дня с индексом day
         market_profile = new CMarketProfile(ExtPrefixUniq, start_time, stop_time, day_rate[0].high, day_rate[0].low, bars_in_day);
         //--- добавляем указатель на созданный объект CMarketProfile в список
         mp_list.Add(market_profile);
        }
      //--- устанавливаем размеры холста и параметры рисования линий
      market_profile.UpdateSizes();
      //--- рассчитываем профили для каждой торговой сесии
      market_profile.CalculateSessions();
      //--- рисуем Профиль рынка
      market_profile.Draw(InpMultiplier);
     }
//--- по завершении цикла после создания и обновления всех объектов, перерисуем график
   ChartRedraw(0);

//--- возвращаем количество баров для следующего вызова OnCalculate
   return(rates_total);
  }

Логика обработчика полностью расписана в комментариях к коду. Если вкратце, то она такова:

  • В цикле по количеству отображаемых дней профиля рынка;
    • получаем в структуру день, соответствующий индексу цикла;
      • получаем количество баров текущего периода графика, входящих в день, выбранный в цикле;
      • либо получаем ранее созданный объект профиля рынка для выбранного дня, либо создаём новый, если его ещё нет в списке;
      • получаем размер дневного бара от Low до High в пикселях графика и переинициализируем массивы (векторы) торговых сессий;
    • в соответствии с новым размером бара выбранного дня изменяем размер холста;
    • перерассчитываем профиль рынка дня по каждой сессии;
    • перерисовываем на холсте профили каждой торговой сессии;
  • По окончании цикла перерисовываем график.

В обработчике OnDeinit() индикатора удаляем все созданные графические объекты:

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- удаляем все графические объекты Market Profile после использования
   Print("Indicator \"Market Profile Canvas\" stopped, delete all objects CMarketProfile with prefix=", ExtPrefixUniq);

//--- в цикле по количеству объектов CMarketProfile в списке
   int size=mp_list.Count();
   for(int i=0; i<size; i++)
     {
      //--- получаем указатель на объект CMarketProfile из списка по индексу цикла
      CMarketProfile *market_profile;
      mp_list.TryGetValue(i, market_profile);
      //--- если указатель валидный и объект существует - удаляем его
      if(market_profile!=NULL)
         if(CheckPointer(market_profile)!=POINTER_INVALID)
            delete market_profile;
     }
//--- перерисовываем график для немедленного отображения результата
   ChartRedraw(0);
  }

В обработчике событий OnChartEvent() изменяем размеры холста каждого дня профиля рынка:

//+------------------------------------------------------------------+
//| Custom indicator chart's event handler                           |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
//--- если это пользовательское событие - уходим
   if(id>=CHARTEVENT_CUSTOM)
      return;

//--- если есть изменение чарта, обновляем размеры всех объектов класса CMarketProfile с перерисовкой графика
   if(CHARTEVENT_CHART_CHANGE==id)
     {
      //--- в цикле по количеству объектов CMarketProfile в списке
      int size=mp_list.Count();
      for(int i=0; i<size; i++)
        {
         //--- получаем указатель на объект CMarketProfile по индексу цикла
         CMarketProfile *market_profile;
         mp_list.TryGetValue(i, market_profile);
         //--- если объект получен и если он находится в видимой области графика
         if(market_profile)
            if(market_profile.isVisibleOnChart())
              {
               //--- обновляем размеры холста и перерисовываем гистограммы профиля рынка
               market_profile.UpdateSizes();
               market_profile.Draw(InpMultiplier);
              }
        }
      //--- после перерасчёта всех Профилей, обновляем график
      ChartRedraw();
     }
  }

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

Функция, возвращающая объект профиля рынка, созданный для указанного времени начала дня:

//+------------------------------------------------------------------+
//| Returns CMarketProfile or NULL by the date                       |
//+------------------------------------------------------------------+
CMarketProfile* GetMarketProfileByDate(string prefix, datetime time)
  {
//--- в цикле по количеству объектов CMarketProfile в списке
   int size=mp_list.Count();
   for(int i=0; i<size; i++)
     {
      //--- получаем указатель на объект CMarketProfile по индексу цикла
      CMarketProfile *market_profile;
      mp_list.TryGetValue(i, market_profile);
      //--- если указатель валидный и объект существует,
      if(market_profile!=NULL)
         if(CheckPointer(market_profile)!=POINTER_INVALID)
           {
            //--- если объект Профиля рынка, полученный по указателю, создан для искомого времени - возвращаем указатель
            if(market_profile.Check(prefix, time))
               return(market_profile);
           }
     }
//--- ничего не найдено - возвращаем NULL
   return(NULL);
  }

Функция используется в цикле индикатора по торговым дням и возвращает указатель на объект класса CMarketProfile из списка, который был создан для дневного бара с каким-либо временем открытия дня. Позволяет получать нужный объект по времени для дальнейшего его обновления.

Заключение

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

В итоге проведённой оптимизации, каждый торговый день, в количестве, указанном в настройках (по умолчанию 7) отображается на собственном холсте (объект OBJ_BITMAP), где рисуются в виде гистограмм три торговые сессии — азиатская, европейская и американская, каждая своим, заданным в настройках, цветом. Для трёх торговых дней профиль рынка в итоге будет иметь такой вид:

Здесь мы имеем всего три графических объекта, на которых гистограммы торговых сессий нарисованы при помощи класса CCanvas. Хорошо видно, что перерисовка "на лету" изображений даже для трёх графических объектов "Рисунок" вызывает заметное моргание и подёргивания изображений. Это говорит о том, что есть ещё поле для проведения дальнейшей оптимизации кода. В любом случае теперь, вместо нескольких тысяч графических объектов, мы имеем всего три. Что даёт заметный выигрыш в потреблении ресурсов. А визуальные артефакты можно исправить, проведя дальнейший анализ кода на предмет их устранения (вспомним, например, нереализованный метод isChartScaleChanged() класса CMarketProfile, с помощью которого можно попытаться сделать перерисовку только в момент события реального изменения масштаба графика, а не при любых его изменениях).

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (3)
__zeus__
__zeus__ | 8 янв. 2025 в 06:32
Почему бы не написать совершенный профиль объемов 
Ihor Herasko
Ihor Herasko | 8 янв. 2025 в 08:38
__zeus__ #:
Почему бы не написать совершенный профиль объемов 

Что понимается под "совершенностью"?

Artyom Trishkin
Artyom Trishkin | 8 янв. 2025 в 10:58
__zeus__ #:
Почему бы не написать совершенный профиль объемов 
Поддержу Игоря в его вопросе
Алгоритм Искусственного Племени (Artificial Tribe Algorithm, ATA) Алгоритм Искусственного Племени (Artificial Tribe Algorithm, ATA)
В статье подробно рассматриваются ключевые компоненты и инновации алгоритма оптимизации ATA, представляющего собой эволюционный метод с уникальной двойной системой поведения, которая адаптируется в зависимости от ситуации. Используя скрещивание для углубленного исследования, и миграцию для поиска в случае застревания в локальных оптимумах, ATA сочетает в себе индивидуальное и социальное обучение.
Оптимизация портфеля на форексе: Синтез VaR и теории Марковица Оптимизация портфеля на форексе: Синтез VaR и теории Марковица
Как осуществляется портфельная торговля на Форекс? Как могут быть синтезированы портфельная теория Марковица для оптимизации пропорций портфеля и VaR модель для оптимизации риска портфеля? Создаем код по портфельной теории, где, с одной стороны, получим низкий риск, а с другой — приемлемую долгосрочную доходность.
Критерии тренда в трейдинге Критерии тренда в трейдинге
Тренды являются важной частью многих торговых стратегий. В этой статье мы рассмотрим некоторые инструменты, используемые для определения трендов и их характеристик. Понимание и правильная интерпретация трендов могут значительно повысить эффективность трейдинга и минимизировать риски.
Нейросети в трейдинге: Гибридный торговый фреймворк с предиктивным кодированием (StockFormer) Нейросети в трейдинге: Гибридный торговый фреймворк с предиктивным кодированием (StockFormer)
Предлагаем познакомиться с гибридной торговой системой StockFormer, которая объединят предиктивное кодирование и алгоритмы обучения с подкреплением (RL). Во фреймворке используются 3 ветви Transformer с интегрированным механизмом Diversified Multi-Head Attention (DMH-Attn), который улучшает ванильный модуль внимания за счет многоголового блока Feed-Forward, что позволяет захватывать разнообразные паттерны временных рядов в разных подпространствах.