Мастер MQL5: Расширение стандартной библиотеки для установки ордеров, стопов и целей по вычисляемым ценам

Andrey Shpilev | 14 июля, 2014

Введение

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

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

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

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

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

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


1. Стандартный алгоритм принятия решений

Эксперты, сгенерированные в Мастере MQL5, основываются на экземпляре класса CExpert. В этом классе объявлен указатель на объект класса CExpertSignal. Этот объект для краткости в дальнейшем будем называть главным сигналом. Главный сигнал содержит указатели на подчиненные фильтры (модули сигналов – наследники класса CExpertSignal).

На новом тике, если нет открытых позиций и ордеров, эксперт обращается к главному сигналу для проверки возможности открытия позиции. Главный сигнал поочередно опрашивает подчиненные фильтры, и на основе полученных прогнозов вычисляет средневзвешенный прогноз (направление). Если его значение превышает порог (значение параметра m_threshold_open в главном сигнале), эксперту передаются параметры ордера и результат проверки условий типа bool. Если условия выполняются, открывается позиция по рыночной цене, либо отложенный ордер на определенном расстоянии от нее (рис. 1). Стопы также могут находиться только на фиксированном расстоянии. Отступы цены открытия, стоп-лосса и тейк-профита от рыночной цены задаются в настройках советника и хранятся внутри главного сигнала в переменных m_price_level, m_stop_level, m_take_level соответственно.

Итак, в данный момент для отправки ордера должны быть выполнены два условия:

  1. Нет открытых позиций по текущему инструменту;
  2. Абсолютное значение средневзвешенного прогноза превышает пороговое значение (тренд достаточно сильный).

Рис. 1. Схема принятия решений о входе в рынок

Рис. 1. Схема принятия решений о входе в рынок

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


2. Модифицированный алгоритм принятия решений

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

Модифицированный алгоритм (рис. 2) позволяет работать не только с отложенными ордерами. Его суть в том, что он отделяет точку входа (цену) от определения тренда (средневзвешенный прогноз). Т.е. для принятия решения во-первых, определяется предпочтительное направление торговли по фильтрам; во-вторых, выбирается подходящий по направлению набор параметров ордера от ценовых модулей. Если доступно несколько таких наборов, будет выбран набор, полученный от модуля с наибольшим весом (значением параметра m_weight). Если есть направление, но не выявлено доступных точек входа в данный момент, советник бездействует.

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

Рис. 2. Модифицированная схема принятия решений о входе в рынок

Итак, для отправки нового ордера потребуется соблюдение следующих условий:

  1. Нет открытых позиций и ордеров по текущему инструменту;
  2. Абсолютное значение средневзвешенного прогноза превышает пороговое значение;
  3. Найдена хотя бы одна цена открытия ордера.

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


3. Разработка модифицированных классов эксперта и сигнального модуля

Расширение библиотеки основано на двух классах: CExpertSignalAdvanced и CExpertAdvanced, наследованных от CExpertSignal и CExpert соответственно.

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

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

3.1. CExpertSignalAdvanced

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

class CExpertSignalAdvanced : public CExpertSignal
  {
protected:
   //---члены данных для хранения параметров размещаемых ордеров
   double            m_order_open_long;         //цена открытия ордера на покупку
   double            m_order_stop_long;         //стоп-лосс ордера на покупку
   double            m_order_take_long;         //тейк-профит ордера на покупку
   datetime          m_order_expiration_long;   //время истечения ордера на покупку
   double            m_order_open_short;        //цена открытия ордера на продажу
   double            m_order_stop_short;        //стоп-лосс ордера на продажу
   double            m_order_take_short;        //тейк-профит ордера на продажу
   datetime          m_order_expiration_short;  //время истечения ордера на продажу
   //---             
   int               m_price_module;            //индекс первого ценового модуля в массиве m_filters
public:
                     CExpertSignalAdvanced();
                    ~CExpertSignalAdvanced();
   virtual void      CalcPriceModuleIndex() {m_price_module=m_filters.Total();}
   virtual bool      CheckOpenLong(double &price,double &sl,double &tp,datetime &expiration);
   virtual bool      CheckOpenShort(double &price,double &sl,double &tp,datetime &expiration);
   virtual double    Direction(void);		//вычисление средневзвешенного прогноза по данным сигнальных модулей
   virtual double    Prices(void);		//обновление параметров размещаемых ордеров по данным ценовых модулей
   virtual bool      OpenLongParams(double &price,double &sl,double &tp,datetime &expiration);
   virtual bool      OpenShortParams(double &price,double &sl,double &tp,datetime &expiration);
   virtual bool      CheckUpdateOrderLong(COrderInfo *order_ptr,double &open,double &sl,double &tp,datetime &ex);
   virtual bool      CheckUpdateOrderShort(COrderInfo *order_ptr,double &open,double &sl,double &tp,datetime &ex);
   double            getOpenLong()              { return m_order_open_long;         }
   double            getOpenShort()             { return m_order_open_short;        }
   double            getStopLong()              { return m_order_stop_long;         }
   double            getStopShort()             { return m_order_stop_short;        }
   double            getTakeLong()              { return m_order_take_long;         }
   double            getTakeShort()             { return m_order_take_short;        }
   datetime          getExpLong()               { return m_order_expiration_long;   }
   datetime          getExpShort()              { return m_order_expiration_short;  }
   double            getWeight()                { return m_weight;                  }
  };

