Мультивалютный мониторинг торговых сигналов (Часть 4): Улучшаем функциональность и систему поиска сигналов

Alexander Fedosov | 18 марта, 2020

Содержание

Введение

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

Собственный индикатор для настройки торгового сигнала

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

Прежде всего добавим возможность выбора использования собственного индикатора в окне добавления и редактирования сигнала. Напомню, что реализация интерфейса данного окна находится в файле SetWindow.mqh нашего проекта. Заходим в него и там находим метод CreateIndicatorType(). Именно его и предстоит изменить.

//+------------------------------------------------------------------+
//| Создает выпадающее меню с выбором типа индикатора                |
//+------------------------------------------------------------------+
bool CProgram::CreateIndicatorType(const int x_gap,const int y_gap)
{
//--- Передать объект панели
   m_indicator_type.MainPointer(m_set_window);
//---
#define SIZE 10
//--- Массив значений пунктов в списке
   string pattern_names[SIZE]=
   {
      "ATR","CCI","DeMarker","Force Ind","WPR","RSI","Momentum","ADX","ADX Wilder","Custom"
   };
//--- Установим свойства перед созданием
   m_indicator_type.XSize(200);
   m_indicator_type.YSize(26);
   m_indicator_type.LabelYGap(4);
   m_indicator_type.ItemsTotal(SIZE);
   m_indicator_type.Font(m_base_font);
   m_indicator_type.FontSize(m_base_font_size);
   m_indicator_type.BackColor(m_background);
   m_indicator_type.GetButtonPointer().Font(m_base_font);
   m_indicator_type.GetButtonPointer().FontSize(m_base_font_size);
   m_indicator_type.GetButtonPointer().BackColor(clrWhite);
   m_indicator_type.GetButtonPointer().XGap(100);
   m_indicator_type.GetButtonPointer().XSize(100);
   m_indicator_type.GetListViewPointer().Font(m_base_font);
   m_indicator_type.GetListViewPointer().FontSize(m_base_font_size);
   m_indicator_type.GetListViewPointer().ItemYSize(25);
   m_indicator_type.GetListViewPointer().YSize(200);
//--- Сохраним значения пунктов в список комбо-бокса
   for(int i=0; i<SIZE; i++)
      m_indicator_type.SetValue(i,pattern_names[i]);
//--- Получим указатель списка
   CListView *lv=m_indicator_type.GetListViewPointer();
//--- Установим свойства списка
   lv.LightsHover(true);
   m_indicator_type.SelectItem(1);
//--- Создадим элемент управления
   if(!m_indicator_type.CreateComboBox("Indicator Type",x_gap,y_gap))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(1,m_indicator_type);
   return(true);
}

Теперь рассмотрим, что изменилось в сравнение с прежним этапом. Для начала была добавлена макроподстановка SIZE, которая будет означать количество элементов в выпадающим списке выбора. Она необходима, чтобы была возможность изменять это число в одном месте — без необходимости делать эти замены во всех местах кода. Далее, соответственно новому размеру добавим в конце новый элемент списка Custom. Визуально изменение выглядит как показано на рис.1 ниже.

Рис.1 Добавление пункта выбора собственного индикатора.  

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

int  iCustom(
   string           symbol,     // имя символа
   ENUM_TIMEFRAMES  period,     // период
   string           name        // папка/имя_пользовательского индикатора
   ...                          // список входных параметров индикатора
   );

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

   CTextEdit         m_custom_path;
   CTextEdit         m_custom_param;

   bool              CreateCustomEdit(CTextEdit &text_edit,const int x_gap,const int y_gap,const string default_text);

Так как метод будет применяться в окне создания/редактирования торгового сигнала, то в файле SetWindow.mqh его и реализуем:

//+------------------------------------------------------------------+
//| Поле ввода для собственного индикатора                           |
//+------------------------------------------------------------------+
bool CProgram::CreateCustomEdit(CTextEdit &text_edit,const int x_gap,const int y_gap,const string default_text)
{
//--- Сохраним указатель на главный элемент
   text_edit.MainPointer(m_set_window);
//--- Свойства
   text_edit.XSize(100);
   text_edit.YSize(24);
   text_edit.Font(m_base_font);
   text_edit.FontSize(m_base_font_size);
   text_edit.GetTextBoxPointer().AutoSelectionMode(true);
   text_edit.GetTextBoxPointer().XGap(1);
   text_edit.GetTextBoxPointer().XSize(325);
   text_edit.GetTextBoxPointer().DefaultTextColor(clrSilver);
   text_edit.GetTextBoxPointer().DefaultText(default_text);
   text_edit.GetTextBoxPointer().BorderColor(clrBlack);
//--- Создадим элемент управления
   if(!text_edit.CreateTextEdit("",x_gap,y_gap))
      return(false);
   text_edit.IsLocked(true);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(1,text_edit);
   return(true);
}

Посредством метода CreateCustomEdit() создадим два поля ввода для наших целей. Для этого в том же файле в теле метода CreateSetWindow() находим раздел Настройки выбранного индикатора и дополняем его таким кодом:

   if(!CreateCustomEdit(m_custom_path,240,22+10+2*(25+10),"Enter the indicator path"))
      return(false);
   if(!CreateCustomEdit(m_custom_param,240,22+10+3*(25+10),"Enter indicator parameters separated by commas"))
      return(false);

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

Рис.2 Добавление полей ввода для настроек собственного индикатора.

Однако, на данном этапе разработки они будут неактивны. Это связано с тем, что их доступность должна строго зависеть от выбора Типа индикатора(Indicator type), то есть доступ к ним будет лишь при выборе в выпадающем списке строки Custom. Поэтому для реализации этой задачи обратимся к методу RebuildParameters() и модернизируем его. Но для начала перейдем в секцию Событие по выбору пункта выпадающего списка в методе OnEvent() и добавим проверку на событие нужного нам списка с выбором типа индикатора.

//--- Выбор пункта в выпадающем списке комбобокса
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
   {
      //--- Тип индикатора
      if(lparam==m_indicator_type.Id())
         RebuildParameters(m_indicator_type.GetListViewPointer().SelectedItemIndex());
   }

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::RebuildParameters(int index)
{
   switch(index)
   {
   case  0:
      m_period_edit.LabelText("ATR Period");
      m_applied_price.Hide();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  1:
      m_period_edit.LabelText("CCI Period");
      m_applied_price.Show();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  2:
      m_period_edit.LabelText("DeMarker Period");
      m_applied_price.Hide();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  3:
      m_period_edit.LabelText("Force Index Period");
      m_applied_price.Hide();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  4:
      m_period_edit.LabelText("WPR Period");
      m_applied_price.Hide();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  5:
      m_period_edit.LabelText("RSI Period");
      m_applied_price.Show();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  6:
      m_period_edit.LabelText("Momentum Period");
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  7:
      m_period_edit.LabelText("ADX Period");
      m_applied_price.Hide();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  8:
      m_period_edit.LabelText("ADXW Period");
      m_applied_price.Hide();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   case  9:
      m_period_edit.LabelText("Buffer Number");
      m_applied_price.Hide();
      m_custom_param.IsLocked(false);
      m_custom_path.IsLocked(false);
      break;
   default:
      m_period_edit.LabelText("Ind Period");
      m_applied_price.Hide();
      m_custom_param.IsLocked(true);
      m_custom_path.IsLocked(true);
      break;
   }
   m_period_edit.Update(true);
}

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

