Шаблон проектирования MVC и возможность его использования

24 марта 2021, 15:20
Andrei Novichkov
30
554

Введение

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

А самое печальное, что про прошествии некоторого времени в исходниках разобраться будет совершенно невозможно даже автору кода. Что уж говорить о том случае, когда такую мешанину поручают разобрать другому. Задача превращается в практически неразрешимую в том случае, если к этому времени автор кода оказывается недоступен по каким либо причинам. Бесструктурный код очень сложно сопровождать и вносить изменения в том случае, если программа представляет собой нечто более сложное, чем просто "Hello, world". В этом одна из причин появления шаблонов проектирования. Они привносят в проект определенную структуру, делают его более понятным, более наглядным.


Шаблон MVC и зачем он нужен

Данный шаблон появился довольно давно (в 1978 году), но описан был впервые гораздо позже, в 1988 году, когда было опубликовано его окончательное описание. С тех пор шаблон видоизменяется, усложняется, порождая новые подходы.

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

Рассмотрим, что представляет собой каждый компонент по отдельности.

  1. Представление (View). Представление отвечает за визуальное отображение. В более общем случае оно отправляет данные пользователю. Заметим, что в действительности способов представления данных пользователю может быть несколько. Например, данные могут быть представлены таблицей, графиком или диаграммой одновременно. Другими словами, в рамках одного приложения, построенного по схеме MVC, может быть несколько Представлений. Представления получают данные от Модели, не имея никакого понятия о том, что происходит внутри Модели.
  2. Модель (Model). Модель содержит данные. Она устанавливает связь с базами данных, посылает запросы в сеть, в другие источники. Она модифицирует данные, проверяет их, хранит и удаляет. Модель ничего не знает о том, как работает Представление и сколько вообще Представлений имеется в наличии, но она имеет необходимые интерфейсы, по которым Представления могут запрашивать данные. Ничего большего Представления делать не могут — они не могут заставить Модель изменить свое состояние. Этим занимается Контроллер. Внутренне, Модель может состоять из нескольких других Моделей, выстроенных в иерархию или работающих равноправно. В этом отношении на Модель не накладывается ограничений, кроме уже упомянутого — Модель держит свое внутреннее устройство в секрете от Представления и Контроллера.
  3. Контроллер (Controller). Контроллер осуществляет связь между пользователем и Моделью.  Контроллер не знает о том, что Модель делает с данными, но он может сказать Модели, что пора обновить содержимое. В общем случае, Контроллер, по своему интерфейсу, работает с Моделью, не пытаясь понять, что происходит у нее внутри.

Визуально, связь между отдельными компонентами шаблона MVC выглядит примерно так:

И все-таки особо строгих правил и ограничений на применение MVC не имеется. Разработчику следует быть внимательным, чтобы не поместить часть логики (а то и всю логику) работы с данными Модели в Контроллер и не залезать в Представление. Кроме того, сам Контроллер следует делать более "тонким", не перегружая его. Заметим еще, что схема MVC используется и для других паттернов проектирования, "Наблюдатель" и "Стратегия", например.

А теперь попытаемся понять, каким образом использовать шаблон MVC в MQL, а самое главное — нужно ли это вообще?


Самый простой индикатор с точки зрения MVC

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

.......
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot Label1
#property indicator_label1  "Label1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDarkSlateBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  2
//--- indicator buffers
double         lb[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
   SetIndexBuffer(0, lb, INDICATOR_DATA);
   ArraySetAsSeries(lb, true);
   IndicatorSetString(INDICATOR_SHORTNAME, "Primitive1");
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(rates_total <= 4)
      return 0;

   ArraySetAsSeries(close, true);
   ArraySetAsSeries(open, true);

   int limit = rates_total - prev_calculated;

   if(limit == 0)
     {
     }
   else
      if(limit == 1)
        {

         lb[1] = (open[1] + close[1]) / 2;
         return(rates_total);

        }
      else
         if(limit > 1)
           {

            ArrayInitialize(lb, EMPTY_VALUE);

            limit = rates_total - 4;
            for(int i = limit; i >= 1 && !IsStopped(); i--)
              {
               lb[i] = (open[i] + close[i]) / 2;
              }
            return(rates_total);

           }

   lb[0] = (open[0] + close[0]) / 2;

   return(rates_total);
  }