В классе CExpertSignalAdvanced объявлены члены данных для хранения параметров ордеров. Значения этих переменных обновляются в методе Prices(). Эти переменные играют роль буфера.

Затем объявлен параметр m_price_module, который хранит индекс первого ценового модуля в массиве m_filters, объявленном в CExpertSignal. В этом массиве находятся указатели на подключенные модули сигналов. В начале хранятся указатели на стандартные модули (фильтры), затем, начиная с индекса m_price_module идут указатели на ценовые модули. Было решено хранить всё в одном массиве для того, чтобы не менять методы инициализации индикаторов и таймсерий. Тем более, что в сумме через один массив можно подключить 64 модуля, а этого обычно хватает с избытком.

Также в классе CExpertSignalAdvanced объявлены вспомогательные методы для получения значений защищенных членов данных. Их имена начинаются с get (см. декларацию класса).

3.1.1. Конструктор

Конструктор CExpertSignalAdvanced инициализирует переменные, объявленные внутри класса:

CExpertSignalAdvanced::CExpertSignalAdvanced()
  {
   m_order_open_long=EMPTY_VALUE;
   m_order_stop_long=EMPTY_VALUE;
   m_order_take_long=EMPTY_VALUE;
   m_order_expiration_long=0;
   m_order_open_short=EMPTY_VALUE;
   m_order_stop_short=EMPTY_VALUE;
   m_order_take_short=EMPTY_VALUE;
   m_order_expiration_short=0;
   m_price_module=-1;
  }

3.1.2. CalcPriceModuleIndex()

Метод CalcPriceModuleIndex() присваивает в m_price_module текущее число элементов массива, которое равно индексу следующего добавляемого модуля. Метод вызывается перед добавлением первого ценового модуля. Тело функции находится в декларации класса.

virtual void      CalcPriceModuleIndex() {m_price_module=m_filters.Total();}

3.1.3. CheckOpenLong(...) и CheckOpenShort(...)

Метод CheckOpenLong(...) вызывается из экземпляра класса CExpert и работает следующим образом:

  1. Проверяем наличие подключенных ценовых модулей. Если их нет, вызываем одноименный метод родительского класса;
  2. Получаем средневзвешенный прогноз (направление) от метода Direction();
  3. Проверяем соблюдение условий для входа: сравниваем направление с EMPTY_VALUE и пороговым значением m_threshold_open;
  4. Обновляем значения параметров ордера методом Prices(), передаем их эксперту функцией OpenLongParams(...), сохраняем результат этой функции;
  5. Возвращаем сохраненный результат.
bool CExpertSignalAdvanced::CheckOpenLong(double &price,double &sl,double &tp,datetime &expiration)
  {
//--- если не найдено ценовых модулей, вызываем метод базового класса CExpertSignal
   if(m_price_module<0)
      return(CExpertSignal::CheckOpenLong(price,sl,tp,expiration));

   bool   result   =false;
   double direction=Direction();
//--- запретительный сигнал
   if(direction==EMPTY_VALUE)
      return(false);
//--- проверка превышения порога
   if(direction>=m_threshold_open)
     {
      Prices();
      result=OpenLongParams(price,sl,tp,expiration);//there's a signal if m_order_open_long!=EMPTY_VALUE
     }
//--- возврат результата
   return(result);
  }

CheckOpenShort(...) устроен и используется аналогично, поэтому его рассмотрение опустим.

3.1.4 Direction()

Метод Direction() опрашивает фильтры и вычисляет средневзвешенный прогноз. Этот метод является почти точной копией одноименного метода родительского класса CExpertSignal за небольшим исключением: в цикле обращаемся не ко всем элементам массива m_filters, а только к тем, чей индекс от 0 и меньше m_price_module. Всё остальное совпадает сCExpertSignal::Direction().

double CExpertSignalAdvanced::Direction(void)
  {
   long   mask;
   double direction;
   double result=m_weight*(LongCondition()-ShortCondition());
   int    number=(result==0.0)? 0 : 1;      // число опрошенных модулей
//--- цикл по фильтрам
   for(int i=0;i<m_price_module;i++)
     {
      //--- маска для битовых карт (переменных, содержащих флаги)
      mask=((long)1)<<i;
      //--- проверка флага игнорирования сигнала фильтра
      if((m_ignore&mask)!=0)
         continue;
      CExpertSignal *filter=m_filters.At(i);
      //--- проверка указателя
      if(filter==NULL)
         continue;
      direction=filter.Direction();
      //--- запретительный сигнал
      if(direction==EMPTY_VALUE)
         return(EMPTY_VALUE);
      if((m_invert&mask)!=0)
         result-=direction;
      else
         result+=direction;
      number++;
     }
//--- усреднение суммы взвешенных прогнозов
   if(number!=0)
      result/=number;
//--- возврат результата
   return(result);
  }

3.1.5. Prices()

Метод Prices() перебирает вторую часть массива m_filters, начиная с индекса m_price_module до конца, опрашивает ценовые модули и обновляет значения соответствующих переменных класса с помощью функций OpenLongParams(...) и OpenShortParams(...). Перед циклом значения параметров сбрасываются.

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

