Создание активных панелей управления на MQL5 для торговли

Евгений | 15 апреля, 2010


Введение

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

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

Так каков же выход? Выход - в создании собственных элементов управления, ведь в MetaTrader 5 появились такие замечательные компоненты как "Кнопка", "Поле ввода" и "Графическая метка". Этим и займёмся.


2. Функции панели

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

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

2.1. Открытие позиции

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

Имя
Тип
Описание
 ActP_buy_button1  Кнопка
 Кнопка совершения сделки на покупку
 ActP_sell_button1
 Кнопка
 Кнопка совершения сделки на продажу
 ActP_DealLines_check1
 Флажок
 Флаг установки/сброса вспомогательных линий
 ActP_Exe_radio1
 Переключатели
 Группа переключателей для выбора типа исполнения сделки
 ActP_SL_edit1
 Поле ввода
 Поле для ввода стопа
 ActP_TP_edit1
 Поле ввода
 Поле для ввода тейка
 ActP_Lots_edit1
 Поле ввода
 Поле для ввода объёма
 ActP_dev_edit1
 Поле ввода
 Поле для ввода допустимого отклонения при открытии
 ActP_mag_edit1
 Поле ввода
 Поле для ввода magic номера
 ActP_comm_edit1  Поле ввода  Поле для ввода комментария

Таблица 1. Список элементов панели "Открытие сделки"

2.2. Установка отложенного ордера

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

Имя
Тип
Описание
 ActP_buy_button2  Кнопка
 Кнопка установки ордера на покупку
 ActP_sell_button2
 Кнопка
 Кнопка установки ордера на продажу
 ActP_DealLines_check2
 Флажок
 Флаг установки/сброса вспомогательных линий
 ActP_lim_check2  Флажок  Флаг установки/сброса ордера stoplimit 
 ActP_Exe_radio2
 Переключатели
 Группа переключателей для выбора типа исполнения ордера
 ActP_exp_radio2  Переключатели  Группа переключателей для выбора типа истечения ордера
 ActP_SL_edit2
 Поле ввода
 Поле для ввода стопа
 ActP_TP_edit2
 Поле ввода
 Поле для ввода тейка
 ActP_Lots_edit2
 Поле ввода
 Поле для ввода объёма
 ActP_limpr_edit2
 Поле ввода  Поле для ввода цены ордера stoplimit
 ActP_mag_edit2
 Поле ввода
 Поле для ввода magic номера
 ActP_comm_edit2  Поле ввода  Поле для ввода комментария
 ActP_exp_edit2  Поле ввода  Поле для ввода времени истечения
 ActP_Pr_edit2  Поле ввода  Поле для ввода цены исполнения ордера 

Таблица 2. Список элементов панели "Установка отложенного ордера"

2.3. Модификация/закрытие сделки

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

Имя
Тип
Описание
 ActP_ord_button5   Выпадающий список   Список для выбора торговой операции 
 ActP_mod_button4  Кнопка
 Кнопка модификации сделки 
 ActP_del_button4
 Кнопка
 Кнопка закрытия сделки 
 ActP_DealLines_check4
 Флажок
 Флаг установки/сброса вспомогательных линий
 ActP_SL_edit4
 Поле ввода
 Поле для ввода стопа
 ActP_TP_edit4
 Поле ввода
 Поле для ввода тейка
 ActP_Lots_edit4
 Поле ввода
 Поле для ввода объёма 
 ActP_dev_edit4
 Поле ввода
 Поле для ввода допустимого отклонения 
 ActP_mag_edit4
 Поле ввода
 Поле для отображения magic номера (только чтение)
 ActP_Pr_edit4  Поле ввода  Поле для отображения цены открытия (только чтение)

Таблица 3. Список элементов панели "Модификация/закрытие сделки"

2.4. Модификация/удаление ордера

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

Имя
Тип
Описание
 ActP_ord_button5  Выпадающий список  Список для выбора ордера
 ActP_mod_button3  Кнопка
 Кнопка модификации ордера 
 ActP_del_button3
 Кнопка
 Кнопка удаления ордера 
 ActP_DealLines_check3
 Флажок
 Флаг установки/сброса вспомогательных линий
 ActP_exp_radio3  Переключатели  Группа переключателей для выбора типа истечения ордера
 ActP_SL_edit3
 Поле ввода
 Поле для ввода стопа
 ActP_TP_edit3
 Поле ввода
 Поле для ввода тейка
 ActP_Lots_edit3
 Поле ввода
 Поле для отображения объёма (только чтение)
 ActP_limpr_edit3
 Поле ввода  Поле для ввода цены ордера stoplimit 
 ActP_mag_edit3
 Поле ввода
 Поле для отображения magic номера (только чтение)
 ActP_comm_edit3  Поле ввода  Поле для ввода комментария
 ActP_exp_edit3  Поле ввода  Поле для ввода времени истечения
 ActP_Pr_edit3  Поле ввода  Поле для ввода цены исполнения ордера 
 ActP_ticket_edit3  Поле ввода  Поле для отображения тикета ордера (только чтение) 

Таблица 4. Список элементов панели "Модификация/удаление ордера"

2.5. Настройки

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

Имя
Тип
Описание
 ActP_col1_button6  Выпадающий список
 Список для выбора цвета кнопок
 ActP_col2_button6
 Выпадающий список
 Список для выбора цвета меток
 ActP_col3_button6
 Выпадающий список
 Список для выбора цвета текста
 ActP_font_edit6
 Поле ввода
 Поле для ввода размера шрифта

Таблица 5. Список элементов панели "Настройки"

Также добавим кнопку для возможности сворачивания панели, если в ней нет нужды. Вы могли заметить наличие такого инструмента, как "вспомогательные линии", что они такое и зачем они нужны? С помощью этих линий мы сможем установить стоп, тейк, цену срабатывания отложенного ордера, цену ордера stoplimit (горизонтальные линии), а также время истечения отложенного ордера (вертикальная линия), просто путём перетаскивания этих линий мышью на нужную цену/время.

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


3. Общий подход к созданию интерфейса

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

А теперь представьте, мы долго, нудно и упорно высчитывали координаты объектов, чтобы он не перекрывали друг друга, были отчётливо видны, а потом появилась необходимость добавить новый объект, и вся наша схема летит в тартарары.

Те, кто знаком со средами быстрой разработки приложений (Delphi, C++ Builder и.т.д.) знают, насколько быстро там можно сверстать самый замысловатый интерфейс.

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

Код скрипта может выглядеть так:

//+------------------------------------------------------------------+
//|                                  Component properties writer.mq5 |
//|              Copyright Copyright 2010, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

input int interfaceID=1; //входной параметр - идентификатор сохраняемого интерфейса
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   //откроем файл для записи   
   int handle=FileOpen("Active_Panel_scheme_"+IntegerToString(interfaceID)+".bin", FILE_WRITE|FILE_BIN);
   if(handle!=INVALID_HANDLE)
     {
      //переберём все объекты на экране
      for(int i=0;i<ObjectsTotal(0);i++)
        {
         string name=ObjectName(0,i);
         //и запишем их свойства в файл
         FileWriteString(handle,name,100);
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_TYPE));

         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_XDISTANCE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_YDISTANCE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_XSIZE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_YSIZE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_COLOR));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_STYLE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_WIDTH));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_BACK));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_SELECTED));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_SELECTABLE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_READONLY));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_FONTSIZE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_STATE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_BGCOLOR));

         FileWriteString(handle,ObjectGetString(0,name,OBJPROP_TEXT),100);
         FileWriteString(handle,ObjectGetString(0,name,OBJPROP_FONT),100);
         FileWriteString(handle,ObjectGetString(0,name,OBJPROP_BMPFILE,0),100);
         FileWriteString(handle,ObjectGetString(0,name,OBJPROP_BMPFILE,1),100);

         FileWriteDouble(handle,ObjectGetDouble(0,name,OBJPROP_PRICE));
        }
      //закроем файл
      FileClose(handle);
      Alert("Done!");
     }
  }
//+------------------------------------------------------------------+

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

Скрипт готов, займёмся созданием интерфейса.

И первое, с чего начнём, так это с главного меню, организуем его по типу вкладок. Зачем нам нужны вкладки? А затем, что количество объектов большое, и уместить их на одном экране будет проблематично. А раз объекты сгруппированы по назначению (см. таблицы выше), то удобнее всего будет поместить каждую группу на отдельную вкладку.

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

Рис. 1 Вкладки панелей

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

Особое внимание уделим именам объектов, не забываем, что все они должны начинаться с "ActP", также добавим в имя строку "main", это будет обозначать, что объект принадлежит главному меню панели.

Рис. 2 Список объектов (вкладки панелей)

Подобным образом "набросаем" на новые графики и содержимое вкладок. Содержимое каждой вкладки должно быть размещено на отдельном графике!

Вкладка "С рынка":

Рис. 3 Элементы вкладки "С рынка"

Вкладка "отложенный":

Рис. 4 Элементы вкладки "Отложенный"

Вкладка "Настройки":

Рис. 5 Элементы вкладки "Настройки"

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

Рис. 5 Элементы вкладки "Мод/закр"

После создадим подвкладки. Для работы со сделками:

Рис. 6 Подвкладка для работы со сделками

И для работы с ордерами:

Рис. 7 Подвкладка для работы с ордерами

Вот и всё, интерфейс создан.