//+------------------------------------------------------------------+

Вычисления сводятся к расчету среднего значения open[i] + close[i]. Исходный код индикатора выложен в прилагаемом архиве MVC_primitive_1.zip.

Индикатор написан очень плохо и опытный взгляд это сразу заметит. Допустим, возникла необходимость изменить способ расчета и вместо open[i] + close[i] использовать просто  close[i]. Нетрудно заметить, что даже в таком примитивном индикаторе есть три места, где следует выполнить изменения. А если такие изменения будут продолжены, да еще и усложнены? Выход очевиден — вынести вычисления в отдельную функцию. Это даст возможность при необходимости изменить логику вносить исправления только в неё.

Теперь обработчик вместе с новой функцией выглядят так:

double Prepare(const datetime &t[], const double &o[], const double &h[], const double &l[], const double &c[], int shift) {
   
   ArraySetAsSeries(c, true);
   ArraySetAsSeries(o, true);
   
   return (o[shift] + c[shift]) / 2;
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[]) {
                
   if(rates_total <= 4) return 0;
   
   int limit = rates_total - prev_calculated;
   
   if (limit == 0)        {
   } else if (limit == 1) {
   
      lb[1] = Prepare(time, open, high, low, close, 1);      
      return(rates_total);

   } else if (limit > 1)  {
   
      ArrayInitialize(lb, EMPTY_VALUE);
      
      limit = rates_total - 4;
      for(int i = limit; i >= 1 && !IsStopped(); i--) {
         lb[i] = Prepare(time, open, high, low, close, i);
      }
      return(rates_total);
      
   }
   lb[0] = Prepare(time, open, high, low, close, 0);

   return(rates_total);
}

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

А теперь посмотрим на получившийся код с точки зрения шаблона MVC.

  • Представление. Т. к. это, как мы уже сказали, компонент, который представляет данные пользователю, то очевидно, что в него должен войти код, связанный с индикаторными буферами. Сюда же стоит отнести и код из OnInit(), в нашем случае весь целиком.
  • Модель. В нашем индикаторе очень простая модель, состоящая из одной строки. Мы вычисляем среднюю между open и close. Затем Представление обновляется уже без нашего участия. Следовательно, в компонент Модель войдет только функция Prepare, которая нами написана сразу с учетом будущего развития.
  • Контроллер. Компонент отвечает за связь между двумя другими компонентами и взаимодействие с пользователем. Исходя из этого, мы оставляем ему обработчики событий, входные параметры индикатора и на этом все. Он же вызывает функцию Prepare, служащую входом в Модель. Такой вызов будет заставлять Модель менять свое состояние в связи с поступлением новых новых тиков и при изменении ценовой истории по символу.

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

//+------------------------------------------------------------------+
//|                                              MVC_primitive_2.mq5 |
//|                                Copyright 2021, Andrei Novichkov. |
//|                    https://www.mql5.com/en/users/andreifx60/news |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, Andrei Novichkov."
#property link      "https://www.mql5.com/en/users/andreifx60/news"

#property version   "1.00"

#property indicator_chart_window

#property indicator_buffers 1
#property indicator_plots   1

#include "View\MVC_View.mqh"
#include "Model\MVC_Model.mqh"


//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit() {

   return Initialize();
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[]) {
                
   if(rates_total <= 4) return 0;
   
   int limit = rates_total - prev_calculated;
   
   if (limit == 0)        {
   } else if (limit == 1) {
   
      lb[1] = Prepare(time, open, high, low, close, 1);      
      return(rates_total);

   } else if (limit > 1)  {
   
      ArrayInitialize(lb, EMPTY_VALUE);
      
      limit = rates_total - 4;
      for(int i = limit; i >= 1 && !IsStopped(); i--) {
         lb[i] = Prepare(time, open, high, low, close, i);
      }
      return(rates_total);
      
   }
   lb[0] = Prepare(time, open, high, low, close, 0);

   return(rates_total);
}
//+------------------------------------------------------------------+

Несколько слов касательно свойств индикатора:

#property indicator_buffers 1
#property indicator_plots   1

