English 中文 Español Deutsch 日本語 Português
Горизонтальные диаграммы на графиках MеtaTrader 5

Горизонтальные диаграммы на графиках MеtaTrader 5

MetaTrader 5Примеры | 8 августа 2018, 09:57
4 980 43
Andrei Novichkov
Andrei Novichkov

Введение

Задачи по отрисовке горизонтальных диаграмм на графике терминала не часто, но встречаются разработчику. Что это за задачи? Это индикаторы распределения объемов за какой-то определенный период. Задачи по распределению цены, различные стаканы и т.п. Встречаются и более экзотические ситуации по распределению показаний каких то пользовательских (или стандартных) индикаторов. Но в любом случае, общим у всех вышеперечисленных задач будет одно — создание, размещение на графике, масштабирование, перемещение и удаление диаграмм. Заметим несколько моментов:

  1. Диаграмм может быть несколько (как правило, именно так и есть).
  2. Диаграммы, которые нас интересуют,  в подавляющем большинстве случаев столбчатого типа.
  3. Столбцы диаграммы располагаются по горизонтали.

Посмотрим на достаточно распространенном примере, как могут выглядеть такие диаграммы:



Еще один пример. Те же диаграммы, но отрисованные другими графическими примитивами:


В данном случае это индикатор распределения тиковых объемов по дням. На этом примере достаточно ясно видно, какие задачи пришлось решать разработчику:

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

И еще раз заметим, что диаграмм несколько, что в дальнейшем даст нам возможность говорить о массиве диаграмм.

Еще один, заключительный пример:

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

  • "Тиковые объемы на продажу"
  • "Тиковые объемы на покупку"
  • "Суммарные тиковые объемы"

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

Постановка задачи

Наметим две части реализации задуманного:

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

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

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

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

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

i-я ценовая привязка = начало ценового интервала диаграммы + i * шаг разбиения интервала.

Отметим, что для уже приведенных примеров с тиковыми объемами, ценовой промежуток, на котором могут быть построены диаграммы, это промежуток между Low и High рассматриваемого периода.

Вопрос отображения диаграмм так же следует детализировать. Итак, уже получены массивы с привязками для диаграмм по принципу - одна диаграмма, один набор массивов для привязки. Еще одно очевидное наблюдение: Очень часто на графике располагаются диаграммы, построенные из одинаковых графических примитивов, одного цвета, выполненные в одном "стиле". Имеется в виду, например, диаграммы на первом и втором скриншотах. А вот на третьем скриншоте все диаграммы разные. Они отличаются по цвету и "направлению". У одной столбцы "направлены" справа налево, у двух других слева направо. Логично было бы объединить "однотипные" диаграммы, как на первых двух скриншотах в один массив и отдать его в управлению некоторому менеджеру. Ну а что бы еще больше стандартизировать задачу стоит использовать принцип:

  • Для каждого набора "однотипных" диаграмм создавать своего управляющего менеджера, даже если набор будет состоять из одной диаграммы. Таким образом, для диаграмм на первых двух скриншотах будет создан массив из трех диаграмм (как минимум) под управлением одного менеджера, а для диаграмм с третьего скриншота придется создавать три массива по одной диаграмме в каждом и три менеджера, по одному на каждый массив.

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

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


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


На этом можно считать постановку задачи завершенной и переходить непосредственно к коду:

Константы и входные параметры

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

Расположение диаграмм:

enum HD_POSITION
{
        HD_LEFT  = -1,
        HD_RIGHT =  1,
        HD_CNDLE =  2 
};

Имеется три варианта размещения диаграмм — с привязкой к левому краю терминала (HD_LEFT), к правому (HD_RIGHT) и к свече (HD_CNDLE), или к свечам. На всех трех скриншотах в начале статьи диаграммы располагаются с использованием HD_CNDLE. На первых двух с привязкой к свечам в начале определенных периодов (началу дня), на третьем к одной свече, расположенной в начале текущего дня.

Внешний вид диаграммы (вид графических примитивов):

enum HD_STYLE 
{
        HD_LINE      = OBJ_HLINE,        
        HD_RECTANGLE = OBJ_RECTANGLE,    
};

