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

Enrico Lambino | 3 ноября, 2017


Оглавление

  1. Введение
  2. Класс торгового советника
  3. Инициализация
  4. Определение нового бара
  5. Обработчик OnTick
  6. Контейнер торговых экспертов
  7. Сохранение данных
  8. Примеры
  9. Окончательные заметки
  10. Заключение

Введение

В примерах из предыдущих статей серии  (1, 2, 3, 4, 5, 6, 7, 8, 9) компоненты торговых советников были хаотично разбросаны по основному заголовочному файлу советника. За это отвечали пользовательские функции. Завершим работу созданием классов CExpertAdvisor и CExpertsAdvisors. С их помощью организуется более гармоничное взаимодействие между отдельными компонентами эксперта. Также рассмотрим некоторые общие вопросы использования советника: загрузку и сохранение изменчивых данных и обнаружение новых баров.

Класс торгового советника

Класс CExpertAdvisorBase показан в коде ниже. До сих пор большинство различий между MQL4 и MQL5 сглаживались путем обработки другими объектами класса. Они были описаны в предыдущих статьях этой серии.

class CExpertAdvisorBase : public CObject
  {
protected:
   //--- параметры торговли
   bool              m_active;
   string            m_name;
   int               m_distance;
   double            m_distance_factor_long;
   double            m_distance_factor_short;
   bool              m_on_tick_process;
   //--- параметры сигнала
   bool              m_every_tick;
   bool              m_one_trade_per_candle;
   datetime          m_last_trade_time;
   string            m_symbol_name;
   int               m_period;
   bool              m_position_reverse;
   //--- объекты сигналов
   CSignals         *m_signals;
   //--- торговые объекты   
   CAccountInfo      m_account;
   CSymbolManager    m_symbol_man;
   COrderManager     m_order_man;
   //--- объекты времени торговли
   CTimes           *m_times;
   //--- свеча
   CCandleManager    m_candle_man;
   //--- события
   CEventAggregator *m_event_man;
   //--- контейнер
   CObject          *m_container;
public:
                     CExpertAdvisorBase(void);
                    ~CExpertAdvisorBase(void);
   virtual int       Type(void) const {return CLASS_TYPE_EXPERT;}
   //--- инициализация
   bool              AddEventAggregator(CEventAggregator*);
   bool              AddMoneys(CMoneys*);
   bool              AddSignal(CSignals*);
   bool              AddStops(CStops*);
   bool              AddSymbol(const string);
   bool              AddTimes(CTimes*);
   virtual bool      Init(const string,const int,const int,const bool,const bool,const bool);
   virtual bool      InitAccount(void);
   virtual bool      InitCandleManager(void);
   virtual bool      InitEventAggregator(void);
   virtual bool      InitComponents(void);
   virtual bool      InitSignals(void);
   virtual bool      InitTimes(void);
   virtual bool      InitOrderManager(void);
   virtual bool      Validate(void) const;
   //--- контейнер
   void              SetContainer(CObject*);
   CObject          *GetContainer(void);
   //--- активация и дезактивация
   bool              Active(void) const;
   void              Active(const bool);
   //--- методы установки и получения       
   string            Name(void) const;
   void              Name(const string);
   int               Distance(void) const;
   void              Distance(const int);
   double            DistanceFactorLong(void) const;
   void              DistanceFactorLong(const double);
   double            DistanceFactorShort(void) const;
   void              DistanceFactorShort(const double);
   string            SymbolName(void) const;
   void              SymbolName(const string);
   //--- указатели на объекты
   CAccountInfo     *AccountInfo(void);
   CStop            *MainStop(void);
   CMoneys          *Moneys(void);
   COrders          *Orders(void);
   COrders          *OrdersHistory(void);
   CStops           *Stops(void);
   CSignals         *Signals(void);
   CTimes           *Times(void);
   //--- менеджер ордеров
   string            Comment(void) const;
   void              Comment(const string);
   bool              EnableTrade(void) const;
   void              EnableTrade(bool);
   bool              EnableLong(void) const;
   void              EnableLong(bool);
   bool              EnableShort(void) const;
   void              EnableShort(bool);
   int               Expiration(void) const;
   void              Expiration(const int);
   double            LotSize(void) const;
   void              LotSize(const double);
   int               MaxOrdersHistory(void) const;
   void              MaxOrdersHistory(const int);
   int               Magic(void) const;
   void              Magic(const int);
   uint              MaxTrades(void) const;
   void              MaxTrades(const int);
   int               MaxOrders(void) const;
   void              MaxOrders(const int);
   int               OrdersTotal(void) const;
   int               OrdersHistoryTotal(void) const;
   int               TradesTotal(void) const;
   //--- менеджер сигналов   
   int               Period(void) const;
   void              Period(const int);
   bool              EveryTick(void) const;
   void              EveryTick(const bool);
   bool              OneTradePerCandle(void) const;
   void              OneTradePerCandle(const bool);
   bool              PositionReverse(void) const;
   void              PositionReverse(const bool);
   //--- дополнительные свечи
   void              AddCandle(const string,const int);
   //--- обнаружение нового бара
   void              DetectNewBars(void);
   //-- события
   virtual bool      OnTick(void);
   virtual void      OnChartEvent(const int,const long&,const double&,const string&);
   virtual void      OnTimer(void);
   virtual void      OnTrade(void);
   virtual void      OnDeinit(const int,const int);
   //--- восстановление
   virtual bool      Save(const int);
   virtual bool      Load(const int);

protected:
   //--- свечной менеджер   
   virtual bool      IsNewBar(const string,const int);
   //--- менеджер ордеров
   virtual void      ManageOrders(void);
   virtual void      ManageOrdersHistory(void);
   virtual void      OnTradeTransaction(COrder*) {}
   virtual datetime  Time(const int);
   virtual bool      TradeOpen(const string,const ENUM_ORDER_TYPE,double,bool);
   //--- менеджер символов
   virtual bool      RefreshRates(void);
   //--- деинициализация
   void              DeinitAccount(void);
   void              DeinitCandle(void);
   void              DeinitSignals(void);
   void              DeinitSymbol(void);
   void              DeinitTimes(void);
  };