Эти две строки можно убрать в Представление (в файл MVC_View.mqh). Однако, после этого мы будем получать замечание компилятора:

no indicator plot defined for indicator

Поэтому оставляем эти две строчки в основном файле с кодом Контроллера. Исходный код этого индикатора находится в прилагаемом архиве MVC_primitive_2.zip.

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

class CView 
  {
   public:
      void CView();
      void ResetBuffers();
      int  Initialize();
      void SetData(double value, int shift = 0);
      
   private:
      double _lb[];
      int    _Width;
      string _Name;
      string _Label;    
  
  };// class CView

void CView::CView() 
  {
      _Width = 2;
      _Name  = "Primitive" ;
      _Label = "Label1"; 
  }// void CView::CView()

void CView::ResetBuffers()
  {
   ArrayInitialize(_lb, EMPTY_VALUE);
  }

int CView::Initialize() 
  {
      SetIndexBuffer     (0,   _lb, INDICATOR_DATA);
      ArraySetAsSeries   (_lb, true);
   
      IndicatorSetString (INDICATOR_SHORTNAME, _Name);
      IndicatorSetInteger(INDICATOR_DIGITS,    _Digits);
   
      PlotIndexSetString (0, PLOT_LABEL,      _Label);
      PlotIndexSetInteger(0, PLOT_DRAW_TYPE,  DRAW_LINE);
      PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrDarkSlateBlue);
      PlotIndexSetInteger(0, PLOT_LINE_STYLE, STYLE_SOLID);
      PlotIndexSetInteger(0, PLOT_LINE_WIDTH, _Width);   
      
      return(INIT_SUCCEEDED);   
  }

void CView::SetData(double value,int shift) 
  {   
   _lb[shift] = value;
  }

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

Еще довольно сомнительными представляются вызовы, которые производятся в объекте первого же Представления:

      IndicatorSetString (INDICATOR_SHORTNAME, _Name);
      IndicatorSetInteger(INDICATOR_DIGITS, _Digits);

Ясно, что в реальных условиях их необходимо будет убрать из класса CView, помня о том, что:

  • Представлений может быть много.
  • Эти две строки кода не относятся к Представлению! Это инициализация индикатора в целом, поэтому оставляем их в файле Контроллера, в обработчике OnInit.

Исходный код индикатора выложен в прилагаемом архиве MVC_primitive_3.zip.


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


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

Но, возможно, все эти рассуждения верны только для индикаторов? Посмотрим на структуру советников и можно ли применить шаблон MVC там.


MVC в советниках

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

datetime dtNow;

int iBuy, iSell;

int OnInit() 
  {
   iBuy  = iSell = 0;
   
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason) 
  {

  }

void OnTick() 
  {
      if (IsNewCandle() ) 
        {
         double o = iOpen(NULL,PERIOD_CURRENT,1); 
         double c = iClose(NULL,PERIOD_CURRENT,1); 
         if (c < o) 
           { // Enter Sell
            if (GetSell() == 1) return;
            if (GetBuy()  == 1) CloseBuy();
            EnterSell();
           }
         else 
           {      // Enter Buy
            if (GetBuy()  == 1) return;
            if (GetSell() == 1) CloseSell();
            EnterBuy();
           }           
        }// if (IsNewCandle() )   
  }// void OnTick()

bool IsNewCandle() 
  {
   datetime d = iTime(NULL, PERIOD_CURRENT, 0);
   if (dtNow == -1 || dtNow != d) 
     {
      dtNow = d;
      return true;
     }  
   return false;
  }// bool IsNewCandle()

void CloseBuy()  {iBuy = 0;}

void CloseSell() {iSell = 0;}

void EnterBuy()  {iBuy = 1;}

void EnterSell() {iSell = 1;}

int GetBuy()     {return iBuy;}

int GetSell()    {return iSell;}

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

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

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

void OnTick() 
  {
      if (IsNewCandle() ) 
        {
         double o = iOpen(NULL,PERIOD_CURRENT,1); 
         double c = iClose(NULL,PERIOD_CURRENT,1); 
         if (MaySell(o, c) ) EnterSell();
         if (MayBuy(o, c)  ) EnterBuy();
        }// if (IsNewCandle() )   
  }// void OnTick()

