Кроссплатформенный торговый советник: Сигналы

Enrico Lambino | 12 июня, 2017


Оглавление

Введение

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

Цели

Классы CSignal и CSignals, о которых мы поговорим, преследуют следующие цели:

  • Совместимость реализации и с MQL4, и с MQL5
  • Автоматизация большинства процессов, связанных с оценкой торговых сигналов
  • Простота реализации

Торговые сигналы

И CSignal, и его класс-контейнер, CSignals, отвечают за оценку всех сигналов с учетом текущего состояния рынка. Торговые сигналы делятся на две основные группы: сигналы на вход и на выход. Чтобы сигнал на вход привел к тому, чтобы советник совершил сделку, он должен иметь то же направление, что и все остальные входные сигналы (либо все на покупку, либо все на продажу). Что же касается сигналов на выход, то каждый сигнал независим, и может влиять на окончательный результат только сам по себе, в зависимости от своего собственного итога. Сигналы на выход также оцениваются совокупно. Так, например, если сигнал № 1 сигнализирует об условиях закрытия всех sell-позиций, а сигнал № 2 — об условиях на закрытие всех buy-позиций, результатом этого взаимодействия может быть закрытие вообще всех сделок.

Типы сигналов

Советник предусматривает четыре различных типа сигнала, которые интерпретируются объектами сигналов (и вследствие этого — советником) на основании того, вход или выход из рынка они подразумевают. Нижеприведенная таблица описывает типы сигналов, их значения и интерпретацию в зависимости от типа. 

 Тип сигнала
 Значение ВходВыход
CMD_VOID-1
Отменяет все остальные сигналы
Выход из всех сделок
CMD_NEUTRAL0
Игнорируется
Игнорируется
CMD_LONG1.
Открытие длинной позиции
Выход из коротких сделок
CMD_SHORT2
Открытие короткой позиции
Выход из длинных сделок

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

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

Сигнал 1: CMD_VOID

Сигнал 2: CMD_LONG

Сигнал 3: CMD_SHORT

Итог: CMD_VOID

Мы видим, что сигнал 1 отменяет другие два, и конечным итогом станет CMD_VOID. Но обратите также внимание на то, что сигналы 2 и 3 разнонаправленные. Таким образом, независимо от значения сигнала 1, в этой ситуации конечным итогом станет неторговая ситуация для советника.

Рассмотрим теперь немного измененную ситуацию, как показано ниже:

Сигнал 1: CMD_VOID

Сигнал 2: CMD_LONG

Сигнал 3: CMD_LONG

Итог: CMD_VOID

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

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

CMD_NEUTRAL имеет целочисленное значение 0 и означает отсутствие сигнала. Он приблизительно аналогичен варианту "воздержался" в выборном процессе. "Нейтральный" сигнал отказывается от своего права влиять на общий результат и оставляет решение остальным сигналам. Однако если у нас есть только один сигнал и он "воздерживается", для советника складывается ситуация без выходов и входов, которая аналогична той, если бы несколько сигналов имели разное направление.

Возьмем теперь ситуацию с использованием CMD_NEUTRAL, слегка изменив условия первого примера в этом разделе:

Сигнал 1: CMD_NEUTRAL

Сигнал 2: CMD_LONG

Сигнал 3: CMD_SHORT

Итог: CMD_VOID

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

Другое дело, если оставшиеся сигналы единодушны в направлении. В нашем четвертом примере (он показан ниже) первый сигнал нейтрален, а оставшиеся однонаправлены. В этом случае сигнал 1 игнорируется, а 2 и 3 оцениваются и в итоге дают совместный сигнал на покупку.

Сигнал 1: CMD_NEUTRAL

Сигнал 2: CMD_LONG

Сигнал 3: CMD_LONG

Итог: CMD_LONG

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

Сигнал 1: CMD_NEUTRAL

Сигнал 2: CMD_LONG

Сигнал 3: CMD_LONG

Сигнал 4: CMD_NEUTRAL

Сигнал 5: CMD_LONG

Сигнал 6: CMD_NEUTRAL

в конечном итоге выдаст общий сигнал CMD_LONG, а не CMD_NEUTRAL.

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

Важно, что присвоенное значение CMD_NEUTRAL равно 0, и что мы используем это пользовательское перечисление как замену для ENUM_ORDER_TYPE. Использование этого перечисления имеет несомненные преимущества.

