Универсальный осциллятор с графическим интерфейсом

Dmitry Fedoseev | 16 ноября, 2016

Содержание

Введение

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

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

Анализ задачи

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

Параметры каждого конкретного индикатора будут передаваться в конструктор дочернего класса. В этом случае при создании объекта редактор MetaEditor будет открывать подсказку со списком параметров, что значительно облегчит процесс разработки (рис. 1).

 
Рис. 1. Подсказка с параметрами конструктора при создании объекта

Основная сложность могла бы возникнуть при практическом использовании такого индикатора. Дело в том, что у разных осцилляторов сильно различаются наборы внешних параметров. Если для каждого осциллятора делать свои параметры, отличающиеся префиксом, то вручную пользоваться индикатором будет возможно, но для использования  посредством функции iCustom() или IndicatorCreate() он может оказаться непригодным из-за большого количества параметров. В функцию IndicatorCreate() можно передавать не более 256 параметров, а в функцию iCustom() — не более 64. В этом количестве учитываются и общие параметры типа символа или имени индикатора, так что фактическое количество доступных параметров немного меньше. Возможно использование небольшого универсального набора параметров, но тогда пользоваться индикатором будет неудобно: придется всегда обращаться к справочному руководству, чтобы знать, какие параметры задействованы для того или другого индикатора.

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

Набор параметров

Определимся с требуемым минимальным набором внешних параметров. Посмотрим в терминале список осцилляторов: Главное меню - Вставка - Индикаторы - Осцилляторы, внесем их в таблицу.

Таблица 1. Все осцилляторы терминала

ФункцияНазваниеБуферыПараметры
iATRAverage True Range1. линия1. int ma_period — период усреднения
iBearsPowerBears Power1. гистограмма1. int ma_period — период усреднения
iBullsPowerBulls Power1. линия1. int ma_period — период усреднения
iCCICommodity Channel Index1. линия1. int ma_period — период усреднения
2. ENUM_APPLIED_PRICE applied_price — тип цены
iChaikinChaikin Oscillator1. линия1. int fast_ma_period — быстрый период  
2. int slow_ma_period — медленный период
3. ENUM_MA_METHOD ma_method — тип сглаживания
4. ENUM_APPLIED_VOLUME applied_volume — используемый объем 
iDeMarkerDeMarker1. линия1. int ma_period — период усреднения
iForceForce Index1. линия1. int ma_period — период усреднения  
2. ENUM_MA_METHOD ma_method — тип сглаживания  
3. ENUM_APPLIED_VOLUME applied_volume — тип объема для расчета 
iMomentumMomentum1. линия1. int mom_period — период усреднения
2. ENUM_APPLIED_PRICE applied_price — тип цены
iMACDMoving Averages Convergence-Divergence1. гистограмма
2. линия
1. int fast_ema_period — период быстрой средней
2. int slow_ema_period — период медленной средней
3. int signal_period — период усреднения разности
4. ENUM_APPLIED_PRICE  applied_price — тип цены
iOsMAMoving Average of Oscillator (MACD histogram)1. гистограмма1. int fast_ema_period — период быстрой средней 
2. int slow_ema_period — период медленной средней
3. int signal_period — период усреднения разности 
4. ENUM_APPLIED_PRICE  applied_price — тип цены
iRSIRelative Strength Index1. линия1. int ma_period — период усреднения 
2. ENUM_APPLIED_PRICE applied_price — тип цены
iRVIRelative Vigor Index1. линия
2. линия
1. int ma_period — период усреднения
iStochasticStochastic Oscillator1. линия
2. линия
1. int Kperiod — K-период (количество баров для расчетов)
2. int Dperiod — D-период (период первичного сглаживания)
3. int slowing — окончательное сглаживание
4. ENUM_MA_METHOD ma_method — тип сглаживания
5. ENUM_STO_PRICE price_field — способ расчета стохастика 
iTriXTriple Exponential Moving Averages Oscillator1. линия1. int ma_period — период усреднения 
2. ENUM_APPLIED_PRICE applied_price — тип цены
iWPRWilliams' Percent Range1. линия1. int calc_period — период усреднения

По колонке "Параметры" составим список всех типов параметров и определим их максимальное количество.

Таблица 2. Типы и количество параметров 

ТипКоличество
int3
ENUM_APPLIED_PRICE1
ENUM_MA_METHOD1
ENUM_APPLIED_VOLUME1
ENUM_STO_PRICE1
По колонке "Буферы" видно, что всего может быть использовано два индикаторных буфера и что для разных индикаторов требуется разный тип рисования. Конечно, можно все их отрисовать линиями, но поскольку некоторые из них принято отражать в виде гистограмм, а  возможности терминала позволяют это сделать, то при смене типа индикатора постараемся обеспечить и смену типа рисования. Также надо будет подумать и о рисовании горизонтальных уровней, поскольку для некоторых индикаторов (RSI, CCI и т.п.) желательно их наличие на графике.

План работы

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

  1. Создание классов для универсального осциллятора и создание этого осциллятора без графического интерфейса.
  2. Создание классов для графического интерфейса.
  3. Объединение универсального осциллятора и графического интерфейса. 

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

Рассмотрим значения по умолчанию у разных осцилляторов. Например, у Stochastic периоды 5, 3, 3 (первый параметр больше второго), а у MACD — 12, 26, 9 (первый параметр меньше второго). У индикатора MACD первый параметр — период быстрой средней, а второй — это период медленной средней; значит, первый параметр должен быть меньше второго. Такое же соотношение первого и второго параметров подойдет и осциллятору Чайкина (у него тоже задаются периоды быстрой и медленной средних). Для индикатора Stochastic не столь важно данное соотношение, он в любом случае будет выглядеть соответственно движению цены. Если же индикатору MACD установить первый параметр больше второго, индикатор будет направлен в сторону, противоположную движению цены (при установке параметров по умолчанию этого желательно избегать).

При использовании графического интерфейса хотелось бы, чтобы индикаторы начинали работу, во-первых, с привычными наборами параметров по умолчанию: MACD с периодами 12, 26, 9, Stochastic с периодами 5, 3, 3, и т.п. Кроме этого, желательно иметь возможность, чтобы при выборе нового индикатора он начинал работу или с параметрами по умолчанию или с теми же параметрами, с которыми работал предыдущий индикатор. Например, исследуются индикаторы RSI и CCI, и нам интересно посмотреть, как меняется вид линии у разных индикаторов, но при одинаковом значении периода. Это следует иметь в виду в дальнейшем при разработке классов.

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