Даже на этом незначительном участке заметно, что код стал, как минимум, короче. Но стал ли он очевиднее стороннему разработчику? И здесь будут справедливы те соображения, которые были выдвинуты для примера с индикатором:

- Чем сложнее и объёмнее советник, тем более полезным и оправданным будет применение MVC.

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

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

Для опытов я нашел старый черновик выпуска 2013 года с набросками советника $OrdersInTheMorning, стратегия которого состояла в следующем:

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

Поскольку советник разрабатывался для MetaTrader 4, его пришлось переделать для работы на MetaTrader 5, что было сделано весьма небрежно. Приведем основные функции советника в первоначальном виде:

#property copyright "Copyright 2013, MetaQuotes Software Corp."
#property link      "http://www.metaquotes.net"

#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
input double delta = 200;
input double volumes = 0.03; 
input double sTopLossKoeff = 1;
input double tAkeProfitKoeff = 2; 
input int iTHour = 0; 
input bool bHLprocess = true;
input bool oNlyMondeyOrders = false; 
input string sTimeToCloseOrders = "22:00"; 
input string sTimeToOpenOrders  = "05:05"; 
input double iTimeIntervalForWork = 0.5;
input int iSlippage = 15; 
input int iTradeCount = 3; 
input int iTimeOut = 2000;

int dg;
bool bflag;

string smb[] = {"AUDJPY","CADJPY","EURJPY","NZDJPY","GBPJPY","CHFJPY"};

int init ()
{
   if ( (iTimeIntervalForWork < 0) || (iTimeIntervalForWork > 24) )
   {
      Alert ("... ",iTimeIntervalForWork);
   }
   return (0);
}

void OnTick()
{
   if ((oNlyMondeyOrders == true) && (DayOfWeek() != 1) ) 
   {
   }
   else
   {
         int count=ArraySize(smb);
         bool br = true;
         for (int i=0; i<count;i++)
         {
            if (!WeekOrderParam(smb[i], PERIOD_H4, delta*SymbolInfoDouble(smb[i],SYMBOL_POINT) ) )
               br = false;
         }
         if (!br)
            Alert("...");
         bflag = true; 
    }//end if if ((oNlyMondeyOrders == true) && (DayOfWeek() != 1) )  else...
    
   if ((oNlyMondeyOrders == true) && (DayOfWeek() != 5) ) 
   {
   }
   else
   {
         if (OrdersTotal() != 0)
            Alert ("...");      
   }//end if ((oNlyMondeyOrders == true) && (DayOfWeek() != 5) )  else...
}
  
  bool WeekOrderParam(string symbol,int tf, double dlt)
  {
   int j = -1;
   datetime mtime = 0;
   int k = 3;
   Alert(symbol);
   if (iTHour >= 0)
   {
      if (oNlyMondeyOrders == true)
      {
         for (int i = 0; i < k; i++)
         {
            mtime = iTime(symbol,0,i);
            if (TimeDayOfWeek(mtime) == 1)
            {
               if (TimeHour(mtime) == iTHour)
               {
                  j = i;
                  break;
               }
            }
         }
      }
      else
      {
         for (int i = 0; i < k; i++)
         {
            mtime = iTime(symbol,0,i);
            if (TimeHour(mtime) == iTHour)
            {
               j = i;
               break;
            }
         }   
      }
      if (j == -1) 
      {
         Print("tf?");
         return (false);
      }
   }//end if (iTHour >= 0)
   else 
      j = 0;
   Alert(j);
   double bsp,ssp;
   if (bHLprocess)
   {
      bsp = NormalizeDouble(iHigh(symbol,0,j) + dlt, dg); 
      ssp = NormalizeDouble(iLow(symbol,0,j) - dlt, dg); 
   }
   else
   {
      bsp = NormalizeDouble(MathMax(iOpen(symbol,0,j),iClose(symbol,0,j)) + dlt, dg); 
      ssp = NormalizeDouble(MathMin(iOpen(symbol,0,j),iClose(symbol,0,j)) - dlt, dg);  
   }
   double slsize = NormalizeDouble(sTopLossKoeff * (bsp - ssp), dg); 
   double tpb = NormalizeDouble(bsp + tAkeProfitKoeff*slsize, dg); 
   double tps = NormalizeDouble(ssp - tAkeProfitKoeff*slsize, dg);
   datetime expr = 0;
   return (mOrderSend(symbol,ORDER_TYPE_BUY_STOP,volumes,bsp,iSlippage,ssp,tpb,NULL,0,expr,CLR_NONE) && mOrderSend(symbol,ORDER_TYPE_SELL_STOP,volumes,ssp,iSlippage,bsp,tps,NULL,0,expr,CLR_NONE) );
  }
  
 int mOrderSend( string symbol, int cmd, double volume, double price, int slippage, double stoploss, double takeprofit, string comment = "", int magic=0, datetime expiration=0, color arrow_color=CLR_NONE) 
 {
   int ticket = -1;
      for (int i = 0; i < iTradeCount; i++)
      {
//         ticket=OrderSend(symbol,cmd,volume,price,slippage,stoploss,takeprofit,comment,magic,expiration,arrow_color);
         if(ticket<0)
            Print(symbol,": ",GetNameOP(cmd), GetLastError() ,iTimeOut);
         else
            break;
      }
   return (ticket);
 }  
 

