Мультивалютный мониторинг торговых сигналов (Часть 5): Составные сигналы

29 апреля 2020, 08:37
Alexander Fedosov
0
1 961

Содержание

Введение

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

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

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


Составные сигналы

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

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

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

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


Рис. 1 Добавление элементов пользовательского интерфейса

Левая часть рис.1 нам уже знакома и представляет собой кнопку добавления сигнала, а также ниже список уже добавленных простых сигналов. Теперь предстоит создать кнопку добавления для составного(1), список добавленных составных сигналов(2), а также третью группу переключаемых кнопок(3), необходимых для назначения простым сигналам признака применения:

  • S(Simple) — Простой сигнал. В мониторе используется как обособленный и его нельзя будет выбрать в построении составного сигнала.
  • C(Composite) — Простой сигнал, применяемый только при построении составного. Не используется в мониторе как обособленный сигнал.
  • B(Both) — Оба применения. Такой сигнал как ищется в мониторе как отдельный, так и предоставляет возможность использовать его в качестве элемента составного сигнала.

Ниже на рис.2 представлена схема инструмента построения составного сигнала. Элементы с названиями Simple 1 и Simple 2 — это список уже созданных простых сигналов, которые могут участвовать в сборке более сложного. Возвращаясь к признакам использования простых сигналов — у заданных на рис.2 должна стоять метка "С" или "В", иначе бы мы их не наблюдали в списке.

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

Далее идет секция под названием Rule(Правило), и тут происходит построение торгового сигнала. В каждый из трех слотов можно добавить сигнал из списка выше и поставить между ними переключаемыми кнопками нужные логические операторы, тем самым создавая правило данного составного сигнала. Например, если поставить Simple 1 AND Simple 2, то составной сигнал в мониторе будет отображаться лишь при одновременном наличии обоих простых сигналов в данный момент времени.


Реализация функционала

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


Рис.3 Изменение элемента управления редактированием сигнала

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

#define SIMPLE_SIGNALS 10
#define COMPOSITE_SIGNALS 5

 Далее переходим в конец тела метода создания главного окна CreateStepWindow() и вместо кода:

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

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

   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      if(!CreateLabel(m_signal_label[i],10,40*i+95,"Signal_"+string(i)))
         return(false);
      if(!CreateSignalSet(m_signal_ind[i],"Edit",C'255,195,50',150,40*i+90))
         return(false);
      if(!CreateSignalSet(m_signal_type[i],"S",C'75,190,240',150+35,40*i+90))
         return(false);
   }

Здесь мы видим два новых массива экземпляров класса CButton m_signal_ind[] и m_signal_type[], а также массив CTextLabel m_signal_label[]. Добавим их в базовый класс проекта CProgram.

   CTextLabel        m_signal_label[SIMPLE_SIGNALS];
   CButton           m_signal_ind[SIMPLE_SIGNALS];
   CButton           m_signal_type[SIMPLE_SIGNALS];

А также объявим и реализуем новый метод CreateSignalSet().

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateSignalSet(CButton &button,string text,color baseclr,const int x_gap,const int y_gap)
{
//--- Сохраним указатель на окно
   button.MainPointer(m_step_window);
//--- Установим свойства перед созданием
   button.XSize(30);
   button.YSize(30);
   button.Font(m_base_font);
   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(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);
}

Этими действиями мы сформировали новый список простых сигналов с обновленным интерфейсом взаимодействия из рис.3. Несложным добавлением переменных m_c_signal_label[] и m_c_signal_ind[] в базовый класс, а также в метод CreateStepWindow() следующего ниже кода добавим список составных сигналов.

CTextLabel        m_c_signal_label[COMPOSITE_SIGNALS];
CButton           m_c_signal_ind[COMPOSITE_SIGNALS];
//---
   for(int i=0; i<COMPOSITE_SIGNALS; i++)
   {
      if(!CreateLabel(m_c_signal_label[i],300,40*i+95,"Signal_"+string(i)))
         return(false);
      if(!CreateSignalSet(m_c_signal_ind[i],"Edit",C'255,195,50',150+290,40*i+90))
         return(false);
   }

Теперь после пересоздания инструмента отображения и редактирования простых и составных сигналов необходимо на Шаге 3 первичной настройки приложения добавить кнопку создания составных сигналов и текстовый заголовок. Для этого переменную m_add_signal, которая является экземпляром класса CButton и используется в добавлении кнопки AddSignal(Добавить сигнал), превратим в статичный массив m_add_signal[2]. Заменим код в теле метода CreateStepWindow()

   if(!CreateIconButton(m_add_signal,m_lang[15],10,30))
      return(false);

на новый с добавлением кнопки создания составного сигнала:

   if(!CreateIconButton(m_add_signal[0],m_lang[15],10,30))
      return(false);
   if(!CreateIconButton(m_add_signal[1],m_lang[43],300,30))
      return(false);

Чтобы отобразить заголовки списков простых и составных сигналов, необходимо переменную m_signal_header сделать статичным массивом m_signal_header[2]. То есть текущий код:

   if(!CreateLabel(m_signal_header,10,30+30+10,m_lang[16]))
      return(false);

Заменить на новый с двумя заголовками:

   if(!CreateLabel(m_signal_header[0],10,30+30+10,m_lang[16]))
      return(false);
   if(!CreateLabel(m_signal_header[1],300,30+30+10,m_lang[44]))
      return(false);

После этих изменений на Шаге 3 получится следующее обновление интерфейса:

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

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

Поэтому найдем фрагмент кода, отвечающий за добавление нового простого торгового сигнала, дополним его и всё это перенесем в новый метод AddSimpleSignal():

