Построение советника с использованием отдельных модулей

7 ноября 2019, 10:31
Andrei Novichkov
1
3 247

Введение

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

Как это происходит в настоящее время

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

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

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



Получилась довольно запутанная схема. И это только при трех модулях и двух обработчиках советника — OnStart и OnTick. Если усложнить советник, как обычно и бывает, внутренние взаимосвязи станут гораздо более запутанными. Такой советник будет затруднительно сопровождать, а если возникнет необходимость отключить / подключить еще один модуль, то это тоже вызовет немалые сложности. Да и первоначальная отладка и поиск ошибок тоже будет не из простых. Одна из причин таких сложностей в связях, проектируемых бессистемно. Нужно запретить модулям общаться друг с другом и с обработчиками советника, как только в этом появится некоторая необходимость, и сразу же появится определенный порядок:

  • Все модули создаются в OnInit.
  • Логика советника заключена в OnTick.
  • Модули обмениваются информацией только с OnTick.
  • Если в том есть необходимость, модули уничтожаются в OnDeinit.

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


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

Советник для экспериментов

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

class CTradeMod {
public:
   double dBaseLot;
   double dProfit;
   double dStop;
   long   lMagic;
   void   Sell();
   void   Buy();
};

void CTradeMod::Sell()
{
  CTrade Trade;
  Trade.SetExpertMagicNumber(lMagic);
  double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
  Trade.Sell(dBaseLot,NULL,0,ask + dStop,ask - dProfit);
} 

void CTradeMod::Buy()
{
  CTrade Trade;
  Trade.SetExpertMagicNumber(lMagic);
  double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
  Trade.Buy(dBaseLot,NULL,0,bid - dStop,bid + dProfit);
} 

Модуль выполнен в виде класса с открытыми полями и методами. Пока нам не нужен реализованный в модуле метод Buy(), но он понадобится в дальнейшем. Значение отдельных полей вполне очевидно — объем, торговые уровни, Magic. Как пользоваться модулем тоже понятно — создаем его и вызываем метод Sell(), когда появится сигнал на вход.

В советник будет включен еще один модуль:

class CParam {
public:
   double dStopLevel;      
   double dFreezeLevel;   
    CParam() {
      new_day();      
    }//EA_PARAM()
    void new_day() {
      dStopLevel   = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL) * Point();
      dFreezeLevel = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL) * Point();
    }//void new_day()
};
На этом модуле стоит остановиться особо. Это вспомогательный модуль, в котором собраны различные параметры, используемые остальными модулями и обработчиками советника. К сожалению, бывают случаи, когда можно увидеть примерно такой код:
...
input int MaxSpread = 100;
...
OnTick()
 {
   if(ask - bid > MaxSpread * Point() ) return;
....
 }

Очевидно, насколько неэффективен данный фрагмент. Но, если мы все входные (и не только входные) параметры, нуждающиеся в обновлении или преобразовании, поместим в отдельный модуль (здесь это MaxSpread * Point() ), то тем самым не будем засорять глобальное пространство и будем легко и с хорошей производительностью контролировать их состояние, как сделано в вышеприведенном модуле CParam со значениями stops_level и freeze_level.

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

Кроме того, вполне возможно, что конкретно для модуля CParam стоило бы сделать исключение и отойти от правил, позволив обращаться к такому модулю не только обработчику OnTick(), но и всем остальным модулям и обработчикам тоже.

Наконец, блок входных параметров и обработчики советника:

input double dlot   = 0.01;
input int    profit = 50;
input int    stop   = 50;
input long   Magic  = 123456;

CParam par;
CTradeMod trade;

int OnInit()
  {  
   trade.dBaseLot = dlot;
   trade.dProfit  = profit * _Point;
   trade.dStop    = stop   * _Point;
   trade.lMagic   = Magic;
   
   return (INIT_SUCCEEDED);
  }
  
void OnDeinit(const int reason)
  {
  }