Большинство методов, объявленных в этом классе, выполняют роль оберток методов его компонентов. Ключевые методы класса описаны ниже.

Инициализация

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

После создания экземпляра CExpertAdvisor следующим вызывается его метод Init. Вот код этого метода:

bool CExpertAdvisorBase::Init(string symbol,int period,int magic,bool every_tick=true,bool one_trade_per_candle=true,bool position_reverse=true)
  {
   m_symbol_name=symbol;
   CSymbolInfo *instrument;
   if((instrument=new CSymbolInfo)==NULL)
      return false;
   if(symbol==NULL) symbol=Symbol();
   if(!instrument.Name(symbol))
      return false;
   instrument.Refresh();
   m_symbol_man.Add(instrument);
   m_symbol_man.SetPrimary(m_symbol_name);
   m_period=(ENUM_TIMEFRAMES)period;
   m_every_tick=every_tick;
   m_order_man.Magic(magic);
   m_position_reverse=position_reverse;
   m_one_trade_per_candle=one_trade_per_candle;
   CCandle *candle=new CCandle();
   candle.Init(instrument,m_period);
   m_candle_man.Add(candle);
   Magic(magic);
   return false;
  }

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

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

В конце функции OnInit экземпляр CExpertAdvisor должен вызвать его метод InitComponents. Нижеследующий фрагмент кода демонстрирует указанный метод для CExpertBase:

bool CExpertAdvisorBase::InitComponents(void)
  {
   if(!InitSignals())
     {
      Print(__FUNCTION__+": error in signal initialization");
      return false;
     }
   if(!InitTimes())
     {
      Print(__FUNCTION__+": error in time initialization");
      return false;
     }
   if(!InitOrderManager())
     {
      Print(__FUNCTION__+": error in order manager initialization");
      return false;
     }
   if(!InitCandleManager())
     {
      Print(__FUNCTION__+": error in candle manager initialization");
      return false;
     }
   if(!InitEventAggregator())
     {
      Print(__FUNCTION__+": error in event aggregator initialization");
      return false;
     }
   return true;
  }

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

Определение нового бара

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

class CCandleBase : public CObject
  {
protected:
   bool              m_new;
   bool              m_wait_for_new;
   bool              m_trade_processed;
   int               m_period;
   bool              m_active;
   MqlRates          m_last;
   CSymbolInfo      *m_symbol;
   CEventAggregator *m_event_man;
   CObject          *m_container;
public:
                     CCandleBase(void);
                    ~CCandleBase(void);
   virtual int       Type(void) const {return(CLASS_TYPE_CANDLE);}
   virtual bool      Init(CSymbolInfo*,const int);
   virtual bool      Init(CEventAggregator*);
   CObject          *GetContainer(void);
   void              SetContainer(CObject*);
   //--- методы установки и получения
   void              Active(bool);
   bool              Active(void) const;
   datetime          LastTime(void) const;
   double            LastOpen(void) const;
   double            LastHigh(void) const;
   double            LastLow(void) const;
   double            LastClose(void) const;
   string            SymbolName(void) const;
   int               Timeframe(void) const;
   void              WaitForNew(bool);
   bool              WaitForNew(void) const;
   //--- обработка
   virtual bool      TradeProcessed(void) const;
   virtual void      TradeProcessed(bool);
   virtual void      Check(void);
   virtual void      IsNewCandle(bool);
   virtual bool      IsNewCandle(void) const;
   virtual bool      Compare(MqlRates &) const;
   //--- восстановление
   virtual bool      Save(const int);
   virtual bool      Load(const int);
  };