Вариантов внешнего вида два — отрезки горизонтальных линий (HD_LINE) и прямоугольники (HD_RECTANGLE). На первом и третьем скриншотах в начале статьи диаграммы состоят из примитивов HD_LINE, а на втором из HD_RECTANGLE.

Направление "горизонтальных столбцов" диаграмм:

enum HD_DIRECT 
{
   HD_LEFTRIGHT = -1,
   HD_RIGHTLEFT =  1 
};

На третьем скриншоте в начале статьи диаграмма, состоящая из отрезков красного цвета отрисована, как HD_RIGHTLEFT, а две другихе как HD_LEFTRIGHT.

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

Опишем применяемый способ. Он весьма прост и сводится к округлению ценового уровня до ближайшего десятка, либо до сотни. Соответственно, размер шага ценовых уровней так же будет равняться десяти, либо ста. Количество ценовых уровней при таком подходе получится переменным. Для желающих получить максимальную точность вычислений (вместе с повышенным потреблением ресурсов) будет оставлен способ HD_MIN без округления:

enum HD_ZOOM {
   HD_MIN    = 0,  //1
   HD_MIDDLE = 1,  //10
   HD_BIG    = 2   //100
}; 

По умолчанию будет применяться способ HD_MIDDLE.

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

Перейдем к блоку входных параметров:

input HD_STYLE        hdStyle      = HD_LINE;
input int             hdHorSize    = 20;
input color           hdColor      = clrDeepSkyBlue;
input int             hdWidth      = 2;
input ENUM_TIMEFRAMES TargetPeriod = PERIOD_D1;
input ENUM_TIMEFRAMES SourcePeriod = PERIOD_M1;
input HD_ZOOM         hdStep       = HD_MIDDLE;   
input int             MaxHDcount   = 5;
input int             iTimer       = 1;   

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

Назначение параметра HD_STYLE вполне очевидно  и не требует дополнительных разъяснений.

  • Параметр hdHorSize играет очень большую роль. Он определяет максимально большой размер "горизонтального столбца" диаграммы в свечах. В данном случае самый длинный "горизонтальный столбец" не может превышать двадцать свечей. Понятно, что чем больше этот параметр, тем точнее диаграмма. Ограничением служит то, что при большом размере этого параметра диаграммы начнут накладываться друг на друга.
  • Параметры hdColor и hdWidth относятся к внешнему виду диаграмм, это, соответственно, цвет и толщина линий.
  • Параметр TargetPeriod содержит исследуемый таймфрейм. В данном случае, в индикаторе будет показано распределение тиковых объемов за день.
  • Параметр SourcePeriod содержит таймфрейм, с которого индикатор берет исходные данные для построения распределения. В данном случае используется минутный таймфрейм. Этим параметром следует пользоваться осмотрительно. Если в качестве исследуемого таймфрейма будет месячный таймфрейм, то вычисления могут затянуться.
  • Параметр hdStep содержит параметр округления ценовых уровней. Об этом параметре и на что влияет его выбор мы уже говорили.
  • Параметр MaxHDcount содержит максимальное количество диаграмм на графике. Следует помнить, что каждая диаграмма состоит из множества графических примитивов и слишком большое количество диаграмм может замедлить работу терминала.
  • Параметр iTimer содержит частоту срабатывания таймера. При срабатывании проверяется создания новых свечей и выполняются необходимые действия. Можно было бы здесь поместить результат вызова PeriodSeconds(SourcePeriod). Однако по умолчанию стоит одна секунда, что сделано в целях более точного определения момента появления новых свечей.

Инициализация

На этом этапе необходимо создать объект — менеджер для управления диаграммами. Поскольку все диаграммы будут однотипными, то и менеджер понадобится только один. Поскольку класс самого менеджера пока еще не написан, просто запомним, что создается он здесь, в обработчике OnInit(). Здесь же создаются, но не отрисовываются две диаграммы:

  1. Диаграмма, отвечающая за распределение тиковых объемов за текущий период. Эта диаграмма будет периодически перерисовываться.
  2. Диаграмма, которая будет отрисовывать распределение тиковых объемов на истории. Эти распределения перерисовываться не будут, поэтому после отрисовки диаграмма будет "забывать" о них, отдавая управление терминалу.

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

