preview
Простые решения для удобной работы с индикаторами

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

MetaTrader 5Примеры | 12 сентября 2024, 10:46
265 0
Aleksandr Slavskii
Aleksandr Slavskii

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

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


Делаем панель

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

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

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

Имя панели (индикатора)                              "Закрепить"   "Свернуть"

Название настройки 1Поле ввода
Название настройки 2
Поле ввода

Код, описывающий ячейку, будет содержать: имя объекта, тип объекта, № строки, текст ячейки, ширину ячейки в % от ширины панели. Ширина панели и ширина ячейки будут взаимозависимы. Сумма процентов всех ячеек одной строки должна  равняться 100%.

Допустим, нужно в строке указать три объекта, тогда номер строки у всех трёх объектов будет одинаковый, а ширина, например, 30% + 30% + 40% = 100%. В большинстве случаев строку достаточно разделить на две части: 50% для названия параметра настроек и 50% под поле ввода.

Как я уже говорил, код панели был задуман максимально простым. Поэтому я планировал обойтись без объектно-ориентированного программирования (ООП). Однако полностью отказаться от ООП не получилось. Копировать много кода из индикатора в индикатор было бы неудобно. Поэтому я оформил код панели в виде класса во включаемом файле.

Я использовал класс вместо обычных отдельных функций в основном потому, что в деструкторе удобно удалять объекты панели, иначе пришлось бы удалять в OnDeinit() индикатора, а там это сделать было бы сложнее.

Также будет включаемый файл Object.mqh с методами рисования объектов, и в него же я записал геттеры и сеттеры для большего удобства обращения к функциям. Не буду описывать, что такое геттеры и сеттеры. Те, кто ещё не знает об этом, могут обратиться к Яндексу — он расскажет лучше меня.

Частично идею панели я позаимствовал из этих статей: статья 1, статья 2

Все файлы кода описанного здесь, прикреплены внизу статьи. Я рекомендую скачать их, разложить по папкам и только потом приступать к изучению кода. У меня для файла Object.mqh создана отдельная папка Object в папке include. Для файла  Panel.mqh создана отдельная папка Panel в папке include. Соответственно и путь к этим файлам в моём коде указан с учётом вложенных папок.

Начнём с подключения включаемого файла Object.mqh и объявления input переменных. Нам надо объявить переменные, в которых укажем: цвета панели, текста, кнопок, рамок, а также дополнительные цвета, в которые будет окрашиваться панель при скрытии индикатора, размер шрифта, стиль шрифта, отступы панели от краёв графика.

Input настройки:

//+------------------------------------------------------------------+
#include <Object\\Object.mqh>
//+------------------------------------------------------------------+
input group "--- Input Panel ---"
input int    shiftX           = 3;               // Отступ панели по оси X
input int    shiftY           = 80;              // Отступ панели по оси Y
input bool   NoPanel          = false;           // Без панели
input int    fontSize         = 9;               // Размер шрифта
input string fontType          = "Arial";        /* Стиль шрифта*/ //"Arial", "Consolas"
input string PanelHiddenShown = "❐";             // Панель скрыта/показана
input string PanelPin         = "∇";             /* Панель закрепить*/ // ⮂ ↕  ↔  ➽ 🖈 ∇
input string PanelUnpin       = "_";             // Панель открепить
input color  clrTitleBar      = C'109,117,171';  // Цвет фона названия панели (1)
input color  clrTitleBar2     = clrGray;         // Цвет фона названия панели (2)
input color  clrDashboard     = clrDarkGray;     // Цвет фона панели
input color  clrTextDashboard = clrWhite;        // Цвет текста на панели
input color  clrBorder        = clrDarkGray;     // Цвет окантовки
input color  clrButton1       = C'143,143,171';  // Цвет фона кнопок (1)
input color  clrButton2       = C'213,155,156';  // Цвет фона кнопок (2)
input color  clrButton3       = clrGray;         // Цвет фона кнопок (3)
input color  clrTextButton1   = clrBlack;        // Цвет текста на кнопках (1)
input color  clrTextButton2   = clrWhite;        // Цвет текста на кнопках (2)
input color  clrEdit1         = C'240,240,245';  // Цвет фона в поле ввода (1)
input color  clrEdit2         = clrGray;         // Цвет фона в поле ввода (2)
input color  clrTextEdit1     = C'50,50,50';     // Цвет текста в поле ввода (1)
input color  clrTextEdit2     = clrWhite;        // Цвет текста в поле ввода (2)
//+------------------------------------------------------------------+

Далее сам класс CPanel:

//+------------------------------------------------------------------+
class CPanel
  {
private:

   enum ENUM_FLAG   //флаги
     {
      FLAG_PANEL_HIDDEN = 1,  // панель скрыта
      FLAG_PANEL_SHOWN  = 2,  // панель показана
      FLAG_IND_HIDDEN   = 4,  // индикатор скрыт
      FLAG_IND_SHOWN    = 8,  // индикатор показан
      FLAG_PANEL_FIX    = 16, // панель закреплена
      FLAG_PANEL_UNPIN  = 32  // панель откреплена
     };

   int               sizeObject;
   int               widthPanel, heightPanel;
   int               widthLetter, row_height;
   int               _shiftX, _shiftY;
   long              mouseX, mouseY;
   long              chartWidth, chartHeight;
   string            previousMouseState;
   long              mlbDownX, mlbDownY, XDistance, YDistance;
   string            _PanelHiddenShown, _PanelPin, _PanelUnpin;

   struct Object
     {
      string         name;
      string         text;
      ENUM_OBJECT    object;
      int            line;
      int            percent;
      int            column;
      int            border;
      color          txtColr;
      color          backClr;
      color          borderClr;
     };
   Object            mObject[];

   int               prefixInd;
   string            Chart_ID;
   string            addedNames[];
   long              addedXDisDiffrence[], addedYDisDiffrence[];
   int               WidthHidthCalc(int line, string text = "", int percent = 50,  ENUM_OBJECT object = OBJ_RECTANGLE_LABEL);
   void              Add(string name); // запомним имя и точку привязки объекта
   void              HideShow(bool hide = false);       // скрыть//показать
   void              DestroyPanel();   // удаляем все объекты

public:
                     CPanel(void);
                    ~CPanel(void);

   string            namePanel;    // имя панели
   string            indName;      // имя индикатора должно соответствовать короткому имени индикатора
   string            prefix;       // префикс для имён объектов панели
   bool              hideObject;   // Будет использоваться как флаг в индикаторах в которых нужно скрывать графические объекты
   int               sizeArr;
   double            saveBuffer[]; // массив для сохранения координат точки привязки панели, свойств панели (состояния флагов) а также последних настроек индикатора

   enum ENUM_BUTON  // флаги разрешения создания кнопок
     {
      BUTON_1 = 1,
      BUTON_2 = 2
     };

   void              Init(string name, string indName);
   void              Resize(int size) {sizeArr = ArrayResize(saveBuffer, size + 3); ZeroMemory(saveBuffer);};
   void              Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0);
   bool              OnEvent(int id, long lparam, double dparam, string sparam);
   int               Save() {ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError();}
   bool              Load(string name) {return (FileLoad("pnl\\" + (string)ChartID() + name, saveBuffer) > 0);}

   void              Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1);
   void              ApplySaved();
   void              HideShowInd(bool hide);
  };