Применим скрипт на каждый из графиков для сохранения каждой вкладки в отдельный файл. Для этого входной параметр "interfaceID" должен быть разным для каждой вкладки:

Вкладка под номером 5 соответствует кнопке главного "Свернуть", т.к. никаких объектов на этой вкладке нет её пропустим.

После всех манипуляций в папке Папка терминала -> MQL5 -> Files появятся следующие файлы:

Рис. 8 Список файлов схем панелей


4. Загрузка элементов интерфейса

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

Создадим его:

#property copyright "Copyright 2010, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window //разместим индикатор в отдельном окне

int OnInit()
  {
//--- indicator buffers mapping
   //установим короткое имя индикатора
   IndicatorSetString(INDICATOR_SHORTNAME, "AP");
//---
   return(0);
  }

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[])
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }

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

Теперь займёмся экспертом-панелью. Создадим нового эксперта.

В функции OnInit() разместим такие операторы:

double Bid,Ask;         //переменные для текущих цен
datetime time_current;  //время последнего тика
int wnd=-1;             //индекс окна с индикатором
bool last_loaded=false; //флаг указывающий, первая инициализация, или нет
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   //запустим таймер с периодичностью в 1 секунду
   EventSetTimer(1); 
   //получим последние цены
   get_prices();
   //определим окно с индикатором
   wnd=ChartWindowFind(0,"AP");
   //если первая инициализация - создадим интерфейс
   if(!last_loaded) create_interface();
//---
   return(0);
  }

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

Функция  create_interface() имеет вид:

//+------------------------------------------------------------------+
//| Функция создания интерфейса                                      |
//+------------------------------------------------------------------+
void create_interface()
  {
   //если выбран сброс настроек
   if(Reset_Expert_Settings)
     {
     //сбрасываем
      GlobalVariableDel("ActP_buttons_color");
      GlobalVariableDel("ActP_label_color");
      GlobalVariableDel("ActP_text_color");
      GlobalVariableDel("ActP_font_size");
     }

   //создадим интерфейс главного меню
   ApplyScheme(0);
   //создадим интерфейс вкладки "С рынка"
   ApplyScheme(1);
   //установим объекты как невыделяемые
   Objects_Selectable("ActP",false);
   //перерисуем график
   ChartRedraw();
  }

Первым делом проверим входной параметр "Сбросить_настройки", и если он установлен, удалим глобальные переменные отвечающие за настройки. Как это действие влияет на панель будет описано ниже. Далее функция ApplyScheme() создаст интерфейс из файла.

//+------------------------------------------------------------------+
//| Функция загрузки интерфейса                                      |
//| ID - идентификатор сохранённого интерфейса                       |
//+------------------------------------------------------------------+
bool ApplyScheme(int ID)
  {
   string fname="Active_Panel_scheme_custom_"+IntegerToString(ID)+".bin";
   //если не существует сохранённая схема загрузим стандартную
   if(!FileIsExist(fname)) fname="Active_Panel_scheme_"+IntegerToString(ID)+".bin";
   //откроем файл для чтения
   int handle=FileOpen(fname,FILE_READ|FILE_BIN);
   //файл открыт
   if(handle!=INVALID_HANDLE)
     {
      //считаем всё
      while(!FileIsEnding(handle))
        {
         string obj_name=FileReadString(handle,100);
         int _wnd=wnd;
         //вспомогательные линии забросим в главное окно
         if(StringFind(obj_name,"line")>=0) _wnd=0;
         ENUM_OBJECT obj_type=FileReadInteger(handle);
         //создаём объект
         ObjectCreate(0, obj_name, obj_type, _wnd, 0, 0);   
         //и применяем к нему свойства                                                   
         ObjectSetInteger(0,obj_name,OBJPROP_XDISTANCE,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_YDISTANCE,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_XSIZE,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_YSIZE,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_COLOR,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_STYLE,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_WIDTH,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_BACK,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_SELECTED,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_SELECTABLE,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_READONLY,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_FONTSIZE,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_STATE,FileReadInteger(handle));
         ObjectSetInteger(0,obj_name,OBJPROP_BGCOLOR,FileReadInteger(handle));

         ObjectSetString(0,obj_name,OBJPROP_TEXT,FileReadString(handle,100));
         ObjectSetString(0,obj_name,OBJPROP_FONT,FileReadString(handle,100));
         ObjectSetString(0,obj_name,OBJPROP_BMPFILE,0,FileReadString(handle,100));
         ObjectSetString(0,obj_name,OBJPROP_BMPFILE,1,FileReadString(handle,100));

         ObjectSetDouble(0,obj_name,OBJPROP_PRICE,FileReadDouble(handle));

         //установим цвета с объектов
         if(GlobalVariableCheck("ActP_buttons_color") && obj_type==OBJ_BUTTON)
            ObjectSetInteger(0,obj_name,OBJPROP_BGCOLOR,GlobalVariableGet("ActP_buttons_color"));
         if(GlobalVariableCheck("ActP_label_color") && obj_type==OBJ_LABEL)
            ObjectSetInteger(0,obj_name,OBJPROP_COLOR,GlobalVariableGet("ActP_label_color"));
         if(GlobalVariableCheck("ActP_text_color") && (obj_type==OBJ_EDIT || obj_type==OBJ_BUTTON))
            ObjectSetInteger(0,obj_name,OBJPROP_COLOR,GlobalVariableGet("ActP_text_color"));
         if(GlobalVariableCheck("ActP_font_size") && (obj_type==OBJ_EDIT || obj_type==OBJ_LABEL))
            ObjectSetInteger(0,obj_name,OBJPROP_FONTSIZE,GlobalVariableGet("ActP_font_size"));
         //установим глобальную переменную с размером шрифта
         if(obj_name=="ActP_font_edit6" && GlobalVariableCheck("ActP_font_size"))
            ObjectSetString(0,obj_name,OBJPROP_TEXT,IntegerToString(GlobalVariableGet("ActP_font_size")));
        }
      //закроем файл
      FileClose(handle);
      return(true);
     }
   return(false);
  }

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

Функция Objects_Selectable() делает все объекты, кроме вспомогательных линий невыделяемыми, чтобы включить анимацию кнопок и ненароком не удалить какой - либо объект.

//+------------------------------------------------------------------+
//| Функция установки объектов как невыделяемых                      |
//+------------------------------------------------------------------+
void  Objects_Selectable(string IDstr,bool flag)
  {
   //переберём все объекты
   for(int i=ObjectsTotal(0);i>=0;i--)
     {
      string n=ObjectName(0,i);
      //если объект принадлежит панели
      if(StringFind(n,IDstr)>=0)
        {
         //линии не трогаем
         if(!flag)
            if(StringFind(n,"line")>-1) continue; 
         //установим невыделяемыми все, кроме линий
         ObjectSetInteger(0,n,OBJPROP_SELECTABLE,flag); 
        }
     }
  }

Теперь займёмся функцией OnTick(). Она будет нам служить только лишь для получения последних цен на рынке.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   //получим последние цены
   get_prices();
  }

Функция get_prices() имеет вид:

//+------------------------------------------------------------------+
//| Функция получения информации о тике                              |
//+------------------------------------------------------------------+
void get_prices()
  {
   MqlTick tick;
   //если тик был
   if(SymbolInfoTick(Symbol(),tick))
     {
      //получим информацию
      Bid=tick.bid;
      Ask=tick.ask;
      time_current=tick.time;
     }
  }

И не забудем про OnDeinit():

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   //если причина деинициализации - не смена таймфрейма или символа
   if(reason!=REASON_CHARTCHANGE)
     {
      //сбросим флаг инициализации
      last_loaded=false;  
      //удалим все объекты панели
      ObjectsDeleteAll_my("ActP");
      //удалим файлы с сохранённым состоянием вкладок
      FileDelete("Active_Panel_scheme_custom_1.bin");
      FileDelete("Active_Panel_scheme_custom_2.bin");
      FileDelete("Active_Panel_scheme_custom_3.bin");
      FileDelete("Active_Panel_scheme_custom_4.bin");
      FileDelete("Active_Panel_scheme_custom_5.bin");
     }
   //иначе установим флаг
   else last_loaded=true;
   //остановим таймер
   EventKillTimer();
  }

Сперва проверим причину деинициализации, если это смена таймфрейма и/или символа не будем удалять элементы панели. В иных случаях удалим всё с помощью функции ObjectsDeleteAll_my(). 

//+------------------------------------------------------------------+
//| Функция удаления всех объектов панели                            |
//| IDstr - строка-идентификатор                                     |
//+------------------------------------------------------------------+
void  ObjectsDeleteAll_my(string IDstr)
  {
   //переберём все объекты
   for(int i=ObjectsTotal(0);i>=0;i--)
     {
      string n=ObjectName(0,i);
      //если в имени есть идентификатор - удалим объект
      if(StringFind(n,IDstr)>=0) ObjectDelete(0,n);
     }
  }

Скомпилировав и запустив советника, получим такой результат:

Рис. 9 Пример работы советника

Но вот только толку от этой красоты пока мало, поэтому попытаемся заставить объекты реагировать на наши манипуляции с ними.


5. Обработка событий

Интерфейс создан, теперь нужно заставить его работать. Все наши действия с объектами генерируют определённые события. Функция OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam ) является обработчиком группы событий ChartEvent. Из всех событий нас интересуют следующие:

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

Итак, первое событие, с которого начнём - клик по кнопке главного меню.

5.1. Обработка событий главного меню

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