Далее происходит инициализация переменных для последующих вычислений ценовых уровней с шагом. Этих переменных две, они являются производными от Digit() и Point():

   hdDigit = Digits() - (int)hdStep; 
   switch (hdStep)
    {
      case HD_MIN:
         hdPoint =       Point();
         break;
      case HD_MIDDLE:
         hdPoint = 10 *  Point();
         break;     
      case HD_BIG:
         hdPoint = 100 * Point();
         break;      
      default:
         return (INIT_FAILED);
    }

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


Основные вычисления

Дальнейшая задача разбивается на два этапа:

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

Выполним часть работы в обработчике OnCalculate() пока в форме псевдокода:

int OnCalculate(...)
  {
   if (prev_calculated == 0 || rates_total > prev_calculated + 1) {
   }else {
      if (!bCreateHis) 
       {
         int br = 1;
         while (br < MaxHDcount) {
           {
            if(Calculate for bar "br") 
                 {
                  sdata.bRemovePrev = false;
                  Print("Send data to the new Diagramm");
                 }

           }
         ChartRedraw();
         bCreateHis = true;
      }
   }  
   return(rates_total);
  }  

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

Работа по созданию и отрисовке диаграммы, включающей текущий период, выполняется в обработчике OnTimer(). Псевдокода для данного обработчика приведено не будет в виду очевидной простоты и ясности задачи:

  1. Дожидаемся появления новой свечи на таймфрейме SourcePeriod.
  2. Выполняем необходимые вычисления.
  3. Отправляем данные диаграмме  за текущий период для создания новых примитивов и отрисовки.
К функции, в которой выполняются основные вычисления для определенного бара таймфрейма TargetPeriod вернемся немного позже.

Прочие обработчики и функции индикатора не так интересны и будут доступны в прилагаемом коде. Сейчас же пришла пора приступить к описанию классов, отвечающих за создание, отрисовку и управление диаграммами:

Класс управления диаграммами

Начнем с менеджера по управлению горизонтальными диаграммами. Это будет класс, который сам не содержит графических примитивов, а управляет массивом других классов, которые эти примитивы содержат. А поскольку все диаграммы в одном менеджере однотипные (как уже говорилось выше), то многие свойства этих диаграмм будут одинаковыми. Следовательно, не имеет смысла хранить один и тот же набор свойств в каждой диаграмме, а стоит поместить единственный набор свойств в менеджер. Назовем класс менеджера "CHDiags" и приступим к написанию кода:

  1.   Закрытые поля класса CHDiags, содержащие, в том числе, набор свойств, одинаковый для всех диаграмм под управлением данного менеджера:
private:    
                HD_POSITION m_position;  
                HD_STYLE    m_style;     
                HD_DIRECT   m_dir;       
                int         m_iHorSize;     
                color       m_cColor;    
                int         m_iWidth;     
                int         m_id;
                int         m_imCount;       
                long        m_chart;    
                datetime    m_dtVis; 
   static const string      m_BaseName;  
                CHDiagDraw* m_pHdArray[];

Опишем данный набор:

  • m_position,  m_style,  m_dir — Эти три параметра описывают привязку и внешний вид диаграмм, про них уже было сказано.
  • m_iHorSize — Это максимально возможный горизонтальный размер диаграммы. Об этом параметре так же было сказано.
  • m_cColor и  m_iWidth — Цвет диаграммы и толщина линий.
Вышеописанные поля представляют собой свойства, единые для всех диаграмм, управляемых менеджером.
  • m_id — Уникальный идентификатор менеджера. С учетом того, что менеджер может быть не один, у него должен быть уникальный идентификатор. Он потребуется для формирования уникальных имен объектов.
  • m_chart — Идентификатор графика, на котором отображаются диаграммы. По умолчанию значение этого поля равно нулю (текущий график).
  • m_imCount — Максимальное количество диаграмм на графике. В конечном итоге количество диаграмм на графике будет определяться этим и следующим полями.
  • m_dtVis — Левее этой временной отметки диаграммы не создавать.
  • m_BaseName — Чрезвычайно важный параметр, определяющий "базовое" имя. Все элементы диаграмм, как и сами диаграммы для успешного создания должны иметь уникальные имена. Такие имена будут даваться на основе данного "базового" имени.
Все вышеописанные поля доступны с помощью  функций вида GetXXXX()
  • m_pHdArray[] — Массив с указателями на объекты, содержащие отдельные диаграммы. Это поле не является свойством и для него нет функции GetXXXX().

Функций вида SetXXXX() для свойств не предусмотрено. Все они (за исключением m_BaseName) устанавливаются в конструкторе класса. Еще одним исключением является поле m_dtVis. Оно устанавливается в конструкторе параметром типа bool, имеющего следующий смысл:

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

После создания менеджера можно создавать объекты — диаграммы. Это делает метод класса CHDiags:

int CHDiags::AddHDiag(datetime dtCreatedIn)

Метод возвращает индекс созданного объекта класса CHDiagDraw в массиве m_pHdArray менеджера, либо -1 при ошибке. В качестве параметра методу dtCreatedIn передается временная привязка начала диаграмм. Например, для рассматриваемого индикатора здесь будет передаваться время открытия дневной свечи. В том случае, если временная привязка не используется (свечи привязаны к границам окна терминала), здесь нужно передать TimeCurrent(). В том случае, если диаграмма оказывается расположенной левее временной отметки поля m_dtVis, то объект создан не будет. Как работает метод, будет понятно из следующего кода:

int CHDiags::AddHDiag(datetime dtCreatedIn) {
   if(dtCreatedIn < m_dtVis ) return (-1);
   int iSize = ArraySize(m_pHdArray);
   if (iSize >= m_imCount) return (-1);
   if (ArrayResize(m_pHdArray,iSize+1) == -1) return (-1);
   m_pHdArray[iSize] = new CHDiagDraw(GetPointer(this) );
   if (m_pHdArray[iSize] == NULL) {
      return (-1);
   }
   return (iSize);
}//AddHDiag()

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

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

   bool        RemoveDiag(const string& dname);
   void        RemoveContext(int index, bool bRemovePrev);
   int         SetData(const HDDATA& hddata, int index); 
  1. Первый метод, как понятно из его названия, полностью удаляет диаграмму из менеджера и с графика, используя в качестве параметра имя диаграммы. В настоящий момент это зарезервированная опция.
  2. Второй удаляет только графические примитивы, из которых диаграмма состоит. Диаграмма удаляется с графика, но присутствует в менеджере, будучи "пустой". Значение флага  bRemovePrev поясняется далее.
  3. Третий метод передает диаграмме структуру с исходными данными для создания графических примитивов и отрисовки диаграммы. В этом и предыдущем методах в качестве параметра используется индекс диаграммы в массиве m_pHdArray менеджера.

Последний метод, который стоит бегло упомянуть, это метод класса CHDiags:

void        Align();

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

Прочие методы класса менеджера диаграмм CHDiags второстепенны и доступны в прилагаемом файле.

Класс отрисовки и управления графическими примитивами диаграмм

Назовем этот класс "CHDiagDraw" и наследуем его от CObject. В конструкторе класса мы получаем указатель на менеджер управления (сохраняем его в поле m_pProp) . Здесь же определяется уникальное имя диаграммы.

Далее, мы должны реализовать метод Type():

int CHDiagDraw::Type() const
  {
   switch (m_pProp.GetHDStyle() ) {
      case HD_RECTANGLE:
         return (OBJ_RECTANGLE);
      case HD_LINE:
         return (OBJ_TREND);
   }
   return (0);
  }

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

Основные вычисления в классе CHDiagDraw выполняются методом, который вызывается методом SetData менеджера:

int       SetData(const HDDATA& hddata);

Задача метода определить размеры диаграммы и создать нужное количество примитивов в определенном месте графика. Для этого методу в точке вызова передается ссылка на экземпляр структуры:

struct HDDATA
 {
   double   pcur[];
   double   prmax; 
   double   prmin;   
   int      prsize;
   double   vcur[];
   datetime dtLastTime;
   bool     bRemovePrev;
 };

Опишем поля данной структуры подробнее:

  • pcur[] — Массив ценовых уровней диаграммы. Для создаваемых графических примитивов это массив ценовых привязок.
  • prmax — Максимальное значение по горизонтали, которое может иметь диаграмма. В данном случае это максимальное значение тикового объема, проторгованного на определенном уровне.
  • prmin — Зарезервированный параметр.
  • prsize — Количество уровней диаграммы. Другими словами, это количество примитивов, из которых будет состоять диаграмма.
  • vcur[] — Массив величин, определяющий "горизонтальный размер столбиков" диаграммы. В данном случае в массиве содержатся тиковые объемы, проторгованные на соответствующих уровнях массива pcur[]. Размер массивов pcur и vcur должен совпадать и быть равным prsize.
  • dtLastTime — Место расположения диаграммы. Для графических примитивов это привязка по времени. Это поле имеет более высокий приоритет, чем аргумент метода AddHDiag менеджера.
  • bRemovePrev — Если этот флаг установлен в true, то при каждом обновлении данных диаграмма будет полностью перерисовываться с удалением прежних графических примитивов. Если установить флаг в false, то диаграмма "откажется" от управления старыми графическими примитивами и будет отрисовывать новую диаграмму без удаления прежних графических примитивов, как бы "забыв" про них.

Приведем код метода SetData полностью, в виду его важности:

int CHDiagDraw::SetData(const HDDATA &hddata) 
  {
   RemoveContext(hddata.bRemovePrev);
   if(hddata.prmax == 0.0 || hddata.prsize == 0) return (0);
   double dZoom=NormalizeDouble(hddata.prmax/m_pProp.GetHDHorSize(),Digits());
   if(dZoom==0.0) dZoom=1;
   ArrayResize(m_hItem,hddata.prsize);
   m_hItemCount=hddata.prsize;
   int iTo,t;
   datetime dtTo;

   string n;
   double dl=hddata.pcur[0],dh=0;
   

   GetBorders(hddata);
   for(int i=0; i<hddata.prsize; i++) 
     {
      if (hddata.vcur[i] == 0) continue;
      t=(int)MathCeil(hddata.vcur[i]/dZoom);
      switch(m_pProp.GetHDPosition()) 
        {
         case HD_LEFT:
         case HD_RIGHT:
            iTo=m_iFrom+m_pProp.GetHDPosition()*t;
            dtTo=m_pProp.GetBarTime(iTo);
            break;
         case HD_CNDLE:
            iTo   = m_iFrom + m_pProp.GetHDDirect() * t;
            dtTo  = m_pProp.GetBarTime(iTo);
            break;
         default:
            return (-1);
        }//switch (m_pProp.m_position)
      n=CHDiags::GetUnicObjNameByPart(m_pProp.GetChartID(),m_hname,m_iNameBase);
      m_iNameBase++;
      bool b=false;
      switch(m_pProp.GetHDStyle()) 
        {
         case HD_LINE:
            b=CHDiags::ObjectCreateRay(m_pProp.GetChartID(),n,dtTo,hddata.pcur[i],m_dtFrom,hddata.pcur[i]);
            break;
         case HD_RECTANGLE:
            if(dl!=hddata.pcur[i]) dl=dh;
            dh=(i == hddata.prsize-1) ? hddata.pcur[i] :(hddata.pcur[i]+hddata.pcur[i+1])/2;
            b = ObjectCreate(m_pProp.GetChartID(),n,OBJ_RECTANGLE,0,dtTo,dl,m_dtFrom,dh);
            break;
        }//switch(m_pProp.m_style)
      if(!b) 
        {
         Print("ERROR while creating graphic item: ",n);
         return (-1);
           } else {
         m_hItem[i]=n;
         ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_COLOR, m_pProp.GetHDColor() );
         ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_WIDTH, m_pProp.GeHDWidth() );
         ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_SELECTABLE, false);
         ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_BACK, true);
        }//if (!ObjectCreateRay(n, dtTo, hddata.pcur[i], m_dtFrom, hddata.pcur[i]) )    
     }// for (int i = 0; i < l; i++)      
   return (hddata.prsize);
  }//int CHDiagDraw::SetData(const HDDATA& hddata)

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

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