Итак, имеется блок инициализации, обработчик OnTick и вспомогательные функции. Обработчики, как и раньше, оставим в Контроллере, скорректировав устаревший вызов init. Теперь обратим внимание на OnTick . Внутри обработчика некоторые проверки и цикл, в котором вызывается вспомогательная функция WeekOrderParam, концентрирующая в себе принятие решений на вход в рынок и открытие позиций. Такой подход абсолютно неверен, что и видно при первом же взгляде на эту функцию — она длинная, имеет многократно вложенные условия и циклы. Эту функцию придется делить, как минимум, на две части. Последняя функция mOrderSend особых вопросов не вызывает и относится к Представлению, на основании высказанных ранее соображений. Помимо изменения структуры советника в соответствии с шаблоном, придется корректировать и сам код. Будем делать краткие комментарии по ходу работы.

Сразу перенесем перечень валютных пар во входные параметры. Удалим из обработчика OnInit мусор. Создадим файл EA_Init.mqh, куда вынесем все подробности инициализации и подключим его к основному. В новом файле создадим класс и всю инициализацию выполним в нем:

class CInit {
public:
   void CInit(){}
   void Initialize(string pair);
   string names[];
   double points[];     
   int iCount;
};

void CInit::Initialize(string pair) {
   
   iCount = StringSplit(pair, StringGetCharacter(",", 0), names);
   ArrayResize(points, iCount);
   for (int i = 0; i < iCount; i++) {
      points[i] = SymbolInfoDouble(names[i], SYMBOL_POINT);
   }
}

Код предельно простой и вопросов вызывать не должен. Все же отметим несколько моментов:

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

Создаем объект типа созданного класса в основном файле советника и вызываем его метод инициализации в обработчике OnInit.

Теперь займемся Моделью. Удалим из обработчика OnTick все содержимое. Создадим папку Model и в ней файл Model.mqh. Создадим в новом файле класс CModel и в нем два метода проверки условий входа в рынок и выхода из рынка. Кроме того, в этом же классе сохраним флаг, сигнализирующий о том, что позиции открыты или закрыты.  Обратите внимание, что если бы не было необходимости в хранении этого флага, то существование всего класса было бы сомнительным. Хватило бы всего пары функций. Заметим, что в реальных условиях пришлось бы дополнительно выполнять определенные проверки. Объем, наличие средств и прочее. Все это следует отнести к Модели, как было замечено и ранее.  А пока файл, содержащий Модель, выглядит так:

class CModel {
public:
         void CModel(): bFlag(false) {}
         bool TimeToOpen();
         bool TimeToClose();
private:
   bool bFlag;   
};

bool CModel::TimeToOpen() {

   if (bFlag) return false;

   MqlDateTime tm;
   TimeCurrent(tm);
   if (tm.day_of_week != 1) return false;
   if (tm.hour < iHourOpen) return false;
   
   bFlag = true;

   return true;   
}