//+------------------------------------------------------------------+
//| Добавляет новый простой торговый сигнал                          |
//+------------------------------------------------------------------+
void CProgram::AddSimpleSignal(long lparam)
{
   if(lparam==m_new_signal.Id())
   {
      if(m_number_signal<0)
      {
         if(SaveSignalSet(m_total_signals))
         {
            m_set_window.CloseDialogBox();
            if(m_total_signals<SIMPLE_SIGNALS)
            {
               m_total_signals++;
               m_signal_label[m_total_signals-1].Show();
               m_signal_label[m_total_signals-1].LabelText(m_signal_name.GetValue());
               m_signal_label[m_total_signals-1].Update(true);
               m_signal_type[m_total_signals-1].Show();
               m_signal_ind[m_total_signals-1].Show();
            }
            else
               MessageBox("Maximum number of signals is "+string(SIMPLE_SIGNALS),"Signal Monitor");
            //---
            if(m_total_signals>1)
            {
               m_add_signal[1].IsLocked(false);
               m_add_signal[1].Update(true);
            }
         }
      }
      else
      {
         if(SaveSignalSet(m_number_signal,false))
         {
            m_set_window.CloseDialogBox();
            m_signal_label[m_number_signal].LabelText(m_signal_name.GetValue());
            m_signal_label[m_number_signal].Update(true);
         }
      }
   }
}

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

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

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

uchar             signal_name[50];

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

bool              SaveSignalSet(int index,bool first_save=true);

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

bool CProgram::SaveSignalSet(int index,bool first_save=true)
{
//---
   if(first_save && !CheckSignalNames(m_signal_name.GetValue()))
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Это имя уже используется","Монитор сигналов");
      else
         MessageBox("This name is already in use","Signal Monitor");
      return(false);
   }
//---
   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>SIMPLE_SIGNALS-1)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Максимальное число сигналов не должно быть больше "+string(SIMPLE_SIGNALS),"Монитор сигналов");
      else
         MessageBox("Maximum number of signals is "+string(SIMPLE_SIGNALS),"Signal Monitor");
      return(false);
   }
//--- Сохраняем выбор
//--- Имя индикатора
   if(m_signal_name.GetValue()=="")
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Введите Имя Сигнала","Монитор сигналов");
      else
         MessageBox("Enter the Signal Name","Signal Monitor");
      FileClose(h);
      return(false);
   }
   else if(StringLen(m_signal_name.GetValue())<3)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Имя Сигнала должно быть не менее 3 букв","Монитор сигналов");
      else
         MessageBox("Signal Name must be at least 3 letters","Signal Monitor");
      FileClose(h);
      return(false);
   }
   else
      StringToCharArray(m_signal_name.GetValue(),m_signal_set[index].signal_name);
//--- Тип индикатора
   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("Конфигурация "+m_signal_name.GetValue()+" успешно сохранена");
//---
   return(true);
}

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CheckSignalNames(string name)
{
//--- Поиск установленных сигналов
   SIGNAL signal_set[];
   int cnt=0;
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      if(FileIsExist("Signal Monitor\\signal_"+string(i)+".bin"))
         cnt++;
   }
   if(cnt<1)
      return(true);
   //---
   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]);
      if(CharArrayToString(m_signal_set[i].signal_name)==name)
      {
         FileClose(h);
         return(false);
      }
      FileClose(h);
   }
   return(true);
}

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

Рис.5 Новый формат созданных простых торговых сигналов 

Для возможности редактирования простых сигналов создадим новый метод EditSimpleSignal() и вызовем его в обработчике событий OnEvent()

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::EditSimpleSignal(long lparam)
{
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      if(lparam==m_signal_ind[i].Id())
      {
         LoadSignalSet(i);
         m_new_signal.LabelText(m_lang[38]);
         m_new_signal.Update(true);
         m_set_window.OpenWindow();
         m_number_signal=i;
      }
   }
}

Аналогично, для переключения признака применения — создаем новый метод SignalSwitch() и вызываем в обработчике.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::SignalSwitch(long lparam)
{
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      //---
      if(lparam==m_signal_type[i].Id())
      {
         if(m_signal_type[i].LabelText()=="S")
            SetButtonParam(m_signal_type[i],"C",clrMediumOrchid);
         else if(m_signal_type[i].LabelText()=="C")
            SetButtonParam(m_signal_type[i],"B",C'240,120,0');
         else if(m_signal_type[i].LabelText()=="B")
            SetButtonParam(m_signal_type[i],"S",C'75,190,240');
      }
   }
}

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

Рис.6 Визуальное переключение признака применения простого торгового сигнала

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

Ранее мы уже создали кнопку добавления нового составного сигнала Add Composite Signal. Теперь необходимо реализовать диалоговое окно с возможностью создания, настройки и редактирования ссотавных сигналов. Напомню, что при создании окна редактирования простого сигнала мы создавали включаемый файл SetWindow.mqh. Поэтому, по аналогии, создадим файл CSetWindow.mqh и в нем будем, согласно намеченному плану из рис.2, создавать интерфейс будущего редактора составных сигналов. В созданном только что файле подключим Program.mqh для доступа к базовому классу CProgram.

//+------------------------------------------------------------------+
//|                                                   CSetWindow.mqh |
//|                                Copyright 2020, Alexander Fedosov |
//|                           https://www.mql5.com/ru/users/alex2356 |
//+------------------------------------------------------------------+
#include "Program.mqh"

А в файле Program.mqh подключим CSetWindow.mqh к уже имеющимся элементам управления:

//+------------------------------------------------------------------+
//| Добавление элементов управления                                  |
//+------------------------------------------------------------------+
#include "MainWindow.mqh"
#include "SetWindow.mqh"
#include "StepWindow.mqh"
#include "CSetWindow.mqh"

Для создания диалогового окна введем новый метод в базовом классе CreateCompositeEdit() и реализуем его в недавно созданном подключаемом файле.