//+------------------------------------------------------------------+
CPanel::CPanel(void) {}
//+------------------------------------------------------------------+
CPanel::~CPanel(void) {DestroyPanel(); ChartRedraw();}
//+------------------------------------------------------------------+

Методы класса обсудим чуть ниже на примерах.

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

#property indicator_chart_window
#property indicator_plots 0
input int _param = 10;
#include <Panel\\Panel.mqh>
CPanel mPanel;
int param = _param;
//+------------------------------------------------------------------+
int OnInit()
  {
   string short_name = "Ind Pnl(" + (string)param + ")";
   mPanel.Init("Ind Pnl", short_name);
   mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60);
   mPanel.Record("param", OBJ_EDIT, 1, IntegerToString(param), 40);
   mPanel.Create(0);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   return(rates_total);
  }
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);
  }
//+------------------------------------------------------------------+

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

А теперь, на примере этого индикатора, подробно разберём код панели.

Сразу после input-параметров индикатора, подключаем файл с классом панели и объявляем класс панели.

#property indicator_chart_window
#property indicator_plots 0
input int _param = 10;
#include <Panel\\Panel.mqh>
CPanel mPanel;
int param = _param;

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

Так как input-переменные являются константными, то изменять их нельзя. Но можно создать копию input-переменных и в дальнейшем работать с ними и изменять их из поля ввода панели. 

Далее в функции индикатора OnInit() добавим код панели.

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

string short_name = "Ind Pnl(" + (string)_param + ")";

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

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

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

Первый метод класса CPanel, который мы добавим в индикатор, это метод Init() в который передаём два имени: имя панели и имя индикатора. 

mPanel.Init("Ind Pnl", short_name);

И первое, что делает метод Init(), проверяет, не отключена ли панель в настройках.

void CPanel::Init(string name, string short_name)
  {
   if(NoPanel)
      return;

Далее инициализируем переменные:

   namePanel = name;
   indName = short_name;
   MovePanel = true;
   sizeObject = 0;
   Chart_ID = (string)ChartID();
   int lastX = 0, lastY = 0;

Зададим разрешение на отправку всем mql5-программам на графике сообщений о событиях перемещения и нажатия кнопок мышки (CHARTEVENT_MOUSE_MOVE), а так же разрешим отправку сообщений о событии создания графического объекта (CHARTEVENT_OBJECT_CREATE):

   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);

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

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

// зададим шрифт и размер шрифта
   TextSetFont(fontType, fontSize * -10);
//получим ширину и высоту одного символа
   TextGetSize("0", widthLetter, row_height);
// рассчитаем высоту ячейки
   row_height += (int)(row_height / 2);

В настройках панели есть значки, которые используются для скрытия/отображения панели  ❐, а также для её закрепления/открепления  ∇ и _.

Значки я нашёл на просторах интернета и их можно поменять в настройках.

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

   string space = " ";
   _PanelHiddenShown = space + PanelHiddenShown + space;
   _PanelPin         = space + PanelPin + space;
   _PanelUnpin       = space + PanelUnpin + space;

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

   MathSrand((int)GetMicrosecondCount());
   prefixInd = MathRand();
   prefix = (string)prefixInd;

Хочу обратить ваше внимание на то, что если индикаторов с панелью на графике несколько, то нельзя использовать при создании префикса функцию GetTickCount(), потому что при переключении таймфреймов времени проходит настолько мало, что если использовать миллисекунды вместо микросекунд, префиксы некоторых панелей могут совпадать.

При перетаскивании панели используется функция OnChartEvent(), в ней определяется положение мыши на графике и на объекте. А ведь панель может наехать на другую панель, и тогда у панелей получается конфликт, они обе считают, что мышь перетаскивает именно эту панель. Как итог, перетаскиваются все панели, которые находятся под мышкой. Для того, чтобы избежать конфликтов, создадим глобальную переменную и, при нажатии мыши на панели, какая первая панель запишет в эту глобальную переменную свой префикс, ту и будет двигать мышь. Принцип — кто первый встал того и тапки.

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

   GlobalVariableTemp("CPanel");
   GlobalVariableSet("CPanel", 0);

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

   sizeArr = ArraySize(saveBuffer);
   if(sizeArr == 0)
      Resize(0);

И хоть мы передаём в функцию количество настроек индикатора = 0, Resize(0); в самой функции добавляется три ячейки для запоминания настроек панели. То есть для того, чтоб запомнить положение панели на графике, её состояние (закреплена/откреплена, свёрнута/развёрнута) и ещё состояние индикатора (показан/скрыт), используется три ячейки массива saveBuffer.

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

С шаблоном оказалось сложнее. Если сохранить шаблон, в котором есть индикатор с панелью, то мы в этом шаблоне никак не можем сохранить координаты, где была панель в момент создания шаблона.

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

Сохранили шаблон:

 

Применили шаблон:

 

Именно эти текстовые метки мы используем для определения положения панели на момент создания шаблона.

   string delPrefix = "";
   int j = 0, total = ObjectsTotal(0, 0, OBJ_LABEL);
   for(int i = 0; i < total; i++)
     {
      string nameObject = ObjectName(0, i, 0, OBJ_LABEL);
      if(StringFind(nameObject, "TitleText " + indName) >= 0) // если в шаблоне есть объекты с именем этого индикатора
        {
         lastX = (int)GetXDistance(nameObject);// определим координаты Х панели в шаблоне
         lastY = (int)GetYDistance(nameObject);// определим координаты Y панели в шаблоне
         StringReplace(nameObject, "TitleText " + indName, ""); // запомним префикс объекта, для последующего его удаления
         delPrefix = nameObject;
        }
     }

В переменные lastX и lastY записываются координаты точки привязки объекта  — текстовая метка с именем, в котором после префикса есть имя индикатора, то есть координаты текста с названием панели.

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

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

   if(delPrefix != "")// удаляем артефактные объекты сохранённые в шаблоне
      ObjectsDeleteAll(0, delPrefix);

Далее идёт проверка и выбор нужного варианта точки привязки панели.

   if(lastX != 0 || lastY != 0)//если используем шаблон
     {
      lastX = lastX - widthLetter / 2;
      lastY = lastY - (int)(row_height / 8);
      saveBuffer[sizeArr - 1] = _shiftX = lastX;
      saveBuffer[sizeArr - 2] = _shiftY = lastY;
     }
   else//если используются данные из файла
      if(saveBuffer[sizeArr - 1] != 0 || saveBuffer[sizeArr - 2] != 0)
        {
         _shiftX = (int)saveBuffer[sizeArr - 1];
         _shiftY = (int)saveBuffer[sizeArr - 2];
        }
      else//если это первый запуск индикатора
        {
         saveBuffer[sizeArr - 1] = _shiftX = shiftX;
         saveBuffer[sizeArr - 2] = _shiftY = shiftY;
        }