double CExpertSignalAdvanced::Prices(void)
  {
   m_order_open_long=EMPTY_VALUE;
   m_order_stop_long=EMPTY_VALUE;
   m_order_take_long=EMPTY_VALUE;
   m_order_expiration_long=0;
   m_order_open_short=EMPTY_VALUE;
   m_order_stop_short=EMPTY_VALUE;
   m_order_take_short=EMPTY_VALUE;
   m_order_expiration_short=0;
   int    total=m_filters.Total();
   double last_weight_long=0;
   double last_weight_short=0;
//--- цикл по ценовым модулям
   for(int i=m_price_module;i<total;i++)
     {   
      CExpertSignalAdvanced *prm=m_filters.At(i);
      if(prm==NULL)
         continue;
//--- игнорируем текущий модуль, если он вернул EMPTY_VALUE
      if(prm.Prices()==EMPTY_VALUE)continue;
      double weight=prm.getWeight();
      if(weight==0.0)continue;
//--- выбор непустых значений от модулей с наибольшим весом
      if(weight>last_weight_long && prm.getExpLong()>TimeCurrent())
         if(prm.OpenLongParams(m_order_open_long,m_order_stop_long,m_order_take_long,m_order_expiration_long))
            last_weight_long=weight;
      if(weight>last_weight_short && prm.getExpShort()>TimeCurrent())
         if(prm.OpenShortParams(m_order_open_short,m_order_stop_short,m_order_take_short,m_order_expiration_short))
            last_weight_short=weight;
     }
   return(0);
  }

3.1.6. OpenLongParams(...) и OpenShortParams(...)

В рамках класса CExpertSignalAdvanced Метод OpenLongParams(...) передает по ссылкам значения параметров ордера на покупку из переменных класса в параметры, которые принимает на вход.

Назначение этого метода в родительском классе было немного другим: в главном сигнале он вычислял необходимые параметры на основе рыночной цены и заданных отступов. Теперь он только передает уже готовые параметры. Если цена открытия корректна (не равна EMPTY_VALUE), то метод возвращает true, иначе false.

bool CExpertSignalAdvanced::OpenLongParams(double &price,double &sl,double &tp,datetime &expiration)
  {
   if(m_order_open_long!=EMPTY_VALUE)
     {
      price=m_order_open_long;
      sl=m_order_stop_long;
      tp=m_order_take_long;
      expiration=m_order_expiration_long;
      return(true);
     }
   return(false);
  }

OpenShortParams(...) устроен и используется аналогично, поэтому его рассмотрение опустим.

3.1.7. CheckUpdateOrderLong(...) и CheckUpdateOrderShort(...)

Методы CheckUpdateOrderLong(...) и CheckUpdateOrderShort(...) вызываются классом CExpertAdvanced. Они используются для обновления уже размещенного отложенного ордера согласно последним актуальным значениям ценовых уровней.

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

bool CExpertSignalAdvanced::CheckUpdateOrderLong(COrderInfo *order_ptr,double &open,double &sl,double &tp,datetime &ex)
  {
   Prices();   //обновляем цены
//--- проверяем наличие изменений
   double point=m_symbol.Point();
   if(   MathAbs(order_ptr.PriceOpen() - m_order_open_long)>point
      || MathAbs(order_ptr.StopLoss()  - m_order_stop_long)>point
      || MathAbs(order_ptr.TakeProfit()- m_order_take_long)>point
      || order_ptr.TimeExpiration()!=m_order_expiration_long)
      return(OpenLongParams(open,sl,tp,ex));
//--- обновление не требуется   
   return (false);
  }

CheckUpdateOrderShort(...) устроен и используется аналогично, поэтому его рассмотрение опустим.

3.2. CExpertAdvanced

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

class CExpertAdvanced : public CExpert
  {
protected:
   virtual bool      CheckTrailingOrderLong();
   virtual bool      CheckTrailingOrderShort();
   virtual bool      UpdateOrder(double price,double sl,double tp,datetime ex);
public:
                     CExpertAdvanced();
                    ~CExpertAdvanced();
  };

Как видно, методов немного, и конструктор с деструктором пустые.

3.2.1. CheckTrailingOrderLong() и CheckTrailingOrderShort()

Метод CheckTrailingOrderLong() переопределяет одноименный метод базового класса и вызывает метод CheckUpdateOrderLong(...) главного сигнала, чтобы выяснить необходимость модификации ордера. Если модификация требуется, вызывается метод UpdateOrder(...) и возвращается результат.

bool CExpertAdvanced::CheckTrailingOrderLong(void)
  {
   CExpertSignalAdvanced *signal_ptr=m_signal;
//--- проверка возможности изменения ордера на покупку
   double price,sl,tp;
   datetime ex;
   if(signal_ptr.CheckUpdateOrderLong(GetPointer(m_order),price,sl,tp,ex))
      return(UpdateOrder(price,sl,tp,ex));
//--- возврат без действий
   return(false);
  }

Метод CheckTrailingOrderShort() устроен и используется аналогично.

3.2.2. UpdateOrder()