Рис.3 Добавление полей ввода для собственного индикатора.

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

      //--- Нажатие на кнопку Добавить сигнал
      if(lparam==m_add_signal.Id())
      {
         if(m_total_signals>4)
         {
            MessageBox("Maximum number of signals is 5","Signal Monitor");
            return;
         }
         m_set_window.OpenWindow();
         RebuildParameters(1);
         m_number_signal=-1;
         RebuildTimeframes();
         m_new_signal.LabelText("Add");
         m_new_signal.Update(true);
         m_indicator_type.SelectItem(1);
         m_indicator_type.GetButtonPointer().Update(true);
      }

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


Расширение системы правил поиска торгового сигнала

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

Перейдем в базовый класс CProgram и добавим новую переменную, экземпляр класса CСombobox, и создадим реализующий элемент интерфейса метод:

CComboBox         m_rule_interval;

bool              CreateRuleInterval(const int x_gap,const int y_gap);

Реализацию метода добавим в файл SetWindow.mqh потому что этот выпадающий список принадлежит окну настроек.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateRuleInterval(const int x_gap,const int y_gap)
{
//--- Передать объект панели
   m_rule_interval.MainPointer(m_set_window);
//--- Массив значений пунктов в списке
   string pattern_names[2]=
   {
      "Compare","Interval",
   };
//--- Установим свойства перед созданием
   m_rule_interval.XSize(160);
   m_rule_interval.YSize(26);
   m_rule_interval.LabelYGap(4);
   m_rule_interval.ItemsTotal(2);
   m_rule_interval.Font(m_base_font);
   m_rule_interval.FontSize(m_base_font_size);
   m_rule_interval.BackColor(m_background);
   m_rule_interval.GetButtonPointer().Font(m_base_font);
   m_rule_interval.GetButtonPointer().FontSize(m_base_font_size);
   m_rule_interval.GetButtonPointer().BackColor(clrWhite);
   m_rule_interval.GetButtonPointer().XGap(90);
   m_rule_interval.GetButtonPointer().XSize(80);
   m_rule_interval.GetListViewPointer().Font(m_base_font);
   m_rule_interval.GetListViewPointer().FontSize(m_base_font_size);
   m_rule_interval.GetListViewPointer().ItemYSize(26);
//--- Сохраним значения пунктов в список комбо-бокса
   for(int i=0; i<2; i++)
      m_rule_interval.SetValue(i,pattern_names[i]);
//--- Получим указатель списка
   CListView *lv=m_rule_interval.GetListViewPointer();
//--- Установим свойства списка
   lv.LightsHover(true);
   m_rule_interval.SelectItem(0);
//--- Создадим элемент управления
   if(!m_rule_interval.CreateComboBox("Rule",x_gap,y_gap))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(1,m_rule_interval);
   return(true);
}

Так как новый режим установки правила Интервал должен иметь нижний и верхний предел, то добавим, помимо уже существующего поля ввода численного значения, еще одно. То, что было, теперь будет верхним пределом интервала, а то, что добавим — нижним. Само собой, нужно учитывать при использовании индикаторов, чьи значения находятся ниже нуля, как в случае с индикатором WPR. Потому, в таком случае понятия верхний и нижний предел будут меняться местами. Для того, чтобы не создавать отдельный метод для реализации поля ввода для нижнего предела интервала, мы просто модифицируем уже текущую переменную, отвечающую за существующее поле ввода и метод CreateRule(). Переменная станет массивом:

CTextEdit         m_rule_value[2];

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

bool              CreateRuleValue(CTextEdit &text_edit,const int x_gap,const int y_gap);

Конечно, изменим и реализацию этого метода.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateRuleValue(CTextEdit &text_edit,const int x_gap,const int y_gap)
{
//--- Сохраним указатель на главный элемент
   text_edit.MainPointer(m_set_window);
//--- Свойства
   text_edit.XSize(80);
   text_edit.YSize(24);
   text_edit.GetTextBoxPointer().XGap(1);
   text_edit.LabelColor(C'0,100,255');
   text_edit.Font(m_base_font);
   text_edit.FontSize(m_base_font_size);
   text_edit.MaxValue(999);
   text_edit.StepValue(0.1);
   text_edit.MinValue(-999);
   text_edit.SetDigits(3);
   text_edit.SpinEditMode(true); 
//--- Создадим элемент управления
   if(!text_edit.CreateTextEdit("",x_gap,y_gap))
      return(false);
   text_edit.SetValue(string(5));
   text_edit.GetTextBoxPointer().AutoSelectionMode(true);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(1,text_edit);
   return(true);
}

Также нужно изменить некоторые значения уже существующего метода CreateRule():

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateRule(const int x_gap,const int y_gap)
{
//--- Передать объект панели
   m_rule_type.MainPointer(m_set_window);
//--- Массив значений пунктов в списке
   string pattern_names[5]=
   {
      ">",">=","==","<","<="
   };
//--- Установим свойства перед созданием
   m_rule_type.XSize(80);
   m_rule_type.YSize(26);
   m_rule_type.LabelYGap(4);
   m_rule_type.ItemsTotal(5);
   m_rule_type.Font(m_base_font);
   m_rule_type.FontSize(m_base_font_size);
   m_rule_type.BackColor(m_background);
   m_rule_type.GetButtonPointer().Font(m_base_font);
   m_rule_type.GetButtonPointer().FontSize(m_base_font_size);
   m_rule_type.GetButtonPointer().BackColor(clrWhite);
   m_rule_type.GetButtonPointer().XGap(1);
   m_rule_type.GetButtonPointer().XSize(80);
   m_rule_type.GetListViewPointer().Font(m_base_font);
   m_rule_type.GetListViewPointer().FontSize(m_base_font_size);
   m_rule_type.GetListViewPointer().ItemYSize(26);
//--- Сохраним значения пунктов в список комбо-бокса
   for(int i=0; i<5; i++)
      m_rule_type.SetValue(i,pattern_names[i]);
//--- Получим указатель списка
   CListView *lv=m_rule_type.GetListViewPointer();
//--- Установим свойства списка
   lv.LightsHover(true);
   m_rule_type.SelectItem(0);
//--- Создадим элемент управления
   if(!m_rule_type.CreateComboBox("",x_gap,y_gap))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(1,m_rule_type);
   return(true);
}

Теперь можно найти раздел Настройки условий в методе создания окна настроек CreateSetWindow() и изменить его код на такой:

//--- Настройки условий
   if(!CreateRuleValue(m_rule_value[0],200,22+10+5*(25+10)))
      return(false);
   if(!CreateRuleValue(m_rule_value[1],300,22+10+5*(25+10)))
      return(false);
   if(!CreateRule(200,22+10+5*(25+10)))
      return(false);
   if(!CreateRuleInterval(10,22+10+5*(25+10)))
      return(false);

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

Рис.4 Добавление выбора режима правил поиска сигналов.

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

//--- Выбор пункта в выпадающем списке комбобокса
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
   {
      ...
      //--- Тип правила
      if(lparam==m_rule_interval.Id())
      {
         switch(m_rule_interval.GetListViewPointer().SelectedItemIndex())
         {
         case  0:
            m_rule_value[0].Hide();
            m_rule_type.Show();
            break;
         case  1:
            m_rule_value[0].Show();
            m_rule_type.Hide();
            break;
         default:
            break;
         }
      }
   }