В папке Include создадим папку "UniOsc", в ней будут располагаться все дополнительные файлы индикатора. Набор используемых осцилляторов определен выше в таблице 1. В соответствии с ним создадим перечисление для выбора типа осциллятора. Возможно, перечисление потребуется не только в файле индикатора, поэтому разместим его в отдельном файле с именем "UniOscDefines.mqh" (файл располагается в папке "UniOsc"): 

enum EOscUnyType{
   OscUni_ATR,
   OscUni_BearsPower,
   OscUni_BullsPower,  
   OscUni_CCI,
   OscUni_Chaikin,
   OscUni_DeMarker,
   OscUni_Force,
   OscUni_Momentum,
   OscUni_MACD,
   OscUni_OsMA,
   OscUni_RSI,
   OscUni_RVI,
   OscUni_Stochastic,
   OscUni_TriX,
   OscUni_WPR
};

Больше в этом файле ничего не будет.

Создадим файл "CUniOsc.mqh" для класса индикатора, запишем в него шаблон класса COscUni:

class COscUni{
   protected:
    
   public:

};

В шаблоне определена секция protected, поскольку некоторые члены класса должны быть защищены, но доступны для потомков класса (члены секции private защищены, но недоступны потомкам).

Основной метод базового класса — метод, соответствующий функции OnCalculate() индикатора, назовем его Calculate(). Первые два параметра метода будут соответствовать аналогичным параметрам функции OnCalculate(): rates_total (общее количество баров) и prew_calculate (количество обсчитанных баров). Никакие массивы с данными в метод Calculate() не нужно передавать, поскольку используются данные другого индикатора. Но нужно передать два индикаторных буфера, которые будут заполняться данными. Даже при использовании индикаторов с одним буфером все равно нужно следить за чистотой второго буфера, поэтому в любом случае в метод Calculate() будут передаваться два индикаторных буфера. Код метода Calculate() будет зависеть от типа используемого осциллятора — один ли у него буфер с данными или два. Значит, метод Calculate() будет виртуальным:

virtual int Calculate( const int rates_total,
               const int prev_calculated,
               double & buffer0[],
               double & buffer1[]
){
   return(rates_total);
}

При загрузке различных индикаторов понадобится переменная для хэндла индикатора. Объявим ее в секции protected.

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

int m_handle;           // хэндл индикатора
int m_bufferscnt;       // количество используемых буферов    
string m_name;          // имя индикатора      
string m_label1;        // название буфера 1    
string m_label2;        // название буфера 2
int m_drawtype1;        // тип рисования буфера 1    
int m_drawtype2;        // тип рисования буфера 2      
string m_help;          // небольшая справка по параметрам индикатора
int m_digits;           // количество знаков после запятой у значений индикатора
int m_levels_total;     // количество уровней
double m_level_value[]; // массив для значений уровней

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

bool CheckHandle(){
   return(m_handle!=INVALID_HANDLE);
}

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

int Handle(){
    return(m_handle);
}

В конструкторе класса инициализируем хэндл, а в деструкторе проверим его и при необходимости вызовем функцию IndicatorRelease():

void COscUni(){
   m_handle=INVALID_HANDLE;
}

void ~COscUni(){
   if(m_handle!=INVALID_HANDLE){
      IndicatorRelease(m_handle);
   }
} 

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

string Name(){ // имя осциллятора
   return(m_name);
}    
  
int BuffersCount(){ // количество буферов осцилляторов
   return(m_bufferscnt);
}

string Label1(){ // название первого буфера
   return(m_label1);
}

string Label2(){ // название второго буфера
   return(m_label2);
}

int DrawType1(){ // тип рисования первого буфера
   return(m_drawtype1);
}

int DrawType2(){ // тип рисования второго буфера
   return(m_drawtype2);
}  

string Help(){ // подсказка по использованию параметров
   return(m_help);
}

int Digits(){ // количество знаков после запятой у индикатора
   return(m_digits);
}

int LevelsTotal(){ // количество уровней индикатора
   return(m_levels_total);
}

double LevelValue(int index){ // получение значения уровня по индексу
   return(m_level_value[index]);
}

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

Дочерние классы Calculate

Создадим два дочерних класса: для индикаторов с одним буфером и для индикаторов с двумя буферами. Для индикаторов с одним буфером:

class COscUni_Calculate1:public COscUni{
   public:
      void COscUni_Calculate1(){
         m_bufferscnt=1;
      }
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & buffer0[],
                     double & buffer1[]
      ){
        
         int cnt,start;
        
         if(prev_calculated==0){
            cnt=rates_total;
            start=0;
         }
         else{
            cnt=rates_total-prev_calculated+1;
            start=prev_calculated-1;
         }  
        
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
        
         for(int i=start;i<rates_total;i++){
            buffer1[i]=EMPTY_VALUE;
         }        
        
         return(rates_total);
      }
};

Рассмотрим этот класс. Класс имеет конструктор "COscUni_Calculate1", в конструкторе устанавливается количество буферов, в данном случае 1. В методе Calculate(), в зависимости от значений переменных rates_total и prev_calculate, вычисляется количество элементов буфера для копирования (переменная cnt) и индекс бара, с которого необходимо очистить второй буфер (переменная start). В случае неудачного копирования данных (при вызове функции CopyBuffer()) из метода возвращается 0, чтобы на следующем тике все расчеты выполнились с самого начала. В конце метода, как обычно, возвращается rates_total.

Дочерний класс для индикаторов с двумя буферами:

class COscUni_Calculate2:public COscUni{
   public:
      void COscUni_Calculate2(){
         m_bufferscnt=2;
      }  
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & buffer0[],
                     double & buffer1[]
      ){
         int cnt;
         if(prev_calculated==0){
            cnt=rates_total;
         }
         else{
            cnt=rates_total-prev_calculated+1;
         }          
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
         if(CopyBuffer(m_handle,1,0,cnt,buffer1)<=0){
            return(0);
         }
         return(rates_total);
      }
};