protected:
   //--- Формы
   bool              CreateStepWindow(const string caption_text);
   bool              CreateSetWindow(const string caption_text);
   bool              CreateColorWindow(const string caption_text);
   bool              CreateFastEdit(const string caption_text);
   bool              CreateCompositeEdit(const string caption_text);
//+------------------------------------------------------------------+
//| Создает окно создания и редактирования составных сигналов        |
//+------------------------------------------------------------------+
bool CProgram::CreateCompositeEdit(const string caption_text)
{
//--- Добавим указатель окна в массив окон
   CWndContainer::AddWindow(m_composite_edit);
//--- Свойства
   m_composite_edit.XSize(590);
   m_composite_edit.YSize(590);
//--- Координаты
   int x=10;
   int y=10;
//---
   m_composite_edit.CaptionHeight(22);
   m_composite_edit.IsMovable(true);
   m_composite_edit.CaptionColor(m_caption);
   m_composite_edit.CaptionColorLocked(m_caption);
   m_composite_edit.CaptionColorHover(m_caption);
   m_composite_edit.BackColor(m_background);

   m_composite_edit.FontSize(m_base_font_size);
   m_composite_edit.Font(m_base_font);
   m_composite_edit.WindowType(W_DIALOG);
//--- Создание формы
   if(!m_composite_edit.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
   return(true);
}

Теперь добавим его вызов его в основном методе создания интерфейса.

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

Так как созданное окно является диалоговом, то оно должно открываться по определенном событию, в нашем случае это по нажатию на кнопку Add Composite Signal (Добавить составной сигнал). Чтобы это реализовать создадим метод OpenCompositeSignalEditor() и для демонстрации пропишем в его теле пока что только одну команду — открыть диалоговое окно. Само собой, его необходимо вызвать в обработчике событий.

void CProgram::OpenCompositeSignalEditor(long lparam)
{
   if(lparam==m_add_signal[1].Id())
   {
      m_composite_edit.OpenWindow();
   }
}

На рис.7 результат добавления диалогового окна и открытие его по нажатию на кнопку.

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

Теперь предстоит наполнить диалоговое окно элементами интерфейса показаные на рис.2, а именно:

  • Поле ввода имени нового составного сигнала.
  • Список доступных простых сигналов для создания составного.
  • Интерфейс создания логического правила составного сигнала.

Все новые методы будем добавлять в базовом классе CProgram, реализовывать в файле CSetWindow.mqh, и в нем же в теле метода созданного диалогового окна применять. Первым создадим метод поля ввода CreateCSignalName(), реализуем его и добавим к диалоговом окну.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateCSignalName(CTextEdit &text_edit,const int x_gap,const int y_gap)
{
//--- Сохраним указатель на главный элемент
   text_edit.MainPointer(m_composite_edit);
//--- Свойства
   text_edit.XSize(110);
   text_edit.YSize(24);
   text_edit.Font(m_base_font);
   text_edit.FontSize(m_base_font_size);
   text_edit.GetTextBoxPointer().XGap(110);
   text_edit.GetTextBoxPointer().XSize(200);
   text_edit.GetTextBoxPointer().DefaultTextColor(clrSilver);
   text_edit.GetTextBoxPointer().DefaultText(m_lang[45]);
//--- Создадим элемент управления
   if(!text_edit.CreateTextEdit(m_lang[45],x_gap,y_gap))
      return(false);
//--- Добавим объект в общий массив групп объектов
   CWndContainer::AddToElementsArray(4,text_edit);
   return(true);
}
Теперь с помощью нового метода CreateSimpleSignal() создадим набор кнопок с двумя состояниями. Они будут выполнять функцию выбора простых сигналов для добавление в слоты(рис.2) при формировании составного.

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateSimpleSignal(CButton &button,const int x_gap,const int y_gap)
{
//---
   color baseclr=clrDodgerBlue;
   color pressed=clrCrimson;
//--- Сохраним указатель на окно
   button.MainPointer(m_composite_edit);
//--- Установим свойства перед созданием
   button.XSize(110);
   button.YSize(40);
   button.Font(m_base_font);
   button.FontSize(m_base_font_size);
   button.BackColor(baseclr);
   button.BackColorHover(baseclr);
   button.BackColorPressed(pressed);
   button.BackColorLocked(clrWhiteSmoke);
   button.BorderColor(baseclr);
   button.BorderColorHover(baseclr);
   button.BorderColorPressed(pressed);
   button.BorderColorLocked(clrWhiteSmoke);
   button.LabelColor(clrWhite);
   button.LabelColorPressed(clrWhite);
   button.LabelColorHover(clrWhite);
   button.LabelColorLocked(clrWhiteSmoke);
   button.IsCenterText(true);
   button.TwoState(true);
//--- Создадим элемент управления
   if(!button.CreateButton(" — ",x_gap,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(4,button);
   return(true);
}

Далее дополним метод CreateCompositeEdit() и проверим что получилось на данном этапе.

....
//--- Имя сигнала
   if(!CreateCSignalName(m_c_signal_name,10,40))
      return(false);
//--- Создание списка выбора простых сигналов
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      if(i<5)
      {
         if(!CreateSimpleSignal(m_simple_signal[i],10+115*i,90))
            return(false);
      }
      else
      {
         if(!CreateSimpleSignal(m_simple_signal[i],10+115*(i-5),90+45))
            return(false);
      }
   }
...

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

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

Следующий шагом предстоит применить к этому шаблону следующую логику:

  • При нажатии на кнопку Add Composite Signal(Добавить составной сигнал) нужно проверить, чтобы в наличие было хотя бы два созданных сигнала.
  • Мало того, нужно чтобы в списке уже созданных простых сигналов было как минимум два, имеющие признак применения либо C(Composite) или B(Both).
  • Если условия выше выполняются, то показываем, применимо к созданному шаблону переключаемых кнопок, доступные простые сигналы для формировании составного.

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::LoadForCompositeSignal(void)
{
   int cnt=0;
//---
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      m_simple_signal[i].IsLocked(true);
      m_simple_signal[i].Update(true);
   }
//--- Проверка на наличие минимум двух сигналов с нужным признаком
   for(int i=0; i<SIMPLE_SIGNALS; i++)
      if(m_signal_label[i].IsVisible() && (m_signal_type[i].LabelText()=="C" || m_signal_type[i].LabelText()=="B"))
         cnt++;
//---
   if(cnt<2)
      return(false);
   else
      cnt=0;
//---
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      if(m_signal_label[i].IsVisible() && (m_signal_type[i].LabelText()=="C" || m_signal_type[i].LabelText()=="B"))
      {
         m_simple_signal[cnt].IsLocked(false);
         cnt++;
      }
   }
//---
   cnt=0;
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      if(m_signal_label[i].IsVisible() && (m_signal_type[i].LabelText()=="C" || m_signal_type[i].LabelText()=="B"))
      {
         m_simple_signal[cnt].LabelText(m_signal_label[i].LabelText());
         m_simple_signal[cnt].Update(true);
         cnt++;
      }
   }
   return(true);
}