Следующим изменением будет перенос некоторых событий при загрузке интерфейса в отдельный раздел метода OnEvent(). Для этого создадим секцию событие Завершение создания интерфейса и перенесем код из метода CreateGUI(). В итоге в этом методе останется такой код:

//+------------------------------------------------------------------+
//| Создаёт графический интерфейс программы                          |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
{
//--- Загрузка языка
   ChangeLanguage();
//--- Шаг 1-3. Окно выбора символов.
   if(!CreateStepWindow(m_lang[0]))
      return(false);
//---
   if(!CreateSetWindow(m_lang[17]))
      return(false);
//--- Создание формы 2 для цветовой палитры
   if(!CreateColorWindow("Color Picker"))
      return(false);
//--- Завершение создания GUI
   CWndEvents::CompletedGUI();
   return(true);
}

А новая секция будет выглядеть так:

//--- Завершение создания интерфейса
   if(id==CHARTEVENT_CUSTOM+ON_END_CREATE_GUI)
   {
      m_back_button.Hide();
      m_add_signal.Hide();
      m_signal_header.Hide();
      m_label_button[1].IsPressed(true);
      m_label_button[1].Update(true);
      for(int i=0; i<5; i++)
         m_signal_editor[i].Hide();
      m_rule_value[0].Hide();
   }

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

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::SaveSignalSet(int index)
{
//---
   int h=FileOpen("Signal Monitor\\signal_"+string(index)+".bin",FILE_WRITE|FILE_BIN);
   if(h==INVALID_HANDLE)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Не удалось создать файл конфигурации","Монитор сигналов");
      else
         MessageBox("Failed to create configuration file","Signal Monitor");
      return(false);
   }
   if(index>4)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Максимальное число сигналов не должно быть больше 5","Монитор сигналов");
      else
         MessageBox("Maximum number of signals is 5","Signal Monitor");
      return(false);
   }
//--- Сохраняем выбор
//--- Тип индикатора
   m_signal_set[index].ind_type=m_indicator_type.GetListViewPointer().SelectedItemIndex();
//--- Период индикатора
   if(m_signal_set[index].ind_type!=9)
   {
      m_signal_set[index].ind_period=(int)m_period_edit.GetValue();
      //--- Тип применяемой цены
      m_signal_set[index].app_price=m_applied_price.GetListViewPointer().SelectedItemIndex();
   }
   else
   {
      string path=m_custom_path.GetValue();
      string param=m_custom_param.GetValue();
      if(path=="")
      {
         if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
            MessageBox("Введите путь к индикатору","Монитор сигналов");
         else
            MessageBox("Enter the indicator path","Signal Monitor");
         FileClose(h);
         return(false);
      }
      if(param=="")
      {
         if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
            MessageBox("Введите параметры индикатора через запятую","Монитор сигналов");
         else
            MessageBox("Enter indicator parameters separated by commas","Signal Monitor");
         FileClose(h);
         return(false);
      }
      StringToCharArray(path,m_signal_set[index].custom_path);
      StringToCharArray(param,m_signal_set[index].custom_val);
      m_signal_set[index].ind_period=(int)m_period_edit.GetValue();
   }
//--- Тип правила
   m_signal_set[index].rule_int=m_rule_interval.GetListViewPointer().SelectedItemIndex();
//--- Тип сравнения
   m_signal_set[index].rule_type=m_rule_type.GetListViewPointer().SelectedItemIndex();
//--- Значение правила
   m_signal_set[index].rule_value1=(double)m_rule_value[0].GetValue();
   m_signal_set[index].rule_value2=(double)m_rule_value[1].GetValue();
//--- Тип отображения текстовой метки
   m_signal_set[index].label_type=m_label_button[0].IsPressed()?0:1;
//--- Сохраняем значение текстового поля при втором типе
   if(m_label_button[1].IsPressed())
      StringToCharArray(StringSubstr(m_text_box.GetValue(),0,3),m_signal_set[index].label_value);
//--- Цвет текстовой метки
   m_signal_set[index].label_color=m_color_button[0].CurrentColor();
//--- Цвет заднего фона
   if(m_set_param[0].IsPressed())
      m_signal_set[index].back_color=m_color_button[1].CurrentColor();
   else
      m_signal_set[index].back_color=clrNONE;
//--- Цвет канта
   if(m_set_param[1].IsPressed())
      m_signal_set[index].border_color=m_color_button[2].CurrentColor();
   else
      m_signal_set[index].border_color=clrNONE;
//--- Значение подксазки
   m_signal_set[index].tooltip=m_set_param[2].IsPressed();
   if(m_signal_set[index].tooltip)
      StringToCharArray(m_tooltip_text.GetValue(),m_signal_set[index].tooltip_text);
//--- Выбранное изображение
   m_signal_set[index].image=m_set_param[3].IsPressed();
   if(m_signal_set[index].image)
      m_signal_set[index].img_index=m_pictures_slider.GetRadioButtonsPointer().SelectedButtonIndex();
//--- Выбранные таймфреймы
   int tf=0;
   for(int i=0; i<21; i++)
   {
      if(!m_tf_button[i].IsLocked() && m_tf_button[i].IsPressed())
      {
         m_signal_set[index].timeframes[i]=true;
         StringToCharArray(m_tf_button[i].LabelText(),m_signal_set[index].tf_name[i].tf);
         tf++;
      }
      else
         m_signal_set[index].timeframes[i]=false;
   }
//---
   if(tf<1)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Не выбран ни один таймфрейм","Монитор сигналов");
      else
         MessageBox("No timeframes selected","Signal Monitor");
      FileClose(h);
      return(false);
   }
//---
   FileWriteStruct(h,m_signal_set[index]);
   FileClose(h);
   Print("Конфигурация signal_"+string(index)+" успешно сохранена");
//---
   return(true);
}

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

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

struct SIGNAL
{
   int               ind_type;
   int               ind_period;
   int               app_price;
   int               rule_int;
   int               rule_type;
   double            rule_value1;
   double            rule_value2;
   int               label_type;
   uchar             label_value[10];
   color             label_color;
   color             back_color;
   color             border_color;
   bool              tooltip;
   uchar             tooltip_text[100];
   bool              image;
   int               img_index;
   bool              timeframes[21];
   TFNAME            tf_name[21];
   uchar             custom_path[100];
   uchar             custom_val[100];
};