В самом конце метода Init() отправим записаться в массив структур те объекты панели, которые не нужно редактировать, они для всех панелей будут одинаковые.

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

   Record("TitleBar");
   Record("MainDashboardBody");
   Record("TitleText " + indName, OBJ_LABEL, 0, namePanel, 100);
   Record("PinUnpin", OBJ_LABEL, 0, _PanelPin, 0);
   Record("CollapseExpand", OBJ_LABEL, 0, _PanelHiddenShown, 0);

Метод Init() рассмотрели, переходим к следующему методу Record().

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

//+------------------------------------------------------------------+
void CPanel::Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0)
  {
   if(NoPanel)
      return;
   int column = WidthHidthCalc(line + 1, text, percent, object);

   ArrayResize(mObject, sizeObject + 1);
   mObject[sizeObject].column = column;       // столбец
   mObject[sizeObject].name = prefix + name;  // имя объекта
   mObject[sizeObject].object = object;       // тип объекта
   mObject[sizeObject].line = line + 1;       // номер строки
   mObject[sizeObject].text = text;           // текст (если есть))
   mObject[sizeObject].percent = percent;     // процент от ширины панели
   mObject[sizeObject].txtColr = txtColr;     // цвет текста
   mObject[sizeObject].backClr = backClr;     // цвет основания
   mObject[sizeObject].borderClr = borderClr; // цвет окантовки
   mObject[sizeObject].border = 0;            // сдвиг от края панели
   sizeObject++;
  }
//+------------------------------------------------------------------+

В начале метода  Record() идёт обращение к методу WidthHidthCalc(), в котором рассчитывается ширина панели и её высота.

Метод WidthHidthCalc() разберём подробнее.

В этом методе рассчитывается ширина панели с учётом самого широкого элемента. Например, если в индикаторе  "Ind Pnl", представленном выше, изменить название на более длинное.

Было:

mPanel.Init("Ind Pnl", short_name);

Сделали:

mPanel.Init("Ind Pnl 0000000000000000000", short_name);

Получаем вот такую панель:

Или если, например, изменим название настройки индикатора, получится следующее.

Было:

mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60);

Сделаем:

mPanel.Record("paramText 0000000000000000000", OBJ_LABEL, 1, "param", 60);

Результат:

Панель сама подстраивается под размеры текста. Все расчёты ширины и высоты панели производятся в функции WidthHidthCalc().

Сначала получаемширину текста ячейки.

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

int CPanel::WidthHidthCalc(int line, string text = "", int percent = 50,  ENUM_OBJECT object = OBJ_RECTANGLE_LABEL)
  {
   static int lastLine = -1, column = 0;
   int width, height;
   if(line == 1)
      TextGetSize(text + _PanelPin + _PanelHiddenShown, width, height); //получим ширину и высоту текста для строки с именем панели
   else
      TextGetSize(text, width, height); //получим ширину и высоту текста

Текст должен иметь отступ от границы ячейки, и такой отступ мы сделаем равным половине символа. Мы уже нашли ширину одного символа в функции Init() и записали в переменную widthLetter.

Чтобы обеспечить отступ текста с двух сторон, нужно добавить к полученной ширине текста ширину ещё одного символа, а для текста на  объекте "Кнопка" - OBJ_BUTTON, нужно будет добавить ещё один символ для отступа от краёв кнопки.

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

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

   double indent = 0;
   if(object == OBJ_BUTTON)
      indent += widthLetter;

   if(text != "" && percent != 0)
     {
      //расчитаем ширину панели исходя из размера текста и процента отведённого под этот текст
      int tempWidth = (int)MathCeil((width + widthLetter + indent) * 100 / percent);
      if(widthPanel < tempWidth)
         widthPanel = tempWidth;
     }

Расчёт ширины панели в тестовом индикаторе будет выглядеть следующим образом.

Сначала считается ширина названия с учётом значков. Название "Ind Pnl"  + " ∇ " + " ❐ " получилась ширина 71 рх плюс ширина одного символа 7 рх, итого 78 рх —  это 100% ширины панели.

Текст ячейки — "param",  ширина 36 px с учётом добавленных отступов 7 рх,  получилось 43 px, на эту ячейку выделено 60% от ширины панели, значит, ширина панели будет равна 43 * 100 / 60 = 72 px. Это меньше, чем нужно для названия панели, значит, ширина панели будет равна ячейке с названием.

Далее определяется номер столбца и/или добавляется высота панели, если это новая строка.

   if(lastLine != line)// если это новая строка в панели, то увеличим высоту всей панели
     {
      heightPanel = row_height * line;
      lastLine = line;
      column = 0; // обнулим количество столбцов в новой строке
     }
   else
      column++; // добавим новый столбец

   return column;
  }

Вот мы и разобрали подробно работу двух  из десяти методов класса CPanel.

После того, как программа определила будущие размеры панели и записала параметры объектов в массив структур mObject[], мы переходим к следующему методу —  Create(). Этот метод будет строить панель на основе полученных ранее размеров.

В начале метода, как обычно, выполняется проверка, нужна ли эта панель. Затем следует код для записи двух предопределённых кнопок. Одна для скрытия индикатора, вторая для удаления. В зависимости от выбранных флагов, можно выбрать следующее 0-без кнопок, 1-одна кнопка скрыть/показать индикатор, 2-одна кнопка удалить индикатор, 3-будут созданы обе кнопки.

Почему эти кнопки здесь, а не в коде индикатора? Просто чтобы меньше кода вставлять в код индикаторов.

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

void CPanel::Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1)
  {
   if(NoPanel)
      return;

   if((Button & BUTON_1) == BUTON_1)// если нужно создавать кнопки
      Record("hideButton", OBJ_BUTTON, mObject[sizeObject - 1].line, "Ind Hide", 50);
   if((Button & BUTON_2) == BUTON_2)// если нужно создавать кнопки
      Record("delButton", OBJ_BUTTON, mObject[sizeObject - 2].line, "Ind Del", 50, clrTextButton1, clrButton2);

   ENUM_ANCHOR_POINT ap = ANCHOR_LEFT_UPPER;
   int X = 0, Y = 0, xSize = 0, ySize = 0;

   if(shiftx != -1 && shifty != -1)
     {
      _shiftX = shiftx;
      _shiftY = shifty;
     }

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

Немного отвлёкся, продолжим разбирать метод  Create(). Далее код, который создаёт два прямоугольника, прямоугольник заголовка и прямоугольник тела панели:

// прямоугольник заголовка
   RectLabelCreate(0, mObject[0].name, 0, _shiftX, _shiftY, widthPanel, row_height, (mObject[0].backClr == 0 ? clrTitleBar : mObject[0].backClr),
                   BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[0].borderClr == 0 ? clrBorder2 : mObject[0].borderClr), STYLE_SOLID, 1, false, false, true, 1, indName);
   Add(mObject[0].name);//запомним точку привязки объекта