Проверка появления новой свечи на графике осуществляется методом Check:

CCandleBase::Check(void)
  {
   if(!Active())
      return;
   IsNewCandle(false);
   MqlRates rates[];
   if(CopyRates(m_symbol.Name(),(ENUM_TIMEFRAMES)m_period,1,1,rates)==-1)
      return;
   if(Compare(rates[0]))
     {
      IsNewCandle(true);
      TradeProcessed(false);
      m_last=rates[0];
     }
  }

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

Как показано в коде выше, фактическое сравнение времени открытия и цены открытия бара происходит методом Compare этого класса, как показано в коде ниже:

bool CCandleBase::Compare(MqlRates &rates) const
  {
   return (m_last.time!=rates.time ||
           (m_last.open/m_symbol.TickSize())!=(rates.open/m_symbol.TickSize()) || 
           (!m_wait_for_new && m_last.time==0));
  }

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

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

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

Подобно другим типам классов, которые мы описали ранее, класс CCandle также должен иметь свой контейнер, CCandleManager. В коде ниже показано объявление класса СCandleManagerBase:

class CCandleManagerBase : public CArrayObj
  {
protected:
   bool              m_active;
   CSymbolManager   *m_symbol_man;
   CEventAggregator *m_event_man;
   CObject          *m_container;
public:
                     CCandleManagerBase(void);
                    ~CCandleManagerBase(void);
   virtual int       Type(void) const {return(CLASS_TYPE_CANDLE_MANAGER);}
   virtual bool      Init(CSymbolManager*,CEventAggregator*);
   virtual bool      Add(const string,const int);
   CObject          *GetContainer(void);
   void              SetContainer(CObject *container);
   bool              Active(void) const;
   void              Active(bool active);
   virtual void      Check(void) const;
   virtual bool      IsNewCandle(const string,const int) const;
   virtual CCandle *Get(const string,const int) const;
   virtual bool      TradeProcessed(const string,const int) const;
   virtual void      TradeProcessed(const string,const int,const bool) const;
   //--- recovery
   virtual bool      Save(const int);
   virtual bool      Load(const int);
  };

Экземпляр класса СCandle создается на основе имени инструмента и таймфрейма. Наличие CCandleManager упрощает советнику отслеживание нескольких графиков по заданному инструменту — например, можно проверить появление новой свечи на EURUSD M15 и EURUSD H1 в одном и том же советнике. Экземпляры CCandle с одинаковыми символом и таймфреймом избыточны, и их желательно избегать. При поиске определенного экземпляра CCandle нужно просто вызвать соответствующий метод, находящийся в CCandleManager, и указать символ и таймфрейм. В свою очередь, CCandleManager, найдет соответствующий экземпляр CCandle и вызовет предназначенный для него метод.

Кроме проверки возникновения новой свечи, CCandle и CCandleManager выполняют еще одну функцию: проверяют, открыл ли советник сделку по данному символу и таймфрейму. Можно проверять недавнюю сделку и только по символу, не учитывая таймфрейм. Чтобы реализовать такое поведение, сам экземпляр CExpertAdvisor должен установить/сбросить флаг, отвечающий за это. Переключатель для обоих классов можно установить с использованием метода TradeProcessed.

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

bool CCandleManagerBase::TradeProcessed(const string symbol,const int timeframe) const
  {
   CCandle *candle=Get(symbol,timeframe);
   if(CheckPointer(candle))
      return candle.TradeProcessed();
   return false;
  }

CCandle присваивает новое значение одному из его членов класса, m_trade_processed. За это отвечают показанные ниже методы:

bool CCandleBase::TradeProcessed(void) const
  {
   return m_trade_processed;
  }

CCandleBase::TradeProcessed(bool value)
  {
   m_trade_processed=value;
  }

Обработчик OnTick

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


CExpertAdvisorBase OnTick


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

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

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

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

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

В конце обработки тика советник установит тиковый флаг на false, и после этого ему будет разрешено обработать другой тик.

Контейнер торговых экспертов

Аналогично другим объектам класса, описанным в предыдущих статьях, класс CExpertAdvisor также имеет свой контейнер — CExpertAdvisors. В нижеследующем фрагменте кода показано объявление его базового класса, CExpertAdvisorsBase:

class CExpertAdvisorsBase : public CArrayObj
  {
protected:
   bool              m_active;
   int               m_uninit_reason;
   CObject          *m_container;
public:
                     CExpertAdvisorsBase(void);
                    ~CExpertAdvisorsBase(void);
   virtual int       Type(void) const {return CLASS_TYPE_EXPERTS;}
   virtual int       UninitializeReason(void) const {return m_uninit_reason;}
   //--- методы получения и установки
   void              SetContainer(CObject *container);
   CObject          *GetContainer(void);
   bool              Active(void) const;
   void              Active(const bool);
   int               OrdersTotal(void) const;
   int               OrdersHistoryTotal(void) const;
   int               TradesTotal(void) const;
   //--- инициализация
   virtual bool      Validate(void) const;
   virtual bool      InitComponents(void) const;
   //--- события
   virtual void      OnTick(void);
   virtual void      OnChartEvent(const int,const long&,const double&,const string&);
   virtual void      OnTimer(void);
   virtual void      OnTrade(void);
   virtual void      OnDeinit(const int,const int);
   //--- восстановление
   virtual bool      CreateElement(const int);
   virtual bool      Save(const int);
   virtual bool      Load(const int);
  };

Этот контейнер в первую очередь отражает публичные методы класса. Пример этого — обработчик OnTick. Метод просто перебирает каждый экземпляр СExpertAdvisor для вызова его метода OnTick:

void CExpertAdvisorsBase::OnTick(void)
  {
   if(!Active()) return;
   for(int i=0;i<Total();i++)
     {
      CExpertAdvisor *e=At(i);
      e.OnTick();
     }
  }

Наличие этого контейнера дает возможность хранить множественные экземпляры CExpertAdvisor. Это, возможно, единственный способ запустить несколько советников на одном графике. Просто инициализируйте несколько экземпляров CExpertAdvisor, сохраните их указатели в один контейнер CExpertAdvisors, а затем используйте метод OnTick контейнера, чтобы методы OnTick каждого экземпляра CExpertAdvisor срабатывали. То же самое можно сделать для каждого экземпляра класса CExpert из Стандартной библиотеки MQL5, используя класс CArrayObj или его наследники.

Сохранение данных

Некоторые данные, которые используются в экземплярах CExpertAdvisor, хранятся только в памяти компьютера. Как правило, необходимые данные сохраняются в платформе, а советник получает их из нее с помощью функции вызова. Однако для данных, которые создаются динамически, пока запущен советник, это не так. Когда эксперт запускает событие OnDeinit, советник удаляет все объекты и таким образом теряет их данные.

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

Большая часть волатильных данных в классе CExpertAdvisor может находиться в одном из его членов, который является экземпляром COrderManager. Здесь создаются экземпляры COrder и COrderStop (и их потомки), в то время как эксперт выполняет свою обычную работу. Поскольку эти экземпляры создаются динамически с использованием OnTick, они не восстанавливаются при новой инициализации советника. Поэтому в советнике должен быть реализован метод сохранения и извлечения этих изменяющихся данных. Один способ имплементации этой деятельности — использовать наследник класса CFileBin, CExpertFile. В коде ниже показано объявление его базового класса, CExpertFileBase.

class CExpertFileBase : public CFileBin
  {
public:
                     CExpertFileBase(void);
                    ~CExpertFileBase(void);
   void              Handle(const int handle) { m_handle=handle; };
   uint              WriteBool(const bool value);
   bool              ReadBool(bool &value);
  };

Здесь мы расширим CFileBin, чтобы явно объявлять методы записи и считывания данных типа bool.

В конце файла этого класса мы объявляем экземпляр класса CExpertFile. Этот экземпляр будет использоваться всё время работы советника, если он должен сохранять и загружать изменяющиеся данные. В качестве альтернативы, можно просто довериться методам Save и Load, унаследованным от CObject, и обрабатывать сохранение и загрузку привычным способом. Но это может быть очень скрупулезная работа. Множество сил и строк кода можно сэкономить, если использовать только CFile (или его наследники).

//CExpertFileBase class definition
//+------------------------------------------------------------------+ 
#ifdef __MQL5__
#include "..\..\MQL5\File\ExpertFile.mqh"
#else
#include "..\..\MQL4\File\ExpertFile.mqh"
#endif
//+------------------------------------------------------------------+ 
CExpertFile file;
//+------------------------------------------------------------------+ 

Менеджер ордеров сохраняет изменяющиеся данные методом Save:

bool COrderManagerBase::Save(const int handle)
  {
   if(handle==INVALID_HANDLE)
      return false;
   file.WriteDouble(m_lotsize);
   file.WriteString(m_comment);
   file.WriteInteger(m_expiration);
   file.WriteInteger(m_history_count);
   file.WriteInteger(m_max_orders_history);
   file.WriteBool(m_trade_allowed);
   file.WriteBool(m_long_allowed);
   file.WriteBool(m_short_allowed);
   file.WriteInteger(m_max_orders);
   file.WriteInteger(m_max_trades);
   file.WriteObject(GetPointer(m_orders));
   file.WriteObject(GetPointer(m_orders_history));
   return true;
  }