Функция UpdateOrder() сначала проверяет, есть ли актуальная цена (не EMPTY_VALUE). Если нет, ордер удаляется, иначе модифицируем ордер согласно полученным параметрам.

bool CExpertAdvanced::UpdateOrder(double price,double sl,double tp,datetime ex)
  {
   ulong  ticket=m_order.Ticket();
   if(price==EMPTY_VALUE)
      return(m_trade.OrderDelete(ticket));
//--- изменяем ордер, возвращаем результат
   return(m_trade.OrderModify(ticket,price,sl,tp,m_order.TypeTime(),ex));
  }

На этом разработка наследников стандартных классов завершена.


4. Разработка ценовых модулей

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

Процесс разработки ценовых модулей аналогичен написанию модулей торговых сигналов: единственное отличие в том, что нужно переопределить не LongCondition(), ShortCondition() или Direction() как в сигнальных модулях, а Prices() – метод, отвечающий за обновление цен внутри модуля. Поэтому читателю желательно иметь представление о разработке модулей сигналов. Помочь в этом могут статьи "Создай торговый робот за 6 шагов!" и "Генератор торговых сигналов пользовательского индикатора".

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

4.1. Ценовой модуль на основе индикатора "Delta ZigZag"

Индикатор Delta ZigZag строит уровни по указанному количеству последних вершин. Пересечение этих уровней ценой означает вероятный разворот тренда.

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

Рис. 3. Иллюстрация работы ценового модуля на индикаторе Delta ZigZag на примере ордера на покупку

Рис. 3. Иллюстрация работы ценового модуля на индикаторе "Delta ZigZag" на примере ордера на покупку

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

4.1.1. Дескриптор модуля

// wizard description start
//+------------------------------------------------------------------+
//| Description of the class                                         |
//| Title=DeltaZZ Price Module                                       |
//| Type=SignalAdvanced                                              |
//| Name=DeltaZZ Price Module                                        |
//| ShortName=DeltaZZ_PM                                             |
//| Class=CPriceDeltaZZ                                              |
//| Page=not used                                                    |
//| Parameter=setAppPrice,int,1, Applied price: 0 - Close, 1 - H/L   |
//| Parameter=setRevMode,int,0, Reversal mode: 0 - Pips, 1 - Percent |
//| Parameter=setPips,int,300,Reverse in pips                        |
//| Parameter=setPercent,double,0.5,Reverse in percent               |
//| Parameter=setLevels,int,2,Peaks number                           |
//| Parameter=setTpRatio,double,1.6,TP:SL ratio                      |
//| Parameter=setExpBars,int,10,Expiration after bars number         |
//+------------------------------------------------------------------+
// wizard description end

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

4.1.2. Декларация класса

class CPriceDeltaZZ : public CExpertSignalAdvanced
  {
protected:
   CiCustom          m_deltazz;           //объект индикатора DeltaZZ
   //--- настройки модуля
   int               m_app_price;
   int               m_rev_mode;
   int               m_pips;
   double            m_percent;
   int               m_levels;
   double            m_tp_ratio;          //соотношение tp:sl
   int               m_exp_bars;          //время жизни ордера в барах
   //--- метод инициализации индикатора
   bool              InitDeltaZZ(CIndicators *indicators);
   //--- вспомогательные методы
   datetime          calcExpiration() { return(TimeCurrent()+m_exp_bars*PeriodSeconds(m_period)); }
   double            getBuySL();          //function for searching latest minimum of ZZ for buy SL
   double            getSellSL();         //function for searching latest maximum of ZZ for sell SL
public:
                     CPriceDeltaZZ();
                    ~CPriceDeltaZZ();
   //--- методы изменения настроек модуля
   void              setAppPrice(int ap)           { m_app_price=ap; }
   void              setRevMode(int rm)            { m_rev_mode=rm;  }
   void              setPips(int pips)             { m_pips=pips;    }
   void              setPercent(double perc)       { m_percent=perc; }
   void              setLevels(int rnum)           { m_levels=rnum;  }
   void              setTpRatio(double tpr)        { m_tp_ratio=tpr; }
   void              setExpBars(int bars)          { m_exp_bars=bars;}
   //--- метод проверки корректности настроек
   virtual bool      ValidationSettings(void);
   //--- метод создания индикаторов
   virtual bool      InitIndicators(CIndicators *indicators);
   //--- главный метод ценового модуля, обновляющий выходные данные
   virtual double    Prices();
  }; 

В модуле объявлены защищенные данные: объект индикатора типа CiCustom и параметры согласно типам, указанным в дескрипторе.

4.1.3. Конструктор

Конструктор инициализирует параметры класса значениями по умолчанию. Эти значения впоследствии повторно инициализируются при подключении модуля к главному сигналу согласно входным параметрам советника.

CPriceDeltaZZ::CPriceDeltaZZ() : m_app_price(1),
                                 m_rev_mode(0),
                                 m_pips(300),
                                 m_percent(0.5),
                                 m_levels(2),
                                 m_tp_ratio(1),
                                 m_exp_bars(10)
  {
  }

4.1.4. ValidationSettings()

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