// прямоугольник панели
   RectLabelCreate(0, mObject[1].name, 0, _shiftX, row_height - 1 + _shiftY, widthPanel, heightPanel - row_height, (mObject[1].backClr == 0 ? clrDashboard : mObject[1].backClr),
                   BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[1].borderClr == 0 ? clrBorder1 : mObject[1].borderClr), STYLE_SOLID, 1, false, false, true, 0, indName);
   Add(mObject[1].name);

После создания каждого объекта вызывается функция Add(), которая записывает в массивы имя и координаты этого объекта относительно левого верхнего угла графика.

//+------------------------------------------------------------------+
void CPanel::Add(string name)//запомним имя и точку привязки объекта
  {
   int size = ArraySize(addedNames);
   ArrayResize(addedNames, size  + 1);
   ArrayResize(addedXDisDiffrence, size + 1);
   ArrayResize(addedYDisDiffrence, size + 1);

   addedNames[size] =  name;
   addedXDisDiffrence[size] = GetXDistance(addedNames[0]) - GetXDistance(name);
   addedYDisDiffrence[size] = GetYDistance(addedNames[0]) - GetYDistance(name);
  }
//+------------------------------------------------------------------+

В дальнейшем эти массивы с координатами будут использованы при перемещении панели.

Вернёмся опять к коду метода  Create(). Дальше все объекты создаются в цикле в той последовательности, в какой их записали в массив структур mObject[]. Сначала расчет координат и размеров, затем создание объекта.

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

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

 for(int i = 2; i < sizeObject; i++)
     {
      // рассчитаем координаты точки привязки объекта
      if(mObject[i].column != 0)
        {
         X = mObject[i - 1].border + widthLetter / 2;
         mObject[i].border = mObject[i - 1].border + (int)MathCeil(widthPanel * mObject[i].percent / 100);
        }
      else
        {
         X = _shiftX + widthLetter / 2;
         mObject[i].border = _shiftX + (int)MathCeil(widthPanel * mObject[i].percent / 100);
        }

      Y = row_height * (mObject[i].line - 1) + _shiftY + (int)(row_height / 8);
      //---
      switch(mObject[i].object)
        {
         case  OBJ_LABEL:
            ap = ANCHOR_LEFT_UPPER;
            // точки привязки объектов "закрепить" и "свернуть", в отличии от всех остальных, сделаем в правом верхнем углу.
            if(i == 3)
              {
               int w, h;
               TextGetSize(_PanelHiddenShown, w, h);
               X = _shiftX + widthPanel - w;
               ap = ANCHOR_RIGHT_UPPER;
              }
            if(i == 4)
              {
               X = _shiftX + widthPanel;
               ap = ANCHOR_RIGHT_UPPER;
              }

            LabelCreate(0, mObject[i].name, 0, X, Y, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize,
                        (mObject[i].txtColr == 0 ? clrTextDashboard : mObject[i].txtColr), 0, ap, false, false, true, 1);
            break;

         case  OBJ_EDIT:
            xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter;
            ySize = row_height - (int)(row_height / 4);

            EditCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, mObject[i].text, fontType, fontSize, ALIGN_LEFT, false, CORNER_LEFT_UPPER,
                       (mObject[i].txtColr == 0 ? clrTextEdit1 : mObject[i].txtColr),
                       (mObject[i].backClr == 0 ? clrEdit1 : mObject[i].backClr),
                       (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, true, 1);
            break;

         case  OBJ_BUTTON:
            xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter;
            ySize = row_height - (int)(row_height / 4);

            ButtonCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize,
                         (mObject[i].txtColr == 0 ? clrTextButton1 : mObject[i].txtColr),
                         (mObject[i].backClr == 0 ? clrButton1 : mObject[i].backClr),
                         (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, false, true, 1);
            break;
        }
      Add(mObject[i].name);
     }

После того, как все объекты панели будут построены, мы удалим массив структур mObject[], так как он больше не будет нужен. 

   ArrayFree(mObject);

   ApplySaved();

   ChartRedraw();

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

ApplySaved()

Если это первый запуск индикатора на этом графике, то массив  saveBuffer[] заполняется стартовыми настройками.

Если же в массиве  saveBuffer[] уже есть сохранённые данные, то мы применяем их вместо стартовых настроек.

//+------------------------------------------------------------------+
void CPanel::ApplySaved()
  {
//свернём панель сразу после запуска индикатора, если это сохранено в файле
   if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN)
      CPanel::OnEvent(CHARTEVENT_OBJECT_CLICK, 0, 0, addedNames[4]);
   else
      saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_SHOWN;

//скроем индикатор сразу после запуска индикатора, если это сохранено в файле
   if(((uint)saveBuffer[sizeArr - 3] & FLAG_IND_HIDDEN) == FLAG_IND_HIDDEN)
     {
      HideShowInd(true);
      SetButtonState(prefix + "hideButton", true);
      hideObject = true;
     }
   else
     {
      saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_IND_SHOWN;
      hideObject = false;
     }

//закрепим панель сразу после запуска индикатора, если это сохранено в файле
   if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_FIX) == FLAG_PANEL_FIX)
      SetText(addedNames[3], _PanelUnpin);
   else
      saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_UNPIN;

   int Err = Save();
   if(Err != 0)
      Print("!!! Save Error = ", Err, "; Chart_ID + indName =", Chart_ID + indName);
  }
//+------------------------------------------------------------------+

Как вы наверное заметили, в функции ApplySaved() используются ещё такие функции  Save(),  HideShowInd() и OnEvent(), если вы это читаете, напишите, пожалуйста в комменте слово — "заметил", очень интересно узнать, читает кто-то эти описания или нет.

Перейдём к описанию этих функций по порядку.  В Save() сохраняем полученные настройки. Чтоб не засорять папку Files, под сохранённые настройки панелей выделим отдельную папку pnl

Вот так выглядит функция  Save():

int Save()
  {
   ResetLastError();
   FileSave("pnl\\" + Chart_ID + indName, saveBuffer);
   return GetLastError();
  }

HideShowInd()

Задача этой функции просто поменять цвет заголовка панели и цвет и текст кнопки. Из массива saveBuffer удаляем предыдущий флаг и записываем новый.

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

//+------------------------------------------------------------------+
void CPanel::HideShowInd(bool hide)
  {
// сменим цвет и текст кнопок в зависимости от состояния панели, скрыта/показана, а также цвет заголовка
   if(hide)
     {
      SetColorBack(prefix + "TitleBar", clrTitleBar2);
      SetColorBack(prefix + "hideButton", clrButton3);
      SetText(prefix + "hideButton", "Ind Show");
      saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_SHOWN) | FLAG_IND_HIDDEN;
      hideObject = true;
     }
   else
     {
      SetColorBack(prefix + "TitleBar", clrTitleBar);
      SetColorBack(prefix + "hideButton", clrButton1);
      SetText(prefix + "hideButton", "Ind Hide");
      saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_HIDDEN) | FLAG_IND_SHOWN;
      hideObject = false;
     }
   Save();
   ChartRedraw();
  }