Этот класс еще проще, чем класс для индикаторов с одним буфером. В начале метода Calculate() вычисляется количество элементов (переменная cnt) для копирования и выполняется копирование буферов. 

Дочерние классы индикаторов

Теперь создадим дочерние классы непосредственно для осцилляторов. Эти классы будут дочерними для класса COscUni_Calculate1 или COscUni_Calculate2. Во всех этих классах будет только по одному конструктору. В конструктор каждого класса будут передаваться параметры, соответствующие непосредственно осциллятору этого класса, и еще пара дополнительных параметров в начале. Дополнительные параметры будут определять, использовать ли значения параметров переданные в конструктор или установить значения по умолчанию (переменная с именем use_default). Второй параметр с именем keep_previous определяет, устанавливать ли значения по умолчанию всем параметрам индикатора или только тем, которые еще вообще не использовались.

Первый индикатор по списку — ATR, начнем писать дочерний класс для него. Сначала шаблон класса:

class COscUni_ATR:public COscUni_Calculate1{
   public:
   void COscUni_ATR(bool use_default,bool keep_previous,int & ma_period){

   }
};

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

Пишем код в конструкторе:

if(use_default){
   if(keep_previous){
      if(ma_period==-1)ma_period=14;
   }
   else{
      ma_period=14;
   }      
}  

В этой части кода, если use_default=true, выполняется установка значения по умолчанию. Если keep_previous=true, значение по умолчанию устанавливается только если полученный параметр имеет значение -1, то есть, он не использовался ранее. Соответственно, при инициализации универсального осциллятора, надо будет присвоить всем переменные для параметров значение -1.

Теперь самая главная строка кода в конструкторе дочернего класса — загрузка индикатора:

m_handle=iATR(Symbol(),Period(),ma_period);

В конце несколько строк для установки параметров отображения:

m_name=StringFormat("ATR(%i)",ma_period); // имя индикатора
m_label1="ATR"; // названия буфера
m_drawtype1=DRAW_LINE;   // тип рисования
m_help=StringFormat("ma_period - Period1(%i)",ma_period); // подсказка  
m_digits=_Digits+1; // количество знаков после запятой у значений  
m_levels_total=0; // количество уровней    

Рассмотрим некоторые этапы создания дочернего класса для индикатора посложнее — для MACD. Принцип создания тот же, только кода будет несколько больше. Поэтому рассмотрим фрагменты. Установка параметров по умолчанию:

if(use_default){
   if(keep_previous){
      if(fast_ema_period==-1)fast_ema_period=12;
      if(slow_ema_period==-1)slow_ema_period=26;
      if(signal_period==-1)signal_period=9;
      if(applied_price==-1)applied_price=PRICE_CLOSE;            
   }
   else{
      fast_ema_period=12;
      slow_ema_period=26;
      signal_period=9;
      applied_price=PRICE_CLOSE;
   }      
}

Установка параметров отображения:

m_handle=iMACD(Symbol(),
               Period(),
               fast_ema_period,
               slow_ema_period,
               signal_period,
               (ENUM_APPLIED_PRICE)applied_price);

m_name=StringFormat( "iMACD(%i,%i,%i,%s)",
                     fast_ema_period,
                     slow_ema_period,
                     signal_period,
                     EnumToString((ENUM_APPLIED_PRICE)applied_price));
                    
m_label1="Main";
m_label2="Signal";      
m_drawtype1=DRAW_HISTOGRAM;            
m_drawtype2=DRAW_LINE;

m_help=StringFormat( "fast_ema_period - Period1(%i), "+
                     "slow_ema_period - Period2(%i), "+
                     "signal_period - Period3(%i), "+
                     "applied_price - Price(%s)",
                     fast_ema_period,
                     slow_ema_period,
                     signal_period,
                     EnumToString((ENUM_APPLIED_PRICE)applied_price));  
                    
m_digits=_Digits+1;

Подробнее рассмотрим параметры конструктора:

void COscUni_MACD(bool use_default,
                  bool keep_previous,
                  int & fast_ema_period,
                  int & slow_ema_period,
                  int & signal_period,
                  long & applied_price
){

Обратите внимание, переменная applied_price для стандартного перечисления ENUM_APPLIED_PRICE объявлена как long. Это сделано для того, чтобы иметь возможность присвоить переменной значение -1, означающее, что параметр еще не использовался.

Рассмотрим еще один фрагмент из класса для индикатора RSI — часть кода, в которой выполняется установка уровней: 

m_levels_total=3;
ArrayResize(m_level_value,3);
m_level_value[0]=30;
m_level_value[1]=50;
m_level_value[2]=70;

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

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

Создание универсального осциллятора (начало)

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

Создайте новый индикатор, пусть его имя будет "iUniOsc". Затем в мастере создания индикатора выберите тип функции OnCalculate(...open,high,low,close), создайте одну внешнюю переменную (чтобы потом легче найти место для внешних переменных) и два буфера типа Line.

Перед внешней переменной подключите файлы с перечислением и с классами осцилляторов: 

#include <UniOsc/UniOscDefines.mqh>
#include <UniOsc/CUniOsc.mqh>

Создайте внешнюю переменную для выбора типа осциллятора:

input EOscUnyType          Type        =  OscUni_ATR;

Переменные UseDefault и KeepPrevious:

input bool                 UseDefault  =  true;
input bool                 KeepPrev    =  true;

Универсальные переменные непосредственно для параметров осцилляторов:

input int                  Period1     =  14;
input int                  Period2     =  14;
input int                  Period3     =  14;
input ENUM_MA_METHOD       MaMethod    =  MODE_EMA;
input ENUM_APPLIED_PRICE   Price       =  PRICE_CLOSE;  
input ENUM_APPLIED_VOLUME  Volume      =  VOLUME_TICK;  
input ENUM_STO_PRICE       StPrice     =  STO_LOWHIGH;

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

input color                ColorLine1  =  clrLightSeaGreen;
input color                ColorLine2  =  clrRed;
input color                ColorHisto  =  clrGray;

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

int                  _Period1;
int                  _Period2;
int                  _Period3;
long                 _MaMethod;
long                 _Price;  
long                 _Volume;  
long                 _StPrice;
EOscUnyType          _Type;

Объявим переменную-указатель для объекта универсального осциллятора:

COscUni * osc;

Еще объявим пару переменных:

string ProgName;
string ShortName;

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

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

void PrepareParameters(){

   _Type=Type;

   if(UseDefault && KeepPrev){
      _Period1=-1;
      _Period2=-1;
      _Period3=-1;
      _MaMethod=-1;
      _Volume=-1;
      _Price=-1;  
      _StPrice=-1;
   }
   else{  
      _Period1=Period1;
      _Period2=Period2;
      _Period3=Period3;
      _MaMethod=MaMethod;
      _Volume=Volume;
      _Price=Price;  
      _StPrice=StPrice;
   }
}

Если используются UseDefault и KeepPrevious, всем переменным присваиваются значения -1, чтобы в конструкторе класса отличить переменные, которые мы еще не использовали и только установить для них значения по умолчанию. В остальных случаях присваиваются значения из окна свойств, которые или будут использоваться как есть, или будут подменены на значения по умолчанию при создании объекта.  

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

void LoadOscillator(){
   switch(_Type){
      case OscUni_ATR:
         osc=new COscUni_ATR(UseDefault,KeepPrev,_Period1);
      break;
      case OscUni_BearsPower:
         osc=new COscUni_BearsPower(UseDefault,KeepPrev,_Period1);
      break;
      case OscUni_BullsPower:
         osc=new COscUni_BullsPower(UseDefault,KeepPrev,_Period1);
      break;      
      ...
   }  
}

После загрузки осциллятора проверяем хэндл:

if(!osc.CheckHandle()){
   Alert("Ошибка загрузки индикатора "+osc.Name());
   return(INIT_FAILED);
}

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

void SetStyles(){  

   // установка стилей
   if(osc.BuffersCount()==2){
      PlotIndexSetInteger(0,PLOT_DRAW_TYPE,osc.DrawType1());
      PlotIndexSetInteger(1,PLOT_DRAW_TYPE,osc.DrawType2());
      PlotIndexSetInteger(0,PLOT_SHOW_DATA,true);
      PlotIndexSetInteger(1,PLOT_SHOW_DATA,true);
      PlotIndexSetString(0,PLOT_LABEL,osc.Label1());
      PlotIndexSetString(1,PLOT_LABEL,osc.Label2());
      if(osc.DrawType1()==DRAW_HISTOGRAM){
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorHisto);
      }
      else{
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorLine1);        
      }
      PlotIndexSetInteger(1,PLOT_LINE_COLOR,ColorLine2);  
   }
   else{
      PlotIndexSetInteger(0,PLOT_DRAW_TYPE,osc.DrawType1());
      PlotIndexSetInteger(1,PLOT_DRAW_TYPE,DRAW_NONE);  
      PlotIndexSetInteger(0,PLOT_SHOW_DATA,true);
      PlotIndexSetInteger(1,PLOT_SHOW_DATA,false);  
      PlotIndexSetString(0,PLOT_LABEL,osc.Label1());
      PlotIndexSetString(1,PLOT_LABEL,"");
      if(osc.DrawType1()==DRAW_HISTOGRAM){
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorHisto);
      }
      else{
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorLine1);        
      }        
   }
  
   // установка digits
   IndicatorSetInteger(INDICATOR_DIGITS,osc.Digits());

   // установка уровней
   int levels=osc.LevelsTotal();
   IndicatorSetInteger(INDICATOR_LEVELS,levels);
   for(int i=0;i<levels;i++){
      IndicatorSetDouble(INDICATOR_LEVELVALUE,i,osc.LevelValue(i));
   }

}    

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

Ниже приведен полный код функции OnInit() с вызовом только что созданных функций:

int OnInit(){

   SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
   SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA);

   PrepareParameters();

   LoadOscillator();
  
   if(!osc.CheckHandle()){
      Alert("Ошибка загрузки индикатора "+osc.Name());
      return(INIT_FAILED);
   }

   SetStyles();
  
   Print("Parameters matching: "+osc.Help());
  
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
  
   return(INIT_SUCCEEDED);
}

Обратите внимание: в конце функции выполняется вызов функции Print c подсказкой об используемых параметрах окна свойств, затем устанавливается короткое имя индикатора.

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

В приложении к статье имеется полностью готовый индикатор с именем "iUniOsc" (позже в его код будет внесено незначительное изменение, и он будет немного отличаться от индикатора, полученного на данном этапе). 

План создания графического интерфейса

Для создания графического интерфейса можно просто воспользоваться графическими объектами: для ввода числовых значений использовать графический объект "поле ввода", для параметров типа перечислений (выпадающих списков) — по несколько кнопок. Однако этот подход будет очень трудоемким. К настоящему моменту на MQL5 написано несколько библиотек для создания графического интерфейса. Библиотеки позволяют создавать стандартные элементы управления: диалоговые окна, поля ввода с кнопками для увеличения и уменьшения значения (спинбоксы), выпадающие списки и многое другое. В комплект терминала входит набор стандартных классов для создания панелей и диалогов. В разделе "Статьи" есть огромная серия статей, посвященных созданию графического интерфейса.

Также в статьях есть и небольшая  серия из трех публикаций (статья 1, статья 2, статья 3), посвященная очень простому и быстрому созданию графических интерфейсов. Кроме рассмотрения теории, в этих статьях создается библиотека для быстрой работы с графическими объектами и для создания графического интерфейса. Все перечисленные выше варианты имеют свои преимущества и недостатки, все они очень подробно рассматривались при написании данной статьи. Был выбран последний вариант из перечисленных (библиотека incGUI).

Терминал MetaTrader 5 очень активно развивается и совершенствуется, поэтому некоторые элементы управления из предлагаемой библиотеки можно считать морально устаревшими (в частности, полосы прокрутки),  но тем не менее ими можно пользоваться. Чтобы начать использовать библиотеку, скачайте приложение к статье "Пользовательские графические элементы управления. Часть 3. Формы для MetaTrader 5", распакуйте его, файл "incGUI_v3.mqh" разместите в папке Include, расположенной в папке данных терминала.

Класс формы

Всю работу по созданию графического интерфейса будем проводить в отдельном файле "UniOscGUI.mqh". Первым делом подключим библиотеку:

#include <IncGUI_v3.mqh>