void OnTick()
  {
   int total = PositionsTotal(); 
   ulong ticket, tsell = 0;
   ENUM_POSITION_TYPE type;
   double l, p;
   for (int i = total - 1; i >= 0; i--) {
      if ( (ticket = PositionGetTicket(i)) != 0) {
         if ( PositionGetString(POSITION_SYMBOL) == _Symbol) {
            if (PositionGetInteger(POSITION_MAGIC) == Magic) {
               type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
               l    = PositionGetDouble(POSITION_VOLUME);
               p    = PositionGetDouble(POSITION_PRICE_OPEN);
               switch(type) {
                  case POSITION_TYPE_SELL:
                     tsell = ticket;    
                     break;
               }//switch(type)
            }//if (PositionGetInteger(POSITION_MAGIC) == lmagic)
         }//if (PositionGetString(POSITION_SYMBOL) == symbol)
      }//if ( (ticket = PositionGetTicket(i)) != 0)
   }//for (int i = total - 1; i >= 0; i--)
   if (tsell == 0) 
      {
        double o = iOpen(NULL,PERIOD_CURRENT,1); 
        double c = iClose(NULL,PERIOD_CURRENT,1); 
        if (c < o) 
          {
            trade.Sell();
          }   
      }                         
  }

Советник инициализирует модули в обработчике OnInit(), а затем обращается к ним только из обработчика  OnTick(). В OnTick() советник выполняет цикл по открытым позициям, с целью проверки, что нужная  уже открыта. Если позиция еще не открыта, то советник её открывает при наличии сигнала.

Стоит обратить внимание, что обработчик OnDeinit(const int reason) пока пуст. Модули создаются таким образом, что не нуждаются в явном удалении. Кроме того, нами пока не задействован модуль CParam, потому что проверки при открытии позиции пока не проводятся. Но если бы такие проверки проводились, то модуль CTradeMod мог бы нуждаться в доступе к модулю CParam и разработчику пришлось бы отвечать на уже поставленный выше вопрос — не принять ли модуль CParam в качестве исключения из уже принятых правил. Однако при ближайшем рассмотрении оказывается, что в этом нет необходимости, по крайней мере, в данном случае.

Рассмотрим этот момент немного подробнее. Модулю CTradeMod могут потребоваться данные из модуля CParam для проверки уровней стоп лосса и тейк профита, объема открываемой позиции. Но эту же проверку можно выполнить и в точке принятия решения на открытие позиции — если уровни и объем проверку не проходят, то и открывать ничего не надо. Это означает перенос проверки в обработчик OnTick(). В данном же случае, т.к. значения торговых уровней и объема заданы во входных параметрах, то проверку можно выполнить один раз в обработчике OnInit(), и если она окончится неудачно, то и инициализацию всего советника следует завершить с ошибкой. Итак, как видим, модули CTradeMod и CParam могут действовать независимо. И так будет в большинстве советников — независимые модули действуют через обработчик OnTick() и ничего не знают друг о друге. Но в каких случаях это условие соблюсти невозможно? Увидим далее.

Начинаем улучшения

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

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

Значение такого цикла еще больше возрастает в том случае, если советник использует сетки и усреднение. А как без него обойтись в тех случаях, когда используются виртуальные торговые уровни? Подведя итог сказанному, можно сделать вывод, что на основе этого фрагмента кода следует сделать отдельный модуль.  В простейшем случае это будет модуль, просто обнаруживающий позиции с определенным магиком и сообщающий об этом некоторым способом.  В сложном же случае такой модуль сам мог бы стать "центром", включающим в себя модули попроще, например , модули ведения логов и модули статистики. В этом случае советник начнет приобретать почтенную "древовидную" структуру с  обработчиками OnInit() и OnTick()  в основании. Посмотрим, как мог бы выглядеть подобный модуль:

class CSnap {
public:
           void CSnap()  {
               m_lmagic = -1; 
               m_symbol = Symbol();               
           }
   virtual void  ~CSnap() {}   
           bool   CreateSnap();
           long   m_lmagic;
           string m_symbol;
};
Как уже было сделано раньше, все поля оставляем открытыми. Их два — магик и название символа, на котором выполняется советник. При необходимости значения этим полям могут быть присвоены в обработчике OnInit(). Основную работу выполняет метод CreateSnap():
bool CSnap::CreateSnap() {
   int total = PositionsTotal(); 
   ulong ticket;
   ENUM_POSITION_TYPE type;
   double l, p;
   for (int i = total - 1; i >= 0; i--) {
      if ( (ticket = PositionGetTicket(i)) != 0) {
         if (StringLen(m_symbol) == 0 || PositionGetString(POSITION_SYMBOL) == m_symbol) {
            if (m_lmagic < 0 || PositionGetInteger(POSITION_MAGIC) == m_lmagic) {
               type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
               l    = PositionGetDouble(POSITION_VOLUME);
               p    = PositionGetDouble(POSITION_PRICE_OPEN);
               switch(type) {
                  case POSITION_TYPE_BUY:
// ???????????????????????????????????????
                     break;
                  case POSITION_TYPE_SELL:
// ???????????????????????????????????????
                     break;
               }//switch(type)
            }//if (lmagic < 0 || PositionGetInteger(POSITION_MAGIC) == lmagic)
         }//if (StringLen(symbol) == 0 || PositionGetString(POSITION_SYMBOL) == symbol)
      }//if ( (ticket = PositionGetTicket(i)) != 0)
   }//for (int i = total - 1; i >= 0; i--)
   return true;
}

Код несложный, но мы получаем проблему. Каким образом и кому именно модуль должен передавать полученную информацию?  Что именно следует записать в строках, содержащих вопросительные знаки в последнем фрагменте кода? Казалось бы, все просто. В обработчике OnTick() вызываем метод CreateSnap(), он выполняет работу, сохраняя результаты в полях класса CSnap. Затем обработчик проверяет эти поля и делает какие то выводы.

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

CStrategy* m_strategy;
И метод для присваивания этому полю значения:
     bool SetStrategy(CStrategy* strategy) {
              if(CheckPointer(strategy) == POINTER_INVALID) return false;
                 m_strategy = strategy;
                 return true;
              }//bool SetStrategy(CStrategy* strategy)   

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

Теперь переключатель switch в методе CreateSnap() будет выглядеть так:

               switch(type) {
                  case POSITION_TYPE_BUY:
                     m_strategy.OnBuyFind(ticket, p, l);
                     break;
                  case POSITION_TYPE_SELL:
                     m_strategy.OnSellFind(ticket, p, l);
                     break;
               }//switch(type

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

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

class CModule {
   public:
      CModule() {m_strategy = NULL;}
     ~CModule() {}
     virtual bool SetStrategy(CStrategy* strategy) {
                     if(CheckPointer(strategy) == POINTER_INVALID) return false;
                     m_strategy = strategy;
                     return true;
                  }//bool SetStrategy(CStrategy* strategy)   
   protected:   
      CStrategy* m_strategy;  
};
А теперь будем создавать модули наследованием от класса CModule. Да, мы получим в ряде случаев немного избыточного кода. Но это будет с лихвой компенсировано теми модулями, где такой указатель действительно будет нужен. Ну а в остальных, где такой необходимости нет, просто не следует вызывать метод SetStrategy(...). Базовый класс для модулей может быть полезен и для размещения дополнительных полей и методов, про которые мы пока не знаем. Например, вполне полезным (но в данном случае не реализованным) мог бы быть метод:
public:
   const string GetName() const {return m_sModName;}
protected:
         string m_sModName;

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

Теперь давайте посмотрим, как именно мог бы быть реализован столь важный для нас класс CStrategy:

Стратегия советника

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

class CStrategy  {
public:
   virtual  void    OnBuyFind  (ulong ticket, double price, double lot) = 0;
   virtual  void    OnSellFind (ulong ticket, double price, double lot) = 0;       
};// class CStrategy