Теперь поле ввода порогового значения при сравнении rule_value заменяется на rule_value 1 и rule_value2 для полей нижнего и верхнего предела в режиме Интервал. Также добавлены custom_path и custom_val для хранения данных о пути собственного индикатора и его параметрах. Соответственно изменить и метод загрузки набора параметров торгового сигнала из файла LoadSignalSet():

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::LoadSignalSet(int index)
{
   int h=FileOpen("Signal Monitor\\signal_"+string(index)+".bin",FILE_READ|FILE_BIN);
   if(h==INVALID_HANDLE)
   {
      MessageBox("Конфигурация не найдена","Signal Monitor");
      return(false);
   }
   ZeroMemory(m_signal_set[index]);
   FileReadStruct(h,m_signal_set[index]);
//--- Загрузка типа индикатора
   m_indicator_type.SelectItem(m_signal_set[index].ind_type);
   RebuildParameters(m_signal_set[index].ind_type);
   m_indicator_type.GetButtonPointer().Update(true);
   if(m_signal_set[index].ind_type!=9)
   {
      //--- Загрузка периода индикатора
      m_period_edit.SetValue((string)m_signal_set[index].ind_period);
      m_period_edit.GetTextBoxPointer().Update(true);
      //--- Загрузка применяемой цены, если есть
      if(!m_applied_price.IsLocked())
      {
         m_applied_price.SelectItem(m_signal_set[index].app_price);
         m_applied_price.GetButtonPointer().Update(true);
      }
   }
   else
   {
      m_period_edit.SetValue((string)m_signal_set[index].ind_period);
      m_custom_path.SetValue(CharArrayToString(m_signal_set[index].custom_path));
      m_custom_param.SetValue(CharArrayToString(m_signal_set[index].custom_val));
      m_custom_path.GetTextBoxPointer().Update(true);
      m_custom_param.GetTextBoxPointer().Update(true);
   }
//--- Загрузка правила сигнала
   m_rule_interval.SelectItem(m_signal_set[index].rule_int);
   m_rule_interval.GetButtonPointer().Update(true);
   m_rule_type.SelectItem(m_signal_set[index].rule_type);
   m_rule_type.GetButtonPointer().Update(true);
   m_rule_value[0].SetValue((string)m_signal_set[index].rule_value1);
   m_rule_value[0].GetTextBoxPointer().Update(true);
   m_rule_value[1].SetValue((string)m_signal_set[index].rule_value2);
   m_rule_value[1].GetTextBoxPointer().Update(true);
//--- Загрузка Текстовой метки
   if(m_signal_set[index].label_type==0)
   {
      m_label_button[0].IsPressed(true);
      m_label_button[0].Update(true);
      m_label_button[1].IsPressed(false);
      m_label_button[1].Update(true);
      m_text_box.IsLocked(true);
   }
   else
   {
      m_label_button[0].IsPressed(false);
      m_label_button[0].Update(true);
      m_label_button[1].IsPressed(true);
      m_label_button[1].Update(true);
      m_text_box.IsLocked(false);
      m_text_box.ClearTextBox();
      m_text_box.AddText(0,CharArrayToString(m_signal_set[index].label_value));
      m_text_box.Update(true);
   }
//--- Загрузка цвета текстовой метки
   m_color_button[0].CurrentColor(m_signal_set[index].label_color);
   m_color_button[0].Update(true);
//--- Загрузка цвета фона
   if(m_signal_set[index].back_color==clrNONE)
   {
      m_set_param[0].IsPressed(false);
      m_set_param[0].Update(true);
      m_color_button[1].IsLocked(true);
      m_color_button[1].GetButtonPointer().Update(true);
   }
   else
   {
      m_set_param[0].IsPressed(true);
      m_set_param[0].Update(true);
      m_color_button[1].IsLocked(false);
      m_color_button[1].CurrentColor(m_signal_set[index].back_color);
      m_color_button[1].GetButtonPointer().Update(true);
   }
//--- Загрузка цвета канта
   if(m_signal_set[index].border_color==clrNONE)
   {
      m_set_param[1].IsPressed(false);
      m_set_param[1].Update(true);
      m_color_button[2].IsLocked(true);
      m_color_button[2].GetButtonPointer().Update(true);
   }
   else
   {
      m_set_param[1].IsPressed(true);
      m_set_param[1].Update(true);
      m_color_button[2].IsLocked(false);
      m_color_button[2].CurrentColor(m_signal_set[index].border_color);
      m_color_button[2].GetButtonPointer().Update(true);
   }
//--- Загрузка значения подсказки
   if(!m_signal_set[index].tooltip)
   {
      m_set_param[2].IsPressed(false);
      m_set_param[2].Update(true);
      m_tooltip_text.IsLocked(true);
      m_tooltip_text.Update(true);
   }
   else
   {
      m_set_param[2].IsPressed(true);
      m_set_param[2].Update(true);
      m_tooltip_text.IsLocked(false);
      m_tooltip_text.ClearTextBox();
      m_tooltip_text.AddText(0,CharArrayToString(m_signal_set[index].tooltip_text));
      m_tooltip_text.Update(true);
   }
//--- Загрузка изображения
   if(!m_signal_set[index].image)
   {
      m_set_param[3].IsPressed(false);
      m_set_param[3].Update(true);
      m_pictures_slider.IsLocked(true);
      m_pictures_slider.GetRadioButtonsPointer().Update(true);
   }
   else
   {
      m_set_param[3].IsPressed(true);
      m_set_param[3].Update(true);
      m_pictures_slider.IsLocked(false);
      m_pictures_slider.GetRadioButtonsPointer().SelectButton(m_signal_set[index].img_index);
      m_pictures_slider.GetRadioButtonsPointer().Update(true);
   }
//--- Загрузка выбранных таймфреймов
   for(int i=0; i<21; i++)
   {
      if(!m_tf_button[i].IsLocked())
      {
         m_tf_button[i].IsPressed(m_signal_set[index].timeframes[i]);
         m_tf_button[i].Update(true);
      }
   }
//---
   FileClose(h);
   return(true);
}

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

Настроив сохранение и загрузку набора настроек торгового сигнала теперь можно приступить к дополнению алгоритма поиска сигнала. Для этого перейдем в метод GetSignal() и найдем секцию кода Проверка на условия и ее заменим на следующую:

//--- Проверка на условия
   int s=0;
   if(signal_set.rule_int==0)
   {
      double r_value=signal_set.rule_value2;
      double c_value=val[0];
      m_ind_value=c_value;
      switch(signal_set.rule_type)
      {
      case  0:
         if(c_value>r_value)
            s=1;
         break;
      case  1:
         if(c_value>=r_value)
            s=1;
         break;
      case  2:
         if(c_value==r_value)
            s=1;
         break;
      case  3:
         if(c_value<r_value)
            s=1;
         break;
      case  4:
         if(c_value<=r_value)
            s=1;
         break;
      default:
         s=0;
         break;
      }
   }
   else if(signal_set.rule_int==1)
   {
      double r_value_min=signal_set.rule_value1;
      double r_value_max=signal_set.rule_value2;
      double c_value=val[0];
      m_ind_value=c_value;
      if(c_value>=r_value_min && c_value<=r_value_max)
         s=1;
   }

А также включим добавленные индикаторы в секцию Получение хэндла выбранного индикатора:

//--- Получение хэндла выбранного индикатора
   string str[],name;
   double arr[];
   switch(signal_set.ind_type)
   {
   case  0:
      h=iATR(sy,tf,signal_set.ind_period);
      break;
   case  1:
      h=iCCI(sy,tf,signal_set.ind_period,app_price);
      break;
   case  2:
      h=iDeMarker(sy,tf,signal_set.ind_period);
      break;
   case  3:
      h=iForce(sy,tf,signal_set.ind_period,MODE_SMA,VOLUME_TICK);
      break;
   case  4:
      h=iWPR(sy,tf,signal_set.ind_period);
      break;
   case  5:
      h=iRSI(sy,tf,signal_set.ind_period,app_price);
      break;
   case  6:
      h=iMomentum(sy,tf,signal_set.ind_period,app_price);
      break;
   case  7:
      h=iADX(sy,tf,signal_set.ind_period);
      break;
   case  8:
      h=iADXWilder(sy,tf,signal_set.ind_period);
      break;
   case  9:
      StringSplit(m_custom_param.GetValue(),StringGetCharacter(",",0),str);
      ArrayResize(arr,ArraySize(str));
      for(int i=0; i<ArraySize(str); i++)
         arr[i]=StringToDouble(str[i]);
      name=m_custom_path.GetValue();
      h=GetCustomValue(tf,name,arr);
      break;
   default:
      break;
   }

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