Выполним компиляцию для проверки. При компиляции выявится несколько предупреждений, о которых компилятор не сообщал ранее. Теперь усовершенствованный компилятор позволяет выявить эти недостатки кода и внести исправления. В приложении к статье можно найти исправленный файл "inc_GUI_v4". Вместо "IncGUI_v3.mqh" подключим "IncGUI_v4.mqh" и "UniOscDefines.mqh". 

#include <IncGUI_v4.mqh>
#include <UniOsc/UniOscDefines.mqh>

Сохраним копию индикатора "iUniOsc" с именем "iUniOscGUI". После этого индикатор "iUniOsc" можно немного отредактировать — скрыть параметры UseDefault и KeepPrev. В индикаторе без графического интерфейса они не имеют смысла, но нужно установить им значения false:

bool                 UseDefault  =  false;
bool                 KeepPrev    =  false;

На этом индикатор "iUniOsc" считается полностью завершенным.

Продолжим с индикатором "iUniOscGUI". Подключим к нему файл "UniOscGUI.mqh". Всего должно быть подключено три файла:

#include <UniOsc/UniOscDefines.mqh>
#include <UniOsc/CUniOsc.mqh>
#include <UniOsc/UniOscGUI.mqh>

Компилируя индикатор, можно будет проверять код и тут же видеть на графике графический интерфейс. Но пока вся работа будет выполняться в файле "UniOscGUI.mqh". 

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

Начнем с формы. Подробная пошаговая процедура создания формы изложена в статье "Пользовательские графические элементы управления. Часть 3. Формы для MetaTrader 5". Здесь выполним эту процедуру применительно к нашей задаче.

1. Из файла "IncGUI_v4.mqh" копируем класс CFormTemplate в файл "UniOscGUI.mqh" и переименовываем его в CUniOscForm.

2. Установим свойства. Это делается в методе MainProperties() класса CUniOscForm. Установим следующие свойства:

void MainProperties(){
      m_Name         =  "UniOscForm";
      m_Width        =  FORM_WIDTH;
      m_Height       =  150;
      m_Type         =  0;
      m_Caption      =  "UniOsc";
      m_Movable      =  true;
      m_Resizable    =  true;
      m_CloseButton  =  true;
}

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

#define FORM_WIDTH 210        // ширина формы
#define SPIN_BOX_WIDTH 110    // ширина спинбокса
#define COMBO_BOX_WIDTH 110   // ширина выпадающего списка

После этого форму можно применить в индикаторе. В индикаторе объявим внешнюю переменную UseGUI со значением по умолчанию true (в самом начале окна свойств):

input bool                 UseGUI      =  true;

Затем, после внешних переменных объявим переменную — указатель для класса формы:

CUniOscForm * frm;

В функции OnInit() индикатора, если значение переменной UseGUI равно true, создадим объект и подготовим его к использованию, вызвав необходимые методы для установки дополнительных свойств:

frm=new CUniOscForm();  // создание объекта
frm.Init();             // инициализация
frm.SetSubWindow(0);    // установка подокна в котором отображается форма
frm.SetPos(10,30);      // установка начальной позиции формы
frm.Show();             // включение видимости формы

В функции OnDeinit() скроем форму и удалим объект:

if(CheckPointer(frm)==POINTER_DYNAMIC){
   frm.Hide();
   delete(frm);
}

Из функции OnChartEvent() вызовем метод Event():

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

Если теперь прикрепить индикатор на график, можно увидеть форму (рис. 2).


Рис. 2. Форма созданная классом CUniOscForm при прикреплении индикатора iUniOscGUI на график 


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

При нажатии на кнопку закрытия формы метод Event() возвращает значение 1. Проверим возвращаемое значение и, в случае необходимости, удалим с графика индикатор:

int win=ChartWindowFind(0,ShortName);  // поиск подокна
ChartIndicatorDelete(0,win,ShortName); // удаление индикатора
ChartRedraw();                         // ускорение перерисовки графика

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

Добавим на форму главный элемент управления: выпадающий список для выбора типа осциллятора. Для его создания используется класс CComBox. Добавляем код в класс CUniOscForm. Объявим переменную для объекта:

CComBox m_cmb_main;

Затем в методе OnInitEvent() вызовем метод Init() класса:

m_cmb_main.Init("cb_main",100," select oscillator");

В метод передается имя элемента управления (префикс для имен графических объектов), ширина элемента управления и надпись возле него. 

В методе OnShowEvent() вызовем метод Show():

m_cmb_main.Show(aLeft+10,aTop+10);

При вызове метода указываются координаты расположения элемента на форме (с отступом 10 пикселей от левого верхнего угла пользовательского пространства формы). 

В метод OnHideEvent() вызовем метод Hide():

m_cmb_main.Hide();

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

int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
if(me==1){
   Alert(frm.m_cmb_main.SelectedText());
}  

В метод передаются стандартные параметры события графика, а при возвращении методом значения 1 открывается окно сообщения.

Необходимо заполнить список вариантами. Здесь может быть несколько подходов:

  • все сделать в методе OnInitEvent() формы
  • добавить в класс формы дополнительный метод и вызывать его из индикатора после метода Init()
  • обратиться к методам списка непосредственно из индикатора.

Используем третий вариант (он требует наименьшего количества кода). Сначала в индикаторе создадим массив с вариантами осцилляторов:

EOscUniType osctype[]={
   OscUni_ATR,
   OscUni_BearsPower,
   OscUni_BullsPower,  
   OscUni_CCI,
   OscUni_Chaikin,
   OscUni_DeMarker,
   OscUni_Force,
   OscUni_Momentum,
   OscUni_MACD,
   OscUni_OsMA,
   OscUni_RSI,
   OscUni_RVI,
   OscUni_Stochastic,
   OscUni_TriX,
   OscUni_WPR
};

Затем, после вызова frm.Init() в индикаторе заполним список и установим выбранный по умолчанию пункт:

for(int i=0;i<ArraySize(osctype);i++){
   frm.m_cmb_main.AddItem(EnumToString(osctype[i]));
}
frm.m_cmb_main.SetSelectedIndex(0);

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

 
Рис. 3. Форма со списком осцилляторов и окно сообщения после изменения выбора в списке 

Элементы управления на форме