bool CModel::TimeToClose() {

   if (!bFlag) return false;
   
   MqlDateTime tm;
   TimeCurrent(tm);
   if (tm.day_of_week != 5)  return false;
   if (tm.hour < iHourClose) return false;
   
   bFlag = false;

   return true;   
}

Как и в предыдущем случае, создадим объект типа этого класса в основном файле советника и добавим вызовы его методов в обработчике OnTick.

Теперь займемся Представлением. Создадим папку View и в ней файл View.mqh. Здесь, как уже было сказано выше, будут размещаться средства для открытия / закрытия ордеров и позиций. Здесь же были бы размещены компоненты управления виртуальными уровнями, тралом и различными графическими элементами. В данном случае предпочтение отдано максимальной доступности и простоте кода, поэтому, в качестве разнообразия, попытаемся выполнить компонент Представление без использования классов. Всего в компоненте Представление пока будет три функции. Одна для входа в рынок, вторая для закрытия всех позиций и третья для закрытия ордеров. Обращает внимание, что в каждой из трех функций используется обьект типа CTrade, который всякий раз должен быть создан, что нерационально:

void Enter() {
   
   CTrade trade;
   
   trade.SetExpertMagicNumber(Magic);
   trade.SetMarginMode();
   trade.SetDeviationInPoints(iSlippage);      
   
   double dEnterBuy, dEnterSell;
   double dTpBuy,    dTpSell;
   double dSlBuy,    dSlSell;
   double dSlSize;
   
   for (int i = 0; i < init.iCount; i++) {
      dEnterBuy  = NormalizeDouble(iHigh(init.names[i],0,1) + delta * init.points[i], _Digits);  
      dEnterSell = NormalizeDouble(iLow(init.names[i],0,1)  - delta * init.points[i], _Digits);  
      dSlSell    = dEnterBuy; 
      dSlBuy     = dEnterSell;
      dSlSize    = (dEnterBuy - dEnterSell) * tAkeProfitKoeff;
      dTpBuy     = NormalizeDouble(dEnterBuy + dSlSize, _Digits);
      dTpSell    = NormalizeDouble(dEnterSell - dSlSize, _Digits);
      
      trade.SetTypeFillingBySymbol(init.names[i]);
      
      trade.BuyStop(volumes,  dEnterBuy,  init.names[i], dSlBuy,  dTpBuy);
      trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell);
   }
}

void ClosePositions() {

   CTrade trade;
   
   for (int i = PositionsTotal() - 1; i >= 0; i--) {  
      trade.PositionClose(PositionGetTicket(i) );
   }   
}

void CloseOrder(string pair) {

   CTrade trade;
   
   ulong ticket;
   for (int i = OrdersTotal() - 1; i >= 0; i--) {
      ticket = OrderGetTicket(i);
      if (StringCompare(OrderGetString(ORDER_SYMBOL), pair) == 0) {
         trade.OrderDelete(ticket);
         break;
      }
   }
}

Поэтому изменим код, создав класс CView. Переместим уже созданные функции в новый класс и создадим еще один метод инициализации компонента для закрытого поля типа CTrade. Как и в прочих случаях создадим объект типа созданного класса в основном файле и добавим вызов его метода инициализации в обработчик OnInit.

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

#include <Trade\Trade.mqh>

class CView {

public:
   void CView() {}
   void Initialize();  
   void Enter();
   void ClosePositions();
   void CloseAllOrder();
   void OnTrade();
private:
   void InitTicketArray() {
      ArrayInitialize(bTicket, 0);
      ArrayInitialize(sTicket, 0);
      iOrders = 0;
   }
   CTrade trade; 
   int    iOrders;  
   ulong  bTicket[], sTicket[];

};

void CView::OnTrade() {

   if (OrdersTotal() == iOrders) return;
   
   for (int i = 0; i < init.iCount; i++) {
      if (bTicket[i] != 0 && !OrderSelect(bTicket[i]) ) {
         bTicket[i] = 0; iOrders--;
         if (sTicket[i] != 0) {
            trade.OrderDelete(sTicket[i]);
            sTicket[i] = 0; iOrders--;
         }
         continue;
      }
      
      if (sTicket[i] != 0 && !OrderSelect(sTicket[i]) ) {
         sTicket[i] = 0; iOrders--;
         if (bTicket[i] != 0) {
            trade.OrderDelete(bTicket[i]);
            bTicket[i] = 0; iOrders--;
         }
      }      
   }
}