Перевод списка символов в табличный вид

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

Для начала на первом шаге настройки приложения нужно убрать отображение старых чекбоксов. Для этого в событии Завершение создания интерфейса их скрываем. При этом теперь мы знаем, что число чекбоксов всегда остается неизменным и равно 21, общему числу возможных таймфреймов терминала. Поэтому превратим динамический массив m_checkbox[] в статический с размером 21.

      //--- Скрываем чекбоксы таймфреймов
      for(int i=0; i<21; i++)
         m_checkbox[i].Hide();

Не стоит забывать, что и способ создания чекбоксов, всвязи с их изменившимся назначением, тоже должен быть скорректирован. Потому, заходим в тело метода CreateStepWindow() и секцию Чекбоксы заменяем на следующую:

//--- Чекбоксы
   int k=0;
   string timeframe_names[21]=
   {
      "M1","M2","M3","M4","M5","M6","M10","M12","M15","M20","M30",
      "H1","H2","H3","H4","H6","H8","H12","D1","W1","MN"
   };
   for(int j=0; j<=3; j++)
   {
      for(int i=0; i<7; i++)
      {
         if(k<21)
            if(!CreateCheckBox(m_checkbox[k],10+80*i,m_step_window.CaptionHeight()+70+j*25,timeframe_names[k]))
               return(false);
         k++;
      }
   }

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

m_step_window.ChangeWindowHeight(m_checkbox[m_all_symbols-1].YGap()+30+30);

И делаем высоту окна статичной:

m_step_window.YSize(500);

Теперь необходимо создать базовый объект-таблицу для последующего ее наполнения данными из Обзора рынка. Для этого создадим экземпляр класса CTable и метод реализации таблицы.

//--- Нарисованная таблица
   CTable            m_table;

   bool              CreateTable(const int x_gap,const int y_gap);

И реализуем его в файле главного окна StepWindow.mqh:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateTable(const int x_gap,const int y_gap)
{
#define COLUMNS1_TOTAL 7
#define ROWS1_TOTAL int(MathCeil(m_all_symbols/7))
//--- Сохраним указатель на главный элемент
   m_table.MainPointer(m_step_window);
//--- Массив ширины столбцов
   int width[COLUMNS1_TOTAL];
   ::ArrayInitialize(width,80);
//--- Массив отступа текста в столбцах по оси X
   int text_x_offset[COLUMNS1_TOTAL];
   ::ArrayInitialize(text_x_offset,25);
//--- Массив выравнивания текста в столбцах
   ENUM_ALIGN_MODE align[COLUMNS1_TOTAL];
   ::ArrayInitialize(align,ALIGN_LEFT);
//--- Массив отступа картинок в столбцах по оси X
   int image_x_offset[COLUMNS1_TOTAL];
   ::ArrayInitialize(image_x_offset,5);
//--- Массив отступа картинок в столбцах по оси Y
   int image_y_offset[COLUMNS1_TOTAL];
   ::ArrayInitialize(image_y_offset,4);
//--- Свойства
   m_table.XSize(560);
   m_table.YSize(190);
   m_table.Font(m_base_font);
   m_table.FontSize(m_base_font_size);
   m_table.CellYSize(20);
   m_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL);
   m_table.TextAlign(align);
   m_table.ColumnsWidth(width);
   m_table.TextXOffset(text_x_offset);
   m_table.ImageXOffset(image_x_offset);
   m_table.ImageYOffset(image_y_offset);
   m_table.LabelXGap(5);
   m_table.LabelYGap(4);
   m_table.IconXGap(7);
   m_table.IconYGap(4);
   m_table.MinColumnWidth(0);
   m_table.LightsHover(true);
   m_table.SelectableRow(false);
   m_table.IsWithoutDeselect(false);
   m_table.ColumnResizeMode(true);
   m_table.IsZebraFormatRows(clrWhiteSmoke);
   m_table.AutoXResizeMode(true);
   m_table.AutoXResizeRightOffset(10);
   m_table.AutoYResizeMode(true);
   m_table.AutoYResizeBottomOffset(50);
//--- Заполним таблицу данными
   InitializingTable();
//--- Создадим элемент управления
   if(!m_table.CreateTable(x_gap,y_gap))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(0,m_table);
   return(true);
}

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

//+------------------------------------------------------------------+
//| Инициализация таблицы                                            |
//+------------------------------------------------------------------+
void CProgram::InitializingTable(void)
{
//--- Массив картинок 1
   string image_array1[2]=
   {
      "Images\\EasyAndFastGUI\\Controls\\checkbox_off.bmp",
      "Images\\EasyAndFastGUI\\Controls\\checkbox_on_g.bmp"
   };
//---
   int k=0;
   for(int c=0; c<COLUMNS1_TOTAL; c++)
   {
      //---
      for(int r=0; r<ROWS1_TOTAL; r++)
      {
         if(k<m_all_symbols)
         {
            //--- Установим тип ячейки Чекбокс
            m_table.CellType(c,r,CELL_CHECKBOX);
            m_table.SetImages(c,r,image_array1);
            //--- Установим текст
            m_table.SetValue(c,r,SymbolName(k,false));
         }
         k++;
      }
   }
}

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

Рис.5 Результат перевода списка символов в табличный вид.

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

Для реализации первого необходимо в теле метода OnEvent() заменить секции с названиями шаблонов на следующий код:

//--- All
         if(lparam==m_currency_set[0].Id() && m_currency_set[0].IsPressed())
         {
            m_currency_set[1].IsPressed(false);
            m_currency_set[2].IsPressed(false);
            m_currency_set[1].Update(true);
            m_currency_set[2].Update(true);
            //---
            int k=0;
            for(int c=0; c<7; c++)
            {
               //---
               for(int r=0; r<MathCeil(m_all_symbols/7); r++)
               {
                  if(k<m_all_symbols)
                     m_table.ChangeImage(c,r,1);
                  k++;
               }
            }
            m_table.Update(true);
         }
         //--- Majors
         else if(lparam==m_currency_set[1].Id() && m_currency_set[1].IsPressed())
         {
            m_currency_set[0].IsPressed(false);
            m_currency_set[2].IsPressed(false);
            m_currency_set[0].Update(true);
            m_currency_set[2].Update(true);
            //---
            string pairs[4]= {"EURUSD","GBPUSD","USDCHF","USDJPY"};
            //--- Очистим выбор
            int k=0;
            for(int c=0; c<7; c++)
            {
               //---
               for(int r=0; r<MathCeil(m_all_symbols/7); r++)
               {
                  if(k<m_all_symbols)
                     m_table.ChangeImage(c,r,0);
                  k++;
               }
            }
            //---
            k=0;
            for(int c=0; c<7; c++)
            {
               //---
               for(int r=0; r<MathCeil(m_all_symbols/7); r++)
               {
                  if(k<m_all_symbols)
                  {
                     for(int j=0; j<4; j++)
                     {
                        if(m_table.GetValue(c,r)==pairs[j])
                           m_table.ChangeImage(c,r,1);
                     }
                  }
                  k++;
               }
            }
            m_table.Update(true);
         }
         //--- Crosses
         else if(lparam==m_currency_set[2].Id() && m_currency_set[2].IsPressed())
         {
            m_currency_set[0].IsPressed(false);
            m_currency_set[1].IsPressed(false);
            m_currency_set[0].Update(true);
            m_currency_set[1].Update(true);
            //---
            string pairs[20]=
            {
               "EURUSD","GBPUSD","USDCHF","USDJPY","USDCAD","AUDUSD","AUDNZD","AUDCAD","AUDCHF","AUDJPY",
               "CHFJPY","EURGBP","EURAUD","EURCHF","EURJPY","EURCAD","EURNZD","GBPCHF","GBPJPY","CADCHF"
            };
            //--- Очистим выбор
            int k=0;
            for(int c=0; c<7; c++)
            {
               //---
               for(int r=0; r<MathCeil(m_all_symbols/7); r++)
               {
                  if(k<m_all_symbols)
                     m_table.ChangeImage(c,r,0);
                  k++;
               }
            }
            //---
            k=0;
            for(int c=0; c<7; c++)
            {
               //---
               for(int r=0; r<MathCeil(m_all_symbols/7); r++)
               {
                  if(k<m_all_symbols)
                  {
                     for(int j=0; j<20; j++)
                     {
                        if(m_table.GetValue(c,r)==pairs[j])
                           m_table.ChangeImage(c,r,1);
                     }
                  }
                  k++;
               }
            }
            m_table.Update(true);
         }
         //---
         if((lparam==m_currency_set[0].Id() && !m_currency_set[0].IsPressed())      ||
               (lparam==m_currency_set[1].Id() && !m_currency_set[1].IsPressed())   ||
               (lparam==m_currency_set[2].Id() && !m_currency_set[2].IsPressed())
           )
         {
            //--- Очистим выбор
            int k=0;
            for(int c=0; c<7; c++)
            {
               //---
               for(int r=0; r<MathCeil(m_all_symbols/7); r++)
               {
                  if(k<m_all_symbols)
                     m_table.ChangeImage(c,r,0);
                  k++;
               }
            }
            m_table.Update(true);
         }

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