bool CPriceDeltaZZ::ValidationSettings(void)
  {
//--- проверка настроек дополнительных фильтров
   if(!CExpertSignal::ValidationSettings())
      return(false);
//--- начальная проверка данных
   if(m_app_price<0 || m_app_price>1)
     {
      printf(__FUNCTION__+": Applied price must be 0 or 1");
      return(false);
     }
   if(m_rev_mode<0 || m_rev_mode>1)
     {
      printf(__FUNCTION__+": Reversal mode must be 0 or 1");
      return(false);
     }
   if(m_pips<10)
     {
      printf(__FUNCTION__+": Number of pips in a ray must be at least 10");
      return(false);
     }
   if(m_percent<=0)
     {
      printf(__FUNCTION__+": Percent must be greater than 0");
      return(false);
     }
   if(m_levels<1)
     {
      printf(__FUNCTION__+": Ray Number must be at least 1");
      return(false);
     }
   if(m_tp_ratio<=0)
     {
      printf(__FUNCTION__+": TP Ratio must be greater than zero");
      return(false);
     }
   if(m_exp_bars<0)
     {
      printf(__FUNCTION__+": Expiration must be zero or positive value");
      return(false);
     }
//--- проверка параметров пройдена
   return(true);
  } 

4.1.5. InitIndicators(...)

Метод InitIndicators(...) вызывает одноименный метод базового класса и инициализирует индикатор текущего модуля методом InitDeltaZZ(...).

bool CPriceDeltaZZ::InitIndicators(CIndicators *indicators)
  {
//--- инициализация индикаторов фильтров
   if(!CExpertSignal::InitIndicators(indicators))
      return(false);
//--- создание и инициализация пользовательского индикатора
   if(!InitDeltaZZ(indicators))
      return(false);
//--- успех
   return(true);
  }

4.1.6. InitDeltaZZ(...)

Метод InitDeltaZZ(...) добавляет объект пользовательского индикатора в коллекцию и создает новый индикатор "Delta ZigZag".

bool CPriceDeltaZZ::InitDeltaZZ(CIndicators *indicators)
  {
//--- добавление в коллекцию
   if(!indicators.Add(GetPointer(m_deltazz)))
     {
      printf(__FUNCTION__+": error adding object");
      return(false);
     }
//--- задание параметров индикатора
   MqlParam parameters[6];
//---
   parameters[0].type=TYPE_STRING;
   parameters[0].string_value="deltazigzag.ex5";
   parameters[1].type=TYPE_INT;
   parameters[1].integer_value=m_app_price;
   parameters[2].type=TYPE_INT;
   parameters[2].integer_value=m_rev_mode;
   parameters[3].type=TYPE_INT;
   parameters[3].integer_value=m_pips;
   parameters[4].type=TYPE_DOUBLE;
   parameters[4].double_value=m_percent;
   parameters[5].type=TYPE_INT;
   parameters[5].integer_value=m_levels;
//--- инициализация объекта
   if(!m_deltazz.Create(m_symbol.Name(),m_period,IND_CUSTOM,6,parameters))
     {
      printf(__FUNCTION__+": error initializing object");
      return(false);
     }
//--- количество буферов индикатора
   if(!m_deltazz.NumBuffers(5)) return(false);
//--- ок
   return(true);
  }

После успешного завершения методов ValidationSettings(), InitDeltaZZ(...) и InitIndicators(...) модуль инициализирован и готов к работе.

4.1.7. Prices()

Этот метод является основным для ценового модуля. Именно здесь обновляются параметры ордеров, значения которых передаются в главный сигнал. Метод возвращает результат операции типа double. Это сделано, главным образом, для будущих разработок. В дальнейшем результат метода Prices() может кодировать какие-либо особые ситуации и события, чтобы главный сигнал обрабатывал их соответствующим образом. В данный момент предусмотрена только обработка возвращаемого значения EMPTY_VALUE. При получении этого результата главный сигнал проигнорирует параметры, предлагаемые модулем.

Алгоритм работы метода Prices() в данном модуле:

  1. Берем цены открытия ордеров из индикаторных буферов 3 и 4 для покупок и продаж соответственно;
  2. Обнуляем итоговые параметры ордеров;
  3. Проверяем наличие цены на покупку. Если находим, ищем где поставить стоп-лосс методом getBuySL(), рассчитываем уровень тейк-профита по размеру стоп-лосса и цене открытия, вычисляем время истечения ордера;
  4. Проверяем наличие цены на продажу. Если находим, ищем где поставить стоп-лосс методом getSellSL(), рассчитываем уровень тейк-профита по размеру стоп-лосса и цене открытия, вычисляем время истечения ордера;
  5. Выходим.
Условия наличия цен на покупку и продажу взаимоисключающие в силу особенностей работы индикатора "Delta ZigZag". Буферы 3 и 4 по умолчанию отрисовываются точками (рис. 3).