//+------------------------------------------------------------------+

Эта функция используется только при клике на кнопку скрытия/отображения индикатора Ind Show/Ind Hide если она используется.

Пример скрытия одного из двух индикаторов RSI 

В коде также присутствует функция HideShow(), которая отвечает за скрытие и отображение объектов. Эта функция используется для сворачивания панели и вывода объектов панели на передний план.

Функция принимает аргумент, который указывает, свёрнута панель или нет (true/false). Если панель свёрнута, то необходимо вывести на передний план только четыре её объекта: прямоугольник с названием, само название и два значка — закрепить панель и свернуть её.

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

Если же флаг равен false, то последовательно скрываются, а затем отображаются все объекты панели, и таким образом они выводятся на передний план.

//+------------------------------------------------------------------+
void CPanel::HideShow(bool hide = false) //скрыть и сразу отобразить объекты, для вывода на передний план
  {
   int size = hide ? 5 : ArraySize(addedNames);
   for(int i = 0; i < size; i++)
     {
      SetHide(addedNames[i]);
      SetShow(addedNames[i]);
     }
   if(hide)
      SetHide(addedNames[1]);
  }
//+------------------------------------------------------------------+

Вот так выглядит свёрнутая панель рядом с обычной:

Следующая функция, которую рассмотрим это OnEvent()

В начале функции проверяем, разрешена ли панель в настройках:

bool CPanel::OnEvent(int id, long lparam, double dparam, string sparam)
  {
   if(NoPanel)
      return false;

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

При наступлении события "перемещение мыши" и в памяти записан флаг, разрешающий перемещение панели, то запоминаем координаты мыши.

Если это нажатие левой кнопки, то есть значение аргумента sparam равно 1, и если мышь до этого была не нажата, то считываем значение глобальной переменной.

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

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

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

И последнее здесь, скроем/отобразим панель для выведения её на передний план, при этом передаём аргумент (true/false) в функцию, является панель на данный момент скрытой, или она отображается полностью. 

   if(id == CHARTEVENT_MOUSE_MOVE && ((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN)
     {
      mouseX = (long)lparam;
      mouseY = (long)dparam;

      if(previousMouseState != "1" && sparam == "1")
        {
         int gvg = (int)GlobalVariableGet("Panel");
         if(gvg == prefixInd || gvg == 0)
           {
            XDistance = GetXDistance(addedNames[0]);
            YDistance = GetYDistance(addedNames[0]);

            mlbDownX = mouseX;
            mlbDownY = mouseY;

            if(mouseX >= XDistance && mouseX <= XDistance + widthPanel && mouseY >= YDistance && mouseY <= YDistance + row_height)
              {
               chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
               chartHeight = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
               GlobalVariableSet("Panel", prefixInd);
               HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN); // скроем/отобразим панель, чтоб она была на переднем плане
              }
           }
        }

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

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

В цикле перемещаем все объекты панели.

Записываем новые координаты точки привязки панели в массив для последующей записи в файл. После этого перерисовываем график.

      if((int)GlobalVariableGet("Panel") == prefixInd)
        {
         // не дадим панельке убежать за пределы графика
         long posX = XDistance + mouseX - mlbDownX;
         if(posX < 0)
            posX = 0;
         else
            if(posX + widthPanel > chartWidth)
               posX = chartWidth - widthPanel;

         long posY = YDistance + mouseY - mlbDownY;
         if(posY < 0)
            posY = 0;
         else
            if(posY + row_height > chartHeight)
               posY = chartHeight - row_height;

         // перемещаем панель
         int size = ArraySize(addedNames);
         for(int i = 0; i < size; i++)
           {
            SetXDistance(addedNames[i], posX - addedXDisDiffrence[i]);
            SetYDistance(addedNames[i], posY - addedYDisDiffrence[i]);
           }
         saveBuffer[sizeArr - 1] = (double)(posX);
         saveBuffer[sizeArr - 2] = (double)(posY);
         ChartRedraw(0);
        }

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

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

      if(sparam != "1" && (int)GlobalVariableGet("Panel") == prefixInd)
        {
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
         GlobalVariableSet("Panel", 0);
         Save();
        }

      previousMouseState = sparam;
     }

Мы подробно разобрали механизм перетаскивания панели, далее разберём действия при клике на значки закрепления/открепления панели или сворачивания/разворачивания панели.

Всё это в этой же функции OnEvent().

При наступлении события нажатие мышки на графическом объекте в переменной sparam находится имя объекта, на котором был клик мыши, если оно совпадает с именем объекта "❐",  то проверяем следующий объект, и если он видимый, то скрываем объекты панели, а если он невидим, то отображаем все объекты панели. Меняем флаг видимости панели и записываем его в массив для последующего сохранения в файл.

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

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

   else
      if(id == CHARTEVENT_OBJECT_CLICK)
        {
         if(sparam == addedNames[4]) // prefix+"CollapseExpand"
           {
            if(GetShow(addedNames[5]) == OBJ_ALL_PERIODS)// если панель видимая, скроем её
              {
               SetHide(addedNames[1]);
               for(int i = 5; i < sizeObject; i++)
                  SetHide(addedNames[i]);

               saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_SHOWN) | FLAG_PANEL_HIDDEN;
              }
            else// если панель скрыта, покажем её
              {
               for(int i = 0; i < sizeObject; i++)
                  SetShow(addedNames[i]);

               saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_HIDDEN) | FLAG_PANEL_SHOWN;
              }

            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            GlobalVariableSet("Panel", 0);
            Save();
            ChartRedraw(0);
           }

Следующий код аналогичен вышеописанному, с разницей только в имени объекта "∇"

         else
            if(sparam == addedNames[3]) // prefix+"PinUnpin"
              {
               if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN)
                 {
                  saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_UNPIN) | FLAG_PANEL_FIX;
                  SetText(addedNames[3], _PanelUnpin);
                 }
               else
                 {
                  saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_FIX) | FLAG_PANEL_UNPIN;
                  SetText(addedNames[3], _PanelPin);
                 }

               ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
               GlobalVariableSet("Panel", 0);
               Save();
               ChartRedraw(0);
              }

Завершает код кнопка удаления индикатора:

            else
               if(sparam == prefix + "delButton") // обработка кнопки удаления индикатора
                  ChartIndicatorDelete(0, ChartWindowFind(), indName);
        }

Последнее событие, которое нам нужно обработать, это создание графического объекта

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

Более подробно это явление описано в этой статье.

      else
         if(id == CHARTEVENT_OBJECT_CREATE)//https://www.mql5.com/ru/articles/13179   "Делаем информационную панель для отображения данных в индикаторах и советниках"
           {
            bool select = GetSelect(sparam);
            HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN);// скроем/отобразим панель, чтоб она была на переднем плане
            SetSelect(sparam, select);//воостанавливаем состояние крайнего объекта
           }

   return true;
  }