Большая часть этих данных — примитивы, кроме последних двух, которые являются контейнерами текущих и исторических ордеров. Для этих данных используется метод WriteObject класса CFileBin, который просто вызывает метод Save объекта, который нужно записать. В коде ниже показан метод Save класса COrderBase:

bool COrderBase::Save(const int handle)
  {
   if(handle==INVALID_HANDLE)
      return false;
   file.WriteBool(m_initialized);
   file.WriteBool(m_closed);
   file.WriteBool(m_suspend);
   file.WriteInteger(m_magic);
   file.WriteDouble(m_price);
   file.WriteLong(m_ticket);
   file.WriteEnum(m_type);
   file.WriteDouble(m_volume);
   file.WriteDouble(m_volume_initial);
   file.WriteString(m_symbol);
   file.WriteObject(GetPointer(m_order_stops));
   return true;
  }

Как мы можем здесь видеть, процесс просто повторяется при сохранении объектов. Данные-примитивы сохраняются в файл, как обычно. Для данных сложного типа метод Save объекта вызывается через метод WriteObject класса CFileBin.

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

bool CExpertAdvisorsBase::Save(const int handle)
  {
   if(handle!=INVALID_HANDLE)
     {
      for(int i=0;i<Total();i++)
        {
         CExpertAdvisor *e=At(i);
         if(!e.Save(handle))
            return false;
        }
     }
   return true;
  }

Методы Save вызываются для каждого экземпляра CExpertAdvisor. Единственный хэндл файла означает, что для каждого файла торгового эксперта должен быть только один файл сохранения. Каждый экземпляр CExpertAdvisor мог бы иметь свой собственный файл сохранения, но это было бы более сложным подходом.

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

bool COrderManagerBase::Load(const int handle)
  {
   if(handle==INVALID_HANDLE)
      return false;
   if(!file.ReadDouble(m_lotsize))
      return false;
   if(!file.ReadString(m_comment))
      return false;
   if(!file.ReadInteger(m_expiration))
      return false;
   if(!file.ReadInteger(m_history_count))
      return false;
   if(!file.ReadInteger(m_max_orders_history))
      return false;
   if(!file.ReadBool(m_trade_allowed))
      return false;
   if(!file.ReadBool(m_long_allowed))
      return false;
   if(!file.ReadBool(m_short_allowed))
      return false;
   if(!file.ReadInteger(m_max_orders))
      return false;
   if(!file.ReadInteger(m_max_trades))
      return false;
   if(!file.ReadObject(GetPointer(m_orders)))
      return false;
   if(!file.ReadObject(GetPointer(m_orders_history)))
      return false;
   for(int i=0;i<m_orders.Total();i++)
     {
      COrder *order=m_orders.At(i);
      if(!CheckPointer(order))
         continue;
      COrderStops *orderstops=order.OrderStops();
      if(!CheckPointer(orderstops))
         continue;
      for(int j=0;j<orderstops.Total();j++)
        {
         COrderStop *orderstop=orderstops.At(j);
         if(!CheckPointer(orderstop))
            continue;
         for(int k=0;k<m_stops.Total();k++)
           {
            CStop *stop=m_stops.At(k);
            if(!CheckPointer(stop))
               continue;
            orderstop.Order(order);
            if(StringCompare(orderstop.StopName(),stop.Name())==0)
              {
               orderstop.Stop(stop);
               orderstop.Recreate();
              }
           }
        }
     }
   return true;
  }

Как видим, код метода в COrderManager более сложный, в отличие от аналогичного метода Load в классе CExpertAdvisor. Причина в том, что, в отличие от менеджера ордеров, экземпляры CExpertAdvisor создаются через OnInit, так что контейнеру нужно просто вызвать методы Load для каждого экземпляра CExpertAdvisor, вместо того, чтобы использовать метод ReadObject класса CFileBin.

Экземпляры класса, не созданные во время выполнения OnInit, должны будут создаваться по мере перезагрузки советника. Это достигается расширением метода CreateElement класса CArrayObj. Объект не может создать сам себя, так что он должен создаваться его родительским объектом или контейнером, или даже из исходного кода заголовочного файла. Пример можно видеть в расширенном методе CreateElement, который находится в COrdersBase. В этом классе контейнером является COrders (наследник класса COrdersBase), а созданный объект имеет тип COrder:

bool COrdersBase::CreateElement(const int index)
  {
   COrder*order=new COrder();
   if(!CheckPointer(order))
      return(false);
   order.SetContainer(GetPointer(this));
   if(!Reserve(1))
      return(false);
   m_data[index]=order;
   m_sort_mode=-1;
   return CheckPointer(m_data[index]);
  }