А применить его стоит в ранее созданном методе OpenCompositeSignalEditor():

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::OpenCompositeSignalEditor(long lparam)
{
   if(lparam==m_add_signal[1].Id())
   {
      if(!LoadForCompositeSignal())
      {
         if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
            MessageBox("Необходимо минимум два простых сигнала для создания составного!","Внимание");
         else
            MessageBox("You need at least two simple signals to create a composite!","Warning");
      }
      else
      {
         m_composite_edit.X(m_add_signal[1].X());
         m_composite_edit.Y(m_add_signal[1].Y()+40);
         m_composite_edit.OpenWindow();
         //---
         m_c_signal_name.SetValue("");
         m_c_signal_name.Update(true);
         //--- Очистка выбора сигналов
         for(int i=0; i<SIMPLE_SIGNALS; i++)
         {
            m_simple_signal[i].IsPressed(false);
            m_simple_signal[i].Update(true);
         }
      }
   }
}

В результате получим(рис.9) правильную выборку подходящих по условиям сигналов. Видно что простой сигнал с именем Test 2 не отобран для работы по созданию составного сигнала, потому что имеет признак использования S(Simple).

Рис.9 Пример отбора простых сигналов для добавления их в составной

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

  • Слоты. Три слота в которые будет возможность помещать отобранные простые сигналы.
  • Логические операторы. Две переключаемые кнопки с тремя состояниями AND,OR и (OR).

Здесь стоит понимать разницу между двумя похожими логическими операторами — OR и (OR). Известно, что логический оператор ИЛИ в применении с оператором И может трактоваться по разному — если он используются в паре, то есть связывают три выражения. На рис.10 представлен один такой пример, где в зависимости от скобок результат логического выражения может быть разным.


Рис.10 Отличие оператора OR от (OR)

Добавим слоты и логические операторы в окно создания и редактирования составных сигналов. Для создания слотов реализуем метод CreateRuleSlot():

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateRuleSlot(CButton &button,string text,const int x_gap,const int y_gap)
{
//---
   //color baseclr=C'70,180,70';
   color baseclr=clrLightSteelBlue;
//--- Сохраним указатель на окно
   button.MainPointer(m_composite_edit);
//--- Установим свойства перед созданием
   button.XSize(110);
   button.YSize(40);
   button.Font(m_base_font);
   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(clrWhite);
   button.LabelColorPressed(clrWhite);
   button.LabelColorHover(clrWhite);
   button.IsCenterText(true);
//--- Создадим элемент управления
   if(!button.CreateButton(text,x_gap,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(4,button);
   return(true);
}

А для логических операторов создадим метод CreateRuleSelector():

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CreateRuleSelector(CButton &button,const int x_gap,const int y_gap)
{
//---
   color baseclr=C'75,190,240';
//--- Сохраним указатель на окно
   button.MainPointer(m_composite_edit);
//--- Установим свойства перед созданием
   button.XSize(40);
   button.YSize(40);
   button.Font(m_base_font);
   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(clrWhite);
   button.LabelColorPressed(clrWhite);
   button.LabelColorHover(clrWhite);
   button.IsCenterText(true);
//--- Создадим элемент управления
   if(!button.CreateButton("AND",x_gap,y_gap))
      return(false);
//--- Добавим указатель на элемент в базу
   CWndContainer::AddToElementsArray(4,button);
   return(true);
}

Добавим оба метода к реализации диалогового окна в метод CreateCompositeEdit().

...
//--- Заголовок Правило
   if(!CreateSetLabel1(m_c_set_header[0],10,90+45*2,"1."+m_lang[24]))
      return(false);
//--- Создание слотов для создания составного сигнала
   for(int i=0; i<3; i++)
      if(!CreateRuleSlot(m_rule_element[i],"Slot "+string(i+1),57+10+173*i,int(90+45*2.5)))
         return(false);
//--- Создание логических операторов
   for(int i=0; i<2; i++)
      if(!CreateRuleSelector(m_rule_selector[i],189+173*i,int(90+45*2.5)))
         return(false);
...

На данном этапе визуальная часть секции построения логики составного торгового сигнала выглядит так:

Рис.11 Визуальная часть интерфейса формирования логики составного сигнала

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::LogicSwitch(long lparam)
{
   for(int i=0; i<2; i++)
   {
      if(lparam==m_rule_selector[i].Id())
      {
         if(m_rule_selector[i].LabelText()=="OR")
            SetButtonParam(m_rule_selector[i],"(OR)",clrTomato);
         else if(m_rule_selector[i].LabelText()=="(OR)")
            SetButtonParam(m_rule_selector[i],"AND",C'75,190,240');
         else if(m_rule_selector[i].LabelText()=="AND")
            SetButtonParam(m_rule_selector[i],"OR",clrOrangeRed);
      }
   }
}

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::SignalSelection(long lparam)
{
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      if(lparam==m_simple_signal[i].Id())
      {
         //--- Если сигнал выбран
         if(m_simple_signal[i].IsPressed())
         {
            //--- Ищем первый свободный слот
            for(int j=0; j<3; j++)
            {
               if(m_rule_element[j].BackColor()==clrLightSteelBlue)
               {
                  SetButtonParam(m_rule_element[j],m_simple_signal[i].LabelText(),clrDodgerBlue);
                  break;
               }
            }
         }
         //--- Если сигнал не выбран
         else
         {
            for(int j=0; j<3; j++)
            {
               if(m_rule_element[j].LabelText()==m_simple_signal[i].LabelText())
               {
                  SetButtonParam(m_rule_element[j],"Slot "+string(j+1),clrLightSteelBlue);
                  break;
               }
            }
         }
      }
   }
}

На рис.12 это выглядит более наглядно, чем описывалось чуть выше. 

Рис.12 Пример построения составного торгового сигнала из простых

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

Рис.13 Финальная версия окна создания составного торгового сигнала

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::SaveCompositeSignalSet(int index,bool first_save=true)
{
//---
   if(first_save && !CheckCSignalNames(m_c_signal_name.GetValue()))
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Это имя уже используется","Монитор сигналов");
      else
         MessageBox("This name is already in use","Signal Monitor");
      return(false);
   }
//---
   int h=FileOpen("Signal Monitor\\c_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>COMPOSITE_SIGNALS-1)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Максимальное число сигналов не должно быть больше "+string(COMPOSITE_SIGNALS),"Монитор сигналов");
      else
         MessageBox("Maximum number of signals is "+string(COMPOSITE_SIGNALS),"Signal Monitor");
      return(false);
   }