Все просто. Нам были нужны два метода для вызова в том случае, если метод CreateSnap() обнаруживал нужные позиции, они и добавлены. Класс CStrategy выполнен абстрактным, а методы объявлены виртуальными. Это совершенно логично и полностью оправдано, т.к. мы уже обсудили то, что данный объект должен меняться от одного советника к другому. Следовательно базовый класс может быть использован только для наследования, а его методы будут переопределены.

Теперь осталось добавить файл CStrategy.mqh в файл CModule.mqh:

#include "CStrategy.mqh"

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

Улучшения стратегии советника

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

   virtual  string  Name() const     = 0;
   virtual  void    CreateSnap()     = 0;  
   virtual  bool    MayAndEnter()    = 0;  
   virtual  bool    MayAndContinue() = 0;       
   virtual  void    MayAndClose()    = 0;

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

  • Name()                — Возвращает имя стратегии.
  • CreateSnap()        — Вызывает такой же метод объекта CSnap.
  • MayAndEnter()      — проверяет наличие сигнала на вход и входит, если сигнал есть.
  • MayAndContinue() — проверяет наличие сигнала на вход и входит повторно
  • MayAndClose()      — проверяет наличие сигнала на выход и закрывает все позиции, если такой сигнал есть.

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

Мы же для инициализации добавим метод:

virtual  bool    Initialize(CInitializeStruct* pInit) = 0;
  с инициализирующим объектом CInitializeStruct, который и будет содержать все необходимые указатели. А пока опишем этот объект в файле CStrategy.mqh так:
class CInitializeStruct {};

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

Практическое применение

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

Модули у нас практически готовы, поэтому сосредоточимся на классе, производном от CStrategy:

class CRealStrat1 : public CStrategy   {
public:
   static   string  m_name;
                     CRealStrat1(){};
                    ~CRealStrat1(){};
   virtual  string  Name() const {return m_name;}
   virtual  bool    Initialize(CInitializeStruct* pInit) {
                        m_pparam = ((CInit1* )pInit).m_pparam;
                        m_psnap = ((CInit1* )pInit).m_psnap;
                        m_ptrade = ((CInit1* )pInit).m_ptrade;
                        m_psnap.SetStrategy(GetPointer(this));
                        return true;
                    }//Initialize(EA_InitializeStruct* pInit)
   virtual  void    CreateSnap() {
                        m_tb = 0;
                        m_psnap.CreateSnap();
                    }  
   virtual  bool    MayAndEnter();
   virtual  bool    MayAndContinue() {return false;}       
   virtual  void    MayAndClose()   {}
   virtual  bool    Stop()            {return false;}   
   virtual  void    OnBuyFind  (ulong ticket, double price, double lot) {}
   virtual  void    OnBuySFind (ulong ticket, double price, double lot) {}   
   virtual  void    OnBuyLFind (ulong ticket, double price, double lot) {}
   virtual  void    OnSellFind (ulong ticket, double price, double lot) {tb = ticket;}  
   virtual  void    OnSellSFind(ulong ticket, double price, double lot) {}   
   virtual  void    OnSellLFind(ulong ticket, double price, double lot) {}      
private:
   CParam*          m_pparam;
   CSnap*           m_psnap;  
   CTradeMod*       m_ptrade;   
   ulong            m_tb;            
};
static string CRealStrat1::m_name = "Real Strategy 1";

bool CRealStrat1::MayAndEnter() {
   if (tb != 0) return false;  
   double o = iOpen(NULL,PERIOD_CURRENT,1); 
   double c = iClose(NULL,PERIOD_CURRENT,1); 
   if (c < o) {
      m_ptrade.Sell();
      return true;
   }   
   return false;
} 