//+------------------------------------------------------------------+
//| Обработчики событий                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке главного меню
      if(sparam=="ActP_main_1") {Main_controls_click(1); ChartRedraw(); return;}
       //здесь выполним соответствующие операторы
      if(sparam=="ActP_main_2") {Main_controls_click(2); ChartRedraw(); return;}
      if(sparam=="ActP_main_3") {Main_controls_click(3); ChartRedraw(); return;}
      if(sparam=="ActP_main_4") {Main_controls_click(4); ChartRedraw(); return;}
      if(sparam=="ActP_main_5") {Main_controls_click(5); ChartRedraw(); return;}
   ...   
   }
...
}

Если произошёл клик по кнопке меню, то мы выполним функцию Main_controls_click(), перерисуем график с помощью ChartRedraw() и завершим выполнение функции. Завершать выполнение можно потому, что в один момент времени мы можем кликнуть только по одному объекту, и дальнейшее выполнение приведёт к пустой трате процессорного времени.

//+------------------------------------------------------------------+
//| Обработчик вкладки                                               |
//| ID - номер кликнутой вкладки                                     |
//+------------------------------------------------------------------+
void Main_controls_click(int ID)
  {
   int loaded=ID;
   //переберём все вкладки
   for(int i=1;i<6;i++)
     {
      //для всех, кроме выбранной установим неактивное состояние
      if(i!=ID) 
        {
         //также запомним последнюю активную вкладку
         if(ObjectGetInteger(0,"ActP_main_"+IntegerToString(i),OBJPROP_STATE)==1) loaded=i;
         ObjectSetInteger(0,"ActP_main_"+IntegerToString(i),OBJPROP_STATE,0);
        }
     }
//if(loaded==ID) return;
   //для выбранной установим активное состояние
   ObjectSetInteger(0,"ActP_main_"+IntegerToString(ID),OBJPROP_STATE,1);
   //удалим все выпадающие списки
   DeleteLists("ActP_orders_list_"); 
   DeleteLists("ActP_color_list_");
   //и установим кнопки активации списков в отжатое состояние
   ObjectSetInteger(0,"ActP_ord_button5",OBJPROP_STATE,0);
   ObjectSetInteger(0,"ActP_col1_button6",OBJPROP_STATE,0);
   ObjectSetInteger(0,"ActP_col2_button6",OBJPROP_STATE,0);
   ObjectSetInteger(0,"ActP_col3_button6",OBJPROP_STATE,0);
   //сохраним состояние последней активной
   SaveScheme(loaded);
   //удалим старую вкладку
   DeleteScheme("ActP");
   //и загрузим новую
   ApplyScheme(ID);
   //установим все объекты в невыделяемое состояние
   Objects_Selectable("ActP",false);
  }

С функциями Objects_Selectable() и ApplyScheme() мы ознакомились выше, а DeleteLists() рассмотрим позже.

Функция SaveScheme() сохраняет интерфейс в файл, чтобы при новой его загрузке объекты сохраняли все свои свойства:

//+------------------------------------------------------------------+
//| Функция сохранения интерфейса                                    |
//+------------------------------------------------------------------+
void SaveScheme(int interfaceID)
  {
   //откроем файл для записи
   int handle=FileOpen("Active_Panel_scheme_custom_"+IntegerToString(interfaceID)+".bin",FILE_WRITE|FILE_BIN);
   //если файл открыт
   if(handle!=INVALID_HANDLE)
     {
      //переберём все объекты на экране
      for(int i=0;i<ObjectsTotal(0);i++)
        {
         string name=ObjectName(0,i);
         //если объект принадлежит панели
         if(StringFind(name,"ActP")<0) continue;
         //и это не вкладка
         if(StringFind(name,"main")>=0) continue; 
         //запишем его свойства в файл
         FileWriteString(handle,name,100);
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_TYPE));

         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_XDISTANCE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_YDISTANCE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_XSIZE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_YSIZE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_COLOR));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_STYLE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_WIDTH));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_BACK));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_SELECTED));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_SELECTABLE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_READONLY));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_FONTSIZE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_STATE));
         FileWriteInteger(handle,ObjectGetInteger(0,name,OBJPROP_BGCOLOR));

         FileWriteString(handle,ObjectGetString(0,name,OBJPROP_TEXT),100);
         FileWriteString(handle,ObjectGetString(0,name,OBJPROP_FONT),100);
         FileWriteString(handle,ObjectGetString(0,name,OBJPROP_BMPFILE,0),100);
         FileWriteString(handle,ObjectGetString(0,name,OBJPROP_BMPFILE,1),100);

         FileWriteDouble(handle,ObjectGetDouble(0,name,OBJPROP_PRICE));
        }
      //закроем файл  
      FileClose(handle);
     }
  }

Функция  DeleteScheme() удаляет объекты вкладки.

//+------------------------------------------------------------------+
//| Функция удаления всех объектов панели, кроме вкладки             |
//+------------------------------------------------------------------+
void  DeleteScheme(string IDstr)
  {
   //переберём все объекты
   for(int i=ObjectsTotal(0);i>=0;i--)
     {
      string n=ObjectName(0,i);
      //удалим всё, кроме вкладки
      if(StringFind(n,IDstr)>=0 && StringFind(n,"main")<0) ObjectDelete(0,n); 
     }
  }

Таким образом, выполнив функцию Main_controls_click(), мы удалим старую вкладку, предварительно сохранив её, и загрузим новую.

Скомпилировав советника, посмотрим результаты.

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

Рис. 10 Элементы вкладки "Отложенный"


Рис. 11 Элементы вкладки "Мод/закр"

Рис. 12 Элементы вкладки "Настройки"

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

5.2. Обработка событий компонента "Флажок"

Установка вспомогательных линий и ордера stoplimit осуществляется с помощью компонента "флажок", но такого нет в списке графических объектов MetaTrader 5, так создадим его. Есть объект "графическая метка", фактически это изображение, которое имеет состояния "включено" и "выключено". Состояние меняется кликом по объекту, на каждое состояние можно назначить отдельное изображение. Подберём картинки для каждого из состояний:

Установим в свойствах объекта эти картинки:

Рис. 13. Установка свойства элемента "Флажок"

Рис. 13. Установка свойства элемента "Флажок"

Напомню, чтобы картинки были доступны в списке, они должны находится в папке "Папка терминала->MQL5->Images" и иметь расширение ".bmp".

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

       //клик по флажку установки вспомогательных линий при открытии сделки
      if(sparam=="ActP_DealLines_check1")
      {
          //проверим состояние флага
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
          //если флаг установлен
         if(selected)
         {
             //извлечём значения стопа и тейка из полей ввода
            string SL_txt=ObjectGetString(0, "ActP_SL_edit1", OBJPROP_TEXT);
            string TP_txt=ObjectGetString(0, "ActP_TP_edit1", OBJPROP_TEXT);
            double val_SL, val_TP;
            
              //если в поле стопа не пусто
              //запомним значение
            if(SL_txt!="")
               val_SL=StringToDouble(SL_txt);

              //если пусто
            else
            {
                 //возьмём макс. и мин. цену на графике
               double pr_max=ChartGetDouble(0, CHART_PRICE_MAX);
               double pr_min=ChartGetDouble(0, CHART_PRICE_MIN);
                 //установим стоп на уровне трети экрана 
               val_SL=pr_min+(pr_max-pr_min)*0.33;
            }   
            
              //аналогично обработаем тейк
            if(TP_txt!="")
               val_TP=StringToDouble(TP_txt);
            else
            {
               double pr_max=ChartGetDouble(0, CHART_PRICE_MAX);
               double pr_min=ChartGetDouble(0, CHART_PRICE_MIN);
               val_TP=pr_max-(pr_max-pr_min)*0.33;
            }      
            //переместим линии на новые позиции
            ObjectSetDouble(0, "ActP_SL_line1", OBJPROP_PRICE, val_SL);  
            ObjectSetDouble(0, "ActP_TP_line1", OBJPROP_PRICE, val_TP);  
         }
          //если флаг сброшен
         else
         {
             //уберём линии
            ObjectSetDouble(0, "ActP_SL_line1", OBJPROP_PRICE, 0);
            ObjectSetDouble(0, "ActP_TP_line1", OBJPROP_PRICE, 0);
         }
          //перерсуем график
         ChartRedraw();
          //и закончим выполнение функции
         return;
      }

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

Флажок установки ордера stoplimit на вкладке "Отложенный" имеет следующий обработчик:

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

   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
      //клик по флажку установки ордера stoplimit
      if(sparam=="ActP_limit_check2")
      {
          //проверим состояние флага
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
         if(selected)//если флаг установлен
         {
             //установим новый цвет поля ввода цены ордера
            ObjectSetInteger(0, "ActP_limpr_edit2", OBJPROP_BGCOLOR, White);
             //также сделаем поле доступным для редактировния     
            ObjectSetInteger(0, "ActP_limpr_edit2", OBJPROP_READONLY, false);
             //установим в поле значение текущей цены
            ObjectSetString(0, "ActP_limpr_edit2", OBJPROP_TEXT, DoubleToString(Bid, _Digits));
             //если разрешены вспомогательные линии
             //переместим их
            if(ObjectGetInteger(0, "ActP_DealLines_check2", OBJPROP_STATE)==1)
               ObjectSetDouble(0, "ActP_lim_line2", OBJPROP_PRICE, Bid);
         }  
          //если флаг сброшен
         else
         {
              //сделаем поле недоступным для редактирования
            ObjectSetInteger(0, "ActP_limpr_edit2", OBJPROP_BGCOLOR, LavenderBlush);
              //установим цвет
            ObjectSetInteger(0, "ActP_limpr_edit2", OBJPROP_READONLY, true);
              //и "пустой" текст
            ObjectSetString(0, "ActP_limpr_edit2", OBJPROP_TEXT, "");
              //если разрешены вспомогательные линии
              //переместим их в нулевую точку с глаз долой
            if(ObjectGetInteger(0, "ActP_DealLines_check2", OBJPROP_STATE)==1)
               ObjectSetDouble(0, "ActP_lim_line2", OBJPROP_PRICE, 0);
         }
      }       
   ...   
   }