В начале статьи было определено максимальное количество внешних параметров по типам (три для ввода числовых значений и четыре для стандартных перечислений). Для ввода числовых значений используем элемент CSpinInputBox (поле ввода с кнопками) библиотеки incGUI, для стандартных перечислений — элемент CComBox (выпадающий список).

В начале файла с классом графического интерфейса объявим массивы со значениями стандартных перечислений:

ENUM_APPLIED_PRICE e_price[]={   PRICE_CLOSE,
                                 PRICE_OPEN,
                                 PRICE_HIGH,
                                 PRICE_LOW,
                                 PRICE_MEDIAN,
                                 PRICE_TYPICAL,
                                 PRICE_WEIGHTED
};

ENUM_MA_METHOD e_method[]={MODE_SMA,MODE_EMA,MODE_SMMA,MODE_LWMA};

ENUM_APPLIED_VOLUME e_volume[]={VOLUME_TICK,VOLUME_REAL};

ENUM_STO_PRICE e_sto_price[]={STO_LOWHIGH,STO_CLOSECLOSE};

В классе формы объявим переменные для элементов управления (три типа CSpinInputBox и четыре CComBox): 

CSpinInputBox m_value1;
CSpinInputBox m_value2;      
CSpinInputBox m_value3;
      
CComBox m_price;
CComBox m_method;
CComBox m_volume
CComBox m_sto_price;

В классе формы, в методе OnInitEvent() инициализируем выпадающие списки (объекты класса CComBox) и заполним их, используя объявленные ранее массивы:

m_price.Init("price",COMBO_BOX_WIDTH," price");
m_method.Init("method",COMBO_BOX_WIDTH," method");
m_volume.Init("volume",COMBO_BOX_WIDTH," volume");
m_sto_price.Init("sto_price",COMBO_BOX_WIDTH," price");              

for(int i=0;i<ArraySize(e_price);i++){
   m_price.AddItem(EnumToString(e_price[i]));            
}
for(int i=0;i<ArraySize(e_method);i++){
   m_method.AddItem(EnumToString(e_method[i]));              
}            
for(int i=0;i<ArraySize(e_volume);i++){
   m_volume.AddItem(EnumToString(e_volume[i]));            
}            
for(int i=0;i<ArraySize(e_sto_price);i++){
   m_sto_price.AddItem(EnumToString(e_sto_price[i]));            
}

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

class CUniOscControls{
   protected:
      CSpinInputBox * m_value1;
      CSpinInputBox * m_value2;      
      CSpinInputBox * m_value3;
      CComBox * m_price;
      CComBox * m_method;
      CComBox * m_volume;
      CComBox * m_sto_price;
   public:
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CSpinInputBox & value3,
                        CComBox & price,
                        CComBox & method,
                        CComBox & volume,
                        CComBox & sto_price){
       ...
   }
   void Hide(){
       ...
   }
   int Event(int id,long lparam,double dparam,string sparam){
      ...
      return(0);
   }
   virtual void InitControls(){
   }  
   virtual void Show(int x,int y){
   }  
   virtual int FormHeight(){
      return(0);
   }
};

В начале использования объекта данного класса будет вызываться метод SetPointers(), в метод передаются указатели на все элементы управления, а в методе они сохраняются в собственных переменных класса: 

void SetPointers(CSpinInputBox & value1,
                     CSpinInputBox & value2,      
                     CSpinInputBox & value3,
                     CComBox & price,
                     CComBox & method,
                     CComBox & volume,
                     CComBox & sto_price){
   m_value1=GetPointer(value1);
   m_value2=GetPointer(value2);      
   m_value3=GetPointer(value3);            
   m_price=GetPointer(price);
   m_method=GetPointer(method);
   m_volume=GetPointer(volume);
   m_sto_price=GetPointer(sto_price);
}

Посредством этих указателей будет выполняться скрытие всех элементов управление (метод Hide()):

void Hide(){
   m_value1.Hide();
   m_value2.Hide();
   m_value3.Hide();
   m_price.Hide();
   m_method.Hide();
   m_volume.Hide();
   m_sto_price.Hide();
}

Будут обрабатываться их события (метод Event()):

int Event(int id,long lparam,double dparam,string sparam){
   int e1=m_value1.Event(id,lparam,dparam,sparam);
   int e2=m_value2.Event(id,lparam,dparam,sparam);
   int e3=m_value3.Event(id,lparam,dparam,sparam);
   int e4=m_price.Event(id,lparam,dparam,sparam);
   int e5=m_method.Event(id,lparam,dparam,sparam);
   int e6=m_volume.Event(id,lparam,dparam,sparam);
   int e7=m_sto_price.Event(id,lparam,dparam,sparam);
   if(e1!=0 || e2!=0 || e3!=0 || e4!=0 || e5!=0 ||e6!=0 || e7!=0){
      return(1);
   }
   return(0);
}

Остальные методы виртуальные, для каждого осциллятора будет свой код в дочерних классах. Метод Show() будет использоваться для отображения элементов управления. Метод FormHeight() будет возвращать высоту формы. Метод InitControls() предназначен только для смены надписей возле элементов управления (рис. 4).


Рис. 4. Разные надписи возле одного и того же элемента управления у разных осцилляторов 

Дело в том, что элементы управления из библиотеки incGUI имеют только минимально необходимые наборы методов и не имеют методов для изменения надписей. Но классы разработаны таким образом, что в случае необходимости надпись можно изменить, вызвав метод Init(). Поскольку смена надписи выполняется методом Init(), поэтому метод назван InitControls().  

Рассмотрим несколько дочерних классов. Самый простой из них — для индикатора ATR, самый сложный — для Stochastic.

Для ATR:

class CUniOscControls_ATR:public CUniOscControls{
   void InitControls(){
      m_value1.Init("value1",SPIN_BOX_WIDTH,1," ma_period");
   }
   void Show(int x,int y){
      m_value1.Show(x,y);
   }  
   int FormHeight(){
      return(70);
   }  
};

В методе InitContrlos() выполняется вызов метода Init() элемента управления, самое главное (для чего пришлось делать этот виртуальный метод) — передается текст надписи "ma_period", который будет отображаться справа от элемента управления.

В методе Show() класса формы выполняется вызов метода Show() класса CUniOscControls, при вызове указываются координаты верхнего левого угла первого (верхнего) элемента управления. Метод FormHeight() просто возвращает значение.