//--- Сохраняем выбор
//--- Имя индикатора
   if(m_c_signal_name.GetValue()=="")
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Введите Имя Сигнала","Монитор сигналов");
      else
         MessageBox("Enter the Signal Name","Signal Monitor");
      FileClose(h);
      return(false);
   }
   else if(StringLen(m_c_signal_name.GetValue())<3)
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Имя Сигнала должно быть не менее 3 букв","Монитор сигналов");
      else
         MessageBox("Signal Name must be at least 3 letters","Signal Monitor");
      FileClose(h);
      return(false);
   }
   else
      StringToCharArray(m_c_signal_name.GetValue(),m_c_signal_set[index].signal_name);
//--- Значения слотов
   if(!CheckCorrectSlots(m_rule_element[0].LabelText(),m_rule_element[1].LabelText(),m_rule_element[2].LabelText()))
   {
      if(TerminalInfoString(TERMINAL_LANGUAGE)=="Russian")
         MessageBox("Неверно установлено правило","Монитор сигналов");
      else
         MessageBox("Invalid rule","Signal Monitor");
      FileClose(h);
      return(false);
   }
   StringToCharArray(m_rule_element[0].LabelText(),m_c_signal_set[index].slot_1);
   StringToCharArray(m_rule_element[1].LabelText(),m_c_signal_set[index].slot_2);
   StringToCharArray(m_rule_element[2].LabelText(),m_c_signal_set[index].slot_3);
//--- Значение логических операторов
   for(int i=0; i<2; i++)
   {
      if(m_rule_selector[i].LabelText()=="AND")
         m_c_signal_set[index].logics[i]=1;
      else if(m_rule_selector[i].LabelText()=="OR")
         m_c_signal_set[index].logics[i]=2;
      else if(m_rule_selector[i].LabelText()=="(OR)")
         m_c_signal_set[index].logics[i]=3;
   }
//--- Значение текстовой метки
   StringToCharArray(StringSubstr(m_c_text_box.GetValue(),0,3),m_c_signal_set[index].label_value);
//--- Цвет текстовой метки
   m_c_signal_set[index].label_color=m_c_color_button[0].CurrentColor();
//--- Цвет заднего фона
   if(m_c_set_param[0].IsPressed())
      m_c_signal_set[index].back_color=m_c_color_button[1].CurrentColor();
   else
      m_c_signal_set[index].back_color=clrNONE;
//--- Цвет канта
   if(m_c_set_param[1].IsPressed())
      m_c_signal_set[index].border_color=m_c_color_button[2].CurrentColor();
   else
      m_c_signal_set[index].border_color=clrNONE;
//--- Значение подсказки
   m_c_signal_set[index].tooltip=m_c_set_param[2].IsPressed();
   if(m_c_signal_set[index].tooltip)
      StringToCharArray(m_c_tooltip_text.GetValue(),m_c_signal_set[index].tooltip_text);
//--- Выбранное изображение
   m_c_signal_set[index].image=m_c_set_param[3].IsPressed();
   if(m_c_signal_set[index].image)
      m_c_signal_set[index].img_index=m_c_pictures_slider.GetRadioButtonsPointer().SelectedButtonIndex();
//---
   FileWriteStruct(h,m_c_signal_set[index]);
   FileClose(h);
   Print("Конфигурация "+m_c_signal_name.GetValue()+" успешно сохранена");
//---
   return(true);
}

Однако, если сейчас скомпилировать проект, то появятся три основные ошибки: это отсутствие переменной m_c_signal_set, а также двух методов проверки — CheckCSignalNames() и CheckCorrectSlots(). Типом переменной будет новая структура C_SIGNAL, созданная для хранения в ней набора настроек составного сигнала:

struct C_SIGNAL
{
   uchar             slot_1[50];
   uchar             slot_2[50];
   uchar             slot_3[50];
   int               logics[2];
   uchar             signal_name[50];
   uchar             label_value[10];
   color             label_color;
   color             back_color;
   color             border_color;
   bool              tooltip;
   uchar             tooltip_text[100];
   bool              image;
   int               img_index;
};

Метод CheckCSignalNames() очень схож с методом CheckSignalNames() и также проверяет введенное имя составного сигнала на уникальность. А вот новый метод CheckCorrectSlots() проверяет целостность и корректность созданной логической конструкции составного сигнала:

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::CheckCorrectSlots(string name1,string name2,string name3)
{
   bool slot1=(name1=="Slot 1")?true:false;
   bool slot2=(name2=="Slot 2")?true:false;
   bool slot3=(name3=="Slot 3")?true:false;
   int cnt=0;
   //---
   if(slot1)
      return(false);
   if(slot2 && !slot3)
      return(false);
   //---
   if(!slot1)
      cnt++;
   if(!slot2)
      cnt++;
   if(!slot3)
      cnt++;
   if(cnt<2)
      return(false);
   return(true);
}

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::AddCompositeSignal(long lparam)
{
   if(lparam==m_c_new_signal.Id())
   {
      if(m_c_number_signal<0)
      {
         if(SaveCompositeSignalSet(m_c_total_signals))
         {
            m_composite_edit.CloseDialogBox();
            if(m_c_total_signals<COMPOSITE_SIGNALS)
            {
               m_c_total_signals++;
               m_c_signal_label[m_c_total_signals-1].Show();
               m_c_signal_label[m_c_total_signals-1].LabelText(m_c_signal_name.GetValue());
               m_c_signal_label[m_c_total_signals-1].Update(true);
               m_c_signal_ind[m_c_total_signals-1].Show();
            }
            else
               MessageBox("Maximum number of composite signals is "+string(COMPOSITE_SIGNALS),"Signal Monitor");
         }
      }
      else
      {
         if(SaveCompositeSignalSet(m_c_number_signal))
         {
            m_composite_edit.CloseDialogBox();
            m_c_signal_label[m_c_number_signal].LabelText(m_c_signal_name.GetValue());
            m_c_signal_label[m_c_number_signal].Update(true);
         }
      }
   }
}

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool CProgram::LoadCompositeSignalSet(int index)
{
   int h=FileOpen("Signal Monitor\\c_signal_"+string(index)+".bin",FILE_READ|FILE_BIN);
   if(h==INVALID_HANDLE)
   {
      MessageBox("Конфигурация не найдена","Signal Monitor");
      return(false);
   }
   ZeroMemory(m_c_signal_set[index]);
   FileReadStruct(h,m_c_signal_set[index]);
//--- Загрузка названия индикатора
   m_c_signal_name.SetValue(CharArrayToString(m_c_signal_set[index].signal_name));
   m_c_signal_name.GetTextBoxPointer().Update(true);
//--- Значения слотов
   string slot_1=CharArrayToString(m_c_signal_set[index].slot_1);
   string slot_2=CharArrayToString(m_c_signal_set[index].slot_2);
   string slot_3=CharArrayToString(m_c_signal_set[index].slot_3);
   color back=clrDodgerBlue;
   if(slot_1=="Slot 1")
      back=clrLightSteelBlue;
   else
      back=clrDodgerBlue;
   SetButtonParam(m_rule_element[0],slot_1,back);
   if(slot_2=="Slot 2")
      back=clrLightSteelBlue;
   else
      back=clrDodgerBlue;
   SetButtonParam(m_rule_element[1],slot_2,back);
   if(slot_3=="Slot 3")
      back=clrLightSteelBlue;
   else
      back=clrDodgerBlue;
   SetButtonParam(m_rule_element[2],slot_3,back);
//--- Значение логических операторов
   for(int i=0; i<2; i++)
   {
      switch(m_c_signal_set[index].logics[i])
      {
      case  1:
         SetButtonParam(m_rule_selector[i],"AND",C'75,190,240');
         break;
      case  2:
         SetButtonParam(m_rule_selector[i],"(OR)",clrTomato);
         break;
      case  3:
         SetButtonParam(m_rule_selector[i],"OR",clrOrangeRed);
         break;
      default:
         break;
      }
   }
//--- Загрузка Текстовой метки
   m_c_text_box.ClearTextBox();
   m_c_text_box.AddText(0,CharArrayToString(m_c_signal_set[index].label_value));
   m_c_text_box.Update(true);
//--- Загрузка цвета текстовой метки
   m_c_color_button[0].CurrentColor(m_c_signal_set[index].label_color);
   m_c_color_button[0].Update(true);
//--- Загрузка цвета фона
   if(m_c_signal_set[index].back_color==clrNONE)
   {
      m_c_set_param[0].IsPressed(false);
      m_c_set_param[0].Update(true);
      m_c_color_button[1].IsLocked(true);
      m_c_color_button[1].GetButtonPointer().Update(true);
   }
   else
   {
      m_c_set_param[0].IsPressed(true);
      m_c_set_param[0].Update(true);
      m_c_color_button[1].IsLocked(false);
      m_c_color_button[1].CurrentColor(m_c_signal_set[index].back_color);
      m_c_color_button[1].GetButtonPointer().Update(true);
   }
//--- Загрузка цвета канта
   if(m_c_signal_set[index].border_color==clrNONE)
   {
      m_c_set_param[1].IsPressed(false);
      m_c_set_param[1].Update(true);
      m_c_color_button[2].IsLocked(true);
      m_c_color_button[2].GetButtonPointer().Update(true);
   }
   else
   {
      m_c_set_param[1].IsPressed(true);
      m_c_set_param[1].Update(true);
      m_c_color_button[2].IsLocked(false);
      m_c_color_button[2].CurrentColor(m_c_signal_set[index].border_color);
      m_c_color_button[2].GetButtonPointer().Update(true);
   }
//--- Загрузка значения подсказки
   if(!m_c_signal_set[index].tooltip)
   {
      m_c_set_param[2].IsPressed(false);
      m_c_set_param[2].Update(true);
      m_c_tooltip_text.IsLocked(true);
      m_c_tooltip_text.Update(true);
   }
   else
   {
      m_set_param[2].IsPressed(true);
      m_set_param[2].Update(true);
      m_c_tooltip_text.IsLocked(false);
      m_c_tooltip_text.ClearTextBox();
      m_c_tooltip_text.AddText(0,CharArrayToString(m_c_signal_set[index].tooltip_text));
      m_c_tooltip_text.Update(true);
   }
//--- Загрузка изображения
   if(!m_c_signal_set[index].image)
   {
      m_c_set_param[3].IsPressed(false);
      m_c_set_param[3].Update(true);
      m_c_pictures_slider.IsLocked(true);
      m_c_pictures_slider.GetRadioButtonsPointer().Update(true);
   }
   else
   {
      m_c_set_param[3].IsPressed(true);
      m_c_set_param[3].Update(true);
      m_c_pictures_slider.IsLocked(false);
      m_c_pictures_slider.GetRadioButtonsPointer().SelectButton(m_c_signal_set[index].img_index);
      m_c_pictures_slider.GetRadioButtonsPointer().Update(true);
   }
//---
   FileClose(h);
   return(true);
}