...
}

С флажками покончено. Рассмотрим следующий объект собственного производства - "группа переключателей".

5.3. Обработка событий компонента "Группа переключателей"

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

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

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по переключателю №1 типа исполнения ордера
      if(sparam=="ActP_Exe1_radio2")
      {
          //проверим состояние переключателя
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
          //установим переключатель в нужное положение
         ObjectSetInteger(0,sparam,OBJPROP_STATE, 1);
          //если установлен в положение 1 из 3-х
         if(selected)
         {
              //сборсим остальные переключатели
            ObjectSetInteger(0, "ActP_Exe2_radio2", OBJPROP_STATE, false);
            ObjectSetInteger(0, "ActP_Exe3_radio2", OBJPROP_STATE, false);
              //перерисуем график
            ChartRedraw();
              //закончим выполнение функции
            return;
         }
          //перерисуем график
         ChartRedraw();
          //закончим выполнение функции
         return;
      }
 
       //аналогично для переключателя №2
      if(sparam=="ActP_Exe2_radio2") 
      {
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
         ObjectSetInteger(0,sparam,OBJPROP_STATE, 1);
         if(selected)
         {
            ObjectSetInteger(0, "ActP_Exe1_radio2", OBJPROP_STATE, false);
            ObjectSetInteger(0, "ActP_Exe3_radio2", OBJPROP_STATE, false);
            ChartRedraw();
            return;
         }
         ChartRedraw();
         return;
      }   

       //аналогично для переключателя №3
      if(sparam=="ActP_Exe3_radio2") 
      {
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
         ObjectSetInteger(0,sparam,OBJPROP_STATE, 1);
         if(selected)
         {
            ObjectSetInteger(0, "ActP_Exe1_radio2", OBJPROP_STATE, false);
            ObjectSetInteger(0, "ActP_Exe2_radio2", OBJPROP_STATE, false);
            ChartRedraw();
            return;
         }
         ChartRedraw();
         return;
      }     
   ...   
   }
...
}

Переключатели типа времени истечения ордера отличаются только тем, что при клике по третьему необходимо произвести дополнительные действия. А именно - установить новую дату в поле ввода времени истечения ордера:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту   
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по 3-му переключателю типа времени истечения ордера
      if(sparam=="ActP_exp3_radio2")
      {
          //проверим состояние
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
         ObjectSetInteger(0,sparam,OBJPROP_STATE, 1);
          //если активно
         if(selected)
         {
              //сбросим остальные переключатели
            ObjectSetInteger(0, "ActP_exp1_radio2", OBJPROP_STATE, false);
            ObjectSetInteger(0, "ActP_exp2_radio2", OBJPROP_STATE, false);
              //установим новую дату в поле ввода
            ObjectSetInteger(0, "ActP_exp_edit2", OBJPROP_BGCOLOR, White);
            ObjectSetInteger(0, "ActP_exp_edit2", OBJPROP_READONLY, false);
            ObjectSetString(0, "ActP_exp_edit2", OBJPROP_TEXT, TimeToString(time_current));
              //если разрешены вспомогательные линии
              //установим новое значение линии времени
            if(ObjectGetInteger(0, "ActP_DealLines_check2", OBJPROP_STATE)==1)
               ObjectSetInteger(0, "ActP_exp_line2", OBJPROP_TIME, time_current);
            ChartRedraw();
            return;
         }

          //если не активно
         else
         {
             //сделаем поле ввода недоступным для редактирования
            ObjectSetInteger(0, "ActP_exp_edit2", OBJPROP_BGCOLOR, LavenderBlush);
            ObjectSetInteger(0, "ActP_exp_edit2", OBJPROP_READONLY, true);
             //уберём вспомогательную линию
            if(ObjectGetInteger(0, "ActP_DealLines_check2", OBJPROP_STATE)==1)
               ObjectSetInteger(0, "ActP_exp_line2", OBJPROP_TIME, 0);
         }      
         ChartRedraw();
         return;
   ...   
   }
...
}

С переключателями покончено.

5.4. Создание и обработка событий выпадающих списков

Из выпадающего списка будем производить выбор ордера/сделки для модификации/закрытия/удаления и выбор цветов панели. Начнём со списка сделок/ордеров.

Первое, что нас встречает на вкладке "Мод/закр" - это кнопка с надписью "Выберите ордер -->", это и будет нашей кнопкой активации списка. При клике на ней этот самый список должен из неё выпадать, после чего мы должны произвести выбор и список опять отправится в небытие.  Посмотрим на обработчик события CHARTEVENT_OBJECT_CLICK этой кнопки:

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

   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке активации выпадающего списка выбора ордера
      if(sparam=="ActP_ord_button5")
      {
          //проверим состояние
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);//проверим состояние
          //список активирован
         if(selected)//список активирован
         {
             //удалим интерфейс
            DeleteScheme("ActP", true);
             //массивы для хранения информации об ордерах
            string info[100];
             //массив для тикетов
            int tickets[100];
             //инициализируем его
            ArrayInitialize(tickets, -1);
             //получим информацию
            get_ord_info(info, tickets);
             //и создадим список
            create_list(info, tickets);
         }
          //список деактивирован
         else
         {
             //тогда удалим его
            DeleteLists("ActP_orders_list_");
         }
          //перерисуем график
         ChartRedraw();
          //завершим выполнение функции
         return;
      }      
   ...   
   }
...
}

Наша первоочередная задача - определить, есть ли сделки/ордера в рынке, и если есть, нужно извлечь из них информацию для отображения в списке. Этим занимается функция get_ord_info():

//+------------------------------------------------------------------+
//| Функция получения информации об ордерах                          |
//+------------------------------------------------------------------+
void get_ord_info(string &info[],int &tickets[])
  {
   //инициализируем счётчик
   int cnt=0;
   string inf;
   //если есть открытая позиция
   if(PositionSelect(Symbol()))
     {
      //соберём информацию о ней в строку
      double vol=PositionGetDouble(POSITION_VOLUME);
      int typ=PositionGetInteger(POSITION_TYPE);
      if(typ==POSITION_TYPE_BUY) inf+="BUY ";
      if(typ==POSITION_TYPE_SELL) inf+="SELL ";
      inf+=DoubleToString(vol, MathCeil(MathAbs(MathLog(vol)/MathLog(10))))+" lots";
      inf+=" at "+DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), Digits());
      //запишем результаты
      info[cnt]=inf;
      tickets[cnt]=0;
      //инкркментируем счётчик
      cnt++;
     }

   //переберём все ордера
   for(int i=0;i<OrdersTotal();i++)
     {
      //получим тикет
      int ticket=OrderGetTicket(i);
      //если символ ордера равен символу графика 
      if(OrderGetString(ORDER_SYMBOL)==Symbol())
        {
         //соберём информацию об ордере в строку
         inf="#"+IntegerToString(ticket)+" ";
         int typ=OrderGetInteger(ORDER_TYPE);
         double vol=OrderGetDouble(ORDER_VOLUME_CURRENT);
         if(typ==ORDER_TYPE_BUY_LIMIT) inf+="BUY LIMIT ";
         if(typ==ORDER_TYPE_SELL_LIMIT) inf+="SELL LIMIT ";
         if(typ==ORDER_TYPE_BUY_STOP) inf+="BUY STOP ";
         if(typ==ORDER_TYPE_SELL_STOP) inf+="SELL STOP ";
         if(typ==ORDER_TYPE_BUY_STOP_LIMIT) inf+="BUY STOP LIMIT ";
         if(typ==ORDER_TYPE_SELL_STOP_LIMIT) inf+="SELL STOP LIMIT ";
         inf+=DoubleToString(vol, MathCeil(MathAbs(MathLog(vol)/MathLog(10))))+" lots";
         inf+=" at "+DoubleToString(OrderGetDouble(ORDER_PRICE_OPEN), Digits());
         //запишем результаты
         info[cnt]=inf;
         tickets[cnt]=ticket;
         //инкркментируем счётчик
         cnt++;
        }
     }
  }

Она запишет в массивы информацию и тикеты ордеров и сделки.

Далее функция create_list() на основании этой информации создаст список:

//+------------------------------------------------------------------+
//| Функция создания списка позиций                                  |
//| info - массив с информацией о позициях                           |
//| tickets - массив с тикетами                                      |
//+------------------------------------------------------------------+
void create_list(string &info[],int &tickets[])
  {
   //получим координаты кнопки активации списка
   int x=ObjectGetInteger(0,"ActP_ord_button5",OBJPROP_XDISTANCE);
   int y=ObjectGetInteger(0, "ActP_ord_button5", OBJPROP_YDISTANCE)+ObjectGetInteger(0, "ActP_ord_button5", OBJPROP_YSIZE);
   //и цвета
   color col=ObjectGetInteger(0,"ActP_ord_button5",OBJPROP_COLOR);
   color bgcol=ObjectGetInteger(0,"ActP_ord_button5",OBJPROP_BGCOLOR);
   //и ширину окна
   int wnd_height=ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,wnd);
   int y_cnt=0;
   //переберём массивы
   for(int i=0;i<100;i++)
     {
      //если достигнут конец - завершим цикл
      if(tickets[i]==-1) break;
      //рассчитаем координаты элемента списка
      int y_pos=y+y_cnt*20;
      //если достигнуты пределы окна, начнём новый столбец
      if(y_pos+20>wnd_height) {x+=300; y_cnt=0;}
      y_pos=y+y_cnt*20;
      y_cnt++;
      string name="ActP_orders_list_"+IntegerToString(i)+" $"+IntegerToString(tickets[i]);//создадим элемент
      create_button(name,info[i],x,y_pos,300,20);
      //и установим его свойства
      ObjectSetInteger(0,name,OBJPROP_COLOR,col);
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,0);
      ObjectSetInteger(0,name,OBJPROP_STATE,0);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,8);
      ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bgcol);
     }
  }

Ну и наконец, функция DeleteLists() удаляет элементы списка:

//+------------------------------------------------------------------+
//| Функция удаления списков                                         |
//+------------------------------------------------------------------+
void  DeleteLists(string IDstr)
  {
   //переберём все объекты
   for(int i=ObjectsTotal(0);i>=0;i--)
     {
      string n=ObjectName(0,i);
      //удалим списки
      if(StringFind(n,IDstr)>=0 && StringFind(n,"main")<0) ObjectDelete(0,n);
     }
  }

Итак, теперь при клике на кнопке активации список создаётся. Заставим его работать, ведь при клике на каком-либо элементе списка должны выполняться некоторые действия. А именно: загрузка интерфейса для работы с ордером, и его заполнение информацией об ордере/сделке.

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик не по элементу списка выбора ордера
      if(StringFind(sparam, "ActP_orders_list_")<0)
      {
          //удалим его 
         DeleteLists("ActP_orders_list_");
          //установим кнопку активации в положение "отжата"
         ObjectSetInteger(0, "ActP_ord_button5", OBJPROP_STATE, 0);
          //перерисуем график
         ChartRedraw();
      }     
       //клик по элементу списка выбора ордера
      else
      {
          //установим новое имя для кнопки активации
         ObjectSetString(0, "ActP_ord_button5", OBJPROP_TEXT, ObjectGetString(0, sparam, OBJPROP_TEXT));
          //установим кнопку активации в положение "отжата"
         ObjectSetInteger(0, "ActP_ord_button5", OBJPROP_STATE, 0);
          //извлечём тикет из описания элемента списка
         int ticket=StringToInteger(StringSubstr(sparam, StringFind(sparam, "$")+1));
          //загрузим интерфейс
         SetScheme(ticket);
          //и удалим список
         DeleteLists("ActP_orders_list_");
          //перерисуем график
         ChartRedraw();
      }
   ...   
   }
...
}

Здесь пришлось пойти на хитрость, т.к. мы не знаем заранее размера списка и имён его объектов, то придётся выжимать из него информацию. путём разбора имени элемента списка. Функция SetScheme() установит нужный интерфейс - либо для работы со сделкой, либо для работы с отложенным ордером:

//+------------------------------------------------------------------+
//| Функция установки интерфейса в зависимости от типа позиции       |
//| (сделки или отложенный ордер)                                    |
//| t - тикет                                                        |
//+------------------------------------------------------------------+
void SetScheme(int t)
  {
   //если сделка
   if(t==0)
     {
      //проверим её наличие
      if(PositionSelect(Symbol()))
        {
         //удалим старый интерфейс
         DeleteScheme("ActP",true);
         //и установим новый
         ApplyScheme(6);
         //установим значения объектов
         SetPositionParams();
         //сделаем объекты недоступными для выделения
         Objects_Selectable("ActP",false);
        }
     }
   //если ордер  
   if(t>0)
     {
      //проверим его наличие
      if(OrderSelect(t))
        {
         //удалим старый интерфейс
         DeleteScheme("ActP",true);
         //и установим новый
         ApplyScheme(7);
         //установим значения объектов
         SetOrderParams(t);
         //сделаем объекты недоступными для выделения
         Objects_Selectable("ActP",false);
        }
     }
  }

Функции SetPositionParams() и SetOrderParams() выполняют установку нужных свойств загруженного интерфейса:

//+------------------------------------------------------------------+
//| Функция установки значений объектов при модификации сделки       |
//+------------------------------------------------------------------+
void SetPositionParams()
  {
   //если сделка существует
   if(PositionSelect(Symbol()))
     {
      //извлечём её параметры
      double pr=PositionGetDouble(POSITION_PRICE_OPEN);
      double lots=PositionGetDouble(POSITION_VOLUME);
      double sl=PositionGetDouble(POSITION_SL);
      double tp=PositionGetDouble(POSITION_TP);
      double mag=PositionGetInteger(POSITION_MAGIC);
      //и установим новые значения в объекты
      ObjectSetString(0,"ActP_Pr_edit4",OBJPROP_TEXT,str_del_zero(DoubleToString(pr)));
      ObjectSetString(0,"ActP_lots_edit4",OBJPROP_TEXT,str_del_zero(DoubleToString(lots)));
      ObjectSetString(0,"ActP_SL_edit4",OBJPROP_TEXT,str_del_zero(DoubleToString(sl)));
      ObjectSetString(0,"ActP_TP_edit4",OBJPROP_TEXT,str_del_zero(DoubleToString(tp)));
      if(mag!=0) ObjectSetString(0,"ActP_mag_edit4",OBJPROP_TEXT,IntegerToString(mag));
      //перерисуем график
      ChartRedraw();
     }
   //если сделки нет, выдадим сообщение  
   else MessageBox("Нет открытых сделок по инструменту "+Symbol());
  }
//+------------------------------------------------------------------+
//| Функция установки значений объектов при модификации ордера       |
//| ticket - тикет ордера                                            |
//+------------------------------------------------------------------+
void SetOrderParams(int ticket)
  {
   //если ордер существует
   if(OrderSelect(ticket) && OrderGetString(ORDER_SYMBOL)==Symbol())
     {
      //извлечём его параметры
      double pr=OrderGetDouble(ORDER_PRICE_OPEN);
      double lots=OrderGetDouble(ORDER_VOLUME_CURRENT);
      double sl=OrderGetDouble(ORDER_SL);
      double tp=OrderGetDouble(ORDER_TP);
      double mag=OrderGetInteger(ORDER_MAGIC);
      double lim=OrderGetDouble(ORDER_PRICE_STOPLIMIT);
      datetime expir=OrderGetInteger(ORDER_TIME_EXPIRATION);
      ENUM_ORDER_TYPE type=OrderGetInteger(ORDER_TYPE);
      ENUM_ORDER_TYPE_TIME expir_type=OrderGetInteger(ORDER_TYPE_TIME);
      
      //если ордер stoplimit подкорректируем интерфейс
      if(type==ORDER_TYPE_BUY_STOP_LIMIT || type==ORDER_TYPE_SELL_STOP_LIMIT)
        {
         //установим в поле для ввода цены ордера новую цену
         ObjectSetString(0,"ActP_limpr_edit3",OBJPROP_TEXT,DoubleToString(lim,_Digits));
         ObjectSetInteger(0,"ActP_limpr_edit3",OBJPROP_BGCOLOR,White);
         //установим поле для ввода цены ордера доступным для чтения
         ObjectSetInteger(0,"ActP_limpr_edit3",OBJPROP_READONLY,false);
        }
      //если ордер не stoplimit подкорректируем интерфейс      
      else
        {
         ObjectSetString(0,"ActP_limpr_edit3",OBJPROP_TEXT,"");
         ObjectSetInteger(0,"ActP_limpr_edit3",OBJPROP_BGCOLOR,LavenderBlush);
         ObjectSetInteger(0,"ActP_limpr_edit3",OBJPROP_READONLY,true);
        }

      //проверим тип истечения ордера   
      //и установим переключатели в нужное положение
      switch(expir_type)
        {
         case ORDER_TIME_GTC:
           {
            ObjectSetInteger(0,"ActP_exp1_radio3",OBJPROP_STATE,1);
            ObjectSetInteger(0,"ActP_exp2_radio3",OBJPROP_STATE,0);
            ObjectSetInteger(0,"ActP_exp3_radio3",OBJPROP_STATE,0);
            break;
           }
         case ORDER_TIME_DAY:
           {
            ObjectSetInteger(0,"ActP_exp1_radio3",OBJPROP_STATE,0);
            ObjectSetInteger(0,"ActP_exp2_radio3",OBJPROP_STATE,1);
            ObjectSetInteger(0,"ActP_exp3_radio3",OBJPROP_STATE,0);
            break;
           }
         case ORDER_TIME_SPECIFIED:
           {
            ObjectSetInteger(0,"ActP_exp1_radio3",OBJPROP_STATE,0);
            ObjectSetInteger(0,"ActP_exp2_radio3",OBJPROP_STATE,0);
            ObjectSetInteger(0,"ActP_exp3_radio3",OBJPROP_STATE,1);
            //здесь дополнительно установим новое значение в поле ввода
            ObjectSetString(0,"ActP_exp_edit3",OBJPROP_TEXT,TimeToString(expir));
            break;
           }
        }
      //установим новые значения в объекты
      ObjectSetString(0,"ActP_Pr_edit3",OBJPROP_TEXT,str_del_zero(DoubleToString(pr)));
      ObjectSetString(0,"ActP_lots_edit3",OBJPROP_TEXT,str_del_zero(DoubleToString(lots)));
      ObjectSetString(0,"ActP_SL_edit3",OBJPROP_TEXT,str_del_zero(DoubleToString(sl)));
      ObjectSetString(0,"ActP_TP_edit3",OBJPROP_TEXT,str_del_zero(DoubleToString(tp)));
      ObjectSetString(0,"ActP_ticket_edit3",OBJPROP_TEXT,IntegerToString(ticket));
      if(mag!=0) ObjectSetString(0,"ActP_mag_edit3",OBJPROP_TEXT,IntegerToString(mag));
      ChartRedraw();
     }
   //если ордера нет, выдадим сообщение  
   else MessageBox("Нет ордера с тикетом "+IntegerToString(ticket)+" по инструменту "+Symbol());
  }

