Мультивалютный мониторинг торговых сигналов (Часть 3): Внедряем алгоритмы поиска

Alexander Fedosov | 5 марта, 2020

Содержание

Введение

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

Система сохранения набора символов

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


Рис.1 Первый шаг настройки приложения и элементы настройки Сохраненного набора.

Если два первых были достаточно просты и реализованы ранее, то третий способ предстоит создать. Определимся с тем, что предстоит сделать. Акцент будет на взаимодействии элементов в красной рамке на рис.1, и состоит он в следующем:

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

   bool              SaveSymbolSet(string file_name);
   bool              LoadSymbolSet(string file_name);

Теперь реализуем каждый из добавленных методов. 

//+------------------------------------------------------------------+
//| Сохранение шаблона в файл                                        |
//+------------------------------------------------------------------+
bool CProgram::SaveSymbolSet(string file_name)
{
   if(file_name=="")
   {
      MessageBox("Выберите имя шаблона для записи","Signal Monitor");
      return(false);
   }
   int h=FileOpen("Signal Monitor\\"+file_name+".bin",FILE_WRITE|FILE_BIN);
   if(h==INVALID_HANDLE)
   {
      MessageBox("Не удалось создать файл конфигурации","Signal Monitor");
      return(false);
   }
   else
      MessageBox("Конфигурация "+file_name+" успешно сохранена","Signal Monitor");
//--- Сохраняем выбор таймфреймов и паттернов
   for(int i=0; i<m_all_symbols; i++)
      m_save.tf[i]=m_checkbox[i].IsPressed();
//---
   FileWriteStruct(h,m_save);
   FileClose(h);
//---
   return(true);
}
//+------------------------------------------------------------------+
//| Загрузка данных в панель                                         |
//+------------------------------------------------------------------+
bool CProgram::LoadSymbolSet(string file_name)
{
   if(file_name=="")
   {
      MessageBox("Выберите имя шаблона для загрузки","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);
//--- Загрузка таймфреймов
   for(int i=0; i<m_all_symbols; i++)
   {
      m_checkbox[i].IsPressed(m_save.tf[i]);
      m_checkbox[i].Update(true);
   }
//---
   FileClose(h);
//---
   return(true);
}

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

//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
struct SAVE
{
   bool     tf[100];
};
class CProgram : public CWndEvents
{
...
        SAVE            m_save;

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

         //--- Сохранить шаблон
         if(lparam==m_save_button.Id())
         {
            SaveSymbolSet(m_text_edit.GetValue());
         }
         //--- Загрузить шаблон
         if(lparam==m_load_button.Id())
         {
            LoadSymbolSet(m_text_edit.GetValue());
         }

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

//--- Нажатие кнопки на клавиатуре
   if(id==CHARTEVENT_KEYDOWN)
   {
      if(m_current_step==1)
      {
         short sym=TranslateKey((int)lparam);
         //--- если введённый символ успешно преобразован в Юникод
         if(sym>0)
         {
            if(ShortToString(sym)=="l" || ShortToString(sym)=="д")
               LoadSymbolSet(m_text_edit.GetValue());
            if(ShortToString(sym)=="s" || ShortToString(sym)=="ы")
               SaveSymbolSet(m_text_edit.GetValue());
         }
      }
   }

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

Рис.2 Сохранение и загрузка пользовательского шаблона

Добавление и редактирование торгового сигнала

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

Рис.3 Окно создания и редактирования сигнала.

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

bool              CreateButton3(CButton &button,string text,const int x_gap,const int y_gap);

И добавим две переменные экземпляры класса CButton:

   CButton           m_new_signal;
   CButton           m_cancel_button;

Теперь перейдем в файл SetWindow.mqh и реализуем этот метод.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateButton3(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_set_window);
//--- Установим свойства перед созданием
   button.XSize(60);
   button.YSize(30);
   button.Font(m_base_font);
   button.FontSize(m_base_font_size);
   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(1,button);
   return(true);
}

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

//--- Кнопки добавления/отмены
   if(!CreateButton3(m_new_signal,"Add",m_set_window.XSize()-2*(60+10),m_set_window.YSize()-(30+10)))
      return(false);
   if(!CreateButton3(m_cancel_button,"Cancel",m_set_window.XSize()-(60+10),m_set_window.YSize()-(30+10)))
      return(false);

После компиляции проекта в нижней части окна создания торгового сигнала появятся две кнопки. 

Рис.4 Добавления кнопок создания и отмены создания торгового сигнала.

Теперь необходимо их оживить — добавить события, которые будут происходит по нажатию на них. Если с кнопкой Cancel(Отмена) всё понятно — она не сохраняет никакие действия и настройки из заданного окна и просто его закрывает без добавления сигнала — то на действиях по нажатию кнопки Add(Добавить) следует остановиться подробнее.

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

  1. Происходит сохранение в файл выбранных параметров с помощью элементов интерфейса в окне создания торгового сигнала.
  2. При успешном сохранении происходит закрытие этого окна и в главном окне в списке сигналов появляется первая запись с названием сигнала и возможность по нажатию на нее редактировать ранее созданный.
  3. По нажатию на запись из файла будут применятся ранее сохраненный набор к элементам интерфейса настройки сигнала, а кнопка Add будет преобразована в кнопку Save(Сохранить).

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

struct SIGNAL
{
   int      ind_type;
   int      ind_period;
   int      app_price;
   int      rule_type;
   double   rule_value;
   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];
};

Рассмотрим каждый из добавленных в нее элементов:

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

SIGNAL            m_signal_set[5];

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

   bool              SaveSignalSet(int index);
   bool              LoadSignalSet(int index);

И реализуем их:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::SaveSignalSet(int index)
{
//---
   int h=FileOpen("Signal Monitor\\signal_"+string(index)+".bin",FILE_WRITE|FILE_BIN);
   if(h==INVALID_HANDLE)
   {
      MessageBox("Не удалось создать файл конфигурации","Signal Monitor");
      return(false);
   }
//--- Сохраняем выбор
   //--- Тип индикатора
   m_signal_set[index].ind_type=m_indicator_type.GetListViewPointer().SelectedItemIndex();
   //--- Период индикатора
   m_signal_set[index].ind_period=(int)m_period_edit.GetValue();
   //--- Тип применяемой цены
   m_signal_set[index].app_price=m_applied_price.GetListViewPointer().SelectedItemIndex();
   //--- Тип правила
   m_signal_set[index].rule_type=m_rule_type.GetListViewPointer().SelectedItemIndex();
   //--- Значение правила
   m_signal_set[index].rule_value=(double)m_rule_value.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)
   {
      MessageBox("Не выбран ни один таймфрейм","Signal Monitor");
      FileClose(h);
      return(false);
   }
//---
   FileWriteStruct(h,m_signal_set[index]);
   FileClose(h);
   Print("Конфигурация signal_"+string(index)+" успешно сохранена");
//---
   return(true);
}
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
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);
//--- Загрузка периода индикатора
   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);
   }