Во-первых, мы можем лучше настроить интерпретацию сигнала.

Второе преимущество — в том, что мы можем предотвратить случайное исполнение сделок, вызванное неинициализированными переменными. Например, ORDER_TYPE_BUY имеет целочисленное значение 0. Если наш эксперт напрямую передает переменную int непосредственно в метод, который обрабатывает торговый запрос, и эта переменная не инициализирована или ей не присвоено другое значение (скорее всего нечаянно), тогда значение по умолчанию будет равняться 0, что в итоге приведет к размещению ордера на покупку. И наоборот, при пользовательском перечислении такого несчастного случая не произойдет, так как нулевое значение для переменной будет всегда означать неторговую ситуацию.

Сравнение с CExpertSignal

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

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

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

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

Фазы

OnInit и OnDeinit

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

OnTick

  1. Предварительная фаза (Расчет) — обновляются значения, необходимые для расчетов (проверки сигнала).
  2. Основная фаза (или Проверка Сигнала) — в течение этой фазы определяется текущее состояние сигнала. Лучше, чтобы тело этого метода имело единственную строчку кода, чтобы улучшить его читаемость (чтобы с первого взгляда было видно, что именно делает сигнал).
  3. Окончание (или Фаза Обновления) — определенные элементы некоторых сигналов могут быть обновлены только после выполнения фактической проверки сигнала. Пример этого — отслеживание значения предыдущей цены bid для сравнения с текущей ценой bid или с другими значениями (это возможно сделать из объекта графика или из значения индикатора). Обновление переменной, которая сохраняет предыдущее значение bid в течение предварительной фазы, не имеет смысла, поскольку приведет к тому, что ее значение всегда будет таким же, как текущее значение при проверке сигнала.

Важно отметить, что в MQL5 есть массив тиков, а в MQL4 этой функции нет. Лимитирующим фактором для обеспечения кроссплатформенной совместимости здесь будут стандарты МQL4, к которым код будет иметь привязку. Определяющим фактором здесь станет раздельная имплементация.

Реализация

Класс CSignal

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

bool CSignalBase::Refresh(void)
  {
   for(int i=0;i<m_indicators.Total();i++)
     {
      CSeries *indicator=m_indicators.At(i);
      if(indicator!=NULL)
         indicator.Refresh(OBJ_ALL_PERIODS);
     }
   return true;
  }

Фактический вызов метода Refresh класса CSignal происходит внутри метода Check того же класса. Как показано в коде ниже, этот метод прекращает дальнейшую обработку, если данные не могут быть обновлены (иначе сигналы будут неточными).

void CSignalBase::Check(void)
  {
   if(!Active())
      return;
   if(!Refresh())
      return;
   if(!Calculate())
      return;
   int res=CMD_NEUTRAL;
   if(LongCondition())
     {
      if (Entry())
         m_signal_open=CMD_LONG;
      if (Exit())
         m_signal_close=CMD_LONG;
     }
   else if(ShortCondition())
     {
      if (Entry())
         m_signal_open=CMD_SHORT;
      if (Exit())
         m_signal_close=CMD_SHORT;
     }
   else
   {
      if (Entry())
         m_signal_open=CMD_NEUTRAL;
      if (Exit())
         m_signal_close=CMD_NEUTRAL;
   }
   if(m_invert)
     {
      SignalInvert(m_signal_open);
      SignalInvert(m_signal_close);
     }
   Update();
  }

Внутри метода Check класса CSignal текущий сигнал определяется вызовом методов LongCondition и ShortCondition, которые примерно аналогичны методам, применяемым в Стандартной библиотеке MQL5.

Возвращение текущего сигнала достигается вызовом методов CheckOpenLong и CheckOpenShort, которые должны быть вызваны извне класса (из другого класса или прямо внутри функции OnTick):

bool CSignalBase::CheckOpenLong(void)
  {
   return m_signal_open==CMD_LONG;
  }
bool CSignalBase::CheckOpenShort(void)
  {
   return m_signal_open==CMD_SHORT;
  }

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

