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

29 апреля 2020, 08:37
Alexander Fedosov
0
3 100

Содержание

Введение

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

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

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


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

Для начала следует определится с понятием "составной сигнал" в рамках разработки торгового монитора. Вспомним, что ранее в приложении использовались простые сигналы, создаваемые путем наложения определенных условий на получаемый источник, которым являлись различные индикаторы, такие как 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 (Часть 44): Класс-коллекция объектов индикаторных буферов Работа с таймсериями в библиотеке DoEasy (Часть 44): Класс-коллекция объектов индикаторных буферов
В статье рассмотрим создание класса-коллекции объектов индикаторных буферов и протестируем возможности создания любого количества буферов для программ-индикаторов и возможности работы с ними (максимальное количество буферов, которые можно создать в MQL-индикаторах - 512 буферов).
Работа с таймсериями в библиотеке DoEasy (Часть 43): Классы объектов индикаторных буферов Работа с таймсериями в библиотеке DoEasy (Часть 43): Классы объектов индикаторных буферов
В статье рассмотрим создание классов объектов-индикаторных буферов как наследников абстрактного объекта-буфера, упрощающих объявление и работу с индикаторными буферами при создании собственных программ-индикаторов на основе библиотеки DoEasy.
Набор инструментов для ручной разметки графиков и торговли (Часть I). Подготовка - описание структуры и класс вспомогательных функций Набор инструментов для ручной разметки графиков и торговли (Часть I). Подготовка - описание структуры и класс вспомогательных функций
Данной статье я начинаю описывать набор для графической разметки с помощью сочетаний клавиш. Очень удобно: нажал клавишу — появилась линия тренда, нажал другую — появился веер Фибоначчи с нужными параметрами. А также — возможность переключать таймфреймы, менять порядок "слоев" объектов или удалять все объекты с графика.
Как создать 3D-графику на DirectX в MetaTrader 5 Как создать 3D-графику на DirectX в MetaTrader 5
Компьютерная 3D-графика хорошо подходит для анализа больших объемов данных, так как позволяет визуализировать скрытые закономерности. Такие задачи можно решать и напрямую в MQL5 — функции для работы с DireсtX позволяют при желании написать свою трехмерную игру для MetaTrader 5. Начните изучение с рисования простых объемных фигур.