Ну и финальный штрих - списки должны удаляться при щелчке по графику, для этого используем событие CHARTEVENT_CLICK:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графику
   if(id==CHARTEVENT_CLICK)
   {
       //удалим все выпадающие списки
      DeleteLists("ActP_orders_list_");
      DeleteLists("ActP_color_list_"); 
       //и установим кнопки активации списков в отжатое состояние
      ObjectSetInteger(0, "ActP_ord_button5", OBJPROP_STATE, 0);
      ObjectSetInteger(0, "ActP_col1_button6", OBJPROP_STATE, 0);
      ObjectSetInteger(0, "ActP_col2_button6", OBJPROP_STATE, 0);
      ObjectSetInteger(0, "ActP_col3_button6", OBJPROP_STATE, 0);
      ChartRedraw(); 
      return;
   }
...
}

В итоге имеем симпатичный выпадающий список:

Рис. 14 Пример выпадающего списка панели "Мод/закр"

Осталось создать список выбора цвета на вкладке "Настройки". 

Рассмотрим обработчики кнопок активации:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту   
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке активации выпадающего списка выбора цвета кнопок
      if(sparam=="ActP_col1_button6")
      {
          //проверим состояние
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
          //список активирован
         if(selected)//список активирован
         {
             //создадим список
            create_color_list(100, "ActP_col1_button6", 1);
             //установим положение остальных кнопок в положение "отжаты"
            ObjectSetInteger(0, "ActP_col2_button6", OBJPROP_STATE, 0);
            ObjectSetInteger(0, "ActP_col3_button6", OBJPROP_STATE, 0);
             //удалим другие списки
            DeleteLists("ActP_color_list_2");
            DeleteLists("ActP_color_list_3");                 
         }
          //список деактивирован
         else
         {
             //удалим его
            DeleteLists("ActP_color_list_");
         }
          //перерисуем график
         ChartRedraw();
          //завершим выполнение функции
         return;
      }
   ...   
   }
...
}

Здесь действуем по аналогии со списком выбора ордера.

Отличается функция создания списка:

//+------------------------------------------------------------------+
//| Функция создания списка цветов                                   |
//| y_max - макс. ширина списка                                      |
//| ID - идентификатор списка                                        |
//| num - номер интерфейса                                           |
//+------------------------------------------------------------------+
void create_color_list(int y_max,string ID,int num)
  {
  //получим координаты кнопки активации списка
   int x=ObjectGetInteger(0,ID,OBJPROP_XDISTANCE);
   int y=ObjectGetInteger(0, ID, OBJPROP_YDISTANCE)+ObjectGetInteger(0, ID, OBJPROP_YSIZE);
   //и цвета
   color col=ObjectGetInteger(0,ID,OBJPROP_COLOR);
   //и ширину окна
   int wnd_height=ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,wnd);
   y_max+=y;
   int y_cnt=0;
   //переберём массив с цветами
   for(int i=0;i<132;i++)
     {
      color bgcol=colors[i];
      //рассчитаем координаты элемента списка
      int y_pos=y+y_cnt*20;
      //если достигнуты пределы окна, начнём новый столбец
      if(y_pos+20>wnd_height || y_pos+20>y_max) {x+=20; y_cnt=0;}
      y_pos=y+y_cnt*20;
      y_cnt++;
      //создадим элемент
      string name="ActP_color_list_"+IntegerToString(num)+ID+IntegerToString(i);
      create_button(name,"",x,y_pos,20,20);
      //и установим его свойства
      ObjectSetInteger(0,name,OBJPROP_COLOR,col);
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,0);
      ObjectSetInteger(0,name,OBJPROP_STATE,0);
      ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bgcol);
     }
  }

Далее обработаем клик по элементу списка: 

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик не по элементу списка цвета кнопок
      if(StringFind(sparam, "ActP_color_list_1")<0)
      {
          //удалим список
         DeleteLists("ActP_color_list_1");
          //установим кнопку активации в положение "отжата"
         ObjectSetInteger(0, "ActP_col1_button6", OBJPROP_STATE, 0);
          //перерисуем график
         ChartRedraw();
      }     
       //клик по элементу списка цвета кнопок
      else
      {
          //извлечём цвет из элемента списка
         color col=ObjectGetInteger(0, sparam, OBJPROP_BGCOLOR);
          //и установим его для всех кнопок
         SetButtonsColor(col);
          //установим кнопку активации в положение "отжата"
         ObjectSetInteger(0, "ActP_col1_button6", OBJPROP_STATE, 0);
          //и удалим список
         DeleteLists("ActP_color_list_1");
          //перерисуем график
         ChartRedraw();
      }
   ...   
   }
...
}

Функция SetButtonsColor() устанавливает цвет кнопок:

//+------------------------------------------------------------------+
//| Функция установки цвета кнопок                                   |
//| col - цвет для кнопок                                            |
//+------------------------------------------------------------------+
void SetButtonsColor(color col)
  {
   //переберём все объекты
   for(int i=ObjectsTotal(0);i>=0;i--)
     {
      string n=ObjectName(0,i);
      //если объект принадлежит панели и он кнопка
      //установим цвет
      if(StringFind(n,"ActP")>=0 && ObjectGetInteger(0,n,OBJPROP_TYPE)==OBJ_BUTTON) 
         ObjectSetInteger(0,n,OBJPROP_BGCOLOR,col); 
     }
   //установим глобальную переменную
   GlobalVariableSet("ActP_buttons_color",col);
  }

Оценим результат:

Рис. 15 Установка цвета кнопок

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

Рис. 16 Измененные цвета панели, кнопок и текста

Со списками покончено. Примемся за поля ввода.

5.5. Обработка событий поля ввода

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

Рассмотрим это на примере установки линии стопа:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - окончание редактирования поля ввода
   if(id==CHARTEVENT_OBJECT_ENDEDIT)//событие - окончание редактирования поля ввода
   {
   ...
       //если редактируемое поле - поле для ввода стопа для открытия с рынка
      if(sparam=="ActP_SL_edit1")
      {
          //и при этом разрешены вспомогательные линии
         if(ObjectGetInteger(0,"ActP_DealLines_check1",OBJPROP_STATE)==1)
         {
              //возьмём текст из поля ввода
            double sl_val=StringToDouble(ObjectGetString(0, "ActP_SL_edit1", OBJPROP_TEXT));
              //передвинем линии на новое положение
            ObjectSetDouble(0, "ActP_SL_line1", OBJPROP_PRICE, sl_val);
         }
          //перерисуем график
         ChartRedraw();
          //т.к. мы можем одновременно кликнуть только по одному объекту - дальше можно не выполнять
         return;
      }
   ...   
   }
...
}

Аналогично обрабатываются и другие поля ввода.

5.6 Обработка событий таймера

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

Рассмотрим пример установки слежения за линиями стопа и тейка при активной вкладке "С рынка":

void OnTimer()//обработчик таймера
{
   //выбрана вкладка №1
   if(ObjectGetInteger(0, "ActP_main_1", OBJPROP_STATE)==1)
   {  
      //если разрешены вспомогательные линии
      if(ObjectGetInteger(0,"ActP_DealLines_check1",OBJPROP_STATE)==1)
      {
          //то установим в поля ввода новые значения
         double sl_pr=NormalizeDouble(ObjectGetDouble(0, "ActP_SL_line1", OBJPROP_PRICE), _Digits);
          //стопа
         ObjectSetString(0, "ActP_SL_edit1", OBJPROP_TEXT, DoubleToString(sl_pr, _Digits));
         //и тейка
         double tp_pr=NormalizeDouble(ObjectGetDouble(0, "ActP_TP_line1", OBJPROP_PRICE), _Digits);
         ObjectSetString(0, "ActP_TP_edit1", OBJPROP_TEXT, DoubleToString(tp_pr, _Digits));
      }   
   }
   ...
   //перерисуем график
   ChartRedraw();
}
//+------------------------------------------------------------------+

Аналогично выполняется слежение и за другими линиями.


6. Совершение торговых операций

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

6.1. Открытие сделки

У нас есть кнопки "Buy" и "Sell" на вкладке "С рынка". При клике по ним при правильно заполненных полях должна совершиться сделка.