Код советника простой и в пространных пояснениях не нуждается. Остановимся лишь на некоторых моментах. Метод CreateSnap() класса CRealStrat1 обнуляет поле, в котором находится значение тикета уже открытой позиции на продажу и вызывает метод CreateSnap() модуля CSnap. Модуль CSnap проверяет открытые позиции, и если находит позицию на продажу, открытую данным советником, вызывает метод OnSellFind(...) класса CStrategy, указатель на который имеется у модуля CSnap. В результате вызывается метод OnSellFind(...)  класса CRealStrat1, который повторно меняет значение поля m_tb. Метод MayAndEnter(), видя, что позиция уже открыта, не открывает новую. Никакие другие методы базового класса CStrategy нами не используются, поэтому их реализация представлена пустой.

Другой интересный момент содержит метод Initialize(...). Этот метод добавляет в класс CRealStrat1 указатели на другие модули, которые могут понадобиться при принятии отдельных решений. Класс CStrategy не знает о том, какие модули могут понадобиться классу CRealStrat1 и поэтому использует пустой класс CInitializeStruct. Мы же в файле, содержащим класс CRealStrat1 (хотя это и необязательно), добавляем класс CInit1, наследуя его от  CInitializeStruct:

class CInit1: public CInitializeStruct {
public:
   bool Initialize(CParam* pparam, CSnap* psnap, CTradeMod* ptrade) {
   if (CheckPointer(pparam) == POINTER_INVALID || 
       CheckPointer(psnap)  == POINTER_INVALID) return false;   
      m_pparam = pparam; 
      m_psnap  = psnap;
      m_ptrade = ptrade;
       return true;
   }
   CParam* m_pparam;
   CSnap*  m_psnap;
   CTradeMod* m_ptrade;
};

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

Обработчики OnInit() и OnTick()

Вот как может выглядеть обработчик OnInit() и список глобальных объектов:

CParam     par;
CSnap      snap;
CTradeMod  trade;

CStrategy* pS1;

int OnInit()
  {  
   ...
   pS1 = new CRealStrat1();
   CInit1 ci;
   if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return (INIT_FAILED);
   pS1.Initialize(GetPointer(ci));      
   return (INIT_SUCCEEDED);
  }
  
  В обработчике OnInit() создается только один объект — экземпляр класса CRealStrat1. Проводится его инициализация объектом класса CInit1.  Этот же объект уничтожается в обработчике OnDeinit():
void OnDeinit(const int reason)
  {
      if (CheckPointer(pS1) != POINTER_INVALID) delete pS1;      
  }
Обработчик OnTick(), в результате наших действий получился предельно простым:
void OnTick()
  {
      if (IsNewCandle() ){
         pS1.CreateSnap();
         pS1.MayAndEnter();
         
      }    
  }

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

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

Итак, мы спроектировали советник, состоящий из отдельных "кирпичиков" — модулей. Но это далеко не все. Давайте посмотрим, какую интересную возможность нам открывает такой способ проектирования.

Что дальше

Первое, что приходит в голову, это динамическая замена модулей. Нет особых причин, чтобы не заменить простое расписание более продвинутым, причем именно заменить расписание, как объект, а не просто добавить свойств и методов в существующий, тем самым усложнив отладку и сопровождение. Можно предусмотреть версии "Debug" и "Release" для отдельных модулей, сделать какой то менеджер для управления модулями.  

Но самое интересное не это. Рассмотренный способ проектирования делает возможным динамическую замену стратегии советника, в данном случае объекта класса CRealStrat1. В результате получится советник, в котором находятся два разных "ядра", реализующих две разных стратегии, например, одну для работы по тренду, вторую для флета. Можно добавить еще одну — третью, для скальперской торговли в азиатскую сессию. Другими словами, можно упаковать в одного советника несколько других и динамически их переключать. Как это сделать:

  1. Разработать класс с логикой принятия решения, производный от класса CStrategy (как CRealStrat1)
  2. Разработать класс, инициализирующий эту новую стратегию, производный от CInitializeStruct (CInit1)
  3. Подключить файл с новой разработкой к проекту.
  4. Ввести новую переменную во входные параметры, отвечающую за то, какая стратегия станет активной при старте.
  5. Разработать средства, позволяющие выполнять переключения с одной стратегии на другую — торговую панель.

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

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