Возможно, метод выглядит слишком длинным. Можно было бы вынести код создания и отрисовки примитивов в отдельный, защищенный метод. Однако обе части метода выглядят органически совместимыми, вторая часть служит явным продолжением первой. Это соображение, а также не желание создавать множественные дополнительные вызовы нового метода с большим числом аргументов, привело к тому, что метод SetData был разработан в настоящем виде.

Прочие функции класса CHDiagDraw носят второстепенный характер и служат для интеграции с менеджером.

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

Весь вышеописанный код класса менеджера горизонтальных диаграмм, класса отрисовки диаграмм, структур и перечислений доступен в прилагаемом файле HDiagsE.mqh.

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

Обратно в индикатор

Объявляем два объекта и переменные в глобальном контексте:

CHDiags    *pHd;
int         iCurr, iCurr0;
HDDATA      sdata;

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

Менеджер и обе диаграммы создаем сразу в OnInit(), используя входные параметры индикатора:

   pHd         = new CHDiags(HD_CNDLE, hdStyle, HD_LEFTRIGHT, hdHorSize, hdColor, hdWidth, 0, MaxHDcount);
   if(pHd      == NULL) return (INIT_FAILED);
   iCurr       = pHd.AddHDiag(TimeCurrent() );
   if(iCurr  == -1) return (INIT_FAILED); 
   iCurr0       = pHd.AddHDiag(TimeCurrent() );
   if(iCurr0  == -1) return (INIT_FAILED);    