Посмотрим на обработчики этих кнопок:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке покупки
      if(sparam=="ActP_buy_button1")
      {
          //проверим состояние кнопки
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
          //если она в позиции "нажата"
         if(selected)
         {
             //попытаемся совершить сделку
            deal(ORDER_TYPE_BUY);
             //и установим кнопку в положение "отжата"
            ObjectSetInteger(0, sparam, OBJPROP_STATE, 0);
         }

          //перерисуем график
         ChartRedraw();
          //и закончим выполнение функции
         return;
      }
      //******************************************
       //аналогично для кнопки продажи
      if(sparam=="ActP_sell_button1")
      {
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
         if(selected)
         {
            deal(ORDER_TYPE_SELL);
            ObjectSetInteger(0, sparam, OBJPROP_STATE, 0);
         }
         ChartRedraw();
         return;
      }     
   ...   
   }
...
}

Как видим, выполняется функция deal():

//+------------------------------------------------------------------+
//| Функция открытия сделки                                          |
//+------------------------------------------------------------------+
int deal(ENUM_ORDER_TYPE typ)
  {
   //получим необходимые данные с объектов  
   double SL=StringToDouble(ObjectGetString(0,"ActP_SL_edit1",OBJPROP_TEXT));
   double TP=StringToDouble(ObjectGetString(0, "ActP_TP_edit1", OBJPROP_TEXT));
   double lots=StringToDouble(ObjectGetString(0,"ActP_Lots_edit1",OBJPROP_TEXT));
   int mag=StringToInteger(ObjectGetString(0, "ActP_Magic_edit1", OBJPROP_TEXT));
   int dev=StringToInteger(ObjectGetString(0, "ActP_Dev_edit1", OBJPROP_TEXT));
   string comm=ObjectGetString(0,"ActP_Comm_edit1",OBJPROP_TEXT);
   ENUM_ORDER_TYPE_FILLING filling=ORDER_FILLING_FOK;
   if(ObjectGetInteger(0,"ActP_Exe2_radio1",OBJPROP_STATE)==1) filling=ORDER_FILLING_IOC;

   //оформим запрос
   MqlTradeRequest req;
   MqlTradeResult res;
   req.action=TRADE_ACTION_DEAL;
   req.symbol=Symbol();
   req.volume=lots;
   req.price=Ask;
   req.sl=NormalizeDouble(SL, Digits());
   req.tp=NormalizeDouble(TP, Digits());
   req.deviation=dev;
   req.type=typ;
   req.type_filling=filling;
   req.magic=mag;
   req.comment=comm;
   //отошлём запрос
   OrderSend(req,res);
   //покажем сообщение с результатом
   MessageBox(RetcodeDescription(res.retcode),"Message");
   //вернём код возврата
   return(res.retcode);
  }

Ничего сверхъестественного, сначала считываем необходимые данные с объектов и на их основании создаём торговый запрос.

Проверим работу:

Рис. 17 Торговые операции - результат совершения сделки на покупку

Как видите, сделка на покупку совершена успешно.

6.2. Установка отложенного ордера

Кнопки "Buy" и "Sell" на вкладке "Отложенный" отвечают за установку отложенных ордеров.

Рассмотрим обработчики:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту  
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке установки отложенного ордера на покупку
      if(sparam=="ActP_buy_button2")
      {
          //проверим состояние кнопки
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);//проверим состояние кнопки
          //если нажата
         if(selected)
         {
            ENUM_ORDER_TYPE typ; 
              //возьмём цену, на которую хотим установить ордер
            double pr=NormalizeDouble(StringToDouble(ObjectGetString(0, "ActP_Pr_edit2", OBJPROP_TEXT)), Digits());
              //если не предполагается установка ордера stoplimit
            if(ObjectGetInteger(0, "ActP_limit_check2", OBJPROP_STATE)==0)
            {
                 //если цена установки ниже текущей установим limit ордер
               if(Ask>pr) typ=ORDER_TYPE_BUY_LIMIT; 
                 //иначе - stop ордер
               else typ=ORDER_TYPE_BUY_STOP;
            }
              //если предполагается установка ордера stoplimit
            else
            {
                 //установим нужный тип операции
               typ=ORDER_TYPE_BUY_STOP_LIMIT;
            }   
              //попробуем установить ордер
            order(typ);
              //установим кнопку в положение "отжата"
            ObjectSetInteger(0, sparam, OBJPROP_STATE, 0);
         }
          //перерисуем график
         ChartRedraw();
          //и закончим выполнение функции
         return;
      }     
      //******************************************  

       //аналогично при установке отложенного ордера на продажу
      if(sparam=="ActP_sell_button2")
      {
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
         if(selected)
         {
            ENUM_ORDER_TYPE typ;
            double pr=NormalizeDouble(StringToDouble(ObjectGetString(0, "ActP_Pr_edit2", OBJPROP_TEXT)), Digits());
            if(ObjectGetInteger(0, "ActP_limit_check2", OBJPROP_STATE)==0)
            {
               if(Bid<pr) typ=ORDER_TYPE_SELL_LIMIT;
               else typ=ORDER_TYPE_SELL_STOP;
            }
            else
            {
               typ=ORDER_TYPE_SELL_STOP_LIMIT;
            }   
            order(typ);
            ObjectSetInteger(0, sparam, OBJPROP_STATE, 0);
         }
         ChartRedraw();
         return;
      }        
   ...   
   }
...
}

Здесь определяется тип будущего ордера на основании положения текущей цены относительно цены установки, после чего функция order() устанавливает ордер:

//+------------------------------------------------------------------+
//| Функция установки ордера                                         |
//+------------------------------------------------------------------+
int order(ENUM_ORDER_TYPE typ)
  {
   //получим необходимые данные с объектов
   double pr=StringToDouble(ObjectGetString(0,"ActP_Pr_edit2",OBJPROP_TEXT));
   double stoplim=StringToDouble(ObjectGetString(0,"ActP_limpr_edit2",OBJPROP_TEXT));
   double SL=StringToDouble(ObjectGetString(0, "ActP_SL_edit2", OBJPROP_TEXT));
   double TP=StringToDouble(ObjectGetString(0, "ActP_TP_edit2", OBJPROP_TEXT));
   double lots=StringToDouble(ObjectGetString(0,"ActP_Lots_edit2",OBJPROP_TEXT));
   datetime expir=StringToTime(ObjectGetString(0,"ActP_exp_edit2",OBJPROP_TEXT));
   int mag=StringToInteger(ObjectGetString(0,"ActP_Magic_edit2",OBJPROP_TEXT));
   string comm=ObjectGetString(0,"ActP_Comm_edit2",OBJPROP_TEXT);
   ENUM_ORDER_TYPE_FILLING filling=ORDER_FILLING_FOK;
   if(ObjectGetInteger(0, "ActP_Exe2_radio2", OBJPROP_STATE)==1) filling=ORDER_FILLING_IOC;
   if(ObjectGetInteger(0, "ActP_Exe3_radio2", OBJPROP_STATE)==1) filling=ORDER_FILLING_RETURN;
   ENUM_ORDER_TYPE_TIME expir_type=ORDER_TIME_GTC;
   if(ObjectGetInteger(0, "ActP_exp2_radio2", OBJPROP_STATE)==1) expir_type=ORDER_TIME_DAY;
   if(ObjectGetInteger(0, "ActP_exp3_radio2", OBJPROP_STATE)==1) expir_type=ORDER_TIME_SPECIFIED;

   //оформим запрос
   MqlTradeRequest req;
   MqlTradeResult res; 
   req.action=TRADE_ACTION_PENDING;
   req.symbol=Symbol();
   req.volume=lots;
   req.price=NormalizeDouble(pr,Digits());
   req.stoplimit=NormalizeDouble(stoplim,Digits());
   req.sl=NormalizeDouble(SL, Digits());
   req.tp=NormalizeDouble(TP, Digits());
   req.type=typ;
   req.type_filling=filling;
   req.type_time=expir_type;
   req.expiration=expir;
   req.comment=comm;
   req.magic=mag;
   //отошлём запрос
   OrderSend(req,res);
   //покажем сообщение с результатом
   MessageBox(RetcodeDescription(res.retcode),"Message");
   //вернём код возврата
   return(res.retcode);
  }

Проверим работу:

Рис. 18 Торговые операции - результат выставления ордера на покупку

Ордер buy stoplimit установлен успешно.

6.3. Модификация сделки

Кнопка "Изменить" на вкладке "Мод/закр" при выбранной сделке из списка отвечает за модификацию этой сделки:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке модификации открытой сделки
      if(sparam=="ActP_mod_button4")
      {
          //проверим состояние кнопки
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
          //если нажата
         if(selected)//если нажата
         {
             //модифицируем
            modify_pos();
             //удалим элементы интерфейса 
            DeleteScheme("ActP" ,true);
             //и установим заново (иными словами обновим интерфейс)
            SetScheme(0);
             //установим кнопку в положение "отжата"
            ObjectSetInteger(0, sparam, OBJPROP_STATE, 0);
         }
          //перерисуем график
         ChartRedraw();
          //и закончим выполнение функции
         return;
      }     
   ...   
   }
...
}

Функция modify_pos() непосредственно отвечает за модификацию:

//+------------------------------------------------------------------+
//| Функция модификации сделки                                       |
//+------------------------------------------------------------------+
int modify_pos()
  {
   if(!PositionSelect(Symbol())) MessageBox("Нет открытых сделок по символу "+Symbol(),"Message");
   //получим необходимые данные с объектов
   double SL=StringToDouble(ObjectGetString(0,"ActP_SL_edit4",OBJPROP_TEXT));  
   double TP=StringToDouble(ObjectGetString(0, "ActP_TP_edit4", OBJPROP_TEXT));
   int dev=StringToInteger(ObjectGetString(0,"ActP_dev_edit4",OBJPROP_TEXT));

   //оформим запрос
   MqlTradeRequest req;
   MqlTradeResult res;  
   req.action=TRADE_ACTION_SLTP;
   req.symbol=Symbol();
   req.sl=NormalizeDouble(SL, _Digits);
   req.tp=NormalizeDouble(TP, _Digits);
   req.deviation=dev;
   //отошлём запрос
   OrderSend(req,res);
   //покажем сообщение с результатом
   MessageBox(RetcodeDescription(res.retcode),"Message");
   //вернём код возврата
   return(res.retcode);

  }

Результат:


Рис. 19 Торговые операции - результат модификации свойств сделки (установка TP и SL)

Стоп и тейк уровни изменены успешно.

6.4. Закрытие сделки

Кнопка "Закрыть" на вкладке "Мод/закр" при выбранной сделку из списка отвечает за закрытие(возможно частичное) этой сделки:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке закрытия открытой сделки
      if(sparam=="ActP_del_button4")
      {
          //проверим состояние кнопки
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
          //если нажата
         if(selected)
         {
              //пробуем закрыть
            int retcode=close_pos();
              //если удачно
            if(retcode==10009)
            {
                 //удалим элементы интерфейса
               DeleteScheme("ActP" ,true);
                 //установим новый текст кнопки активации списка
               ObjectSetString(0, "ActP_ord_button5", OBJPROP_TEXT, "Выберите ордер -->");
            }
             //установим кнопку в положение "отжата"
            ObjectSetInteger(0, sparam, OBJPROP_STATE, 0);
         }
          //перерисуем график
         ChartRedraw();
          //и закончим выполнение функции
         return;
      }     
   ...   
   }
...
}

Функция close_pos() отвечает за закрытие:

//+------------------------------------------------------------------+
//| Функция закрытия сделки                                          |
//+------------------------------------------------------------------+
int close_pos()
  {
   if(!PositionSelect(Symbol())) MessageBox("Нет открытых сделок по символу "+Symbol(),"Message");
   //получим необходимые данные с объектов
   double lots=StringToDouble(ObjectGetString(0,"ActP_lots_edit4",OBJPROP_TEXT));
   if(lots>PositionGetDouble(POSITION_VOLUME)) lots=PositionGetDouble(POSITION_VOLUME);
   int dev=StringToInteger(ObjectGetString(0, "ActP_dev_edit4", OBJPROP_TEXT));
   int mag=StringToInteger(ObjectGetString(0, "ActP_mag_edit4", OBJPROP_TEXT));

   //оформим запрос
   MqlTradeRequest req;
   MqlTradeResult res;

   //в завсисмости от типа выберем тип контрсделки
   if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
     {
      req.price=Bid;
      req.type=ORDER_TYPE_SELL;
     }
   else
     {
      req.price=Ask;
      req.type=ORDER_TYPE_BUY;
     }

   req.action=TRADE_ACTION_DEAL;
   req.symbol=Symbol();
   req.volume=lots;
   req.sl=0;
   req.tp=0;
   req.deviation=dev;
   req.type_filling=ORDER_FILLING_FOK;
   req.magic=mag;
   //отошлём запрос
   OrderSend(req,res);
   //покажем сообщение с результатом
   MessageBox(RetcodeDescription(res.retcode),"Message");
   //вернём код возврата 
   return(res.retcode);
  }

Результат - закрыто 1.5 лота из трёх по выбранной сделке:

Рис. 20 Торговые операции - частичное закрытие позиции

6.5. Модификация отложенного ордера

Кнопка "Изменить" на вкладке "Мод/закр" при выбранном ордере из списка отвечает за модификацию этого ордера:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке модификации ордера
      if(sparam=="ActP_mod_button3")
      {
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
         if(selected)
         {     
             //извлечём из подписи кнопки списка тикет ордера
            string button_name=ObjectGetString(0, "ActP_ord_button5", OBJPROP_TEXT);
            long ticket=StringToInteger(StringSubstr(button_name, 1, StringFind(button_name, " ")-1));
              //модифицируем ордер
            modify_order(ticket);
              //обновим интерфейс
            DeleteScheme("ActP" ,true);
            SetScheme(ticket);
              //установим кнопку в положение "отжата"
            ObjectSetInteger(0, sparam, OBJPROP_STATE, 0);
         }
          //перерисуем график
         ChartRedraw();
          //и закончим выполнение функции
         return;
      }     
   ...   
   }
...
}

Функция modify_order() отвечает за модификацию:

//+------------------------------------------------------------------+
//| Функция модификации ордера                                       |
//| ticket - тикет ордера                                            |
//+------------------------------------------------------------------+
int modify_order(int ticket)
  {
   //получим необходимые данные с объектов
   double pr=StringToDouble(ObjectGetString(0,"ActP_Pr_edit3",OBJPROP_TEXT)); 
   double stoplim=StringToDouble(ObjectGetString(0,"ActP_limpr_edit3",OBJPROP_TEXT));
   double SL=StringToDouble(ObjectGetString(0, "ActP_SL_edit3", OBJPROP_TEXT));
   double TP=StringToDouble(ObjectGetString(0, "ActP_TP_edit3", OBJPROP_TEXT));
   double lots=StringToDouble(ObjectGetString(0,"ActP_Lots_edit3",OBJPROP_TEXT));
   datetime expir=StringToTime(ObjectGetString(0,"ActP_exp_edit3",OBJPROP_TEXT));
   ENUM_ORDER_TYPE_TIME expir_type=ORDER_TIME_GTC;
   if(ObjectGetInteger(0, "ActP_exp2_radio3", OBJPROP_STATE)==1) expir_type=ORDER_TIME_DAY;
   if(ObjectGetInteger(0, "ActP_exp3_radio3", OBJPROP_STATE)==1) expir_type=ORDER_TIME_SPECIFIED;

   //оформим запрос на модификацию
   MqlTradeRequest req;
   MqlTradeResult res;
   req.action=TRADE_ACTION_MODIFY;
   req.order=ticket;
   req.volume=lots;
   req.price=NormalizeDouble(pr,Digits());
   req.stoplimit=NormalizeDouble(stoplim,Digits());
   req.sl=NormalizeDouble(SL, Digits());
   req.tp=NormalizeDouble(TP, Digits());
   req.type_time=expir_type;
   req.expiration=expir;
   //отошлём запрос
   OrderSend(req,res);
   //покажем сообщение с результатом
   MessageBox(RetcodeDescription(res.retcode),"Message");
   //вернём код возврата
   return(res.retcode);
  }

Посмотрим результат - ордер модифицирован успешно:

Рис. 21 Торговые операции - модификация отложенного ордера

6.6. Удаление отложенного ордера

Кнопка "Удалить" на вкладке "Мод/закр" при выбранном ордере из списка отвечает за удаление этого ордера: 

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
...
   //событие - клик по графическому объекту  
   if(id==CHARTEVENT_OBJECT_CLICK)
   {
   ...
       //клик по кнопке удаления ордера
      if(sparam=="ActP_del_button3")
      {
          //проверим состояние кнопки
         bool selected=ObjectGetInteger(0,sparam,OBJPROP_STATE);
          //если нажата
         if(selected)
         {
              //извлечём из подписи кнопки списка тикет ордера
            string button_name=ObjectGetString(0, "ActP_ord_button5", OBJPROP_TEXT);
            long ticket=StringToInteger(StringSubstr(button_name, 1, StringFind(button_name, " ")-1));
              //пробуем удалить ордер
            int retcode=del_order(ticket);
              //если удачно
            if(retcode==10009)
            {
                 //удалим элементы интерфейса
               DeleteScheme("ActP" ,true);
                 //установим новый текст кнопки активации списка
               ObjectSetString(0, "ActP_ord_button5", OBJPROP_TEXT, "Выберите ордер -->");
            }
             //установим кнопку в положение "отжата"
            ObjectSetInteger(0, sparam, OBJPROP_STATE, 0);
         }
          //перерисуем график
         ChartRedraw();
          //и закончим выполнение функции
         return;
      }     
   ...   
   }
...
}

Функция del_order() отвечает за удаление ордера:

//+------------------------------------------------------------------+
//| Функция удаления ордера                                          |
//| ticket - тикет ордера                                            |
//+------------------------------------------------------------------+
int del_order(int ticket)
  {
   //оформим запрос на удаление
   MqlTradeRequest req;
   MqlTradeResult res;
   req.action=TRADE_ACTION_REMOVE;
   req.order=ticket;
   //отошлём запрос
   OrderSend(req,res);
   //покажем сообщение с результатом
   MessageBox(RetcodeDescription(res.retcode),"Message");
   //вернём код возврата
   return(res.retcode);
  }

Посмотрим результат - ордер удалён:

Рис. 22 Торговые операции - удаление отложенного ордера


Заключение

Итак, все функции панели протестированы и работают успешно.

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

Для начала работы с панелью необходимо распаковать архив в папку с терминалом, потом набросить индикатор AP на график, и только после этого запустить эксперт Active Panel.