//+------------------------------------------------------------------+
//| Сохранение шаблона в файл                                        |
//+------------------------------------------------------------------+
bool CProgram::SaveSymbolSet(string file_name)
{
   if(file_name=="")
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Выберите имя шаблона для записи","Монитор сигналов");
      else
         MessageBox("Choose a name for the template to save","Signal Monitor");
      return(false);
   }
   int h=FileOpen("Signal Monitor\\"+file_name+".bin",FILE_WRITE|FILE_BIN);
   if(h==INVALID_HANDLE)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Не удалось создать файл конфигурации","Монитор сигналов");
      else
         MessageBox("Failed to create configuration file","Signal Mo
nitor");
      return(false);
   }
   else
      MessageBox("Конфигурация "+file_name+" успешно сохранена","Signal Monitor");
//--- Сохраняем выбор символов
   int k=0;
   for(int c=0; c<7; c++)
   {
      //---
      for(int r=0; r<MathCeil(m_all_symbols/7); r++)
      {
         if(k<m_all_symbols)
            m_save.tf[k]=m_table.SelectedImageIndex(c,r)>0?true:false;
         k++;
      }
   }
//---
   FileWriteStruct(h,m_save);
   FileClose(h);
//---
   return(true);
}
//+------------------------------------------------------------------+
//| Загрузка данных в панель                                         |
//+------------------------------------------------------------------+
bool CProgram::LoadSymbolSet(string file_name)
{
   if(file_name=="")
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Выберите имя шаблона для загрузки","Монитор сигналов");
      else
         MessageBox("Choose a name for the template to load","Signal Monitor");
      return(false);
   }
   int h=FileOpen("Signal Monitor\\"+file_name+".bin",FILE_READ|FILE_BIN);
   if(h==INVALID_HANDLE)
   {
      MessageBox("Конфигурация "+file_name+" не найдена","Signal Monitor");
      return(false);
   }
   ZeroMemory(m_save);
   FileReadStruct(h,m_save);
//--- Загружаем выбор символов
   int k=0;
   for(int c=0; c<7; c++)
   {
      //---
      for(int r=0; r<MathCeil(m_all_symbols/7); r++)
      {
         if(k<m_all_symbols)
         {
            if(m_save.tf[k])
               m_table.ChangeImage(c,r,1);
            else
               m_table.ChangeImage(c,r,0);
         }
         k++;
      }
   }
   m_table.Update(true);
//---
   FileClose(h);
//---
   return(true);
}

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

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

Рис.6 Результат добавления таблицы и ее взаимодействия с элементами интерфейса.

Далее предстоит отредактировать методы перехода между Шагом 1 и Шагом 2. Потому что способом получения данных о выбранных символах мы изменили. За переходы между шагами настройки отвечают два метода, которые следует изменить. Метод To_Step1() нужно изменить так, чтобы при переходе от Шага 2 к Шагу 1 скрывалась возможность выбора таймфреймов и показывалась наша таблица.

//+------------------------------------------------------------------+
//| Переход на шаг 1                                                 |
//+------------------------------------------------------------------+
void CProgram::ToStep_1(void)
{
//--- Меняем заголовок
   m_step_window.LabelText("Signal Monitor Step 1: Choose Symbols");
   m_step_window.Update(true);
//--- Скрываем кнопку Назад
   m_back_button.Hide();
//--- Показываем таблицу
   m_table.Show();
//--- Скрываем таймфреймы
   for(int i=0; i<21; i++)
      m_checkbox[i].Hide();
   string names[3]= {"All","Majors","Crosses"};
//--- Меняем названия кнопок наборов
   for(int i=0; i<3; i++)
   {
      m_currency_set[i].IsPressed(false);
      m_currency_set[i].LabelText(names[i]);
      m_currency_set[i].Update(true);
   }
//--- Показываем блок работы с шаблонами
   m_text_edit.Show();
   m_load_button.Show();
   m_save_button.Show();
//--- Устанавливаем текущий шаг настройки
   m_current_step=1;
}

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::ToStep_2(void)
{
//--- Проверка на выбор хотя бы одного символа
   int cnt=0;
//---
   for(int c=0; c<7; c++)
   {
      for(int r=0; r<MathCeil(m_all_symbols/7); r++)
      {
         if(m_table.SelectedImageIndex(c,r)>0)
            cnt++;
      }
   }
//---
   if(cnt<1)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Не выбран ни один символ!","Внимание");
      else
         MessageBox("No symbols selected!","Warning");
      return;
   }
//--- Скрываем таблицу
   m_table.Hide();
//--- Отображаем таймфреймы
   for(int i=0; i<21; i++)
      m_checkbox[i].Show();
//--- Считаем количество выбранных символов
   ArrayResize(m_symbols,cnt);
   cnt=0;
//--- Запоминаем выбранные символы в массив
   for(int c=0; c<7; c++)
   {
      for(int r=0; r<MathCeil(m_all_symbols/7); r++)
      {
         if(m_table.SelectedImageIndex(c,r)>0)
         {
            m_symbols[cnt]=m_table.GetValue(c,r);
            cnt++;
         }
      }
   }
//--- Установка в Обзоре рынка выбранных символов
   for(int c=0; c<7; c++)
   {
      for(int r=0; r<MathCeil(m_all_symbols/7); r++)
      {
         if(m_table.SelectedImageIndex(c,r)>0)
            SymbolSelect(m_table.GetValue(c,r),true);
         else
            SymbolSelect(m_table.GetValue(c,r),false);
      }
   }
//---
   if(m_current_step==3)
   {
      m_add_signal.Hide();
      m_signal_header.Hide();
      m_next_button.LabelText("Next");
      m_next_button.Update(true);
      for(int i=0; i<5; i++)
         m_signal_editor[i].Hide();
      ClearSaves();
   }