void CView::Initialize() {

   trade.SetExpertMagicNumber(Magic);
   trade.SetMarginMode();
   trade.SetDeviationInPoints(iSlippage);  
   
   ArrayResize(bTicket, init.iCount);
   ArrayResize(sTicket, init.iCount);
   
   InitTicketArray();
}

void CView::Enter() {
   
   double dEnterBuy, dEnterSell;
   double dTpBuy,    dTpSell;
   double dSlBuy,    dSlSell;
   double dSlSize;
   
   for (int i = 0; i < init.iCount; i++) {
      dEnterBuy  = NormalizeDouble(iHigh(init.names[i],0,1) + delta * init.points[i], _Digits);  
      dEnterSell = NormalizeDouble(iLow(init.names[i],0,1)  - delta * init.points[i], _Digits);  
      dSlSell    = dEnterBuy; 
      dSlBuy     = dEnterSell;
      dSlSize    = (dEnterBuy - dEnterSell) * tAkeProfitKoeff;
      dTpBuy     = NormalizeDouble(dEnterBuy + dSlSize, _Digits);
      dTpSell    = NormalizeDouble(dEnterSell - dSlSize, _Digits);
      
      trade.SetTypeFillingBySymbol(init.names[i]);
      
      trade.BuyStop(volumes,  dEnterBuy,  init.names[i], dSlBuy,  dTpBuy);
      bTicket[i] = trade.ResultOrder();
      
      trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell);
      sTicket[i] = trade.ResultOrder();
      
      iOrders +=2;
   }
}

void CView::ClosePositions() {
   
   for (int i = PositionsTotal() - 1; i >= 0; i--) {  
      trade.PositionClose(PositionGetTicket(i) );
   }   
   
   InitTicketArray();   
}

void CView::CloseAllOrder() {
   
   for (int i = OrdersTotal() - 1; i >= 0; i--) {
      trade.OrderDelete(OrderGetTicket(i));
   }
}

Обращает на себя внимание тот факт, что в итоге первоначальный код был полностью переписан. Стал ли он лучше? Несомненно! Итог всей работы находится в прилагаемом архиве EA_Real.zip. Теперь основной файл советника (Контроллер) выглядит так:

input string smb             = "AUDJPY, CADJPY, EURJPY, NZDJPY, GBPJPY, CHFJPY";
input double delta           = 200;
input double volumes         = 0.03; 
input double tAkeProfitKoeff = 2; 
input int    iHourOpen       = 5; 
input int    iHourClose      = 22;
input int    iSlippage       = 15; 
input int    Magic           = 12345;

#include "EA_Init.mqh"
#include "View\View.mqh"
#include "Model\Model.mqh"

CInit  init;
CModel model;
CView  view;
 

int OnInit()
{
   init.Initialize(smb);
   view.Initialize();
  
   return INIT_SUCCEEDED;
}

void OnTick() {
   if (model.TimeToOpen() ) {
      view.Enter();
      return;
   }
   if (model.TimeToClose() ) {
      view.CloseAllOrder();
      view.ClosePositions();
   }
}

void OnTrade() {
   view.OnTrade();
}

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


И последнее

В рассмотренном применении MVC существует один аспект, который вскользь был затронут в начале статьи. Речь идет о взаимодействии компонентов шаблона между собой. С точки зрения пользователя проблемы не существует: есть Контроллер, ему можно добавить диалоговое окно, торговую панель. Есть входные параметры, как часть Контроллера. Но как должны взаимодействовать Модель и Представление? В нашем советнике ответ конкретно на этот вопрос очень простой: никак. Они не взаимодействуют непосредственно, но только через Контроллер, в обработчике OnTick. Кроме того, Представление общается с Контроллером схожим образом — вызывая методы объекта типа CInit "напрямую". В данном случае взаимодействие компонентов организовано через их глобальные объекты, которые видят друг друга. Это допустимо и оправдано простотой самого советника и нашего стремления не перегружать код.