На этом описание кода панели можно считать законченным.


Какие правки нужно внести в код индикатора

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

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

Для единообразия добавил слово "Pnl" к именам индикаторов, которые подверглись модификации.


Индикатор Custom Moving Average

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

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

Было:

ma input

Стало:

Сразу после input параметров подключим включаемый файл Panel.mqh и объявим экземпляр класса CPanel mPanel;

Если директиву #include написать до input-параметров, то все input-параметры, написанные во включаемом файле, окажутся выше input-параметров индикатора, а это будет неудобно при запуске индикатора.

Если всё сделать правильно и запустить индикатор, то при запуске у нас должна появиться вот такая картинка:

Если настройки панели не нужны, то можно просто во включаемом файле Panel.mqh удалить все слова "input" и использовать настройки по умолчанию.

В функцию OnInit() добавляем следующий код.

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

   if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         InpMAPeriod = (int)mPanel.saveBuffer[0];
         InpMAShift  = (int)mPanel.saveBuffer[1];
         InpMAMethod = (int)mPanel.saveBuffer[2];
        }
      else
        {
         mPanel.Resize(3);
         mPanel.saveBuffer[0] = InpMAPeriod;
         mPanel.saveBuffer[1] = InpMAShift;
         mPanel.saveBuffer[2] = InpMAMethod;
        }

Имя панели, имя индикатора

Дальше всё заполняется однотипно: имя объекта, тип объекта, номер строки в панели, сам объект, процентов от ширины панели

      mPanel.Init("Moving Average", short_name);
      mPanel.Record("MAPeriodText", OBJ_LABEL, 1, "MAPeriod:", 50);
      mPanel.Record("MAPeriod", OBJ_EDIT, 1, IntegerToString(InpMAPeriod), 50);
      mPanel.Record("MAShiftText", OBJ_LABEL, 2, "MAShift:", 50);
      mPanel.Record("MAShift", OBJ_EDIT, 2, IntegerToString(InpMAShift), 50);
      mPanel.Record("MAMethodText", OBJ_LABEL, 3, "MAMethod:", 50);
      mPanel.Record("MAMethod", OBJ_EDIT, 3, IntegerToString(InpMAMethod), 50);
      mPanel.Create();
     }

Если на этом этапе запустить индикатор, то получим вот такую панельку:

Итак, панель у нас есть, осталось написать код  для коммуникации с пользователем.

Добавляем в индикатор ещё одну функцию — OnChartEvent().

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

Далее сохраняем все изменения в файл и перезапускаем индикатор.

void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);

   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= 0)
        {
         if(sparam == mPanel.prefix + "MAPeriod")
           {
            mPanel.saveBuffer[0] = InpMAPeriod = (int)StringToInteger(GetText(sparam));
           }
         else
            if(sparam == mPanel.prefix + "MAShift")
              {
               mPanel.saveBuffer[1] = InpMAShift = (int)StringToInteger(GetText(sparam));
               PlotIndexSetInteger(0, PLOT_SHIFT, InpMAShift);
              }
            else
               if(sparam == mPanel.prefix + "MAMethod")
                 {
                  mPanel.saveBuffer[2] = InpMAMethod = (int)StringToInteger(GetText(sparam));
                 }

         mPanel.Save();
         ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT);
        }

Обработка кнопки скрытия/отображения индикатора, в индикаторе МА предельно проста.

Чтобы скрыть линию мувинга, достаточно задать стиль графического построения DRAW_NONE, для отображения соответственно DRAW_LINE. 

   if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
      if(GetButtonState(sparam))
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
         mPanel.HideShowInd(true);
        }
      else
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE);
         mPanel.HideShowInd(false);
        }
  }

На этом изменения в индикаторе Custom Moving Average закончены. Изменённый индикатор имеет название Custom Moving Average Pnl.


Индикатор ParabolicSAR

Модификация индикатора ParabolicSAR абсолютно одинакова с индикатором  Custom Moving Average, за исключением совсем небольших нюансов.

В индикаторе ParabolicSAR не нужно создавать новые переменные с таким же названием как и input-переменные, так как они там уже есть.

Поэтому сразу подключаем включаемый файл:

В OnInit() добавляем код:

  if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         ExtSarStep = mPanel.saveBuffer[0];
         ExtSarMaximum = mPanel.saveBuffer[1];
        }
      else
        {
         mPanel.Resize(2);
         mPanel.saveBuffer[0] = ExtSarStep;
         mPanel.saveBuffer[1] = ExtSarMaximum;
        }
      mPanel.Init("ParabolicSAR", short_name);
      mPanel.Record("SARStepText", OBJ_LABEL, 1, "SARStep:", 50);
      mPanel.Record("SARStep", OBJ_EDIT, 1, DoubleToString(ExtSarStep, 3), 50);
      mPanel.Record("SARMaximumText", OBJ_LABEL, 2, "SARMax:", 50);
      mPanel.Record("SARMaximum", OBJ_EDIT, 2, DoubleToString(ExtSarMaximum, 2), 50);
      mPanel.Create();
     }

 В код индикатора добавляем функцию OnChartEvent().

//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);

   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= 0)
        {
         if(sparam == mPanel.prefix + "SARStep")
            mPanel.saveBuffer[0] = ExtSarStep = StringToDouble(GetText(sparam));
         else
            if(sparam == mPanel.prefix + "SARMaximum")
               mPanel.saveBuffer[1] = ExtSarMaximum = StringToDouble(GetText(sparam));

         mPanel.Save();
         ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT);
        }

   if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
      if(GetButtonState(sparam))
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
         mPanel.HideShowInd(true);
        }
      else
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW);
         PlotIndexSetInteger(0, PLOT_ARROW, 159);
         mPanel.HideShowInd(false);
        }
  }
//+------------------------------------------------------------------+

Всё, на  этом изменения в индикаторе ParabolicSAR закончены.


Индикатор RSI

В индикаторе RSI всё делается точно так же, как в предыдущих двух индикаторах.

В глобале после input-настройки вставляем:

#include <Panel\\Panel.mqh>
CPanel mPanel;

Далее в OnInit():

   if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         ExtPeriodRSI = (int)mPanel.saveBuffer[0];
        }
      else
        {
         mPanel.Resize(1);
         mPanel.saveBuffer[0] = ExtPeriodRSI;
        }
      mPanel.Init("RSI", short_name);
      mPanel.Record("PeriodRSIText", OBJ_LABEL, 1, "PeriodRSI:", 60);
      mPanel.Record("PeriodRSI", OBJ_EDIT, 1, IntegerToString(ExtPeriodRSI), 40);
      mPanel.Create();
     }

OnChartEvent() будет немного отличаться от предыдущих индикаторов.

Обработка объекта-поле ввода, делается точно так же, как в предыдущих индикаторах. А вот скрытие/отображение индикатора обрабатывается по-другому, так как до этого мы рассматривали индикаторы с основного графика, а RSI является подвальным индикатором.