Здесь, кроме создания элемента, мы также устанавливаем его родительский объект или контейнер, чтобы отличить, относится ли он к списку активных сделок (член m_orders класса COrderManagerBase) или исторических (член m_orders_history класса COrderManagerBase).

Примеры

Примеры №№ 1 — 4 из этой статьи — измененные версии четырех примеров из предыдущей статьи (см. Кроссплатформенный торговый советник: Пользовательские стопы, Трейлинг и Безубыток). Давайте взглянем на самый сложный пример expert_custom_trail_ha_ma.mqh, который является модифицированным вариантом custom_trail_ha_ma.mqh.

Перед функцией OnInit мы объявляем следующие экземпляры глобальных объектов:

COrderManager *order_manager;
CSymbolManager *symbol_manager;
CSymbolInfo *symbol_info;
CSignals *signals;
CMoneys *money_manager;
CTimes *time_filters;

Заменяем его экземпляром CExpert. Некоторые из вышеперечисленных экземпляров находятся в самом CExpetAdvisor (например, COrderManager), остальные должны быть созданы методом OnInit (контейнеры классов):

CExpertAdvisors experts;

В начале метода создаем экземпляр CExpertAdvisor. Также вызываем метод Init с базовыми настройками:

int OnInit()
  {
//--- 
   CExpertAdvisor *expert=new CExpertAdvisor();
   expert.Init(Symbol(),Period(),12345,true,true,true);
//--- other code
//--- 
   return(INIT_SUCCEEDED);
  }

Больше не нужно создавать CSymbolInfo / CSymbolManager, поскольку экземпляры класса CExpertAdvisor сами могут создавать экземпляры этих классов.

Функция, определяемая пользователем, тоже должна быть удалена — наш экперт больше в ней не нуждается.

Удаляем из кода и глобальное объявление контейнеров, поскольку они должны быть объявлены из OnInit. Пример этого — контейнер временных фильтров (CTimeFilters), находящийся в функции OnInit:

CTimes *time_filters=new CTimes();

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

После этого добавляем экземпляр CExpertAdvisor к экземпляру CExpertAdvisors. Затем вызываем метод InitComponents экземпляра CExpertAdvisors. Это гарантирует инициализацию всех экземпляров CExpertAdvisor и его компонентов.

int OnInit()
  {
//--- 
//--- другой код
   experts.Add(GetPointer(expert));
   if(!experts.InitComponents())
      return(INIT_FAILED);
//--- другой код
//--- 
   return(INIT_SUCCEEDED);
  }

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

int OnInit()
  {
//--- 
//--- другой код 
 file.Open(savefile,FILE_READ);
   if(!experts.Load(file.Handle()))
      return(INIT_FAILED);
   file.Close();
//--- 
   return(INIT_SUCCEEDED);
  }

Если советник не загружается из файла, он возвращает INIT_FAILED. Однако, если отсутствует файл сохранения (и генерируется INVALID_HANDLE), ошибки инициализации советника не будет, поскольку методы Load классов CExpertAdvisors и CExpertAdvisor возвращают true после получения недействительного хэндла. Этот подход содержит определенный риск, но крайне маловероятно, что сохраненный файл будет открыт другой программой. Просто убедитесь, что каждый экземпляр советника, запущенный на графике, имеет отдельный файл сохранения (так же, как и с магическим номером ордера).

Пятый пример взят не из предыдущей статьи. Вместо этого, в нем все четыре советника из этой статьи скомбинированы в один эксперт. В нем используется слегка измененная версия функции OnInit для каждого из этих советников, которая объявляется как "определяемая пользователем". Ее возвращаемое значение имеет тип CExpertAdvisor*. Если создание эксперта не удалось, возвращается NULL вместо INIT_SUCCEEDED. Фрагмент кода ниже показывает обновленную функцию OnInit заголовочного файла комбинированного эксперта:

int OnInit()
  {
//--- 
   CExpertAdvisor *expert1=expert_breakeven_ha_ma();
   CExpertAdvisor *expert2=expert_trail_ha_ma();
   CExpertAdvisor *expert3=expert_custom_stop_ha_ma();
   CExpertAdvisor *expert4=expert_custom_trail_ha_ma();
      
   if (!CheckPointer(expert1))
      return INIT_FAILED;
   if (!CheckPointer(expert2))
      return INIT_FAILED;
   if (!CheckPointer(expert3))
      return INIT_FAILED;
   if (!CheckPointer(expert4))
      return INIT_FAILED;
   
   experts.Add(GetPointer(expert1));
   experts.Add(GetPointer(expert2));
   experts.Add(GetPointer(expert3));
   experts.Add(GetPointer(expert4));   
   
   if(!experts.InitComponents())
      return(INIT_FAILED);
   file.Open(savefile,FILE_READ);
   if(!experts.Load(file.Handle()))
      return(INIT_FAILED);
   file.Close();
//--- 
   return(INIT_SUCCEEDED);
  }

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