Для Stochastic:

class CUniOscControls_Stochastic:public CUniOscControls{
   void InitControls(){
      m_value1.Init("value1",SPIN_BOX_WIDTH,1," Kperiod");
      m_value2.Init("value2",SPIN_BOX_WIDTH,1," Dperiod");  
      m_value3.Init("value3",SPIN_BOX_WIDTH,1," slowing");          
   }
   void Show(int x,int y){
      m_value1.Show(x,y);
      m_value2.Show(x,y+20);      
      m_value3.Show(x,y+40);
      m_method.Show(x,y+60);      
      m_sto_price.Show(x,y+80);
   }
   int FormHeight(){
      return(150);
   }    
};

В методе Show() выполняется вычисление координат для каждого элемента управления, все остальное уже должно быть понятно.

Наконец, рассмотрим непосредственно добавление элементов управления на форму. В классе формы объявим переменную-указатель на класс с элементами управления:

CUniOscControls * m_controls;

В деструкторе удалим объект:

void ~CUniOscForm(){
   delete(m_controls);
}

Добавим в класс формы метод SetType(). Этот метод будет вызываться для указания типа используемого осциллятора. 

      void SetType(long type){
         if(CheckPointer(m_controls)==POINTER_DYNAMIC){
            delete(m_controls);
            m_controls=NULL;
         }
        
         switch((EOscUniType)type){
            case OscUni_ATR:
               m_controls=new CUniOscControls_ATR();
            break;
            case OscUni_BearsPower:
               m_controls=new CUniOscControls_BearsPower();
            break;
            case OscUni_BullsPower:
               m_controls=new CUniOscControls_BullsPower();
            break;
            case OscUni_CCI:
               m_controls=new CUniOscControls_CCI();
            break;
            case OscUni_Chaikin:
               m_controls=new CUniOscControls_Chaikin();
            break;
            case OscUni_DeMarker:
               m_controls=new CUniOscControls_DeMarker();
            break;
            case OscUni_Force:
               m_controls=new CUniOscControls_Force();
            break;
            case OscUni_Momentum:
               m_controls=new CUniOscControls_Momentum();
            break;
            case OscUni_MACD:
               m_controls=new CUniOscControls_MACD();
            break;
            case OscUni_OsMA:
               m_controls=new CUniOscControls_OsMA();
            break;
            case OscUni_RSI:
               m_controls=new CUniOscControls_RSI();
            break;
            case OscUni_RVI:
               m_controls=new CUniOscControls_RVI();
            break;
            case OscUni_Stochastic:
               m_controls=new CUniOscControls_Stochastic();
            break;
            case OscUni_TriX:
               m_controls=new CUniOscControls_TriX();
            break;
            case OscUni_WPR:
               m_controls=new CUniOscControls_WPR();
            break;
         }
        
         m_controls.SetPointers(m_value1,m_value2,m_value3,m_price,m_method,m_volume,m_sto_price);
         m_controls.InitControls();
        
         m_value1.SetReadOnly(false);
         m_value2.SetReadOnly(false);
         m_value3.SetReadOnly(false);
        
         m_value1.SetMinValue(1);        
         m_value2.SetMinValue(1);
         m_value3.SetMinValue(1);
        
         m_Height=m_controls.FormHeight();        
        
      }  

В начале метода выполняется удаление объекта, если он существовал. Затем, в зависимости от типа индикатора, выполняется загрузка соответствующего класса. Внизу метода вызывается метод SetPointers(), и метод InitControls(). Затем выполняется несколько дополнительных действий: для элементов управления SpinBox включается возможность ввода с клавиатуры (вызов метода ReadOnly()), устанавливаются минимальные значения (вызов метода SetMinValue()), и переменной m_Height присваивается новое значение высоты формы.

В методах OnShowEvent() и OnHideEvent() формы вызовем соответствующие методы объекта m_controls:

void OnShowEvent(int aLeft, int aTop){
   m_cmb_main.Show(aLeft+10,aTop+10);
   m_controls.Show(aLeft+10,aTop+10+20);
}
void OnHideEvent(){
   m_cmb_main.Hide();            
   m_controls.Hide();          
}  

Остается "оживить" события объекта m_controls. В индикатор в функцию OnChartEvent() добавляем вызова метода Event():

int ce=frm.m_controls.Event(id,lparam,dparam,sparam);

В OnInit() индикатора добавляем вызов метода SetType() формы (после вызова метода SetSelectedIndex()):

frm.SetType(_Type);

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

void SetValues(int period1,
               int period2,
               int period3,
               long method,
               long price,
               long volume,  
               long sto_price  
){
  
   m_value1.SetValue(period1);
   m_value2.SetValue(period2);      
   m_value3.SetValue(period3);
  
   for(int i=0;i<ArraySize(e_price);i++){
      if(price==e_price[i]){
         m_price.SetSelectedIndex(i);
         break;
      }
   }
  
   for(int i=0;i<ArraySize(e_method);i++){
      if(method==e_method[i]){
         m_method.SetSelectedIndex(i);
         break;
      }
   }            

   for(int i=0;i<ArraySize(e_volume);i++){
      if(volume==e_volume[i]){
         m_volume.SetSelectedIndex(i);
         break;
      }
   }
  
   for(int i=0;i<ArraySize(e_sto_price);i++){
      if(sto_price==e_sto_price[i]){
         m_sto_price.SetSelectedIndex(i);
         break;
      }
   }

}      

В методе SetValues() элементам управления типа SpinBox значения устанавливаются как есть, а для перечислений выполняется поиск индекса в массивах со значениями перечислений. Вызовем метод SetValues() после вызова метода SetType():

frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice);

На этом этапе графический интерфейс можно считать полностью готовым (рис. 5), но только пока индикатор еще не умеет на него реагировать.


Рис. 5. Вид окна с элементами управления для индикатора ATR 

Завершение создания универсального осциллятора

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

На данном этапе функция OnChatEvent() должна выглядеть следующим образом:

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

   int e=frm.Event(id,lparam,dparam,sparam);  
   if(e==1){
      int win=ChartWindowFind(0,ShortName);
      ChartIndicatorDelete(0,win,ShortName);
      ChartRedraw();
   }
  
   int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
  
   int ce=frm.m_controls.Event(id,lparam,dparam,sparam);

}