enum CSTRAT {
   strategy_1 = 1,
   strategy_2 = 2
};

input CSTRAT strt  = strategy_1;

CSTRAT str_curr = -1;

int OnInit()
  {  
   ...
   if (!SwitchStrategy(strt) ) return (INIT_FAILED);
   ...       
   return (INIT_SUCCEEDED);
  }
  
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
      if (id == CHARTEVENT_OBJECT_CLICK && StringCompare(...) == 0 ) {
         SwitchStrategy((CSTRAT)EDlg.GetStratID() );
      } 
  }  

bool SwitchStrategy(CSTRAT sr) {
   if (str_curr == sr) return true;
   CStrategy* s = NULL;
   switch(sr) {
      case strategy_1:
         {
            CInit1 ci;
            if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return false;
            s = new CRealStrat1();
            s.Initialize(GetPointer(ci));  
         }
         break;
      case strategy_2:
         {
            CInit2 ci;
            if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return false;
            s = new CRealStrat2();
            s.Initialize(GetPointer(ci));              
         }   
         break;
   }
   if (CheckPointer(pS1) != POINTER_INVALID) delete pS1;
   pS1 = s;    
   str_curr = sr;
   return true;
}

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

CStrategy* pS1;

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

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

    Заключение

    Мы выполнили проектирование советника, использовав элементы стандартных паттернов проектирования "Observer"  и "Facade". Полное описание этих (и не только этих) паттернов можно найти здесь: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides "Design Patterns. Elements of Reusable Object-Oriented Software", книге, которую я рекомендую прочесть.

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

     # Имя
    Тип
     Описание
    1
    Ea&Modules.zip Архив Архив с файлами советника.


    Прикрепленные файлы |
    EaxModules.zip (7.74 KB)
    Aleksey Mavrin
    Aleksey Mavrin | 10 ноя 2019 в 16:31
    Модульность, взаимозаменяемость, базовые принципы проектирования. Думаю для большинства кто занимается разработкой на более-менее регулярной основе это очевидно и нового ничего статья не несёт. Но для новичков, кто вообще через MQL знакомится с программированием может и откроет глаза)
    Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXIV): Основной торговый класс - автоматическая коррекция ошибочных параметров Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXIV): Основной торговый класс - автоматическая коррекция ошибочных параметров

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

    Конструктор стратегий на основе технических фигур Меррилла Конструктор стратегий на основе технических фигур Меррилла

    В предыдущей статье была рассмотрена модель применения технических фигур Меррилла к различным данным, таким как ценовое значение на графике валютного инструмента и значениям различных индикаторов из стандартного набора терминала MetaTrader 5: ATR, WPR, CCI, RSI и других.Теперь мы попробуем созданить конструктор стратегий на основе идеи использования технических фигур Меррилла.

    Разработка Pivot Mean Oscillator: новый осциллятор на кумулятивном скользящем среднем Разработка Pivot Mean Oscillator: новый осциллятор на кумулятивном скользящем среднем

    В статье описывается осциллятор Pivot Mean Oscillator (PMO), который представляет собой реализацию торговых сигналов на основе индикатора кумулятивного скользящего среднего для платформ MetaTrader. В частности, сначала будет рассмотрено понятие Pivot Mean (PM) — индекс нормализации временных рядов, который вычисляет соотношение между любой точкой данных и скользящей CMA. Затем построим осциллятор PMO как разницу между скользящими средними, построенными по двум сигналам PM. Также в статье будут показаны эксперименты на символе EURUSD, которые проводились для проверки эффективности индикатора.

    Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXV): Обработка ошибок, возвращаемых торговым сервером Библиотека для простого и быстрого создания программ для MetaTrader (Часть XXV): Обработка ошибок, возвращаемых торговым сервером

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