double CPriceDeltaZZ::Prices(void)
  {
   double openbuy =m_deltazz.GetData(3,0);//получаем последнее значение из буфера 3
   double opensell=m_deltazz.GetData(4,0);//получаем последнее значение из буфера 4
//--- сброс значений параметров
   m_order_open_long=EMPTY_VALUE;
   m_order_stop_long=EMPTY_VALUE;
   m_order_take_long=EMPTY_VALUE;
   m_order_expiration_long=0;
   m_order_open_short=EMPTY_VALUE;
   m_order_stop_short=EMPTY_VALUE;
   m_order_take_short=EMPTY_VALUE;
   m_order_expiration_short=0;
   int digits=m_symbol.Digits();
//--- проверяем наличие цен на покупку
   if(openbuy>0)//если буфер 3 не пустой
     {
      m_order_open_long=NormalizeDouble(openbuy,digits);
      m_order_stop_long=NormalizeDouble(getBuySL(),digits);
      m_order_take_long=NormalizeDouble(m_order_open_long + m_tp_ratio*(m_order_open_long - m_order_stop_long),digits);
      m_order_expiration_long=calcExpiration();
     }
//--- проверяем наличие цен на продажу
   if(opensell>0)//если буфер 4 не пустой
     {
      m_order_open_short=NormalizeDouble(opensell,digits);
      m_order_stop_short=NormalizeDouble(getSellSL(),digits);
      m_order_take_short=NormalizeDouble(m_order_open_short - m_tp_ratio*(m_order_stop_short - m_order_open_short),digits);
      m_order_expiration_short=calcExpiration();
     }
   return(0);
  }

4.1.8. getBuySL() и getSellSL()

Методы getBuySL() и getSellSL() ищут локальные минимумы и максимумы соответственно для установки стоп-лоссов. В каждом методе ищется последнее ненулевое значение в соответствующем буфере  ценовой уровень последнего локального экстремума.

double CPriceDeltaZZ::getBuySL(void)
  {
   int i=0;
   double sl=0.0;
   while(sl==0.0)
     {
      sl=m_deltazz.GetData(0,i);
      i++;
     }
   return(sl);
  }
double CPriceDeltaZZ::getSellSL(void)
  {
   int i=0;
   double sl=0.0;
   while(sl==0.0)
     {
      sl=m_deltazz.GetData(1,i);
      i++;
     }
   return(sl);
  }

4.2. Ценовой модуль на основе внутреннего бара

Внутренний бар (Inside bar) является одним из наиболее широко используемых моделей (паттернов) в методике безындикаторной торговли, называемой Price action. Внутренним считается бар, чьи High и Low не выходят за экстремумы предыдущего бара. Внутренний бар является признаком начала консолидации или возможного разворота ценового движения.

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

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

Рис. 4. Иллюстрация внутренних баров и уровней ценового модуля

Рис. 4. Иллюстрация внутренних баров и уровней ценового модуля

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

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