Вот так будет выглядеть та часть обработчика OnCalculate, для которой ранее пришлось использовать псевдокод:

        {
         int br=1;
         while(br<MaxHDcount) 
           {
            if(PrepareForBar(br++,sdata)) 
              {
               sdata.bRemovePrev = false;
               if(iCurr!=-1) 
                 {
                  Print(br-1," diag level: ",pHd.SetData(sdata,iCurr));
                 }
              }
           }
         ChartRedraw();
         bCreateHis=true;
        }

Осталось рассмотреть функцию, которая для выбранного бара с таймфрейма, для которого строится распределение  (TargetPeriod), заполняет данными структуру типа HDDATA:

bool PrepareForBar(int bar, HDDATA& hdta) {

   hdta.prmax  = hdta.prmin  = hdta.prsize  = 0;
   int iSCount;
   datetime dtStart, dtEnd;
   dtEnd = (bar == 0)? TimeCurrent() : iTime(Symbol(), TargetPeriod, bar - 1);
   hdta.dtLastTime = dtStart = iTime(Symbol(), TargetPeriod, bar);
   
   hdta.prmax = iHigh(Symbol(), TargetPeriod, bar);
   if(hdta.prmax == 0) return (false);
   hdta.prmax      = (int)MathCeil(NormalizeDouble(hdta.prmax, hdDigit) / hdPoint );
   
   hdta.prmin = iLow(Symbol(), TargetPeriod, bar);
   if(hdta.prmin == 0) return (false);
   hdta.prmin      = (int)MathCeil(NormalizeDouble(hdta.prmin, hdDigit) / hdPoint );

   iSCount = CopyRates(Symbol(), SourcePeriod, dtStart, dtEnd, source);
   if (iSCount < 1) return (false);
   
   hdta.prsize = (int)hdta.prmax - (int)hdta.prmin + 10;
   
   ArrayResize(hdta.pcur,  hdta.prsize);
   ArrayResize(hdta.vcur,  hdta.prsize);
   ArrayInitialize(hdta.pcur, 0);
   ArrayInitialize(hdta.vcur, 0);
   
   double avTick;
   int i, delta;
   hdta.prmax = 0;
   
   for (i = 0; i < hdta.prsize; i++) hdta.pcur[i] = (hdta.prmin + i) * hdPoint;
   int rs = 0;
   for (i = 1; i < iSCount; i++) {
      if (source[i].tick_volume == 0.0) continue;
      if (!MqlRatesRound(source[i], (int)hdta.prmin) ) continue;
      delta = (int)(source[i].high - source[i].low);
      if (delta == 0) delta = 1;
      avTick = (double)(source[i].tick_volume / delta);
      int j;
      for (j = (int)source[i].low; j <= (int)(source[i].low) + delta; j++) {
         if (j >= hdta.prsize) {
            Print("Internal ERROR. Wait for next source period or switch timeframe");
            return false;
         }
         hdta.vcur[j] += avTick;
         if (hdta.vcur[j] > hdta.prmax) hdta.prmax = (int)hdta.vcur[j];
      }//for (int j = (int)source[i].low; j <= (int)(source[i].low) + delta; j++)   
      if (j > rs) rs = j; //real size
   }//for (int i = 1; i < iSCount; i++)
   hdta.prsize = rs + 1;
   return (true);
}  

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