virtual bool      LongCondition(void)=0;
virtual bool      ShortCondition(void)=0;
Но если информации из необработанных данных таймсерий и индикаторов недостаточно и требуются дальнейшие расчеты, сначала нужно реализовать метод Calculate, и уже только после него — вышеописанные методы. Подобно индикаторам, которые будут использованы в CSignal, членами класса должны быть также и переменные, в которых сохраняются значения. Поэтому доступ к ним будет обеспечиваться методами LongCondition и ShortCondition.
virtual bool      Calculate(void)=0;
virtual void      Update(void)=0;

Обратите внимание, что метод Calculate относится к булеву типу (bool), а метод Update не возвращает никаких значений. Это значит, что возможно настроить советник так, чтобы отменить проверку сигналов, если выполнение определенных вычислений не удалось. Метод Update имеет тип void, и присвоение ему булева типа больше не нужно, поскольку он вызывается только после получения текущих сигналов.

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

Класс CSignals

CSignals — потомок CArrayObj. Это позволяет ему сохранять экземпляры СObject, которые в данном случае будут хранить экземпляры CSignal.

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

bool CSignalsBase::Init(CSymbolManager *symbol_man)
  {
   m_symbol_man= symbol_man;
   m_event_man = aggregator;
   if(!CheckPointer(m_symbol_man))
      return false;
   for(int i=0;i<Total();i++)
     {
      CSignal *signal=At(i);
      if(!signal.Init(symbol_man))
         return false;
     }
   return true;
  }

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

CSignalsBase::Check(void)
  {
   if(m_signal_open>0)
      m_signal_open_last=m_signal_open;
   if(m_signal_close>0)
      m_signal_close_last=m_signal_close;
   m_signal_open=CMD_NEUTRAL;
   m_signal_close=CMD_NEUTRAL;
   for(int i=0;i<Total();i++)
     {
      CSignal *signal=At(i);      
      signal.Check();
      if(signal.Entry())
        {
         if(m_signal_open>CMD_VOID)
           {
            ENUM_CMD signal_open=signal.SignalOpen();
            if(m_signal_open==CMD_NEUTRAL)
              {    
               m_signal_open=signal_open;
              }
            else if(m_signal_open!=signal_open)
              {               
               m_signal_open=CMD_VOID;
              }
           }
        }
      if(signal.Exit())
        {
         if(m_signal_close>CMD_VOID)
           {
            ENUM_CMD signal_close=signal.SignalClose();
            if(m_signal_close==CMD_NEUTRAL)
              {
               m_signal_close=signal_close;
              }
            else if(m_signal_close!=signal_close)
              {
               m_signal_close=CMD_VOID;
              }
           }
        }
     }
   if(m_invert)
     {
      CSignal::SignalInvert(m_signal_open);
      CSignal::SignalInvert(m_signal_close);
     }
   if(m_new_signal)
     {
      if(m_signal_open==m_signal_open_last)
         m_signal_open = CMD_NEUTRAL;
      if(m_signal_close==m_signal_close_last)
         m_signal_close= CMD_NEUTRAL;
     }
  }

Экземпляры индикатора

Каждый экземпляр CSignal имеет свой собственный набор экземпляров индикатора, которые хранятся в m_indicators (экземпляр CIndicators). В идеале каждый экземпляр индикатора, принадлежащий конкретному экземпляру CSignal, будет независимым от любого другого экземпляра CSignal. Это отклонение от метода, используемого в Стандартной библиотеке MQL5, которая хранит все индикаторы, используемые экспертом, в одном экземпляре CIndicators, который является членом класса CExpert. Несмотря на то, что этот подход склонен дублировать объекты (к примеру, объект индикатора МА по сигналу 1, а потом такой же объект индикатора по сигналу 2), и как следствие — дублировать расчеты, у него есть определенные преимущества.

Как минимум, он рассматривает объекты сигналов по большей части как самостоятельные единицы. Это дает каждому сигналу больше свободы в использовании индикаторов, особенно при выборе символа и таймфрейма. К примеру, для экспертов, работающих на нескольких валютных парах, изготовление индикаторов для обработки данных по другим инструментам может быть затруднительным, если применять только классы эксперта из Стандартной библиотеки. Вероятно, самый простой способ этого избежать (вместо того, чтобы постоянно изменять или расширять CExpert) — написать новый пользовательский индикатор (обеспечивающий доступ к необходимым данным и/или обрабатывающий их), который после этого может использоваться классом CExpert в применении к своим фильтрам и/или сигналам.