Советник использует функцию, определяемую пользователем, для создания экземпляра СExpertAdvisor. Код ниже показывает функцию, использованную для создания экземпляра четвертого советника:

CExpertAdvisor *expert_custom_trail_ha_ma()
{
   CExpertAdvisor *expert=new CExpertAdvisor();
   expert.Init(Symbol(),Period(),magic4,true,true,true);
   CMoneys *money_manager=new CMoneys();
   CMoney *money_fixed=new CMoneyFixedLot(0.05);
   CMoney *money_ff=new CMoneyFixedFractional(5);
   CMoney *money_ratio=new CMoneyFixedRatio(0,0.1,1000);
   CMoney *money_riskperpoint=new CMoneyFixedRiskPerPoint(0.1);
   CMoney *money_risk=new CMoneyFixedRisk(100);

   money_manager.Add(money_fixed);
   money_manager.Add(money_ff);
   money_manager.Add(money_ratio);
   money_manager.Add(money_riskperpoint);
   money_manager.Add(money_risk);
   expert.AddMoneys(GetPointer(money_manager));

   CTimes *time_filters=new CTimes();
   if(time_range_enabled && time_range_end>0 && time_range_end>time_range_start)
     {
      CTimeRange *timerange=new CTimeRange(time_range_start,time_range_end);
      time_filters.Add(GetPointer(timerange));
     }
   if(time_days_enabled)
     {
      CTimeDays *timedays=new CTimeDays(sunday_enabled,monday_enabled,tuesday_enabled,wednesday_enabled,thursday_enabled,friday_enabled,saturday_enabled);
      time_filters.Add(GetPointer(timedays));
     }
   if(timer_enabled)
     {
      CTimer *timer=new CTimer(timer_minutes*60);
      timer.TimeStart(TimeCurrent());
      time_filters.Add(GetPointer(timer));
     }

   switch(time_intraday_set)
     {
      case INTRADAY_SET_1:
        {
         CTimeFilter *timefilter=new CTimeFilter(time_intraday_gmt,intraday1_hour_start,intraday1_hour_end,intraday1_minute_start,intraday1_minute_end);
         time_filters.Add(timefilter);
         break;
        }
      case INTRADAY_SET_2:
        {
         CTimeFilter *timefilter=new CTimeFilter(0,0,0);
         timefilter.Reverse(true);
         CTimeFilter *sub1 = new CTimeFilter(time_intraday_gmt,intraday2_hour1_start,intraday2_hour1_end,intraday2_minute1_start,intraday2_minute1_end);
         CTimeFilter *sub2 = new CTimeFilter(time_intraday_gmt,intraday2_hour2_start,intraday2_hour2_end,intraday2_minute2_start,intraday2_minute2_end);
         timefilter.AddFilter(sub1);
         timefilter.AddFilter(sub2);
         time_filters.Add(timefilter);
         break;
        }
      default: break;
     }
   expert.AddTimes(GetPointer(time_filters));

   CStops *stops=new CStops();
   CCustomStop *main=new CCustomStop("main");
   main.StopType(stop_type_main);
   main.VolumeType(VOLUME_TYPE_PERCENT_TOTAL);
   main.Main(true);
//main.StopLoss(stop_loss);
//main.TakeProfit(take_profit);
   stops.Add(GetPointer(main));

   CTrails *trails=new CTrails();
   CCustomTrail *trail=new CCustomTrail();
   trails.Add(trail);
   main.Add(trails);

   expert.AddStops(GetPointer(stops));

   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);
   SignalMA *signal_ma=new SignalMA(Symbol(),(ENUM_TIMEFRAMES) Period(),maperiod,0,mamethod,maapplied,signal_bar);
   CSignals *signals=new CSignals();
   signals.Add(GetPointer(signal_ha));
   signals.Add(GetPointer(signal_ma));
   expert.AddSignal(GetPointer(signals));
//--- 
   return expert;
}

Как мы можем видеть, код выглядит очень похоже на функцию OnInit заголовочного файла оригинального советника (expert_custom_trail_ha_ma.mqh). Другие функции, определяемые пользователем, организованы таким же образом.

Окончательные заметки

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

К настоящему моменту библиотека, представленная в приложении к этой статье, насчитывает более 10 000 строк кода (включая комментарии). Несмотря на это, она до сих пор не завершена. Чтобы в полной мере использовать возможности MQL4 и MQL5, нужно проделать большую работу.

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

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