Однако в Представлении имеется целых одиннадцать обращений к Контроллеру, несмотря на простоту кода. А в случае дальнейшего развития количество подобных перекрестных связей увеличится многократно, сведя к нулю все преимущества от применения шаблона MVC. Решение этой проблемы в отказе от доступа к глобальным объектам, в предоставлении ссылок на компоненты и методов для доступа к ним. Пример взаимодействия такого рода — MFC и его компоненты Document и View.

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


Заключение

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


Программы, используемые в статье:
 # Имя
Тип
 Описание
1 MVC_primitive_1.zip Архив
Первый и самый плохой вариант индикатора.
2
MVC_primitive_2.zip
Архив
Второй вариант индикатора с разделением на компоненты.
3 MVC_primitive_3.zip Архив Третий вариант индикатора с объектами.
4 EA_primitive.zip Архив
Псевдосоветник
5 MVC_EA_primitive.zip Архив Псевдосоветник по правилам MVC.
 6 EA_Real.zip
 Архив Советник по правилам MVC.

Прикрепленные файлы |
Ea_primitive.zip (0.71 KB)
EA_Real.zip (71.7 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (30)
Andriy Konovalov
Andriy Konovalov | 31 мар 2021 в 19:50

@Andrei Novichkov, понятно, спасибо.

Ещё вопросик: насколько правильно входные input-параметры определять только в Контролере? Не правильнее ли такие входные параметры как iSlippage и Magic определять в Представлении (ведь Контролеру они не нужны)? Тогда после включения файла с Представлением в файл с Контролером эти параметры появятся одной группой во входных настройках советника .

Andrei Novichkov
Andrei Novichkov | 31 мар 2021 в 20:35
Зачем вместо логически законченной сущности плодить две. Или три. Или четыре. Правильно сделать одну и продумать контролируемый способ доступа  для Модели и Представления.
Andriy Konovalov
Andriy Konovalov | 31 мар 2021 в 22:03
Andrei Novichkov:
Зачем вместо логически законченной сущности плодить две. Или три. Или четыре. Правильно сделать одну и продумать контролируемый способ доступа  для Модели и Представления.

Не уверен, что Вы меня поняли. Я не предлагаю плодить новые сущности - нет. Как было три компонента так и останется.

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

Andrei Novichkov
Andrei Novichkov | 31 мар 2021 в 22:14
Во входных параметрах может быть много параметров, среди них и Магик. Разбросать  эти параметры по разным компонентам? По моему, это далеко не лучшее решение.  Но Вы же можете попробовать свою идею. Посмотреть, как это будет выглядеть.
Andriy Konovalov
Andriy Konovalov | 31 мар 2021 в 22:39
Хорошо, спасибо за статью и ответы на вопросы.
Прочие классы в библиотеке DoEasy (Часть 68): Класс объекта-окна графика и классы объектов-индикаторов в окне графика Прочие классы в библиотеке DoEasy (Часть 68): Класс объекта-окна графика и классы объектов-индикаторов в окне графика
В статье продолжим разрабатывать класс объекта-чарта. Добавим к нему список объектов-окон графика, в которых в свою очередь будут доступны списки индикаторов, размещённых в них.
Нейросети — это просто (Часть 13): Пакетная нормализация (Batch Normalization) Нейросети — это просто (Часть 13): Пакетная нормализация (Batch Normalization)
В предыдущей статье мы начали рассматривать методы повышения качества обучения нейронной сети. В данной статье предлагаю продолжить эту тему и рассмотреть такой поход, как пакетная нормализация данных.
Комбинационный скальпинг: сделки из прошлого или повышение результативности будущих сделок Комбинационный скальпинг: сделки из прошлого или повышение результативности будущих сделок
На рассмотрение предлагается описание технологии повышения результативности любой автоматизированной торговой системы. В статье кратко раскрывается идея, базовые основы, возможности и недостатки метода.
Прочие классы в библиотеке DoEasy (Часть 67): Класс объекта-чарта Прочие классы в библиотеке DoEasy (Часть 67): Класс объекта-чарта
В статье создадим класс объекта-чарта (одного графика торгового инструмента) и доработаем класс-коллекцию объектов mql5-сигнал так, чтобы каждый объект-сигнал, хранящийся в коллекции при обновлении списка также обновлял все свои параметры.