//--- Меняем заголовок
   m_step_window.LabelText("Signal Monitor Step 2: Choose Timeframes");
   m_step_window.Update(true);
   string names[3]= {"All","Junior","Senior"};
//--- Меняем названия кнопок наборов
   for(int i=0; i<3; i++)
   {
      m_currency_set[i].LabelText(names[i]);
      m_currency_set[i].IsPressed(false);
      if(m_current_step==3)
         m_currency_set[i].Show();
      m_currency_set[i].Update(true);
   }
//--- Скрываем блок работы с шаблонами
   m_text_edit.Hide();
   m_load_button.Hide();
   m_save_button.Hide();
//--- Показываем кнопку Назад
   m_back_button.Show();
//---
   m_current_step=2;
}

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

//--- All
         if(lparam==m_currency_set[0].Id() && m_currency_set[0].IsPressed())
         {
            m_currency_set[1].IsPressed(false);
            m_currency_set[2].IsPressed(false);
            m_currency_set[1].Update(true);
            m_currency_set[2].Update(true);
            //---
            for(int i=0; i<21; i++)
            {
               m_checkbox[i].IsPressed(true);
               m_checkbox[i].Update(true);
            }
         }
         //--- Junior Timeframes
         else if(lparam==m_currency_set[1].Id() && m_currency_set[1].IsPressed())
         {
            m_currency_set[0].IsPressed(false);
            m_currency_set[2].IsPressed(false);
            m_currency_set[0].Update(true);
            m_currency_set[2].Update(true);
            //---
            string pairs[11]=
            {
               "M1","M2","M3","M4","M5","M6","M10","M12","M15","M20","M30"
            };
            //--- Очистим выбор
            for(int i=0; i<21; i++)
            {
               m_checkbox[i].IsPressed(false);
               m_checkbox[i].Update(true);
            }
            //---
            for(int i=0; i<21; i++)
            {
               for(int j=0; j<11; j++)
                  if(m_checkbox[i].LabelText()==pairs[j])
                  {
                     m_checkbox[i].IsPressed(true);
                     m_checkbox[i].Update(true);
                  }
            }
         }
         //--- Senior Timeframes
         else if(lparam==m_currency_set[2].Id() && m_currency_set[2].IsPressed())
         {
            m_currency_set[0].IsPressed(false);
            m_currency_set[1].IsPressed(false);
            m_currency_set[0].Update(true);
            m_currency_set[1].Update(true);
            //---
            string pairs[10]=
            {
               "H1","H2","H3","H4","H6","H8","H12","D1","W1","MN"
            };
            //--- Очистим выбор
            for(int i=0; i<21; i++)
            {
               m_checkbox[i].IsPressed(false);
               m_checkbox[i].Update(true);
            }
            //---
            for(int i=0; i<21; i++)
            {
               for(int j=0; j<10; j++)
                  if(m_checkbox[i].LabelText()==pairs[j])
                  {
                     m_checkbox[i].IsPressed(true);
                     m_checkbox[i].Update(true);
                  }
            }
         }
         //---
         if((lparam==m_currency_set[0].Id() && !m_currency_set[0].IsPressed())      ||
               (lparam==m_currency_set[1].Id() && !m_currency_set[1].IsPressed())   ||
               (lparam==m_currency_set[2].Id() && !m_currency_set[2].IsPressed())
           )
         {
            //--- Очистим выбор
            for(int i=0; i<21; i++)
            {
               m_checkbox[i].IsPressed(false);
               m_checkbox[i].Update(true);
            }
         }


Быстрое редактирование правил поиска из монитора

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

Рис.7 Схема редактирования сигнала из самого монитора.

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

m_step_window.TooltipsButtonIsUsed(true);

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

//--- Завершение создания интерфейса
   if(id==CHARTEVENT_CUSTOM+ON_END_CREATE_GUI)
   {
      ...
      m_step_window.GetTooltipButtonPointer().Hide();
   }

И отобразить лишь, когда будет загружаться сам монитор.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::AutoResize(const int x_size,const int y_size)
{
   ...
   m_step_window.GetTooltipButtonPointer().Show();
}

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

   CWindow           m_fast_edit;

   bool              CreateFastEdit(const string caption_text);
   bool              CreateFastEditor(CButton &button,string text,const int x_gap,const int y_gap);

Реализуем эти методы:

//+------------------------------------------------------------------+
//| Создает окно создания и редактирования торговых сигналов         |
//+------------------------------------------------------------------+
bool CProgram::CreateFastEdit(const string caption_text)
{
//--- Добавим указатель окна в массив окон
   CWndContainer::AddWindow(m_fast_edit);
//--- Свойства
   m_fast_edit.XSize(180);
   m_fast_edit.YSize(280);
//--- Координаты
   int x=m_step_window.XGap()+m_step_window.XSize()+10;
   int y=m_step_window.YGap();
//---
   m_fast_edit.CaptionHeight(22);
   m_fast_edit.IsMovable(true);
   m_fast_edit.CaptionColor(m_caption);
   m_fast_edit.CaptionColorLocked(m_caption);
   m_fast_edit.CaptionColorHover(m_caption);
   m_fast_edit.BackColor(m_background);
   m_fast_edit.FontSize(m_base_font_size);
   m_fast_edit.Font(m_base_font);
   m_fast_edit.WindowType(W_DIALOG);
//--- Создание формы
   if(!m_fast_edit.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
//---
   for(int i=0; i<5; i++)
   {
      if(!CreateFastEditor(m_fast_editor[i],"Signal_"+string(i),10,40*i+40))
         return(false);
   }
   return(true);
}
//+------------------------------------------------------------------+
//| Создаёт кнопку с картинкой                                       |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\settings_light.bmp"
bool CProgram::CreateFastEditor(CButton &button,string text,const int x_gap,const int y_gap)
{
//---
   color baseclr=C'70,180,70';
   color pressed=C'70,170,70';
//--- Сохраним указатель на окно
   button.MainPointer(m_fast_edit);
//--- Установим свойства перед созданием
   button.XSize(110);
   button.YSize(30);
   button.Font(m_base_font);
   button.FontSize(m_base_font_size);
   button.IconXGap(3);
   button.IconYGap(7);
   button.IconFile("Images\\EasyAndFastGUI\\Icons\\bmp16\\settings_light.bmp");
   button.BackColor(baseclr);
   button.BackColorHover(baseclr);
   button.BackColorPressed(pressed);
   button.BorderColor(baseclr);
   button.BorderColorHover(baseclr);
   button.BorderColorPressed(pressed);
   button.LabelColor(clrWhite);
   button.LabelColorPressed(clrWhite);
   button.LabelColorHover(clrWhite);
   button.IsCenterText(true);
//--- Создадим элемент управления
   if(!button.CreateButton(text,x_gap,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(3,button);
   return(true);
}

И вызовем метод CreateFastEdit()в тело метода CreateGUI().

//+------------------------------------------------------------------+
//| Создаёт графический интерфейс программы                          |
//+------------------------------------------------------------------+
bool CProgram::CreateGUI(void)
{
//--- Шаг 1-3. Окно выбора символов.
   if(!CreateStepWindow("Signal Monitor Step 1: Choose Symbols"))
      return(false);
//---
   if(!CreateSetWindow("Signal Monitor Edit Signal"))
      return(false);
//--- Создание формы 2 для цветовой палитры
   if(!CreateColorWindow("Color Picker"))
      return(false);
//--- Создание формы быстрого редактирования
   if(!CreateFastEdit("Fast Signal Editor"))
      return(false);
//--- Завершение создания GUI
   CWndEvents::CompletedGUI();
   return(true);
}

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

      //--- ОТКРЫТИЕ ОКНА НАСТРОЙКИ
      if(lparam==m_step_window.GetTooltipButtonPointer().Id())
      {
         //--- Координаты
         int x=m_step_window.X()+m_step_window.XSize()+10;
         int y=m_step_window.Y();
         m_fast_edit.X(x);
         m_fast_edit.Y(y);
         m_fast_edit.OpenWindow();
      }

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

Рис.8 Добавление окна быстрого редактирования торговых сигналов.

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

//--- Открытие диалогового окна
   if(id==CHARTEVENT_CUSTOM+ON_OPEN_DIALOG_BOX)
   {
      if(m_current_step<4)
         return;
      for(int i=0; i<5; i++)
      {
         if(!FileIsExist("Signal Monitor\\signal_"+string(i)+".bin"))
            m_fast_editor[i].Hide();
      }
   }

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

      //--- Редактирование торгового сигнала
      for(int i=0; i<5; i++)
      {
         if(lparam==m_fast_editor[i].Id())
         {
            m_fast_edit.CloseDialogBox();
            LoadSignalSet(i);
            m_new_signal.LabelText("Save");
            m_new_signal.Update(true);
            RebuildParameters(m_indicator_type.GetListViewPointer().SelectedItemIndex());
            m_set_window.OpenWindow();
            m_number_signal=i;
         }
      }

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

Локализация приложения

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
enum UPDATE
{
   MINUTE,        // 1 minute
   MINUTE_15,     // 15 minutes
   MINUTE_30,     // 30 minutes
   HOUR,          // 1 hour
   HOUR_4         // 4 hour
};
enum LANG
{
   RUSSIAN,       // Russian
   ENGLISH        // English
};
//+------------------------------------------------------------------+
//| Входные параметры эксперта                                       |
//+------------------------------------------------------------------+
input int                  Inp_BaseFont      =  10;                  // Base Font
input color                Caption           =  C'0,130,225';        // Caption Color
input color                Background        =  clrWhiteSmoke;       // Back color
input LANG                 Language          =  ENGLISH;             // Interface language
input UPDATE               Update            =  MINUTE;              // Update interval

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

   //---
   int               m_language;

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

   program.m_language=Language;

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

   string            m_lang[];

   void              ChangeLanguage(void);

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

//+------------------------------------------------------------------+
//| Изменение языка интерфейса                                       |
//+------------------------------------------------------------------+
void CProgram::ChangeLanguage(void)
{
//---
#define ITEMS 40
   ArrayResize(m_lang,ITEMS);
   string rus[ITEMS]=
   {
      "Монитор Сигналов Шаг 1: Выбор Символов","Все","Мажоры","Кроссы",
      "Назад","Далее","Загрузка(L)","Сохранить(S)","Имя шаблона","Монитор сигналов Шаг 2: Выбор таймфреймов",
      "Все","Младшие","Старшие",
      "Монитор Сигналов Шаг 3: Создание торговых сигналов","Создать","Добавить сигнал","Список сигналов",
      "Редактор торговых сигналов","Тип индикатора","1.Настройки индикатора","Примен. цена",
      "Введите путь индикатора","Введите параметры индикатора через запятую",
      "2.Настройка сигнала","Правило","Метка","Значение","Текст","Цвет метки","Фон","Кант","Подсказка",
      "Изображение","Таймфреймы","Добавить","Отмена","Монитор торговых сигналов","Номер буфера","Сохранить"
   };
   string eng[ITEMS]=
   {
      "Signal Monitor Step 1: Choose Symbols","ALL","Major","Crosses",
      "Back","Next","Load(L)","Save(S)","Template name","Signal Monitor Step 2: Choose Timeframes",
      "ALL","Junior","Senior",
      "Signal Monitor Step 3: Creating Trading Signals","Create","Add Signal","Signal List",
      "Signal Monitor Edit Signal","Indicator Type","1.Indicator Settings","Applied Price",
      "Enter the indicator path","Enter indicator parameters separated by commas",
      "2.Signal Settings","Rule","Label","Value","Text","Label Color","Use Background","Use Border","Use Tooltip",
      "Use Image","Timeframes","Add","Cancel","Signal Monitor","Buffer number","Save"
   };
//--- Russian
   if(m_language==0)
      ArrayCopy(m_lang,rus);
//--- English
   else
      ArrayCopy(m_lang,eng);
}

После этого результатами работы можно наблюдать не только английский интерфейс, как было до это, но и русский(рис.9).

Рис.9 Результат работы локализации интерфейса.


Дополнительные возможности

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

   button.XSize(60);
   button.YSize(30);
   button.IconXGap(2);
   button.IconYGap(11);
   button.LabelXGap(19);
   button.LabelYGap(10);

//--- Символы
   int sy=ArraySize(m_symbols);
   ArrayResize(m_symbol_label,sy);
   for(int i=0; i<sy; i++)
   {
      if(!CreateSymbolLabel(m_symbol_label[i],5,m_step_window.CaptionHeight()+40+i*35,m_symbols[i]))
         return;
      m_symbol_label[i].Update(true);
   }
//--- Таймфреймы
   int tf=ArraySize(m_timeframes);
   ArrayResize(m_timeframe_label,tf);
//---
   for(int i=0; i<tf; i++)
   {
      if(!CreateTimeframeLabel(m_timeframe_label[i],110+65*i,m_step_window.CaptionHeight()+3,m_timeframes[i]))
         return;
      m_timeframe_label[i].Update(true);
   }
//-- Блоки сигналов
   int k=0;
   ArrayResize(m_signal_button,sy*tf);
   for(int j=0; j<sy; j++)
   {
      for(int i=0; i<tf; i++)
      {
         if(!CreateSignalButton(m_signal_button[k],m_timeframe_label[i].XGap()+m_timeframe_label[i].XSize()/2,m_step_window.CaptionHeight()+35+j*35))
            return;
         m_signal_button[k].Update(true);
         k++;
      }
   }
//---
   m_current_step=4;
//--- Изменяем размеры окна
   AutoResize(m_timeframe_label[tf-1].XGap()+m_timeframe_label[tf-1].XSize()+15,m_symbol_label[sy-1].YGap()+m_symbol_label[sy-1].YSize()+10);

В результате получим реализацию монитора более удобную для наблюдения, как показано на рис.10 чуть ниже.

Рис.10 Изменение размеров блоков сигналов и корректировка интерфейса монитора.

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

      //--- НАЖАТИЕ НА БЛОКЕ СИГНАЛА
      for(int i=0; i<ArraySize(m_signal_button); i++)
      {
         if(lparam==m_signal_button[i].Id())
            ChartOpen(GetSymbol(i),GetTimeframe(i));
      }

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


Заключение

В конце статьи приложен архив со всеми перечисленными файлами, отсортированными по папкам. Поэтому для корректной работы достаточно положить папку MQL5 в корень терминала. Для того чтобы найти корень терминала, в котором находится папка MQL5, нужно в MetaTrader 5 нажать комбинацию клавиш   Ctrl+Shift+D или воспользоваться контекстным меню, как показано на рис.11 ниже.


Рис.11 Поиск папки MQL5 в корне терминала MetaTrader 5