Универсальный торговый эксперт: Событийная модель и прототип торговой стратегии (Часть 2)

Vasiliy Sokolov | 14 января, 2016

Содержание

 

Введение

Данная статья продолжает знакомить читателей с универсальным торговым движком CStrategy. В первой статье Универсальный торговый эксперт: Торговые режимы стратегий (Часть 1) были подробно разобраны торговые режимы и функции по их организации. Была предложена универсальная схема эксперта, состоящая из четырех методов, два из которых открывают новые позиции а другие два — закрывают. Разная комбинация вызовов этих методов определяет тот или иной торговый режим, например, такому эксперту можно задать режим только покупок или только продаж, сопровождение ранее открытых позиций или режим ожидания. Благодаря этим режимам эксперт приобретает возможность гибкой настройки своей работы в зависимости от торгового времени или даже дня недели.

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

Также в данной статье описываются два важнейших класса всего торгового движка — это классы CStrategy и CPosition. Первый из них является ядром всей торговой логики эксперта, он объединяет события и режимы в единый гибкий каркас, который непосредственно наследует эксперт пользователя. Второй класс является основой универсальных торговых операций. В него помещены действия с открытой позицией (например, закрытие позиции или изменение ее уровня Stop Loss или Take Profit). Благодаря этому все торговые действия становятся однотипными и платформонезависимыми.

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

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

 

Событийная модель на основе централизованной обработки, перечисление ENUM_MARKET_EVENT_TYPE

MetaTrader 5 предоставляет достаточно много событий. Среди этих событий есть как уведомляющие об изменении рыночной цены (NewTick, BookEvent), так и системные события вроде Timer или TradeTransaction. На каждое из этих событий есть одноименная системная функция с префиксом On*. Такая функция является обработчиком данного события. Например, если требуется обработать поступление нового тика, необходимо разместить соответствующий набор процедур в функции OnTick:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // Обрабатываем поступление нового тика
   // ...
  }

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

void OnBookEvent (const string& symbol)
  {
   // Здесь обрабатываем изменение цен в стакане
   // ...
  }

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

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

Чтобы разобраться в предлагаемой событийной модели, давайте опишем события, связанные с изменением цены или времени с помощью специального перечисления ENUM_MARKET_EVENT_TYPE:

//+------------------------------------------------------------------+
//| Определяет тип рыночного события.                                |
//+------------------------------------------------------------------+
enum ENUM_MARKET_EVENT_TYPE
  {
   MARKET_EVENT_TICK,               // Приход нового тика
   MARKET_EVENT_BAR_OPEN,           // Открытие нового бара
   MARKET_EVENT_TIMER,              // Срабатывание таймера
   MARKET_EVENT_BOOK_EVENT          // Изменение стакана (в т.ч. приход нового тика).
  };

Как видите, перечисление содержит описание как системных событий, так и события, которое напрямую не поддерживается в MetaTrader 5 (MARKET_EVENT_BAR_OPEN — открытие нового бара по рабочему инструменту эксперта).

Предположим, что наш универсальный эксперт состоит из четырех методов торговой логики: InitBuy, InitSell, SupportBuy, SupportSell. Об этим методах мы писали в первой части статьи "Универсальный торговый эксперт". Если в качестве одного из параметров этих методов использовать одно из значений данного перечисления, обрабатывающая логика эксперта всегда может узнать, на основании какого события был произведен текущий вызов метода. Опишем упрощенную схему данного эксперта:

//+------------------------------------------------------------------+
//|                                                     ExampExp.mq5 |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include <Strategy\Series.mqh>
ulong ExpertMagic=12345; // Магический номер эксперта
//+------------------------------------------------------------------+
//| Определяет тип рыночного события.                                |
//+------------------------------------------------------------------+
enum ENUM_MARKET_EVENT_TYPE
  {
   MARKET_EVENT_TICK,               // Приход нового тика по текущему инструменту
   MARKET_EVENT_BAR_OPEN,           // Открытие нового бара по текущему инструменту
   MARKET_EVENT_TIMER,              // Срабатывание таймера
   MARKET_EVENT_BOOK_EVENT          // Изменение стакана (в т.ч. приход тика).
  };