Вот теперь добавим возможность редактирования созданного сигнала посредством нового метода EditCompositeSignal():

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::EditCompositeSignal(long lparam)
{
   for(int i=0; i<COMPOSITE_SIGNALS; i++)
   {
      if(lparam==m_c_signal_ind[i].Id())
      {
         LoadCompositeSignalSet(i);
         m_c_new_signal.LabelText(m_lang[38]);
         m_c_new_signal.Update(true);
         m_composite_edit.OpenWindow();
         m_c_number_signal=i;
         for(int j=0; j<SIMPLE_SIGNALS; j++)
            m_simple_signal[j].IsLocked(true);
      }
   }
}

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

//+------------------------------------------------------------------+
//| Поиск сигналов                                                   |
//+------------------------------------------------------------------+
bool CProgram::SearchSignals(void)
{
//--- Поиск количества созданных простых сигналов
   int cnt1=0;
   for(int i=0; i<SIMPLE_SIGNALS; i++)
   {
      if(FileIsExist("Signal Monitor\\signal_"+string(i)+".bin"))
         cnt1++;
   }
//--- Поиск простых сигналов
   SIGNAL signal_set;
   ZeroMemory(signal_set);
   for(int i=0; i<cnt1; i++)
   {
      //--- Пропускаем сигнал если установлен только для составных
      if(m_signal_type[i].LabelText()=="C")
         continue;
      //---
      if(GetSimpleSignal(signal_set,i))
         return(false);
      //---
      for(int j=0; j<ArraySize(m_signal_button); j++)
      {
         //---
         string sy=GetSymbol(j);
         ENUM_TIMEFRAMES tf=GetTimeframe(j);
         //---
         if(!CheckTimeframe(tf,signal_set))
            continue;
         //---
         if(GetSignal(sy,tf,signal_set))
            SetVisualSignal(signal_set,j);
         else
            SetDefaultVisual(j);
      }
   }
.....

Всвязи с тем, что были введены признаки применения простых сигналов, необходимо было добавить фильтрацию тех простых сигналов, которые должны использоваться только как часть составного, а не как отдельный самодостаточный сигнал. Также добавлен метод GetSimpleSignal(), целью которого стало получение набор настроек из заданного файла по каждому простому сигналу и запись их в структуру. Еще два новых метода SetVisualSignal() и SetDefaultVisual() вместили в себя отображение установленных визуальных настроек согласно текущему найденному сигналу и если сигнала нет, то возврат в визуальному отображению блока сигнала по умолчанию. Если сравнивать реализацию поиска простых сигналов предыдущую и текущая, то новая версия стала более простой в понимании, а также сократилась почти в три раза.

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

.....
//--- Поиск составных сигналов
   C_SIGNAL c_signal_set;
   ZeroMemory(c_signal_set);
   int cnt2=0;
   int signal_number[3];
   ArrayInitialize(signal_number,-1);
//--- Поиск количества созданных составных сигналов
   for(int i=0; i<COMPOSITE_SIGNALS; i++)
   {
      if(FileIsExist("Signal Monitor\\c_signal_"+string(i)+".bin"))
         cnt2++;
   }
//-- Выходим если ни одного
   if(cnt2<1)
      return(true);
//--- Поиск конфигураций с составными сигналами
   for(int i=0; i<cnt2; i++)
   {
      //---
      int h=FileOpen("Signal Monitor\\c_signal_"+string(i)+".bin",FILE_READ|FILE_BIN);
      if(h==INVALID_HANDLE)
      {
         MessageBox("Конфигурация не найдена","Signal Monitor");
         return(false);
      }
      ZeroMemory(c_signal_set);
      FileReadStruct(h,c_signal_set);
      FileClose(h);
      //--- Ищем простые сигналы, используемые в выбранном составном(С или В)
      for(int m=0; m<cnt1; m++)
      {
         if(m_signal_type[m].LabelText()!="S")
            GetSimpleSignal(signal_set,m);
         //---
         if(CharArrayToString(signal_set.signal_name)==CharArrayToString(c_signal_set.slot_1))
            signal_number[0]=m;
         else if(CharArrayToString(signal_set.signal_name)==CharArrayToString(c_signal_set.slot_2))
            signal_number[1]=m;
         else if(CharArrayToString(signal_set.signal_name)==CharArrayToString(c_signal_set.slot_3))
            signal_number[2]=m;
      }
      ArrayPrint(signal_number);
   }
//---
   int used_slots=GetUsedSlots(CharArrayToString(c_signal_set.slot_1),CharArrayToString(c_signal_set.slot_2),CharArrayToString(c_signal_set.slot_3));
//---
   for(int j=0; j<ArraySize(m_signal_button); j++)
   {
      //---
      string sy=GetSymbol(j);
      ENUM_TIMEFRAMES tf=GetTimeframe(j);
      //---
      GetSimpleSignal(signal_set,signal_number[0]);
      bool sig1=GetSignal(sy,tf,signal_set);
      GetSimpleSignal(signal_set,signal_number[1]);
      bool sig2=GetSignal(sy,tf,signal_set);
      if(used_slots==2)
      {
         //--- AND
         if(c_signal_set.logics[0]==1)
            if(sig1 && sig2)
               SetVisualCompositeSignal(c_signal_set,j);
         //--- OR
         if(c_signal_set.logics[0]>1)
            if(sig1 || sig2)
               SetVisualCompositeSignal(c_signal_set,j);
      }
      else if(used_slots==3)
      {
         GetSimpleSignal(signal_set,signal_number[2]);
         bool sig3=GetSignal(sy,tf,signal_set);
         //--- AND OR
         if(c_signal_set.logics[0]==1 && c_signal_set.logics[1]==2)
         {
            if((sig1 && sig2) || sig3)
               SetVisualCompositeSignal(c_signal_set,j);
         }
         //--- AND (OR)
         else if(c_signal_set.logics[0]==1 && c_signal_set.logics[1]==3)
         {
            if(sig1 && (sig2 || sig3))
               SetVisualCompositeSignal(c_signal_set,j);
         }
         //--- OR AND
         else if(c_signal_set.logics[0]==2 && c_signal_set.logics[1]==1)
         {
            if(sig1 || (sig2 && sig3))
               SetVisualCompositeSignal(c_signal_set,j);
         }
         //--- (OR) AND
         else if(c_signal_set.logics[0]==3 && c_signal_set.logics[1]==1)
         {
            if((sig1 || sig2) && sig3)
               SetVisualCompositeSignal(c_signal_set,j);
         }
         //--- AND AND
         else if(c_signal_set.logics[0]==1 && c_signal_set.logics[1]==1)
         {
            if(sig1 && sig2 && sig3)
               SetVisualCompositeSignal(c_signal_set,j);
         }
         //--- OR OR
         else if(c_signal_set.logics[0]>1 && c_signal_set.logics[1]>1)
         {
            if(sig1 || sig2 || sig3)
               SetVisualCompositeSignal(c_signal_set,j);
         }
      }
   }
   return(true);
}

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CProgram::GetUsedSlots(string name1,string name2,string name3)
{
   int cnt=0;
   if(name1!="Slot 1")
      cnt++;
   if(name2!="Slot 2")
      cnt++;
   if(name3!="Slot 3")
      cnt++;
   return(cnt);
}

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

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CProgram::SetVisualCompositeSignal(C_SIGNAL &signal_set,int block)
{
   //---
   SetLabel(block,CharArrayToString(signal_set.label_value),signal_set.label_color);
   //---
   if(signal_set.back_color!=clrNONE)
      SetBackground(block,signal_set.back_color);
   //---
   if(signal_set.border_color!=clrNONE)
      SetBorderColor(block,signal_set.border_color);
   else
      SetBorderColor(block,signal_set.back_color);
   //---
   if(signal_set.tooltip)
      SetTooltip(block,CharArrayToString(signal_set.tooltip_text));
   //---
   if(signal_set.image)
      SetIcon(block,signal_set.img_index);
   else
      SetIcon(block,-1);
}

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