При нажатии на кнопку "Ind Hide" задаём высоту окна индикатора равной нулю. Меняем цвет панельки, цвет и текст кнопки.

При повторном нажатии на кнопку, у неё теперь другое название — "Ind Show", устанавливаем значение  CHART_HEIGHT_IN_PIXELS равным -1Меняем цвет панельки, цвет и текст кнопки.

Цитата из учебника:

"Программная установка свойства CHART_HEIGHT_IN_PIXELS делает невозможным редактирование размера окна/подокна пользователем. Для того чтобы убрать фиксацию размера, следует установить значение свойства в -1".

//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   if(mPanel.OnEvent(id, lparam, dparam, sparam))
     {
      if(id == CHARTEVENT_OBJECT_ENDEDIT)
         if(StringFind(sparam, mPanel.prefix) >= 0)
            if(sparam == mPanel.prefix + "PeriodRSI")
              {
               mPanel.saveBuffer[0] = ExtPeriodRSI = (int)StringToInteger(GetText(sparam));
               mPanel.Save();
               ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT);
              }

      if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") //скрыть подвальный индикатор
        {
         if(GetButtonState(sparam))
           {
            ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), 0);
            mPanel.HideShowInd(true);
           }
         else
           {
            ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), -1);
            mPanel.HideShowInd(false);
           }
        }
     }
  }
//+------------------------------------------------------------------+



Ещё один индикатор

Есть индикаторы, которые вообще не используют графические стили, вместо этого они рисуют графические объекты, обычно стрелки. Это ещё один вариант обработки кнопки «Скрыть/показать индикатор». Давайте его разберём подробнее.

Я не стал искать индикатор со стрелками, просто написал индикатор фракталов, в котором верхние значки отображаются с помощью графического построения PLOT_ARROW, а нижние с помощью отрисовки объектов OBJ_ARROW.

Код индикатора приведу здесь полностью.

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

Как и в предыдущих индикаторах, сразу после input-переменных подключаем файл Panel.mqh и объявляем экземпляр класса CPanel.

Дублируем  input  переменные обычными.

#property indicator_chart_window
#property indicator_plots 1
#property indicator_buffers 1
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrRed
#property indicator_label1 "Fractals"

input int  _day      = 10; // day
input int  _barLeft  = 1;  // barLeft
input int  _barRight = 1;  // barRight
#include <Panel\\Panel.mqh>
CPanel mPanel;

double buff[];
int day = _day, barLeft = _barLeft, barRight = _barRight;
datetime limitTime = 0;

В OnInit() всё как в предыдущих индикаторах.

//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, buff, INDICATOR_DATA);
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0);
   PlotIndexSetInteger(0, PLOT_ARROW, 217);
   PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, -5);
   string short_name = StringFormat("Fractals(%d,%d)", _barLeft, _barRight);
   IndicatorSetString(INDICATOR_SHORTNAME, short_name);

   if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         day = (int)mPanel.saveBuffer[0];
         barLeft = (int)mPanel.saveBuffer[1];
         barRight = (int)mPanel.saveBuffer[2];
        }
      else
        {
         mPanel.Resize(3);
         mPanel.saveBuffer[0] = day;
         mPanel.saveBuffer[1] = barLeft;
         mPanel.saveBuffer[2] = barRight;
        }
      mPanel.Init("Fractals", short_name);
      mPanel.Record("dayText", OBJ_LABEL, 1, "Дней:", 50);
      mPanel.Record("day", OBJ_EDIT, 1, IntegerToString(day), 50);
      mPanel.Record("barLeftText", OBJ_LABEL, 2, "barLeft:", 50);
      mPanel.Record("barLeft", OBJ_EDIT, 2, IntegerToString(barLeft), 50);
      mPanel.Record("barRightText", OBJ_LABEL, 3, "barRight:", 50);
      mPanel.Record("barRight", OBJ_EDIT, 3, IntegerToString(barRight), 50);
      mPanel.Create();
     }

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Основные отличия от работы OnChartEvent() в предыдущих индикаторах, это то, что при каждом изменении параметров индикатора нужно удалять с графика, объекты нарисованные индикатором.

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

При обратном процессе, нужно не только указать тип графического построения DRAW_ARROW, но и указать номер стрелки из набора Wingdings. Затем в цикле делаем все скрытые объекты, видимыми.

//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);

   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= 0)
        {
         if(sparam == mPanel.prefix + "day")
            mPanel.saveBuffer[0] = day = (int)StringToInteger(GetText(sparam));
         else
            if(sparam == mPanel.prefix + "barLeft")
               mPanel.saveBuffer[1] = barLeft = (int)StringToInteger(GetText(sparam));
            else
               if(sparam == mPanel.prefix + "barRight")
                  mPanel.saveBuffer[2] = barRight = (int)StringToInteger(GetText(sparam));

         mPanel.Save();
         ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW);
         ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT);
        }

   if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
     {
      if(GetButtonState(sparam))
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);
         for(int i = ObjectsTotal(0) - 1; i >= 0; i--)
           {
            string name = ObjectName(0, i);
            if(StringFind(name, "DN_") >= 0)
               SetHide(name);
           }
         mPanel.HideShowInd(true);
        }
      else
        {
         PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW);
         PlotIndexSetInteger(0, PLOT_ARROW, 217);
         for(int i = ObjectsTotal(0) - 1; i >= 0; i--)
           {
            string name = ObjectName(0, i);
            if(StringFind(name, "DN_") >= 0)
               SetShow(name);
           }
         mPanel.HideShowInd(false);
        }
     }
  }
//+------------------------------------------------------------------+

Ещё нужно добавить в код индикатора проверку после каждого нарисованного нового объекта на флаг, указывающий на необходимость скрыть индикатор и если этот флаг true, то скрыть вновь нарисованный объект.

//+------------------------------------------------------------------+
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 = prev_calculated - 1;

   if(prev_calculated <= 0)
     {
      ArrayInitialize(buff, 0);
      datetime itime = iTime(_Symbol, PERIOD_D1, day);
      limitTime = itime <= 0 ? limitTime : itime;

      if(limitTime <= 0)
         return 0;
      int shift = iBarShift(_Symbol, PERIOD_CURRENT, limitTime);
      limit = MathMax(rates_total - shift, barRight + barLeft);
     }

   for(int i = limit; i < rates_total && !IsStopped(); i++)
     {
      bool condition = true;
      for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
         if(high[j - 1] >= high[j])
           {
            condition = false;
            break;
           }

      if(condition)
         for(int j = i - barRight + 1; j <= i; j++)
            if(high[j - 1] <= high[j])
              {
               condition = false;
               break;
              }

      if(condition)
         buff[i - barRight] = high[i - barRight];

      condition = true;
      for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
         if(low[j - 1] <= low[j])
           {
            condition = false;
            break;
           }

      if(condition)
         for(int j = i - barRight + 1; j <= i; j++)
            if(low[j - 1] >= low[j])
              {
               condition = false;
               break;
              }

      if(condition)
        {
         string name = mPanel.prefix + "DN_" + (string)time[i - barRight];
         ObjectCreate(0, name, OBJ_ARROW, 0, time[i - barRight], low[i - barRight]);
         ObjectSetInteger(0, name, OBJPROP_ARROWCODE, 218);
         ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlue);
         if(mPanel.hideObject)
            SetHide(name);
        }
     }
   return(rates_total);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW);
  }