Далее читаются MqlRates с таймфрейма, который принят за источник данных за только что определенный период. Для каждой полученной структуры MqlRates считается, что тиковый объем tick_volume равномерно распределен в пределах от low до high структуры. Зная уже вычисленные границы ценового диапазона для всей будущей диаграммы, данное  распределение тикового объема позиционируется на диаграмме. Таким образом формируется массив распределения тиковых объемов за искомый период.


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

На этом вычисления заканчиваются, поля структуры данных для диаграммы заполнены и готовы для передачи методом SetData(...).

Общая схема работы с описанными классами такова:

  1. Подключается HDiagsE.mqh.
  2. Создается обеъект — менеджер, один на каждую группу "однотипных" диаграмм.
  3. Вызовом метода менеджера AddHDiag создаются диаграммы. Метод возвращает их индекс в массиве, известном менеджеру.
  4. Вызовом метода менеджера RemoveContext диаграмма очищается от не актуальных данных. Новые данные передаются в диаграмму вызовом метода менеджера SetData и передачей ему структуры типа HDDATA с данными. Ответственность за корректное заполнение полей этой структуры несет вызывающая сторона.
  5. При необходимости диаграммы можно прижать к левой или правой стороне окна терминала вызовом метода менеджера Align.
  6. Все диаграммы уничтожаются в деструкторе класса менеджера CHDiags.