#include <Expert\ExpertSignalAdvanced.mqh>
// wizard description start
//+------------------------------------------------------------------+
//| Description of the class                                         |
//| Title=Inside Bar Price Module                                    |
//| Type=SignalAdvanced                                              |
//| Name=Inside Bar Price Module                                     |
//| ShortName=IB_PM                                                  |
//| Class=CPriceInsideBar                                            |
//| Page=not used                                                    |
//| Parameter=setTpRatio,double,2,TP:SL ratio                        |
//| Parameter=setExpBars,int,10,Expiration after bars number         |
//| Parameter=setOrderOffset,int,5,Offset for open and stop loss     |
//+------------------------------------------------------------------+
// wizard description end
//+------------------------------------------------------------------+
//| Class CPriceInsideBar                                            |
//| Purpose: Класс генератора ценовых уровней для ордеров на основе  |
//|          паттерна "внутренний бар".                              |
//| Is derived from the CExpertSignalAdvanced class.                 |
//+------------------------------------------------------------------+
class CPriceInsideBar : public CExpertSignalAdvanced
  {
protected:
   double            m_tp_ratio;          //соотношение tp:sl
   int               m_exp_bars;          //время жизни ордера в барах
   double            m_order_offset;      //сдвиг уровней открытия и стоп-лосса
   datetime          calcExpiration()  { return(TimeCurrent()+m_exp_bars*PeriodSeconds(m_period)); }
public:
                     CPriceInsideBar();
                    ~CPriceInsideBar();
   void              setTpRatio(double ratio){ m_tp_ratio=ratio; }
   void              setExpBars(int bars)    { m_exp_bars=bars;}
   void              setOrderOffset(int pips){ m_order_offset=m_symbol.Point()*pips;}
   bool              ValidationSettings();
   double            Prices();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPriceInsideBar::CPriceInsideBar()
  {
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CPriceInsideBar::~CPriceInsideBar()
  {
  }
//+------------------------------------------------------------------+
//| Validation of protected settings                                 |
//+------------------------------------------------------------------+
bool CPriceInsideBar::ValidationSettings(void)
  {
//--- проверка параметров фильтров
   if(!CExpertSignal::ValidationSettings())
      return(false);
//--- начальная проверка данных
  if(m_tp_ratio<=0)
     {
      printf(__FUNCTION__+": TP Ratio must be greater than zero");
      return(false);
     }
   if(m_exp_bars<0)
     {
      printf(__FUNCTION__+": Expiration must be zero or positive value");
      return(false);
     }
//--- проверка пройдена
   return(true);
  }
//+------------------------------------------------------------------+
//| Price levels refreshing                                          |
//+------------------------------------------------------------------+
double CPriceInsideBar::Prices(void)
  {
   double h[2],l[2];
   if(CopyHigh(m_symbol.Name(),m_period,1,2,h)!=2 || CopyLow(m_symbol.Name(),m_period,1,2,l)!=2)
      return(EMPTY_VALUE);
//--- проверка внутреннего бара      
   if(h[0] >= h[1] && l[0] <= l[1])
     {
      m_order_open_long=h[0]+m_order_offset;
      m_order_stop_long=l[0]-m_order_offset;
      m_order_take_long=m_order_open_long+(m_order_open_long-m_order_stop_long)*m_tp_ratio;
      m_order_expiration_long=calcExpiration();
      
      m_order_open_short=m_order_stop_long;
      m_order_stop_short=m_order_open_long;
      m_order_take_short=m_order_open_short-(m_order_stop_short-m_order_open_short)*m_tp_ratio;
      m_order_expiration_short=m_order_expiration_long;
      return(0);
     }
   return(EMPTY_VALUE);
  }  
//+------------------------------------------------------------------+

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

4.3. Ценовой модуль на основе внешнего бара

Внешний бар (Outside bar) – еще один широко используемый паттерн Price Action, также называемый "поглощением". Внешним считается бар, чей диапазон сверху и снизу перекрывает High и Low предыдущего бара. Внешний бар является признаком роста волатильности и последующего направленного движения цены.

На рис. 5 внешние бары обозначены красными эллипсами. При обнаружении внешнего бара ценовой модуль генерирует цены открытия ордеров buystop и sellstop на его экстремумах.

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

Рис. 5. Иллюстрация внешних баров и уровней ценового модуля

Рис. 5. Иллюстрация внешних баров и уровней ценового модуля

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

В данном примере открываются только ордера на продажу, т.к. тренд считается нисходящим. В целом, принцип работы модуля аналогичен предыдущему. Его код отличается только одним методом – Prices(), все остальное то же самое с соответствующими именами. Поэтому приведем только код метода Prices().

double CPriceOutsideBar::Prices(void)
{
   double h[2],l[2];
   if(CopyHigh(m_symbol.Name(),m_period,1,2,h)!=2 || CopyLow(m_symbol.Name(),m_period,1,2,l)!=2)
      return(EMPTY_VALUE);
//--- проверка внешнего бара
   if(h[0] <= h[1] && l[0] >= l[1])
   {
      m_order_open_long=h[1]+m_order_offset;
      m_order_stop_long=l[1]-m_order_offset;
      m_order_take_long=m_order_open_long+(m_order_open_long-m_order_stop_long)*m_tp_ratio;
      m_order_expiration_long=calcExpiration();
      
      m_order_open_short=m_order_stop_long;
      m_order_stop_short=m_order_open_long;
      m_order_take_short=m_order_open_short-(m_order_stop_short-m_order_open_short)*m_tp_ratio;
      m_order_expiration_short=m_order_expiration_long;
      return(0);
   }
   return(EMPTY_VALUE);
} 

4.4. Проверка работоспособности модулей

В текущем разделе приводятся примеры трех модулей для того, чтобы проверить сначала их индивидуальную, а затем совместную работу в одном советнике и убедиться, что всё работает как задумано. В итоге, сгенерируем четыре советника. В качестве фильтра в каждом из них используем модуль торговых сигналов на основе индикатора Awesome Oscillator на временном интервале H12. Ценовые модули будут работать на интервале H6.

Управление капиталом: фиксированный лот, трейлинг стоп не используется. Все тесты проводятся на паре EURUSD на котировках с демо-счета на сервере MetaQuotes-Demo в период с 2013.01.01 по 2014.06.01. Версия терминала: 5.00, билд 965 (27 июня 2014).

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

4.4.1. Испытание модуля на основе индикатора Delta ZigZag

Рис. 6. График баланса при тестировании ценового модуля на основе Delta ZigZag

Рис. 6. График баланса при тестировании ценового модуля на основе индикатора "Delta ZigZag"

4.4.2. Испытание модуля на основе паттерна "внутренний бар"

Рис. 7. График баланса при тестировании ценового модуля на основе внутреннего бара

Рис. 7. График баланса при тестировании ценового модуля на основе внутреннего бара

При виде графика на рисунке 7 остается только напомнить, что цель тестирования на данном этапе – не получить прибыльную стратегию, а проверить работоспособность написанного кода.

4.4.3. Испытание модуля на основе паттерна "внешний бар"

Рис. 8. График баланса при тестировании ценового модуля на основе внешнего бара

Рис. 8. График баланса при тестировании ценового модуля на основе внешнего бара

4.4.4. Испытание совместной работы разработанных ценовых модулей

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

Рис. 9. График баланса при тестировании советника с тремя ценовыми модулями

Рис. 9. График баланса при тестировании советника с тремя ценовыми модулями

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


5. Инструкция по применению

Рассмотрим этапы создания советника с использованием разработанного расширения на примере советника с тремя ценовыми модулями и фильтром по AO.

До начала генерации убедитесь, что заголовочные файлы "CExpertAdvanced.mqh" и "CExpertSignalAdvanced.mqh" должны находится в каталоге данных терминала MetaTrader 5 в папке MQL5/Include/Expert, а файлы ценовых модулей лежат в папке MQL5/Include/Expert/MySignal. Перед запуском советника необходимо обеспечить наличие скомпилированных требуемых индикаторов в папке MQL5/Indicators. В данном случае это файл "DeltaZigZag.ex5". В прикрепленном архиве все файлы находятся на своих местах. Достаточно просто распаковать этот архив в папку с терминалом MetaTrader 5 и подтвердить слияние каталогов.

5.1. Генерация советника

Чтобы приступить к генерации советника, в MetaEditor выберите пункт меню "Файл"->"Создать".

Рис. 10. Создание нового советника при помощи Мастера MQL5

Рис. 10. Создание нового советника при помощи Мастера MQL5

В появившемся окне отметьте пункт "Советник (сгенерировать)" и нажмите "Далее".


Рис. 11. Генерация советника при помощи Мастера MQL5

Рис. 11. Генерация советника при помощи Мастера MQL5

Появится окно задания имени. В данном примере имя советника "TEST_EA_AO_DZZ_IB_OB", параметры Symbol и TimeFrame имеют значения по умолчанию. После задания имени нажмите "Далее".

Рис. 12. Общие параметры советника

Рис. 12. Общие параметры советника

В появившемся окне поочередно добавляем подключаемые модули, нажимая кнопку "Добавить". Весь процесс запечатлен на рисунках ниже.

ВНИМАНИЕ! При добавлении модулей сначала подключаются все модули сигналов (наследники CExpertSignal), и только потом ценовые модули (наследники CExpertSignalAdvanced). При нарушении этого порядка подключения результат работы советника, вероятно, будет непредсказуемым.

Сперва подключаем модуль сигналов индикатора Awesome Oscillator. В данном примере это единственный фильтр.

Рис. 13. Подключение модуля сигналов Awesome Oscillator

Рис. 13. Подключение модуля сигналов Awesome Oscillator

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

Рис. 14. Подключение модуля сигналов DeltaZZ Price Module

Рис. 14. Подключение модуля сигналов DeltaZZ Price Module


Рис. 15. Подключение модуля сигналов Inside Bar Price Module

Рис. 15. Подключение модуля сигналов Inside Bar Price Module


Рис. 16. Подключение модуля сигналов Outside Bar Price Module

Рис. 16. Подключение модуля сигналов Outside Bar Price Module

После того, как все модули добавлены, окно будет выглядеть так:

Рис. 17. Список добавленных модулей торговых сигналов

Рис. 17. Список добавленных модулей торговых сигналов

Значение параметра "Вес" для ценового модуля определяет его приоритет.

После нажатия кнопки "Далее" Мастер предложит выбрать модули управления капиталом и модули трейлинг-стопа. Выберите что-нибудь на свое усмотрение или оставьте как есть, нажимая кнопки "Далее" или "Готово".

После нажатия кнопки "Готово" получим сгенерированный код советника.

5.2. Правка сгенерированного кода

Итак, мы получили код.

1. Находим в самом начале строку

#include <Expert\Expert.mqh>

и редактируем ее так, чтобы получилось

#include <Expert\ExpertAdvanced.mqh>

Это нужно, чтобы подключить файлы разработанных классов.

2. Далее находим строку

CExpert ExtExpert;

и меняем на

CExpertAdvanced ExtExpert;

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

3. Находим строку

CExpertSignal *signal=new CExpertSignal;

и меняем на

CExpertSignalAdvanced *signal=new CExpertSignalAdvanced;

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

4. Находим в коде строку, выполняющую добавление первого ценового модуля в массив m_filters главного сигнала. В данном примере она выглядит так:

signal.AddFilter(filter1);

Перед ней вставляем строку

signal.CalcPriceModuleIndex();

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

Поиск нужной позиции для вставки указанной строки может вызвать затруднение у пользователей. Чтобы упростить поиск и не промахнуться, следует ориентироваться по номеру после слова "filter". Мастер MQL5 автоматически дает имена подключаемым модулям по порядку. Первый модуль называется filter0, второй – filter1, третий – filter2 и т.д. В нашем примере сигнальный модуль только один, поэтому первый подключенный ценовой модуль является вторым по порядку, значит нам нужно искать строку добавления фильтра "signal.AddFilter(filter1);", т.к. нумерация фильтров в коде идет с нуля. Для наглядности продемонстрируем это на рис. 18:

Рис. 18. Имена модулей в коде согласно порядку подключения

Рис. 18. Имена модулей в коде согласно порядку подключения

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

input double Signal_PriceLevel            =0.0;                    // Price level to execute a deal
input double Signal_StopLevel             =50.0;                   // Stop Loss level (in points)
input double Signal_TakeLevel             =50.0;                   // Take Profit level (in points)
input int    Signal_Expiration            =4;                      // Expiration of pending orders (in bars)

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

   signal.PriceLevel(Signal_PriceLevel);
   signal.StopLevel(Signal_StopLevel);
   signal.TakeLevel(Signal_TakeLevel);
   signal.Expiration(Signal_Expiration);

После удаления вышеуказанных строк компиляция завершится успешно.

В прикрепленном коде советника "TEST_EA_AO_DZZ_IB_OB" в комментариях даны пояснения по редактированию. Строки кода, которые можно удалить, закомментированы.


Заключение

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

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

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

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

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