//--- Загрузка правила сигнала
   m_rule_type.SelectItem(m_signal_set[index].rule_type);
   m_rule_type.GetButtonPointer().Update(true);
   m_rule_value.SetValue((string)m_signal_set[index].rule_value);
   m_rule_value.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);
}

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

CButton           m_signal_editor[5];

А также метод, которых будет их создавать.

bool              CreateSignalEditor(CButton &button,string text,const int x_gap,const int y_gap);

Реализуем этот метод в файле StepWindow.mqh, так как эти объекты будут относится к главному окну.

//+------------------------------------------------------------------+
//| Создаёт кнопку с картинкой                                       |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Icons\\bmp16\\settings_light.bmp"

bool CProgram::CreateSignalEditor(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_step_window);
//--- Установим свойства перед созданием
   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(0,button);
   return(true);
}

И добавим с помощью этого метода 5 объектов в тело CreateStepWindow(), которые будут объектами в списке сигналов.

//---
   for(int i=0; i<5; i++)
   {
      if(!CreateSignalEditor(m_signal_editor[i],"Signal_"+string(i),10,40*i+90))
         return(false);
   }

И для того, чтобы эти элементы после создания не показывались при запуске приложения, их нужно в методе 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);
//--- Завершение создания GUI
   CWndEvents::CompletedGUI();
   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();
   return(true);
}

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::ClearSaves(void)
{
   for(int i=0; i<5; i++)
      FileDelete("Signal Monitor\\signal_"+string(i)+".bin");
   m_total_signals=0;
   return(true);
}