Полный код индикатора можно найти в прилагаемом файле VolChart.mq5, а библиотечный файл в HDiagsE.mqh.

Об именах

Практически все объекты, которые имеют отношение к горизонтальным диаграммам имеют имена. Формируются они иерархически следующим образом:

  1. В менеджере имеется приватное поле m_BaseName, которое определяет "базовое имя". С этого имени будут начинаться все остальные.
  2. При создании объекта-менеджера, ему присваивается уникальный идентификатор. За уникальность этого параметра отвечает вызывающий код. Поле m_BaseName и данный идентификатор образуют имя менеджера.
  3. При создании объекта-диаграммы он также получает уникальное имя, основанное на имени менеджера.
  4. Наконец, создаваемые графические примитивы в объекте-диаграмме получают свои уникальные имена, основанные на имени объекта-диаграммы владельца примитивов.

Используемая схема позволяет легко отфильтровывать нужные объекты и управлять ими.

И еще один индикатор

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


Для этого выполним минимальные переделки в коде:

  • Необходимо переделать часть кода инициализации
       pHd         = new CHDiags(HD_RIGHT, hdStyle, HD_RIGHTLEFT, hdHorSize, hdColor, hdWidth, 0, MaxHDcount);
       if(pHd      == NULL) return (INIT_FAILED);
       iCurr       = pHd.AddHDiag(TimeCurrent() );
       if(iCurr    == -1) return (INIT_FAILED); 
  • Удалить весь код из обработчика OnCalculate. 
  • Добавить обработчик OnChartEvent
    void OnChartEvent(const int id,
                      const long &lparam,
                      const double &dparam,
                      const string &sparam)
      {
          switch (id) {
             case CHARTEVENT_CHART_CHANGE:
                pHd.Align();
                break;
            default:
                break;    
          }//switch (id)
      }  
И на этом всё, новый индикатор получен и работает. Его полный код в прилагаемом файле VolChart1.mq5.

Заключение

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

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

Программы и файлы, используемые в статье:

 # Имя
Тип
 Описание
1 VolChart.mq5 Индикатор
Индикатор распределения тиковых объемов.
2
HDiagsE.mqh Библиотечный файл
Библиотечный файл с менеджером горизонтальных диаграмм и горизонтальной диаграммой.
3
VolChart1.mq5
Индикатор
Индикатор распределения тиковых объемов, прижатый к правой стороне окна терминала.


Прикрепленные файлы |
HDiagsE.mqh (36.66 KB)
VolChart.mq5 (12.65 KB)
VolChart1.mq5 (12.21 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (43)
Andrei Novichkov
Andrei Novichkov | 12 сент. 2018 в 08:55
Константин, Вы не внимательно прочитали мой прошлый комментарий? Еще раз: Там НЕТ никакого пересчета. НЕТ. И отображение обновляется не сразу, т.к. сделана задержка. Обе этих задержки сделаны преднамеренно, т.к. я считаю правильным сделать именно так логику работы индикатора. Вы же код не видите, зачем же утверждаете, что он не оптимизирован, да еще и крив?
Konstantin
Konstantin | 13 сент. 2018 в 02:20

извиняюсь )) но не пойму тогда вашу логику, а кому эта задержка нужна?