Ограничения

  1. Доступность индикаторов. Не все индикаторы, доступные в MetaTtrader 4, также доступны в MetaTrader 5 (и наоборот). Поэтому, если нам нужен кроссплатформенный советник, который работает в обеих платформах, индикаторы под MetaTrader 4 должны быть совместимы с Metatrader 5. В противном случае эксперт будет недоступен к использованию в другой платформе. Обычно для стандартных индикаторов это не составляет проблемы, за несколькими исключениями (например, индикатор объемов в MT4 отличается от версии MT5). Пользовательские же индикаторы должны быть выполнены в двух версиях для обеспечения кроссплатформенности.
  2. Доступность определенных данных. Некоторые данные таймсерий просто недоступны в MetaTrader 4. Поэтому некоторые стратегии, основанные на данных, доступных только в MetaTrader 5 (например, на тиковом объеме), сложно или даже невозможно перевести в код на MQL4.

Примеры

Пример #1: вариант Менеджера ордеров

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

void OnTick()
  {
//--- 
   static int bars = 0;
   static int direction = 0;
   int current_bars = 0;
   #ifdef __MQL5__
      current_bars = Bars(NULL,PERIOD_CURRENT);
   #else 
      current_bars = Bars;
   #endif
   if (bars<current_bars)
   {   
      symbol_info.RefreshRates();
      COrder *last = order_manager.LatestOrder();
      if (CheckPointer(last) && !last.IsClosed())
         order_manager.CloseOrder(last);
      if (direction<=0)
      {
         Print("Entering buy trade..");
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
         direction = 1;
      }
      else
      {
         Print("Entering sell trade..");
         order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
         direction = -1;
      }   
      bars = current_bars;
   }
  }

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

Для этого нам нужно расширить класс CSignal в основном заголовочном файле тремя защищенными членами: (1) количество предыдущих баров, (2) предыдущее количество предыдущих баров и (3) текущее направление, как показано в коде ниже:

class SignalOrderManagerExample: public CSignal
  {
protected:
   int               m_bars_prev;
   int               m_bars;
   int               m_direction;
   //остаток класса

Также нам нужно расширить методы, находящиеся в классе CSignal. Для метода Calculate нам также нужно использовать такой же метод расчетов, как в старом примере:

bool SignalOrderManagerExample::Calculate(void)
  {
   #ifdef __MQL5__
      m_bars=Bars(NULL,PERIOD_CURRENT);
   #else
      m_bars=Bars;
   #endif
   return m_bars>0 && m_bars>m_bars_prev;
  }

Методы получения коичества баров на текущем графике отличаются в двух платформах, и поэтому, как в старом примере, мы разобьем имплементацию. Также обратите внимание, что метод Calculate — булева переменная. Как уже обсуждалось выше, если метод Сalculate возвращает false, дальнейшая обработка сигналов с данного тикового события останавливается. Здесь мы явно определяем два правила того, когда дальнейшая обработка сигнала по тиковому событию должна быть выполнена: (1) текущее количество баров выше нуля и (2) текущее количество баров больше, чем предыдушее. 

Затем мы рассмотрим метод Update класса, расширив метод нашего пользовательского класса, который показан в нижеследующих строках кода:

void SignalOrderManagerExample::Update(void)
  {
   m_bars_prev=m_bars;
   m_direction= m_direction<=0?1:-1;
  }

После проверки сигналов мы обновляем предыдущее количество баров (m_bars_prev) до текущего (m_bars). Также обновляем направление. Если текущее значение меньше или равно нулю (предыдущее направление — продажа, или сделка совершается впервые), новое значение этой переменной будет 1. В противном случае ее значение будет -1.

Наконец, мы работаем с генерацией самих сигналов. На основании переменных, необходимых для оценки сигналов по текущему тику, мы определяем условия, которые, в свою очередь, определяют, когда полученный сигнал должен быть сигналом к покупке или продаже. Это достигается расширением методов LongCondition и ShortCondition класса CSignal:

bool SignalOrderManagerExample::LongCondition(void)
  {
   return m_direction<=0;
  }

bool SignalOrderManagerExample::ShortCondition(void)
  {
   return m_direction>0;
  }

Функция Init для этого примера очень похожа на таковую из предыдушего примера. Кроме этого, в данном примере нам надо создать наследник класса CSignal, который мы только что определили (SignalOrderManagerExample), а также его контейнер (CSignals):

int OnInit()
  {
//--- 
   order_manager=new COrderManager();
   symbol_manager=new CSymbolManager();
   symbol_info=new CSymbolInfo();
   if(!symbol_info.Name(Symbol()))
      Print("symbol not set");
   symbol_manager.Add(GetPointer(symbol_info));
   order_manager.Init(symbol_manager,NULL);
   SignalOrderManagerExample *signal_ordermanager=new SignalOrderManagerExample();
   signals=new CSignals();
   signals.Add(GetPointer(signal_ordermanager));
//--- 
   return(INIT_SUCCEEDED);
  }

Здесь мы объявляем signal_ordermanager как указатель на новый объект типа SignalOrderManagerExample, который мы только что определили. Затем сделаем то же самое для CSignals через указатель сигналов, а потом добавляем в него указатель на SignalOrderManagerExample, вызвав его метод Add.

Использование CSignal и CSignals в нашем советнике сделает функцию OnTick гораздо проще:

void OnTick()
  {
//--- 
   symbol_info.RefreshRates();   
   signals.Check();
   if(signals.CheckOpenLong())
     {
      close_last();
      Print("Entering buy trade..");
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
     }
   else if(signals.CheckOpenShort())
     {
      close_last();
      Print("Entering sell trade..");
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
     }
  }

Все остальные расчеты, необходимые для генерации текущего сигнала, перемещены в объекты классов CSignal и CSignals. Таким образом, все, что мы должны сделать — это заставить CSignals выполнить проверку, а потом получить ее результат вызовом методов CheckOpenLong и CheckOpenShort. Нижеприведенные скриншоты показывают результаты тестирования эксперта на платформах MetaTrader 4 и MetaTrader 5:

(MT4)

signal_ordermanager (MT4)

(MT5)

signal_ordermanager (MT5)

Пример #2: советник MA

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

Как и в предыдущем примере, мы создаем пользовательский сигнал расширением класса CSignal:

class SignalMA: public CSignal
  {
protected:
   CiMA             *m_ma;
   CSymbolInfo      *m_symbol;
   string            m_symbol_name;
   ENUM_TIMEFRAMES   m_timeframe;
   int               m_signal_bar;
   double            m_close;   
   //остаток класса

Как видим, обе библиотеки — и MQL4, и MQL5 уже предоставляют объекты класса для индикатора Moving Average. Это упрощает внедрение индикатора в наш пользовательский сигнальный класс. Несмотря на то, что это необязательно, в данном примере мы также сохраним целевой объект через m_symbol, указатель на объект CSybmolInfo. Также объявим переменную m_close, в которой будет сохраняться значение цены закрытия сигнального бара. Остальные защищенные члены класса — параметры индикатора МА.

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

void SignalMA::SignalMA(const string symbol,const ENUM_TIMEFRAMES timeframe,const int period,const int shift,const ENUM_MA_METHOD method,const ENUM_APPLIED_PRICE applied,const int bar)
  {
   m_symbol_name= symbol;
   m_timeframe = timeframe;
   m_signal_bar = bar;
   m_ma=new CiMA();
   m_ma.Create(symbol,timeframe,period,0,method,applied);
   m_indicators.Add(m_ma);   
  }

Полученный сигнал скользящей средней часто сравнивается с какой-либо ценой на графике. Это может быть конкретный тип — например, open или close, или текущие цены bid/ask. В последнем случае нам понадобится объект символа для работы. В нашем примере мы расширим метод Init, чтобы инициализировать получение корректного символа для использования из CSymbolManager. Это пригодится, если мы хотим сравнивать МА не с данными OHLC и их производными, а с ценой bid/ask. 

bool SignalMA::Init(CSymbolManager *symbol_man,CEventAggregator *event_man=NULL)
  {
   if(CSignal::Init(symbol_man,event_man))
     {
      if(CheckPointer(m_symbol_man))
        {
         m_symbol=m_symbol_man.Get();
         if(CheckPointer(m_symbol))
            return true;
        }
     }
   return false;
  }

Следующий расширенный метод — метод Сalculate:

bool SignalMA::Calculate(void)
  {
   double close[];
   if(CopyClose(m_symbol_name,m_timeframe,signal_bar,1,close)>0)
     {
      m_close=close[0];
      return true;
     }   
   return false;
  }

Больше не нужно обновлять данные индикаторов, поскольку это уже выполняется внутри метода Refresh класса CSignal. В качестве альтернативы мы можем также имплементировать потомок класса CSignal, чтобы получить цену закрытия сигнального бара с использованием класса CCloseBuffer. Он также является потомком класса CSeries, так что мы можем добавить его к m_indicators, чтобы экземпляр CCloseBuffer обновлялся вместе с другими индикаторами. В этом случае больше не нужно будет расширять методы Refresh или Calculate класса CSignal. 

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

bool SignalMA::LongCondition(void)
  {
   return m_close>m_ma.Main(m_signal_bar);
  }

bool SignalMA::ShortCondition(void)
  {
   return m_close<m_ma.Main(m_signal_bar);
  }

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

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

SignalMA *signal_ma=new SignalMA(Symbol(),(ENUM_TIMEFRAMES) Period(),maperiod,0,mamethod,maapplied,signal_bar);
signals=new CSignals();
signals.Add(GetPointer(signal_ma));
signals.Init(GetPointer(symbol_manager),NULL);

Следующий код показывает функцию OnTick, которая аналогична функции OnTick из предыдущего примера:

void OnTick()
  {
//--- 
   symbol_info.RefreshRates();
   signals.Check();
   if(signals.CheckOpenLong())
     {
      close_last();
      Print("Entering buy trade..");
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_BUY,symbol_info.Ask());
     }
   else if(signals.CheckOpenShort())
     {
      close_last();
      Print("Entering sell trade..");
      order_manager.TradeOpen(Symbol(),ORDER_TYPE_SELL,symbol_info.Bid());
     }
  }

Нижеприведенные скриншоты показывают результаты тестирования советника в MT4 и MT5. Как видим, советники работают по одинаковой логике:

(MT4)

signal_ma (MT4)

(MT5)

signal_ma (MT5)

Пример #3: советник HA

В следующем примере попробуем использовать в советнике индикатор Хайкен Аши (HA). В отличие от индикатора МА, НА — пользовательский индикатор, так что разработка советника на его основе будет чуть сложнее, чем в предыдущем примере, поскольку нам потребуется еще и объявить класс для индикатора Хайкен Аши посредством расширения CiCustom. Для начала рассмотрим определение класса для CiHA, нашего объекта класса для индикатора HA:

class CiHA: public CiCustom
  {
public:
                     CiHA(void);
                    ~CiHA(void);
   bool              Create(const string symbol,const ENUM_TIMEFRAMES period,
                            const ENUM_INDICATOR type,const int num_params,const MqlParam &params[],const int buffers);
   double            GetData(const int buffer_num,const int index) const;
  };

Нам понадобится расширить два метода — Create и GetData. Для метода Create переопределим конструктор класса:

bool CiHA::Create(const string symbol,const ENUM_TIMEFRAMES period,const ENUM_INDICATOR type,const int num_params,const MqlParam &params[],const int buffers)
  {
   NumBuffers(buffers);
   if(CIndicator::Create(symbol,period,type,num_params,params))
      return Initialize(symbol,period,num_params,params);
   return false;
  }

Здесь мы объявляем количество буферов, которые должен иметь индикатор, а затем инициализировать переданные в них параметры. Параметры индикатора сохраняются в структуре (MqlParam).

Для метода GetData имплементация в двух языках различается. В MQL4 делается прямой вызов функции iCustom, которая выдает значение индикатора на отдельном баре графика. В МQL5 вызов индикатора обрабатывается по-другому. iCustom передает хэндл идикатору (примерно так же, как это происходит в файловых операциях). Чтобы получить значение индикатора в MetaTrader 5 по определенному бару, используется этот хэндл, и функция iCustom не вызывается. В этом случае мы разделяем имплементацию:

double CiHA::GetData(const int buffer_num,const int index) const
  {
   #ifdef __MQL5__
      return CiCustom::GetData(buffer_num,index);
   #else
      return iCustom(m_symbol,m_period,m_params[0].string_value,buffer_num,index);
   #endif
  }

Обратите внимание, что в этом методе в версии MQL5 мы просто возвращаем результат вызова родительского метода (CiCustom). В случае же MQL4 родительский метод (CiCustom) просто возвращает ноль, и таким образом мы должны расширить его, фактически образаясь к функции MQL4 iCustom. Поскольку эта MQL4-функция не использует структуру (MqlParams) для хранения параметров индикатора, ее вызов будет почти всегда разным по каждому пользовательскому индикатору.

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

void SignalHA::SignalHA(const string symbol,const ENUM_TIMEFRAMES timeframe,const int numparams,const MqlParam &params[],const int bar)
  {
   m_symbol_name= symbol;
   m_signal_bar = bar;
   m_ha=new CiHA();
   m_ha.Create(symbol,timeframe,IND_CUSTOM,numparams,params,4);
   m_indicators.Add(m_ha);
  }

Для метода Calculate нам тоже нужно разделить имплементацию, потому что индикаторы Хайкен Аши для MetaTrader 4 и MetaTrader 5 различаются в части расположения буферов. Для старшей версии это Low/High, High/Low, Open и Close, которые занимают первый (буфер № 0), второй, третий и четвертый буферы. Для версии MQL5 это будут Open, High, Low, and Close. Таким образом, нам нужно рассмотреть конкретный буфер для доступа, чтобы получить значение от индикатора, в зависимости от используемой платформы:

bool SignalHA::Calculate(void)
  {
   #ifdef __MQL5__
      m_open=m_ha.GetData(0,signal_bar);
   #else
      m_open=m_ha.GetData(2,signal_bar);
   #endif
      m_close=m_ha.GetData(3,signal_bar);
   return true;
  }

Для версии MQL5 цена открытия свечи HA берется из первого буфера (буфер № 0), в то время как в версии MQL4, она находится в третьем буфере (буфер № 2). Цена закрытия свечи HA находится в четвертом буфере (буфер № 3) для обеих версий, так что мы помещаем заявление вне директивы препроцессора.

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

bool SignalHA::LongCondition(void)
  {
   return m_open<m_close;
  }

bool SignalHA::ShortCondition(void)
  {
   return m_open>m_close;
  }

Функция OnTick для этого эксперта будет такой же, как в предыдущих примерах, так что перейдем к функции OnInit:

int OnInit()
  {
//--- 
   order_manager=new COrderManager();
   symbol_manager=new CSymbolManager();
   symbol_info=new CSymbolInfo();
   if(!symbol_info.Name(Symbol()))
      Print("symbol not set");
   symbol_manager.Add(GetPointer(symbol_info));
   order_manager.Init(symbol_manager,NULL);

   MqlParam params[1];
   params[0].type=TYPE_STRING;
   #ifdef __MQL5__
      params[0].string_value="Examples\\Heiken_Ashi";
   #else
      params[0].string_value="Heiken Ashi";
   #endif
      SignalHA *signal_ha=new SignalHA(Symbol(),0,1,params,signal_bar);
   signals=new CSignals();
   signals.Add(GetPointer(signal_ha));
   signals.Init(GetPointer(symbol_manager),NULL);
//--- 
   return(INIT_SUCCEEDED);
  }

Здесь мы видим, что размещение файла ex4 индикатора Heiken Ashi различается, в зависимости от того, какую версию платформы мы используем. Поскольку MqlParams требует, чтобы первый сохраняемый параметр имел имя пользовательского индикатора (без расширения), нам снова нужно разделить имплементацию при указании первого параметра. В MQL5 индикатор находится по умолчанию в "Indicators\Examples\Heiken Ashi", в то время как в MQL4 он находится в "Indicators\Heiken Ashi".

Нижеприведенные скриншоты демонстрируют результаты теста эксперта в платформах MetaTrater 4 и MetaTrader 5. Как видим, несмотря на то, что индикаторы различаются по способу построения на графике, тем не менее, оба индикатора имеют одну и ту же логику, а советник в обеих версиях может работать на основе той же логики:

(MT4)

signal_ha (MT4)

(MT5)

signal_ha (MT5)

Пример #4: Советник на основе HA и MA

Наш последний пример — комбинация индикаторов MA и HA, включенная в советник. Отличий в этом примере немного. Мы просто добавляем определения классов, находящиеся во втором и третьем примерах, и затем добавляем указатели на экземпляры CSignalMA и CSignalHA в экземпляр CSignals. Ниже показан результат пробного использования эксперта.

(MT4)

signal_ha_ma (MT4)

(MT5)

signal_ha_ma (MT5)

Заключение

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