Теперь дополним обработку события нажатие на кнопку Add Signal(Добавить сигнал):

//--- Нажатие на кнопку Добавить сигнал
      if(lparam==m_add_signal.Id())
      {
         m_set_window.OpenWindow();
         m_number_signal=-1;
         RebuildTimeframes();
         m_new_signal.LabelText("Add");
         m_new_signal.Update(true);
      }

После компиляции проекта механизм добавления нового торгового сигнала готов. Следующим шагом будет реализация редактирования уже созданного сигнала.

Рис.5 Добавление нового торгового сигнала.

Подытожим. На данный момент, как показано на рис.5, реализована возможность по нажатию на кнопку Add Signal добавлять торговые сигналы. Также видно, что добавляется в список Signal List кнопка с названием нового сигнала. В данный момент это заданное название без возможности редактирования. Однако по нажатию на Signal_0 ничего не происходит, это и нужно исправить. Чтобы заново открывалось окошко с настройкам, при этом загружало в интерфейс именно те настройки, которые были сохранены для выбранного сигнала ранее. И второй момент — можно было изменить загруженные настройки и снова их сохранить.

Для этого зайдем в тело метода OnEvent() в базовом классе CProgram и найдем секцию события по нажатию кнопки и в нем добавим код:

//---
      for(int i=0; i<5; i++)
      {
         if(lparam==m_signal_editor[i].Id())
         {
            LoadSignalSet(i);
            m_new_signal.LabelText("Save");
            m_new_signal.Update(true);
            m_set_window.OpenWindow();
            m_number_signal=i;
         }
      }

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


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

Рис.6 Кнопка создания монитора торговых сигналов.

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

//--- Скрываем шаг 3
   m_add_signal.Hide();

   m_signal_header.Hide();
   m_back_button.Hide();
   m_next_button.Hide();
   for(int i=0; i<5; i++)
      m_signal_editor[i].Hide();

Так как у нас появились объекты-кнопки позволяющие отображать и редактировать текущие созданные торговые сигналы, то при переходе на окно самого монитора необходимо их также скрыть, как и все элементы управления Шага 3 до этого.

В первой статье при разработке структуры приложения одним из элементов монитора был блок индикации, изображенный на рис.5 той статьи. Его роль в реальном времени отображать наличие одного из созданных до этого торговых сигналов. Поэтому первым шагом создадим объект, который будет использоваться как блок индикации. Для этого в класс CProgram добавим и реализуем метод CreateSignalButton(). 

bool              CreateSignalButton(CButton &button,const int x_gap,const int y_gap);

А также массив экземпляров класса CButton, необходимых для создания полного набора блоков индикации.

CButton           m_signal_button[];

Теперь зайдем в StepWindow.mqh и в конце файла добавим реализацию созданного метода:

//+------------------------------------------------------------------+
//| Создаёт блок индикации                                           |
//+------------------------------------------------------------------+
bool CProgram::CreateSignalButton(CButton &button,const int x_gap,const int y_gap)
{
//---
   color baseclr=C'220,225,235';
//--- Сохраним указатель на окно
   button.MainPointer(m_step_window);
//--- Установим свойства перед созданием
   button.TwoState(false);
   button.XSize(40);
   button.YSize(20);
   button.IconXGap(2);
   button.IconYGap(button.YSize()/2-8);
   button.LabelXGap(19);
   button.LabelYGap(2);
   button.FontSize(m_base_font_size);
   button.BackColor(baseclr);
   button.BackColorHover(baseclr);
   button.BackColorPressed(baseclr);
   button.BorderColor(baseclr);
   button.BorderColorHover(baseclr);
   button.BorderColorPressed(baseclr);
   button.LabelColor(clrBlack);
   button.LabelColorPressed(clrSlateGray);
   button.IconFile("");
   button.IconFileLocked("");
   button.IsDoubleBorder(true);
//--- Создадим элемент управления
   if(!button.CreateButton("",x_gap-button.XSize()/2,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(0,button);
   return(true);
}

И применим его сразу же в методе построения монитора ToMonitor(). Для этого в теле этого метода находим раздел Таймфреймы и следом за ним дополняем метод кодом. Должно получиться так:

//--- Таймфреймы
   int tf=ArraySize(m_timeframes);
   ArrayResize(m_timeframe_label,tf);
//---
   for(int i=0; i<tf; i++)
   {
      if(!CreateTimeframeLabel(m_timeframe_label[i],110+50*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()+25+j*25))
            return;
         m_signal_button[k].Update(true);
         k++;
      }
   }
//--- Изменяем размеры окна
   AutoResize(m_timeframe_label[tf-1].XGap()+m_timeframe_label[tf-1].XSize()+5,m_symbol_label[sy-1].YGap()+m_symbol_label[sy-1].YSize()+5);

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


Рис.7 Готовый макет торговых сигналов.

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

Для управления этими свойствами создадим в приватной секции нашего базового класса CProgram следующие методы:

   void              SetBorderColor(int index, color clr);
   void              SetLabel(int index, string text,color clr=clrBlack);
   void              SetIcon(int index,int number);
   void              SetBackground(int index,color clr);
   void              SetTooltip(int index,string text="\n");

И там же их реализуем.

//+------------------------------------------------------------------+
//| Установка цвета канта                                            |
//+------------------------------------------------------------------+
void CProgram::SetBorderColor(int index, color clr)
{
   m_signal_button[index].BorderColor(clr);
   m_signal_button[index].BorderColorHover(clr);
   m_signal_button[index].BorderColorPressed(clr);
   m_signal_button[index].Update(true);
}
//+------------------------------------------------------------------+
//| Установка текста метки                                           |
//+------------------------------------------------------------------+
void CProgram::SetLabel(int index, string text,color clr=clrBlack)
{
   m_signal_button[index].LabelColor(clr);
   m_signal_button[index].LabelColorHover(clr);
   m_signal_button[index].LabelColorPressed(clr);
   m_signal_button[index].LabelText(text);
   m_signal_button[index].Update(true);
}
//+------------------------------------------------------------------+
//| Установка заднего фона                                           |
//+------------------------------------------------------------------+
void CProgram::SetBackground(int index,color clr)
{
   m_signal_button[index].BackColor(clr);
   m_signal_button[index].BackColorHover(clr);
   m_signal_button[index].Update(true);
}
//+------------------------------------------------------------------+
//| Установка иконки                                                 |
//+------------------------------------------------------------------+
void CProgram::SetIcon(int index,int number)
{
   //---
   string image[]=
   {
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp",
      "Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp"
   };
   string path=(number>=0)?image[number]:"";
   if(number<0)
      m_signal_button[index].IsCenterText(true);
   else
      m_signal_button[index].IsCenterText(false);
   m_signal_button[index].IconFile(path);
   m_signal_button[index].IconFilePressed(path);
   m_signal_button[index].Update(true);
}
//+------------------------------------------------------------------+
//| Установка подсказки                                              |
//+------------------------------------------------------------------+
void CProgram::SetTooltip(int index,string text="\n")
{
   m_signal_button[index].Tooltip(text);
   m_signal_button[index].ShowTooltip(true);
}

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

   int               GetRow(int index,int row_size);
   int               GetCol(int index,int row_size);
//+------------------------------------------------------------------+
//| Определение ряда по индексу                                      |
//+------------------------------------------------------------------+
int CProgram::GetRow(int index,int row_size)
{
   return(int(MathFloor(index/row_size)+1));
}
//+------------------------------------------------------------------+
//| Определение столбца по индексу                                   |
//+------------------------------------------------------------------+
int CProgram::GetCol(int index,int row_size)
{
   return(int(MathMod(index,row_size)+1));
}

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

//+------------------------------------------------------------------+
//| Возвращает таймфрейм по строке                                   |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CProgram::StringToTimeframe(const string timeframe)
{
   if(timeframe=="M1")  return(PERIOD_M1);
   if(timeframe=="M2")  return(PERIOD_M2);
   if(timeframe=="M3")  return(PERIOD_M3);
   if(timeframe=="M4")  return(PERIOD_M4);
   if(timeframe=="M5")  return(PERIOD_M5);
   if(timeframe=="M6")  return(PERIOD_M6);
   if(timeframe=="M10") return(PERIOD_M10);
   if(timeframe=="M12") return(PERIOD_M12);
   if(timeframe=="M15") return(PERIOD_M15);
   if(timeframe=="M20") return(PERIOD_M20);
   if(timeframe=="M30") return(PERIOD_M30);
   if(timeframe=="H1")  return(PERIOD_H1);
   if(timeframe=="H2")  return(PERIOD_H2);
   if(timeframe=="H3")  return(PERIOD_H3);
   if(timeframe=="H4")  return(PERIOD_H4);
   if(timeframe=="H6")  return(PERIOD_H6);
   if(timeframe=="H8")  return(PERIOD_H8);
   if(timeframe=="H12") return(PERIOD_H12);
   if(timeframe=="D1")  return(PERIOD_D1);
   if(timeframe=="W1")  return(PERIOD_W1);
   if(timeframe=="MN")  return(PERIOD_MN1);
//--- Значение по умолчанию
   return(::Period());
}
//+------------------------------------------------------------------+
//| Определение таймфрейма                                           |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CProgram::GetTimeframe(int index)
{
   int tf=ArraySize(m_timeframes);
   return(StringToTimeframe((m_timeframe_label[GetCol(index,tf)-1].LabelText())));
}
//+------------------------------------------------------------------+
//| Определение символа                                              |
//+------------------------------------------------------------------+
string CProgram::GetSymbol(int index)
{
   int tf=ArraySize(m_timeframes);
   return(m_symbol_label[GetRow(index,tf)-1].LabelText());
}

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

bool              GetSignal(string sy,ENUM_TIMEFRAMES tf,SIGNAL &signal_set);

Для передачи настроек служит структура из набора параметров SIGNAL

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::GetSignal(string sy,ENUM_TIMEFRAMES tf,SIGNAL &signal_set)
{
//--- Получение хэндла индикатора
   int h=INVALID_HANDLE;
   ENUM_APPLIED_PRICE app_price;
   switch(signal_set.app_price)
   {
   case  0:
      app_price=PRICE_CLOSE;
      break;
   case  1:
      app_price=PRICE_OPEN;
      break;
   case  2:
      app_price=PRICE_HIGH;
      break;
   case  3:
      app_price=PRICE_LOW;
      break;
   case  4:
      app_price=PRICE_MEDIAN;
      break;
   case  5:
      app_price=PRICE_TYPICAL;
      break;
   case  6:
      app_price=PRICE_WEIGHTED;
      break;
   default:
      app_price=PRICE_CLOSE;
      break;
   }
//---
   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;
   default:
      break;
   }
   if(h==INVALID_HANDLE)
   {
      Print(sy+". Failed to get handle");
      Print("Handle = ",h,"  error = ",GetLastError());
      return(false);
   }
   //---
   double arr[1];
   if(CopyBuffer(h,0,
    0,1,arr)!=1)
   {
      Print("sy= ",sy,"tf= ",EnumToString(tf)," Failed to get handle data ",GetLastError());
      return(false);
   }
   IndicatorRelease(h);
//--- Проверка на условия
   double r_value=signal_set.rule_value;
   double c_value=arr[0];
   m_ind_value=c_value;
   int s=0;
   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;
   }