Andrei Novichkov
Andrei Novichkov | 13 сент. 2018 в 14:38

Задержки это как раз следствие оптимизации. Первая задержка при инициализации. Если индикатор (в ролике для МТ4) устанавливается на некий таймфрейм символа, для которого нет истории, или она не полна, то диаграммы на истории могут не строиться, или будут строиться с ошибками. И я это явление наблюдал. Обращаю Ваше внимание, что индикатор может устанавливаться на одном таймфрейме, брать исходные данные с другого, а показывать для третьего. Что бы отрисовывать диаграммы на истории только один раз и сразу без ошибок в OnCalculate добавлен кусочек кода, который проверяет готовность исходных данных, а потом еще пропускает пять вызовов OnCalculate для гарантии того, что теперь уж точно все исходные данные готовы. Что бы пользователь не думал, что индикатор висит, в лог выводятся сообщения pass 1 ... pas 2 и т.д. Как видите, никакого пересчета, нагрузка на терминал минимальна. В конечном итоге, пересчитывается только последняя диаграмма. Происходит это раз в минуту. Плохой способ? Возможно. Но ничего более крутого мне в голову не пришло.

Во второй раз задержка происходит при режиме, когда графическими примитивами управляет индикатор, а не терминал. Это сделано для обработки появления множества событий CHARTEVENT_CHART_CHANGE. Т.е. когда пользователь ни с того, ни  с сего начнет дергать график туда сюда. Если пытаться такие события обрабатывать по мере поступления, то индикатор не справится с построениями и получится каша. Поэтому  индикатор просто ничего не делает и при наступлении события возвращает управление. А вот когда пользователь прекращает генерировать событие, индикатор ждет три секунды, что бы убедиться, что новых событий уже точно не будет и только тогда перерисовывает диаграммы на новом месте. Только один раз! Да, получаем задержку. Но мы о ней честно предупреждаем, лишние ресурсы не потребляем, а когда возникают вопросы, объясняем свою позиции. Как это происходит сейчас )

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

Konstantin
Konstantin | 13 сент. 2018 в 16:56
попробуйте реализовать через CCanvas, один объект на чарте, нужна будет помощь пишите в ЛС, а лучше в контакты которые я вам давал ранее, а то сейчас редко тут бываю )) на Python в основном пишу
Andrei Novichkov
Andrei Novichkov | 13 сент. 2018 в 17:13
Да, я помню Вашу мысль. Я планирую использовать этот способ и примерно представляю, как встроить такую диаграмму в разработанную архитектуру.
Моделирование временных рядов с помощью пользовательских символов по заданным законам распределения Моделирование временных рядов с помощью пользовательских символов по заданным законам распределения
В статье приводится обзор возможностей терминала по созданию и работе с пользовательскими символами, предлагаются варианты моделирования торговой истории c помощью пользовательских символов, тренда и различных графических паттернов.
График PairPlot на основе CGraphic для анализа зависимостей между массивами данных (таймсериями) График PairPlot на основе CGraphic для анализа зависимостей между массивами данных (таймсериями)
Часто в процессе технического анализа перед трейдерами ставится задача сравнения нескольких временных рядов. Проведение такого анализа требует соответствующих инструментов. В этой статье я предлагаю построить инструмент для графического анализа и поиска зависимостей между двумя и более временных рядов.
Комбинируем трендовую и флетовую стратегии Комбинируем трендовую и флетовую стратегии
Существуют различные стратегии торговли. Одни ищут направленное движение и торгуют по тренду. Другие определяют диапазоны ценовых колебаний и торгуют внутри таких коридоров. И возникает вопрос, можно ли объединить два подхода для увеличения прибыльности торговли?
Написание биржевых индикаторов с контролем объема на примере индикатора дельты Написание биржевых индикаторов с контролем объема на примере индикатора дельты
В статье рассмотрен алгоритм построения биржевых индикаторов на реальных объемах с использованием функций CopyTicks() и CopyTicksRange(). Также приведены особенности построения таких индикаторов и описаны нюансы их работы в реальном времени и в тестере стратегий.