//+------------------------------------------------------------------+
//| Прототип универсального эксперта.                                |
//+------------------------------------------------------------------+
class CExpert
  {
public:
   void              InitBuy(ENUM_MARKET_EVENT_TYPE &event_id);
   void              InitSell(ENUM_MARKET_EVENT_TYPE &event_id);
   void              SupportBuy(ENUM_MARKET_EVENT_TYPE &event_id);
   void              SupportSell(ENUM_MARKET_EVENT_TYPE &event_id);
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CExpert::InitBuy(ENUM_MARKET_EVENT_TYPE &event_id)
  {
   printf(__FUNCTION__+" EventID: "+EnumToString(event_id));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CExpert::InitSell(ENUM_MARKET_EVENT_TYPE &event_id)
  {
   printf(__FUNCTION__+" EventID: "+EnumToString(event_id));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CExpert::SupportBuy(ENUM_MARKET_EVENT_TYPE &event_id)
  {
   printf(__FUNCTION__+" EventID: "+EnumToString(event_id));
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CExpert::SupportSell(ENUM_MARKET_EVENT_TYPE &event_id)
  {
   printf(__FUNCTION__+" EventID: "+EnumToString(event_id));
  }

ENUM_MARKET_EVENT_TYPE event_type;
CExpert Expert;
datetime last_time;
CTime Time;
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
   EventSetTimer(1);
   return INIT_SUCCEEDED;
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   event_type=MARKET_EVENT_TICK;
   CallExpertLogic(event_type);
   if(last_time!=Time[0])
     {
      event_type=MARKET_EVENT_BAR_OPEN;
      CallExpertLogic(event_type);
      last_time=Time[0];
     }
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnTimer()
  {
   event_type=MARKET_EVENT_TIMER;
   CallExpertLogic(event_type);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void OnBookEvent(const string &symbol)
  {
   event_type=MARKET_EVENT_TIMER;
   CallExpertLogic(event_type);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CallExpertLogic(ENUM_MARKET_EVENT_TYPE &event)
  {
   Expert.InitBuy(event);
   Expert.InitSell(event);
   Expert.SupportBuy(event);
   Expert.SupportSell(event);
  }
//+------------------------------------------------------------------+

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

2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::SupportSell EventID: MARKET_EVENT_BAR_OPEN
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::SupportBuy EventID: MARKET_EVENT_BAR_OPEN
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::InitSell EventID: MARKET_EVENT_BAR_OPEN
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::InitBuy EventID: MARKET_EVENT_BAR_OPEN
2015.11.30 14:13:40.015 ExampExp2 (Si-12.15,M1) CExpert::InitSell EventID: MARKET_EVENT_TICK
2015.11.30 14:13:40.015 ExampExp2 (Si-12.15,M1) CExpert::InitBuy EventID: MARKET_EVENT_TICK
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::SupportSell EventID: MARKET_EVENT_BAR_OPEN
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::SupportBuy EventID: MARKET_EVENT_BAR_OPEN
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::InitSell EventID: MARKET_EVENT_BAR_OPEN
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::InitBuy EventID: MARKET_EVENT_BAR_OPEN
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::SupportSell EventID: MARKET_EVENT_TICK
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::SupportBuy EventID: MARKET_EVENT_TICK
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::InitSell EventID: MARKET_EVENT_TICK
2015.11.30 14:13:39.981 ExampExp2 (Si-12.15,M1) CExpert::InitBuy EventID: MARKET_EVENT_TICK

 

Доступ к событиям, происходящим на других инструментах, структура MarketEvent

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

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

Таким образом, в системных функциях, реагирующих на события NewTick, BookEvent и Timer, можно разместить вызов некоего промежуточного модуля (назовем его условно EventProcessor), который бы в свою очередь анализировал изменение цен на нескольких инструментах одновременно и генерировал бы то или иное событие. Каждое событие имело бы унифицированное описание в виде структуры и отправлялось бы управляющим методам стратегии. Стратегия, получив соответствующее событие в виде структуры, реагировала бы на него или пропускала. При этом системная функция, которая была бы фактическим инициатором этого события для конечного эксперта, оставалось бы неизвестной.

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

Рис. 1. Схема создания и распространения событий

Теперь нам осталось рассмотреть собственно структуру, которая будет передаваться экспертам в качестве события. Эта структура называется MarketEvent, ее определение следующее:

//+------------------------------------------------------------------+
//| Структура, определяющая тип события, инструмент, на котором оно  |
//| возникло, и таймфрейм (для события BarOpen)                      |
//+------------------------------------------------------------------+
struct MarketEvent
  {
   ENUM_MARKET_EVENT_TYPE type;     // Тип события.
   ENUM_TIMEFRAMES   period;        // Период графика, к которому относится событие (только для MARKET_EVENT_BAR_OPEN).
   string            symbol;        // Название инструмента, по которому произошло событие.
  };

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

У событий, для которых анализировать период графика бессмысленно (например, у событий NewTick или Timer), поле period структуры MarketEvent всегда будет заполнено значением PERIOD_CURRENT.

 

Событие "новый бар". Алгоритмы определения новых тиков и баров

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

//+------------------------------------------------------------------+
//|                                              NewTickDetector.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#include <Object.mqh>
//+------------------------------------------------------------------+
//| Определитель нового тика                                         |
//+------------------------------------------------------------------+
class CTickDetector : public CObject
  {
private:
   string            m_symbol;         // Символ, по которому требуется отслеживать приход нового тика.
   MqlTick           m_last_tick;      // Последний запомненный тик.
public:
                     CTickDetector(void);
                     CTickDetector(string symbol);
   string            Symbol(void);
   void              Symbol(string symbol);
   bool              IsNewTick(void);
  };
//+------------------------------------------------------------------+
//| Конструктор по умолчанию устанавливает текущий таймфрейм         |
//| и инструмент.                                                    |
//+------------------------------------------------------------------+
CTickDetector::CTickDetector(void)
  {
   m_symbol=_Symbol;
  }
//+------------------------------------------------------------------+
//| Создает объект с предустановленным символом и таймфреймом.       |
//+------------------------------------------------------------------+
CTickDetector::CTickDetector(string symbol)
  {
   m_symbol=symbol;
  }
//+------------------------------------------------------------------+
//| Устанавливает название инструмента, образование нового тика      |
//| на котором надо отслеживать.                                     |
//+------------------------------------------------------------------+
void CTickDetector::Symbol(string symbol)
  {
   m_symbol=symbol;
  }
//+------------------------------------------------------------------+
//| Возвращает название инструмента, образование нового тика         |
//| на котором отслеживается.                                        |
//+------------------------------------------------------------------+
string CTickDetector::Symbol(void)
  {
   return m_symbol;
  }
//+------------------------------------------------------------------+
//| Возвращает истину, если на заданном инструменте и таймфрейме     |
//| появился новый тик.                                              |
//+------------------------------------------------------------------+
bool CTickDetector::IsNewTick(void)
  {
   MqlTick tick;
   SymbolInfoTick(m_symbol,tick);
   if(tick.last!=m_last_tick.last || 
      tick.time!=m_last_tick.time)
     {
      m_last_tick=tick;
      return true;
     }
   return false;
  }

Его основным рабочим методом является метод IsNewTick. Он возвращает истину, если был получен новый тик по отслеживаемому инструменту. Сам инструмент, который требуется отслеживать, задается с помощью метода Symbol. Класс CTickDetector наследуется от CObject и поэтому может быть размещен как элемент коллекции CArrayObj. Именно это нам и необходимо. Например, можно создать десять экземпляров CTickDetect, каждому из которых назначить свой символ для мониторинга. Последовательно опрашивая коллекцию этих классов типа CArrayObj, можно оперативно узнавать, на каком инструменте образовался новый тик, а затем генерировать соответствующее событие, которое передавало бы эту информацию коллекции экспертов.

Как уже было сказано, помимо новых тиков, очень часто необходимо определять приход новых баров. Эту задачу лучше всего делегировать специальному классу CBarDetector. Он работает примерно так же, как и класс CTickDetector. Основным методом CBarDetector является IsNewBar — метод возвращает истину, если наступил новый бар для инструмента, название которого требуется предварительно установить с помощью метода Symbol. Приведем исходный код этого класса:

//+------------------------------------------------------------------+
//|                                               NewBarDetecter.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Object.mqh>
//+------------------------------------------------------------------+
//| Класс, обнаруживающий появление нового бара на указанном         |
//| инструменте и периоде.                                           |
//+------------------------------------------------------------------+
class CBarDetector : public CObject
  {
private:
   ENUM_TIMEFRAMES   m_timeframe;      // Таймфрейм, по которому требуется отслеживать новый бар
   string            m_symbol;         // Символ, по которому требуется отслеживать новый бар
   datetime          m_last_time;      // время последнего известного бара
public:
                     CBarDetector(void);
                     CBarDetector(string symbol,ENUM_TIMEFRAMES timeframe);
   void              Timeframe(ENUM_TIMEFRAMES tf);
   ENUM_TIMEFRAMES   Timeframe(void);
   void              Symbol(string symbol);
   string            Symbol(void);
   bool              IsNewBar(void);
  };
//+------------------------------------------------------------------+
//| Конструктор по умолчанию устанавливает текущий таймфрейм         |
//| и инструмент.                                                    |
//+------------------------------------------------------------------+
CBarDetector::CBarDetector(void)
  {
   m_symbol=_Symbol;
   m_timeframe=Period();
  }
//+------------------------------------------------------------------+
//| Создает объект с предустановленным символом и таймфреймом.       |
//+------------------------------------------------------------------+
CBarDetector::CBarDetector(string symbol,ENUM_TIMEFRAMES tf)
  {
   m_symbol=symbol;
   m_timeframe=tf;
  }
//+------------------------------------------------------------------+
//| Устанавливает таймфрейм, образование нового бара на котором надо |
//| отслеживать.                                                     |
//+------------------------------------------------------------------+
void CBarDetector::Timeframe(ENUM_TIMEFRAMES tf)
  {
   m_timeframe=tf;
  }
//+------------------------------------------------------------------+
//| Возвращает таймфрейм, образование нового бара на котором         |
//| отслеживается.                                                   |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CBarDetector::Timeframe(void)
  {
   return m_timeframe;
  }
//+------------------------------------------------------------------+
//| Устанавливает название инструмента, образование нового бара      |
//| на котором надо отслеживать.                                     |
//+------------------------------------------------------------------+
void CBarDetector::Symbol(string symbol)
  {
   m_symbol=symbol;
  }
//+------------------------------------------------------------------+
//| Возвращает название инструмента, образование нового бара         |
//| на котором отслеживается.                                        |
//+------------------------------------------------------------------+
string CBarDetector::Symbol(void)
  {
   return m_symbol;
  }
//+------------------------------------------------------------------+
//| Возвращает истину, если на заданном инструменте и таймфрейме     |
//| появился новый бар.                                              |
//+------------------------------------------------------------------+
bool CBarDetector::IsNewBar(void)
  {
   datetime time[];
   if(CopyTime(m_symbol, m_timeframe, 0, 1, time) < 1)return false;
   if(time[0] == m_last_time)return false;
   return m_last_time = time[0];
  }

 

Класс CPositionMT5 — основа платформонезависимого алгоритма

Настало время разобрать один из важнейших классов, обеспечивающих работу универсальных экспертов. Этот класс включает в себя методы по работе с позицией в MetaTrader 5. С технической точки зрения он выполнен весьма просто. По своей сути он является классом-оболочкой, посредником между экспертом и системными функциями по работе с позициями в MetaTrader 5 — PositionSelect и PositionGet... Однако он реализует важнейшую функцию представленных классов — их платформонезависимость.

Если проанализировать внимательно, то все модули, описанные выше, не использовали в своем составе функции, специфичные для какой-то одной торговой платформы (MetaTrader 4 или MetaTrader 5). И это действительно так, потому что MQL4 современных версий и MQL5 являются, по сути, одним и тем же языком программирования с разными наборами функций. Специфические наборы функций для этих обеих платформ в основном сосредоточены в области управления торговыми позициями.

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

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

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

Итак, приведем исходный код этого класса. Он достаточно простой и очевидный:

//+------------------------------------------------------------------+
//|                                                  PositionMT5.mqh |
//|                                 Copyright 2015, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include <Object.mqh>
#include "Logs.mqh"
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| Класс активной позиции, для классических стратегий               |
//+------------------------------------------------------------------+
class CPositionMT5 : public CObject
  {
private:
   ulong             m_id;                // Уникальный идентификатор позиции
   uint              m_magic;             // Уникальный идентификатор эксперта, которому принадлежит данная позиция.
   ENUM_POSITION_TYPE m_direction;         // Направление позиции
   double            m_entry_price;       // Цена входа в позицию
   string            m_symbol;            // Инструмент, по которому открыта позиция
   datetime          m_time_open;         // Время открытия
   string            m_entry_comment;     // Входящий комментарий
   bool              m_is_closed;         // Истина, если позиция была закрыта
   CLog*             Log;                 // Логирование
   CTrade            m_trade;             // Торговый модуль
public:
                     CPositionMT5(void);
   uint              ExpertMagic(void);
   ulong             ID(void);
   ENUM_POSITION_TYPE Direction(void);
   double            EntryPrice(void);
   string            EntryComment(void);
   double            Profit(void);
   double            Volume(void);
   string            Symbol(void);
   datetime          TimeOpen(void);
   bool              CloseAtMarket(string comment="");
   double            StopLossValue(void);
   bool              StopLossValue(double sl);
   double            TakeProfitValue(void);
   bool              TakeProfitValue(double tp);
  };
//+------------------------------------------------------------------+
//| Инициализация основных свойств позиции                           |
//+------------------------------------------------------------------+
void CPositionMT5::CPositionMT5(void) : m_id(0),
                                        m_entry_price(0.0),
                                        m_symbol(""),
                                        m_time_open(0)
  {
   m_id=PositionGetInteger(POSITION_IDENTIFIER);
   m_magic=(uint)PositionGetInteger(POSITION_MAGIC);
   m_direction=(ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
   m_entry_price=PositionGetDouble(POSITION_PRICE_OPEN);
   m_symbol=PositionGetString(POSITION_SYMBOL);
   m_time_open=(datetime)PositionGetInteger(POSITION_TIME);
   m_entry_comment=PositionGetString(POSITION_COMMENT);
   m_trade.SetExpertMagicNumber(m_magic);
  }
//+------------------------------------------------------------------+
//| Возвращает направление позиции.                                  |
//+------------------------------------------------------------------+
ENUM_POSITION_TYPE CPositionMT5::Direction(void)
  {
   return m_direction;
  }
//+------------------------------------------------------------------+
//| Возвращает уникальный идентификатор эксперта, которому           |
//| принадлежит текущая позиция.                                     |
//+------------------------------------------------------------------+
uint CPositionMT5::ExpertMagic(void)
  {
   return m_magic;
  }
//+------------------------------------------------------------------+
//| Возвращает уникальный идентификатор позиции.                     |
//+------------------------------------------------------------------+
ulong CPositionMT5::ID(void)
  {
   return m_id;
  }
//+------------------------------------------------------------------+
//| Возвращает цену входа в позицию.                                 |
//+------------------------------------------------------------------+
double CPositionMT5::EntryPrice(void)
  {
   return m_entry_price;
  }
//+------------------------------------------------------------------+
//| Возвращает входящий комментарий активной позиции.                |
//+------------------------------------------------------------------+
string CPositionMT5::EntryComment(void)
  {
   return m_entry_comment;
  }
//+------------------------------------------------------------------+
//| Возвращает название инструмента, по которому открыта текущая     |
//| позиция.                                                         |
//+------------------------------------------------------------------+
string CPositionMT5::Symbol(void)
  {
   return m_symbol;
  }
//+------------------------------------------------------------------+
//| Возвращает время открытия позиции.                               |
//+------------------------------------------------------------------+
datetime CPositionMT5::TimeOpen(void)
  {
   return m_time_open;
  }
//+------------------------------------------------------------------+
//| Возвращает абсолютный уровень стоп-лосса для текущей позиции.    |
//| Если уровень стоп-лосса не установлен, возвращает 0.0            |
//+------------------------------------------------------------------+
double CPositionMT5::StopLossValue(void)
  {
   if(!PositionSelect(m_symbol))
      return 0.0;
   return PositionGetDouble(POSITION_SL);
  }
//+------------------------------------------------------------------+
//| Устанавливает абсолютный уровень стоп-лосса                      |
//+------------------------------------------------------------------+
bool CPositionMT5::StopLossValue(double sl)
  {
   if(!PositionSelect(m_symbol))
      return false;
   return m_trade.Buy(0.0, m_symbol, 0.0, sl, TakeProfitValue(), NULL);
  }
//+------------------------------------------------------------------+
//| Возвращает абсолютный уровень стоп-лосса для текущей позиции.    |
//| Если уровень стоп-лосса не установлен, возвращает 0.0            |
//+------------------------------------------------------------------+
double CPositionMT5::TakeProfitValue(void)
  {
   if(!PositionSelect(m_symbol))
      return 0.0;
   return PositionGetDouble(POSITION_TP);
  }
//+------------------------------------------------------------------+
//| Устанавливает абсолютный уровень стоп-лосса                      |
//+------------------------------------------------------------------+
bool CPositionMT5::TakeProfitValue(double tp)
  {
   if(!PositionSelect(m_symbol))
      return false;
   return m_trade.Buy(0.0, m_symbol, 0.0, StopLossValue(), tp, NULL);
  }
//+------------------------------------------------------------------+
//| Закрывает текущую позицию по рынку, устанавливая закрывающий     |
//| комментарий, равный comment                                      |
//+------------------------------------------------------------------+
bool CPositionMT5::CloseAtMarket(string comment="")
  {
   if(!PositionSelect(m_symbol))
      return false;
   return m_trade.PositionClose(m_symbol);
  }
//+------------------------------------------------------------------+
//| Возвращает текущий объем позиции.                                |
//+------------------------------------------------------------------+
double CPositionMT5::Volume(void)
  {
   if(!PositionSelect(m_symbol))
      return false;
   return PositionGetDouble(POSITION_VOLUME);
  }
//+------------------------------------------------------------------+
//| Возвращает текущую прибыль позиции в валюте депозита.            |
//+------------------------------------------------------------------+
double CPositionMT5::Profit(void)
  {
   if(!PositionSelect(m_symbol))
      return false;
   return PositionGetDouble(POSITION_PROFIT);
  }

Как видно, основная задача класса — вернуть то или иное свойство текущей открытой позиции. Также класс предоставляет несколько методов по управлению текущей позиции: ее закрытие, изменение ее уровня Take Profit и Stop Loss.

 

Прототип торговой стратегии — класс CStrategy

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

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

//+------------------------------------------------------------------+
//| Базовый класс стратегии-прослойки.                               |
//+------------------------------------------------------------------+
class CStrategy : public CObject
  {
   //...
   void              OnTick(void);
   void              OnTimer(void);
   void              OnBookEvent(string symbol);
   virtual void      OnTradeTransaction(const MqlTradeTransaction &trans,
                                        const MqlTradeRequest &request,
                                        const MqlTradeResult &result);
   //...
  };

Стоит заметить, что все методы-обработчики событий, кроме OnTradeTransaction, не являются виртуальными. Это значит, что на уровне самой стратегии невозможно непосредственно обрабатывать события Tick, Timer или BookEvent. Вместо этого CStrategy занимается обработкой этих событий самостоятельно. Событие TradeTransaction в нем не используется, поэтому данный метод является виртуальным.

Сейчас обратимся к содержимому методов OnTick, OnTimer и OnBookEvent:

//+------------------------------------------------------------------+
//| Вызывается менеджером стратегий, при наступлении системного      |
//| события 'новый тик'.                                             |
//+------------------------------------------------------------------+
void CStrategy::OnTick(void)
  {
   NewTickDetect();
   NewBarsDetect();
  }
//+------------------------------------------------------------------+
//| Вызывается менеджером стратегий, при наступлении  системного     |
//| события 'OnTimer'.                                               |
//+------------------------------------------------------------------+
void CStrategy::OnTimer(void)
  {
   m_event.symbol=Symbol();
   m_event.type=MARKET_EVENT_TIMER;
   m_event.period=(ENUM_TIMEFRAMES)Period();
   CallSupport(m_event);
   CallInit(m_event);
   NewTickDetect();
   NewBarsDetect();
  }
//+------------------------------------------------------------------+
//| Вызывается менеджером стратегий, при наступлении  системного     |
//| события 'OnBookEvent'.                                           |
//+------------------------------------------------------------------+
void CStrategy::OnBookEvent(string symbol)
  {
   m_event.symbol=symbol;
   m_event.type=MARKET_EVENT_BOOK_EVENT;
   m_event.period=PERIOD_CURRENT;
   CallSupport(m_event);
   CallInit(m_event);
   NewTickDetect();
   NewBarsDetect();
  }

Как уже было сказано раньше, системное событие NewTick относится только к тому инструменту, на котором запущен текущий эксперт. Чтобы сделать событие NewTick мультиинструментальным, необходимо использовать специальный обработчик новых тиков, а также отслеживать появление новых баров. Эта задача делегируется соответствующим закрытым методам NewBarDetect и NewTickDetect. Обратимся к их исходному коду:

//+------------------------------------------------------------------+
//| Определяет наступление новых баров и генерирует соответствующее  |
//| событие для эксперта.                                            |
//+------------------------------------------------------------------+
void CStrategy::NewBarsDetect(void)
  {
   if(m_bars_detecors.Total()==0)
      AddBarOpenEvent(ExpertSymbol(),Timeframe());
   for(int i=0; i<m_bars_detecors.Total(); i++)
     {
      CBarDetector *bar=m_bars_detecors.At(i);
      if(bar.IsNewBar())
        {
         m_event.period = bar.Timeframe();
         m_event.symbol = bar.Symbol();
         m_event.type=MARKET_EVENT_BAR_OPEN;
         CallSupport(m_event);
         CallInit(m_event);
        }
     }
  }
//+------------------------------------------------------------------+
//| Детектирует появление новых тиков на мультиинструментах.         |
//+------------------------------------------------------------------+
void CStrategy::NewTickDetect(void)
  {
   if(m_ticks_detectors.Total()==0)
      AddTickEvent(ExpertSymbol());
   for(int i=0; i<m_ticks_detectors.Total(); i++)
     {
      CTickDetector *tick=m_ticks_detectors.At(i);
      if(tick.IsNewTick())
        {
         m_event.period=PERIOD_CURRENT;
         m_event.type=MARKET_EVENT_TICK;
         m_event.symbol=tick.Symbol();
         CallSupport(m_event);
         CallInit(m_event);
        }
     }
  }

Работа этих методов заключается в переборе детекторов новых тиков и баров (класс CTickDetector и CBarDetector соответственно, о них мы писали в соответствующем разделе). Каждый из этих детекторов предварительно настраивается на инструмент, по которому фактически производится торговля экспертом. Если образуется новый тик или бар, вызываются специальные методы CallSupport и CallInit, которые специальным образом вызывают торговые методы непосредственной стратегии.

Общий алгоритм методов-обработчиков событий, включая NewBarsDetect и NewTickDetect, следующий:

  1. детектируется поступление нового события;
  2. заполняется структура MarketEvent с указанием соответствующего идентификатора события и его атрибутов;
  3. вызывается метод CallSupport, который в свою очередь вызывает виртуальные методы SupportBuy и SupportSell с переданным в метод событием;
  4. аналогичным образом вызывается метод CallInit, который вызывает виртуальные методы InitBuy и InitSell с переданным в метод событием.

Нам осталось разобрать лишь методы CallInit и CallSupport, чтобы понять, как передается управление непосредственной стратегии. Приведем исходный код CallInit:

//+------------------------------------------------------------------+
//| Вызывает логику открытия позиции, при условии, что торговое      |
//| состояние явно не запрещает этого делать.                        |
//+------------------------------------------------------------------+
void CStrategy::CallInit(const MarketEvent &event)
  {
   m_trade_state=m_state.GetTradeState();
   if(m_trade_state == TRADE_STOP)return;
   if(m_trade_state == TRADE_WAIT)return;
   if(m_trade_state == TRADE_NO_NEW_ENTRY)return;
   SpyEnvironment();
   if(m_trade_state==TRADE_BUY_AND_SELL || m_trade_state==TRADE_BUY_ONLY)
      InitBuy(event);
   if(m_trade_state==TRADE_BUY_AND_SELL || m_trade_state==TRADE_SELL_ONLY)
      InitSell(event);
  }

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

Похожим образом работает метод CallSupport, но его логика несколько иная. Приведем его исходный код:

//+------------------------------------------------------------------+
//| Вызывает логику сопровождения позиций, при условии, что торговое |
//| состояние не равно TRADE_WAIT.                                   |
//+------------------------------------------------------------------+
void CStrategy::CallSupport(const MarketEvent &event)
  {
   m_trade_state=m_state.GetTradeState();
   if(m_trade_state == TRADE_WAIT)return;
   SpyEnvironment();
   for(int i=ActivePositions.Total()-1; i>=0; i--)
     {
      CPosition *pos=ActivePositions.At(i);
      if(pos.ExpertMagic()!=m_expert_magic)continue;
      if(pos.Direction()==POSITION_TYPE_BUY)
         SupportBuy(event,pos);
      else
         SupportSell(event,pos);
      if(m_trade_state==TRADE_STOP && pos.IsActive())
         ExitByStopRegim(pos);
     }
  }

Метод аналогичным образом получает текущее торговое состояние эксперта и, если оно позволяет перейти к управлению позициями, начинает перебор всех позиций, отрытых на текущий момент. Эти позиции представлены специальным классом CPosition. Получив доступ к каждой позиции, метод сверяет ее магический номер с магическим номером текущего эксперта и, если они совпадают, вызывает соответствующие методы сопровождения: SupportBuy для длинной позиции и SupportSell для короткой.

 

Заключение

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

В следующей статье "Универсальный торговый эксперт: Пользовательские стратегии и вспомогательные торговые классы (Часть 3)" пойдет речь о непосредственном построении торговых стратегий на основе описанных алгоритмов.