Предыдущие статьи этой серии:

Заключение

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


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


Прикрепленные файлы |
MQL5.zip (2028.12 KB)
Работа с таймсериями в библиотеке DoEasy (Часть 43): Классы объектов индикаторных буферов Работа с таймсериями в библиотеке DoEasy (Часть 43): Классы объектов индикаторных буферов

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

Как создать 3D-графику на DirectX в MetaTrader 5 Как создать 3D-графику на DirectX в MetaTrader 5

Компьютерная 3D-графика хорошо подходит для анализа больших объемов данных, так как позволяет визуализировать скрытые закономерности. Такие задачи можно решать и напрямую в MQL5 — функции для работы с DireсtX позволяют при желании написать свою трехмерную игру для MetaTrader 5. Начните изучение с рисования простых объемных фигур.

Работа с таймсериями в библиотеке DoEasy (Часть 44): Класс-коллекция объектов индикаторных буферов Работа с таймсериями в библиотеке DoEasy (Часть 44): Класс-коллекция объектов индикаторных буферов

В статье рассмотрим создание класса-коллекции объектов индикаторных буферов и протестируем возможности создания любого количества буферов для программ-индикаторов и возможности работы с ними (максимальное количество буферов, которые можно создать в MQL-индикаторах - 512 буферов).

Набор инструментов для ручной разметки графиков и торговли (Часть I). Подготовка - описание структуры и класс вспомогательных функций Набор инструментов для ручной разметки графиков и торговли (Часть I). Подготовка - описание структуры и класс вспомогательных функций

Данной статье я начинаю описывать набор для графической разметки с помощью сочетаний клавиш. Очень удобно: нажал клавишу — появилась линия тренда, нажал другую — появился веер Фибоначчи с нужными параметрами... А также — возможность переключать таймфреймы, менять порядок "слоев" объектов или удалять все объекты с графика.