Необходимо обработать событие смены индикатора (переменная me) и событие смены параметров (переменная ce).

Смена индикатора:

if(me==1){

   // перезагрузка индикатора
  
   _Type=osctype[frm.m_cmb_main.SelectedIndex()]; // новый тип
  
   delete(osc); // удаление старого объекта
   LoadOscillator(); // загрузка нового индикатора
  
   if(!osc.CheckHandle()){
      Alert("Ошибка загрузки индикатора "+osc.Name());
   }
  
   SetStyles(); // установка стилей
  
   // установка короткого имени
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

   // обновление формы

   frm.SetType(osctype[frm.m_cmb_main.SelectedIndex()]); // установка типа
   frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice); // установка значений
   frm.Refresh(); // обновление формы
  
   // пересчет индикатора
   EventSetMillisecondTimer(100);

}  

Рассмотрим этот код подробнее. При выборе осциллятора в главном списке метод Event() возвращает 1, в этом случае переменной _Type присваивается новое значение типа, удаляется старый объект, загружается новый объект, устанавливаются стили, короткое имя. После загрузки индикатора обновляется вид формы: устанавливается тип, параметры и вызывается метод Refresh(), чтобы вид формы изменился в соответствии с новыми параметрами. В конце запускается таймер (о нем поговорим немного позже).

Рассмотрим участок кода для смены параметров: 

if(ce==1){
  
   if((int)frm.m_value1.Value()>0){
      _Period1=(int)frm.m_value1.Value();
   }
   if((int)frm.m_value2.Value()>0){
      _Period2=(int)frm.m_value2.Value();
   }
   if((int)frm.m_value3.Value()>0){
      _Period3=(int)frm.m_value3.Value();
   }      
   if(frm.m_method.SelectedIndex()!=-1){
      _MaMethod=e_method[frm.m_method.SelectedIndex()];
   }
   if(frm.m_price.SelectedIndex()!=-1){
      _Price=e_price[frm.m_price.SelectedIndex()];
   }
   if(frm.m_volume.SelectedIndex()!=-1){
      _Volume=e_volume[frm.m_volume.SelectedIndex()];
   }
   if(frm.m_sto_price.SelectedIndex()!=-1){
      _StPrice=e_sto_price[frm.m_sto_price.SelectedIndex()];
   }
  
   delete(osc);
   LoadOscillator();
   if(!osc.CheckHandle()){
      Alert("Ошибка загрузки индикатора "+osc.Name());
   }  
  
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
  
   EventSetMillisecondTimer(100);
    
}

При изменении параметров метод Event() класса элементов управления возвращает 1. В этом случае всем переменным присваиваются новые значения, но выполняется их предварительная проверка. Значения элементов управления SpinBox должны быть больше нуля, а  выпадающих списков — не должны равняться -1. Дальше все так же, как и при смене индикатора.

Теперь о таймере. Для выполнения расчета индикатора требуется некоторое время. Поэтому запускается таймер, и в его  функции выполняется периодическая проверка готовности индикатора при помощи функции BarsCalculated(). Если возвращаемое значение больше нуля, значит, индикатор рассчитан полностью, при этом выполняется вызов метода Calculate() объекта osc:

void OnTimer(){
   if(BarsCalculated(osc.Handle())>0){
      if(osc.Calculate(Bars(Symbol(),Period()),0,Label1Buffer,Label2Buffer)!=0){
         ChartRedraw();    
         EventKillTimer();
      }
   }
}

Первым параметром в метод Calculate() передается количество баров, а вторым - 0, чтобы произошел полный пересчет индикатора. После этого график обновляется (ChartRedaraw()) и отключается таймер. 

Теперь индикатор должен реагировать на графический интерфейс. Значит, он почти готов.

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

input bool                 UseGUI      =  true;

Часть кода функции OnInit(), связанную с созданием формы, будем выполнять только если переменная UseGUI включена:

if(UseGUI){
   frm=new CUniOscForm();
   frm.Init();
   int ind=0;
  
   for(int i=0;i<ArraySize(osctype);i++){
      frm.m_cmb_main.AddItem(EnumToString(osctype[i]));
      if(osctype[i]==_Type){
         ind=i;
      }
   }
  
   frm.m_cmb_main.SetSelectedIndex(ind);      
   frm.SetType(_Type);
   frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice);
  
   frm.SetSubWindow(0);
   frm.SetPos(10,30);
   frm.Show();
}

И еще один небольшой завершающий штрих. Библиотека incGUI поддерживает смену цветовых схем элементов управления. Задействуем эту возможность.

Сразу после внешних параметров добавим следующий код:

enum eColorScheme{
   DefaultScheme=0,
   YellowBrownScheme=1,
   BlueScheme=2,
   GreenScheme=3,
   YellowBlackScheme=4,
   LimeBlackScheme=5,
   AquaBlackScheme=6
};

input eColorScheme ColorScheme=DefaultScheme;

Этот код добавит в окно свойств индикатора выпадающий список для выбора цветовой схемы. В самое начало функции OnInit() добавим одну строку:

ClrScheme.SetScheme(ColorScheme);

Теперь индикатор iUniOscGUI полностью завершен, а графический интерфейс даже может иметь различный цвет (рис. 6).

 
Рис. 6. Различные цветовые схемы графического интерфейса индикатора iUniOscGUI 

Заключение

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

Файлы приложения

  • UniOscDefines.mqh — файл с перечислением типов осцилляторов.
  • CUniOsc.mqh — классы универсального осциллятора.
  • iUniOsc.mq5 — универсальный осциллятор без графического интерфейса.
  • UniOscGUI.mqh — классы для создания графического интерфейса осциллятора. 
  • iUniOscGUI.mq5 — универсальный осциллятор с графическим интерфейсом. 
  • IncGUI_v4.mqh — библиотека для работы с графическими объектами и создания графического интерфейса. Ранее происходила путаница с версией библиотеки. Существовало два файла версии 3 с одинаковыми именами: в статье и в CodeBase (с обновленным классом создания таблиц CTable). В файле IncGUI_v4 не только выполнены исправления, но еще и заменен класс создания таблиц на более новый (из CodeBase).