//---
   if(s>0)
      return(true);
   return(false);
}

Метод GetSignal() получает из структуры SIGNAL информацию о том, какой из предложенных индикаторов мы выбрали для поиска торгового сигнала, какие настройки для него выбрали, какое правило поиска задавали. Но не стоит забывать, что фильтрация по таймфреймам для каждого сигнала может происходить дважды. Первая на втором шаге настройки, а вторая из уже отобранных в окне создания торгового сигнала, как это показано на рис.8 чуть ниже.

Рис.8 Выбор таймфреймов для создаваемого сигнала.

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CheckTimeframe(ENUM_TIMEFRAMES tf,SIGNAL &signal_set)
{
   for(int i=0; i<21; i++)
   {
      if(StringToTimeframe(CharArrayToString(signal_set.tf_name[i].tf))==tf)
         return(true);
   }
   return(false);
}

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

bool              SearchSignals(void);

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::SearchSignals(void)
{
//--- Поиск установленных сигналов
   SIGNAL signal_set[];
   int cnt=0;
   for(int i=0; i<5; i++)
   {
      if(FileIsExist("Signal Monitor\\signal_"+string(i)+".bin"))
         cnt++;
   }
//---
   ArrayResize(signal_set,cnt);
   ZeroMemory(signal_set);
//---
   for(int i=0; i<cnt; i++)
   {
      int h=FileOpen("Signal Monitor\\signal_"+string(i)+".bin",FILE_READ|FILE_BIN);
      if(h==INVALID_HANDLE)
      {
         MessageBox("Конфигурация не найдена","Signal Monitor");
         return(false);
      }
      FileReadStruct(h,signal_set[i]);
      FileClose(h);
      for(int j=0; j<ArraySize(m_signal_button); j++)
      {
         //---
         string sy=GetSymbol(j);
         ENUM_TIMEFRAMES tf=GetTimeframe(j);
         //---
         if(!CheckTimeframe(tf,signal_set[i]))
            continue;
         //---
         if(GetSignal(sy,tf,signal_set[i]))
         {
            //---
            if(signal_set[i].label_type==1)
               SetLabel(j,CharArrayToString(signal_set[i].label_value),signal_set[i].label_color);
            else
               SetLabel(j,DoubleToString(m_ind_value,3),signal_set[i].label_color);
            //---
            if(signal_set[i].back_color!=clrNONE)
               SetBackground(j,signal_set[i].back_color);
            //---
            if(signal_set[i].border_color!=clrNONE)
               SetBorderColor(j,signal_set[i].border_color);
            else
               SetBorderColor(j,signal_set[i].back_color);
            //---
            if(signal_set[i].tooltip)
               SetTooltip(j,CharArrayToString(signal_set[i].tooltip_text));
            //---
            if(signal_set[i].image)
               SetIcon(j,signal_set[i].img_index);
            else
               SetIcon(j,-1);
         }
      }
   }
   return(true);
}

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