И последнее замечание: библиотека, созданная в этой сериии статей, не должна рассматриваться как постоянное решение. Ее можно использовать для более мягкого перехода с MetaTrader 4 на MetaTrader 5. Несовместимость между MQL4 и MQL5 может представлять серьезное препятствие трейдерам, которые собирались перейти на новую платформу. В результате исходный код на MQL4, написанный для их советников, должен быть переработан для совместимости с компилятором MQL5. Библиотека, созданная в этой статье, представляется в качестве средства развернуть советника на новой платформе с небольшими корректировками в исходном коде или вообще без них. Это может помочь трейдеру в его решении о том, продолжать использовать MetaTrader 4 или перейти на MetaTrader 5. В случае решения о переключении использование этой библиотеки может минимизировать количество изменений и позволит трейдеру использовать советник привычным способом. Но если решено пока использовать старую платформу, трейдер все равно получает возможность быстро переключиться на MetaTrader 5 в тот момент, когда разработчики прекратят поддержку MetaTrader 4.

Заключение

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

Программы, использованные в статье

 # Имя
Тип
Описание параметров
1.
expert_breakeven_ha_ma.mqh
Заголовочный файл
Основной заголовочный файл, использованный в первом примере
2.
expert_breakeven_ha_ma.mq4 Советник
Основной исходный файл, используемый для версии MQL4 в первом примере
3.
expert_breakeven_ha_ma.mq5 Советник Основной исходный файл, используемый для версии MQL5 в первом примере
4.
 expert_trail_ha_ma.mqh Заголовочный файл Основной заголовочный файл, использованный во втором примере
5.
 expert_trail_ha_ma.mq4 Советник Основной исходный файл, используемый для версии MQL4 во втором примере
6.
 expert_trail_ha_ma.mq5 Советник Основной исходный файл, используемый для версии MQL5 во втором примере
7.
 expert_custom_stop_ha_ma.mqh Заголовочный файл Основной заголовочный файл, использованный в третьем примере
8.
 expert_custom_stop_ha_ma.mq4 Советник Основной исходный файл, используемый для версии MQL4 в третьем примере
9.
 expert_custom_stop_ha_ma.mq5 Советник Основной исходный файл, используемый для версии MQL5 в третьем примере
10.
 expert_custom_trail_ha_ma.mqh Заголовочный файл Основной заголовочный файл, использованный в четвертом примере
11.
 expert_custom_trail_ha_ma.mq4 Советник Основной исходный файл, используемый для версии MQL4 в четвертом примере
12.
 expert_custom_trail_ha_ma.mq5 Советник Основной исходный файл, используемый для версии MQL5 в четвертом примере
13.
 combined.mqh Заголовочный файл Основной заголовочный файл, использованный в пятом примере
13.
 combined.mq4 Советник Основной исходный файл, используемый для версии MQL4 в пятом примере
15.
 combined.mq5 Советник Основной исходный файл, используемый для версии MQL5 в пятом примере

Файлы классов, созданных в статье

#
Имя
Тип
Описание параметров
1. MQLx\Base\Expert\ExperAdvisorsBase Заголовочный файл
CExpertAdvisors (контейнер CExpertAdvisor, базовый класс)
2.
MQLx\MQL4\Expert\ExperAdvisors Заголовочный файл CExpertAdvisors (версия MQL4)
3.
MQLx\MQL5\Expert\ExperAdvisors Заголовочный файл
CExpertAdvisors (версия MQL5)
4.
MQLx\Base\Expert\ExperAdvisorBase Заголовочный файл
CExpertAdvisor (базовый класс)
5.
MQLx\MQL4\Expert\ExperAdvisor Заголовочный файл
CExpertAdvisor (версия MQL4)
6.
MQLx\MQL5\Expert\ExperAdvisor Заголовочный файл
CExpertAdvisor (версия MQL5)
7.
MQLx\Base\Candle\CandleManagerBase Заголовочный файл CCandleManager (контейнер CCandle, базовый класс)
8.
MQLx\MQL4\Candle\CandleManager Заголовочный файл CCandleManager (версия MQL4)
9.
MQLx\MQL5\Candle\CandleManager Заголовочный файл CCandleManager (версия MQL5)
10.
MQLx\Base\Candle\CandleBase Заголовочный файл CCandle (базовый класс)
11.
MQLx\MQL4\Candle\Candle Заголовочный файл CCandle (версия MQL4)
12.
MQLx\MQL5\Candle\Candle Заголовочный файл
CCandle (версия MQL5)
13.
MQLx\Base\File\ExpertFileBase Заголовочный файл CExpertFile (базовый класс)
13.
MQLx\MQL4\File\ExpertFile Заголовочный файл CExpertFile(версия MQL4)
15.
MQLx\MQL5\File\ExpertFile Заголовочный файл CExpertFile(версия MQL5)