//+------------------------------------------------------------------+

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


Индикатор Setting Objects Pnl

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

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

Создаём панель также, как делали это в индикаторах, с единственным нюансом — в метод Create() передаём в качестве аргументов текущие координаты мыши на графике.

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

После того, как редактирования завершены, панель можно убрать нажав на кнопку "Del Pnl", при этом будет удалён описатель.

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

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

#property indicator_chart_window
#property indicator_plots 0
#define FREE(P) if(CheckPointer(P) == POINTER_DYNAMIC) delete (P)
#include <Panel\\Panel.mqh>
CPanel * mPl;
//+------------------------------------------------------------------+
int OnCalculate(const int, const int, const int, const double &price[]) {return(0);}
//+------------------------------------------------------------------+
void OnChartEvent(const int id,  const long &lparam, const double &dparam, const string &sparam)
  {
   static bool panel = false;

   if(panel)
      mPl.OnEvent(id, lparam, dparam, sparam);

   if(id == CHARTEVENT_OBJECT_CLICK)
      if(!panel)
        {
         if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) < 0)
           {
            int line = 1;
            mPl = new CPanel();
            ENUM_OBJECT ObjectType = (ENUM_OBJECT)GetType(sparam);
            mPl.Init(EnumToString(ObjectType), sparam);
            mPl.Record("Color_Text", OBJ_LABEL, line, "Color", 50);
            mPl.Record("Color", OBJ_EDIT, line, ColorToString((color)GetColor(sparam)), 50);
            line++;
            mPl.Record("StyleText", OBJ_LABEL, line, "Style", 50);
            mPl.Record("Style", OBJ_EDIT, line, IntegerToString(GetStyle(sparam)), 50);
            line++;
            mPl.Record("WidthText", OBJ_LABEL, line, "Width", 50);
            mPl.Record("Width", OBJ_EDIT, line, IntegerToString(GetWidth(sparam)), 50);
            line++;
            if(ObjectType == OBJ_RECTANGLE || ObjectType == OBJ_RECTANGLE_LABEL || ObjectType == OBJ_TRIANGLE || ObjectType == OBJ_ELLIPSE)
              {
               mPl.Record("FillText", OBJ_LABEL, line, "Fill", 50);
               mPl.Record("Fill", OBJ_EDIT, line, IntegerToString(GetFill(sparam)), 50);
               line++;
              }
            mPl.Record("delButton", OBJ_BUTTON, line, "Del Pnl", 100);
            mPl.Create(0, (int)lparam, (int)dparam);
            panel = true;
           }
        }
      else
         if(sparam == mPl.prefix + "delButton")
           {
            FREE(mPl);
            panel = false;
           }

   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPl.prefix) >= 0)
        {
         if(sparam == mPl.prefix + "Color")
            SetColor(mPl.indName, StringToColor(GetText(sparam)));
         else
            if(sparam == mPl.prefix + "Style")
               SetStyle(mPl.indName, (int)StringToInteger(GetText(sparam)));
            else
               if(sparam == mPl.prefix + "Width")
                  SetWidth(mPl.indName, (int)StringToInteger(GetText(sparam)));
               else
                  if(sparam == mPl.prefix + "Fill")
                     SetFill(mPl.indName, (int)StringToInteger(GetText(sparam)));
         ChartRedraw();
        }
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {FREE(mPl);}
//+------------------------------------------------------------------+

Панель настроек объектов вызывается, зажатием клавиши Shift + левый клик мыши на объекте.


Заключение

Плюсы:

  • В целом, получилось удобное в использовании решение.

Минусы:

  • Хотелось "скрытие" индикатора спрятать в класс CPanel, но не получилось.
  • Если не добавить input-переменные в короткое имя индикатора, то вызов нескольких индикаторов с панелью будет невозможен, из-за совпадения имён.
  • Если запустить индикатор на графике, поменять его настройки с помощью панели, а затем сохранить шаблон, то при загрузке шаблона загрузятся не последние параметры индикатора, а те, что были заданы в настройках при запуске индикатора.
  • Не ко всем индикаторам можно приаттачить панель.
Прикрепленные файлы |
MQL5.zip (28.39 KB)
Object.mqh (37.86 KB)
Panel.mqh (49.6 KB)
Ind_Pnl.mq5 (1.61 KB)
Fractals_Pnl.mq5 (12.99 KB)
RSI_Pnl.mq5 (12.37 KB)
ZigzagColor_Pnl.mq5 (24.23 KB)
Нейросети в трейдинге: Обнаружение объектов с учетом сцены (HyperDet3D) Нейросети в трейдинге: Обнаружение объектов с учетом сцены (HyperDet3D)
Предлагаем Вам познакомиться с новым подход обнаружения объектов при помощи гиперсетей. Гиперсеть генерирующих весовые коэффициенты для основной модели, что позволяет учитывать особенности текущего состояния рынка. Такой подход позволяет улучшить точность прогнозирования, адаптируя модель к различным торговым условиям.
Алгоритм стрельбы из лука — Archery Algorithm (AA) Алгоритм стрельбы из лука — Archery Algorithm (AA)
В данной статье подробно рассматривается алгоритм оптимизации, вдохновленный стрельбой из лука, с акцентом на использование метода рулетки в качестве механизма выбора перспективных областей для "стрел". Этот метод позволяет оценивать качество решений и отбирать наиболее многообещающие позиции для дальнейшего изучения.
Количественный подход в управлении рисками: Применение VaR модели для оптимизации мультивалютного портфеля с Python и MetaTrader 5 Количественный подход в управлении рисками: Применение VaR модели для оптимизации мультивалютного портфеля с Python и MetaTrader 5
Эта статья раскрывает потенциал Value at Risk (VaR) модели для оптимизации мультивалютного портфеля. Используя мощь Python и функционал MetaTrader 5, мы демонстрируем, как реализовать VaR-анализ для эффективного распределения капитала и управления позициями. От теоретических основ до практической реализации, статья охватывает все аспекты применения одной из наиболее устойчивых систем расчета рисков — VaR — в алгоритмической торговле.
Возможности Мастера MQL5, которые вам нужно знать (Часть 17): Мультивалютная торговля Возможности Мастера MQL5, которые вам нужно знать (Часть 17): Мультивалютная торговля
По умолчанию торговля несколькими валютами недоступна при сборке советника с помощью Мастера. Мы рассмотрим два возможных приема, к которым могут прибегнуть трейдеры, желающие проверить свои идеи на нескольких символах одновременно.