...
//--- Изменяем размеры окна
   AutoResize(m_timeframe_label[tf-1].XGap()+m_timeframe_label[tf-1].XSize()+5,m_symbol_label[sy-1].YGap()+m_symbol_label[sy-1].YSize()+5);
//---
   SearchSignals();
}

Осталось придумать алгоритм повторных поисков через интервал времени. Для этого перейдем в файл SignalMonitor.mq5 и создадим в начала файла перечисление:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
enum UPDATE
{
   MINUTE,        // 1 Минута
   MINUTE_15,     // 15 Минут
   MINUTE_30,     // 30 Минут
   HOUR,          // 1 Час
   HOUR_4         // 4 Часа
};

Теперь легко можно добавить во входные параметры новую настройку:

input UPDATE               Update            =  HOUR;                // Интервал обновления

Далее создадим две переменные для расчетов.

int cnts=0;
datetime update;

И в инициализации эксперта добавим следующие строчки:

//---
   switch(Update)
   {
   case MINUTE:
      cnts=60;
      break;
   case MINUTE_15:
      cnts=60*15;
      break;
   case MINUTE_30:
      cnts=60*30;
      break;
   case HOUR:
      cnts=3600;
      break;
   case HOUR_4:
      cnts=3600*4;
      break;
   default:
      cnts=1;
      break;
   }
   update=TimeLocal()+cnts;

Тем самым определив интервал обновления и задав следующее время обновления. И теперь в теле функции OnTick() добавляем проверку по времени, если заданный интервал времени пройдет, то заново ищем установленные торговые сигналы.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   if(TimeLocal()>update)
   {
      program.SearchSignals();
      update=TimeLocal()+cnts;
   }
}

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

 

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


Заключение

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


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