English Русский Español Deutsch 日本語 Português
MVC 设计范式及其可能的应用

MVC 设计范式及其可能的应用

MetaTrader 5交易系统 | 7 六月 2021, 09:28
1 257 0
Andrei Novichkov
Andrei Novichkov

概述

我推测许多开发人员都经历过一个阶段,当项目推进时,变得更加复杂并需求新功能,故此代码开始与某种意大利面条类似。 项目尚未完工,但已经很难记住这个或那个方法于何处被调用了,为什么这个调用要位于这里,以及它是如何操作的。

随着时间的推移,即使代码的作者,理解代码也变得越发困难。 当另一个开发人员尝试理解这段代码时,情况就更糟了。 如果代码作者此时出于某些原因无法联络,则该任务实际上变得无解。 非结构化代码十分难以维护和修改,修改任何代码都比 “Hello, world” 更难。 这也是设计范式应运而生的原因之一。 它们为项目引入了确定的结构,令其更清晰,且直观更易于理解。


MVC 范式及其目的

这种范式出现在很久以前(1978 年),但它的首次阐述出现在更晚的 1988 年。 自那时起,该范式一直在深入发展,提升到新的方式。

在本文中,我们将研究“经典 MVC”,没有任何复杂性或附加功能。 这个思路是将现有代码拆分为三个独立的组件:模型、视图和控制器。 根据 MVC 范式,这三个组件可以独立开发和维护。 每个组件都可由单独的开发团队开发,他们承担创建新版本,并修复错误。 显然,这可令整个项目的管理更加容易。 甚而,它能够帮助其他人理解代码。

我们来看看每个组件。

  1. 视图。 视图负责信息的可视化呈现。 在一般情况下,它向用户发送数据。 向用户呈现相同的数据,可以有不同的方式。 例如,数据可以同时用表格、图形或图表来呈现。 换言之,一个基于 MVC 的应用程序可以包含多种视图。 视图从模型接收数据,无需知道模型内部发生了什么。
  2. 模型。 模型包含数据。 它管理与数据库的连接、发送请求、并在不同的资源间进行通信。 如有必要,它会修改数据、验证数据、存储和删除数据。 模型无需知道视图如何操作,以及存在多少视图,但它拥有必要接口,能响应视图请求数据。 视图不能做任何强迫模型更改其状态的事情。 这部分是由控制器来执行。 在内部,一个模型可由若干其他模型组成,它们或按层次结构、或按等同操作来排列。 除了前面提到的限制之外,模型在这方面没有极限 — 模型的内部结构相对于视图和控制器始终保密。
  3. 控制器。 控制器实现用户和模型之间的通信。 控制器不知道模型如何处理数据,但它可以告诉模型更新内容的时间。 通常,控制器通过其接口操控模型,不必尝试了解其内部发生的事情。

MVC 范式的独立组件之间的关系可直观地表示如下:

尽管如此,运用 MVC 并没有特别严格的规则和限制。 开发者要注意不要在控制器里加入模型操作逻辑,以及操控视图的接口。 控制器本身应该轻量化;您你不应该让它超载。 MVC 规划图也用于其他设计范式,例如观察者和策略。

现在,我们来看看如何在 MQL 中运用 MVC 模板,以及是否需要采用它。


从 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] 的平均值。 源代码在随附的 zip 文档 MVC_primitive_1.zip 中提供。

该指标编写得很糟糕,有经验的开发人员很容易就能看出来。 假设需要改变计算方法:只用 close[i] 来替代 open[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);
}

请注意,几乎所有得时间序列都要传递到新函数当中。 为什么呢? 它其实并非必要,因为只用到了两个时间序列:开盘价和收盘价。 然而,我们预计未来该指标可能会有更多变化和改进,其中可能会用到其余的时间序列。 实际上,我们为将来的潜在版本打下了坚实的基础。

现在我们从 MVC 范式的角度研究当前代码。

  • 视图。 由于该组件向用户呈现数据,因此它应该包含与指标缓冲区相关的代码。 这还应该包括来自 OnInit() 的代码 — 在我们的例子中是整个代码。
  • 模型。 我们的指标只有一个非常简单的单线模型,在其中我们计算开盘价和收盘价之间的平均值。 然后,视图务须我们的干预被更新。 因此,模型组件将只包含 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 (没有为指标定义指标绘图槽)

因此,这两行需保留在包含控制器代码的主文件当中。 指标的源代码位于随附的 zip 文档 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。 我们禁止不受控制地访问指标缓冲区,并实现一种特殊的访问方法,其中加入额外的检查。 作为选项,该方法可在基类中声明为虚拟(virtual)。 控制器文件中也有一些细微的变化,但在此不会论及它们。 显然,我们于此需要另一个构造函数,以便我们可以在其中传递缓冲区初始化参数,例如颜色、样式、等等。

此外,第一个视图对象中的调用看起来并不合适:

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

在现实生活中,它们应该从 CView 类中删除,请记住:

  • 它们可以有很多视图。
  • 这两行代码与视图无关! 它是整个指标的初始化,因此应将它们留在控制器文件之中,位于 OnInit 处理程序里。

该指标的源代码在随附的 zip 文档 MVC_primitive_3.zip 中提供。


因此,主要指标文件 — 包含控制器代码的文件 — 明显变短了。 整体代码现在更加安全,且它已为将来的变更和调试做好了准备。 但其他开发人员对于这一点现在清楚吗? 这是非常值得怀疑的。 在这种情况下,将指标代码保留在组合了控制器、模型和视图的文件中可能更合理。 一开始的方式就是这样的。


这个观点似乎合乎逻辑。 但这仅适用于某个特定指标。 想象一个由一沓文件组成的指标,有一个图形面板,并从网络上请求数据。 在这种情况下,MVC 模型就会非常有用。 MVC 范式会令其他开发者易于理解,允许便捷地检测错误,并为潜在的逻辑修改和其他更改提供基础。 您打算为一个特定的开发部分聘任一位专家吗? 这当然很容易做到。 您是否需要添加另一个初始化方案? 这也可以做到。 上面的结论很明显:项目越复杂,MVC 范式就越有用。

它仅适用于指标吗? 我们来查看智能交易系统的结构,探寻运用 MVC 范式的可能条件。


智能交易系统中的 MVC

我们创建一个非常简单的伪智能交易系统。 如果前一根蜡烛看涨,则应开立多头持仓;如果看跌,则应开立空头持仓。 为简单起见,EA 不会开立真实持仓,而只会模拟进场和出场。 场内只能存在一笔持仓。 在这种情况下,EA 代码(附件 zip 文档中的 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 和其他处理程序与控制器相关。 这同样适用于智能交易系统。 但哪些应归于视图? 它不绘制任何图形对象或图表。 如您所知,视图负责向用户呈现数据。 对于智能交易系统,数据呈现是显示持仓。 因此,视图的所有内容均与持仓相关。 这些包括订单、尾随停止、虚拟止损和止盈、加权均价、等等。

然后模型将包括如何开仓、选择手数、定义止盈和止损的决策逻辑。 资金管理也应该在模型中实现。 开始看时,这像是由若干个子模型组成的一个封闭系统:价格分析、交易量计算、账户状态检查(可能检查其他子模型的状态),以及入场决策结果。

我们根据上述研究,修改伪 EA 的结构。 我们没有手数计算,或操控帐户,所以我们执行该做的步骤 — 把与不同组件相关的函数移至它们的子文件夹,并编辑其中的一些。 这就是 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()

即使在本章节中,我们也能够看到代码变得更短了。 对于第三方开发者来说,它是否变得更清晰了呢? 在此,我们之前研究指标时的假设也适用:

- EA 越复杂,MVC 范式就越有用。

完整的 EA 位于随附的 MVC_EA_primitive.zip 存档之中。 现在我们尝试将 MVC 范式应用于“真实”代码。

为此目的,我们选取一个简单的智能交易系统,它不一定可操作,或精心编写的。 与之对比,EA 应该编写得很糟糕 — 这样我们就可以评估运用范式的效果。

为此目的,我找到了 2013 年创建的 $OrdersInTheMorning EA 的旧草稿。 其策略如下:

  • 在周一的指定时间,EA 在离市价一定距离处放置两笔挂单:一笔买入,一笔卖出。 当其中一笔挂单被触发时,第二笔挂单被删除。 而该笔已开订单于周五晚上平单。 它仅操作指定的货币对列表。

由于旧版 EA 是为 MetaTrader 4 开发的,我不得不重新编制 MetaTrader 5 版本(漫不经心的粗略之作)。 此处是主 EA 函数的原始形式::

#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 非常好 — 它参与视图,基于上述思路。 除了根据范式修改 EA 结构之外,代码本身也需要调整。 注释将与相关修改一起给出。

我们首先将货币对列表移到输入参数。 从 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);
   }
}

代码非常简单。 我会解释几点:

  • 类中的所有成员都是公开的,这极不正确。 这只是作为一个例外完成的,避免多种访问私密成员的方法令代码杂乱无章。
  • 类中只有一个方法,故此我们不一定需要一个类。 但在这种情况下,所有数据都可以全局访问,这种情况应尽量避免。
  • 该类实现了与用户的交互,是控制器的一部分。

我们在主 EA 文件中为已创建的类创建一个对象,并在 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;   
}

如同前一种情况,在主 EA 文件中创建该类的对象,并将其方法调用添加到 OnInit 处理程序之中。

现在我们继续处理视图。 在其中创建一个 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 处理程序。 在处理程序中,检查订单数量的变化:若有变化,则删除相应的未触发挂单。 该处理程序是 EA 中唯一棘手的部分。 在 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();
}


至此,若需修改、添加或修复某些内容,我们可以简单地操控智能交易系统的响应组件。 组件位置很容易判定。 我们可以深入开发 EA,实现新功能,添加模型并扩展视图。 我们甚至可以彻底修改其中某个组件,而不会影响其他两个组件。


在所研究的 MVC 应用程序中,有一个方面在文章开头就曾提到过。 它是关于范式组件彼此之间的交互。 从用户的角度来看,没有问题:我们有一个控制器,在其中我们可以添加一个对话框和一个交易面板。 我们还有输入参数作为控制器的一部分。 但在模型和视图之间应如何交互呢? 在我们的智能交易系统中,它们无法直接彼此交互,而仅能通过 OnTick 处理程序中的控制器进行交互。 此外,视图以类似的方式与控制器通信 — 调用 CInint 对象方法 “directly”。 在这种情况下,组件的交互是通过它们的全局对象进行组织的。 这是因为我们的智能交易系统很简单,我不想令代码复杂化。

然而,尽管代码很简单,但视图调用了控制器 11 次。 如果我们进一步开发 EA,交互次数也许会增加很多倍,从而破坏 MVC 范式的正面效果。 这个问题可通过拒绝全局对象,和依据引用访问组件和方法来解决。 这种交互的一个例子是 MFC 及其文档和视图组件。

从范式的角度来看,范式组件之间的交互没有任何规定方式。 因此,我们不会深入探讨这个话题。


结束语

总之,我们看看如何进一步开发指标或智能交易系统的结构,它们是我们最开始应用 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-为基础的智能交易系统。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/9168

附加的文件 |
Ea_primitive.zip (0.71 KB)
EA_Real.zip (71.7 KB)
形态搜索的暴力强推方式(第四部分):最小功能 形态搜索的暴力强推方式(第四部分):最小功能
本文基于上一篇文章中设定的目标,提出了一个改进的暴力强推版本。 我将尝试尽可能广泛地涵盖这个主题,并以该方法获取的设置来运行智能交易系统。 本文还附有一个新的程序版本。
神经网络变得轻松(第十三部分):批次常规化 神经网络变得轻松(第十三部分):批次常规化
在上一篇文章中,我们开始研究旨在提高神经网络训练品质的方法。 在本文中,我们将继续这个主题,并会研讨另一种方法 — 批次数据常规化。
DoEasy 库中的其他类(第六十七部分):图表对象类 DoEasy 库中的其他类(第六十七部分):图表对象类
在本文中,我将创建图表对象类(单个交易金融产品图表),并改进 MQL5 信号对象的集合类,以便在更新列表时也能为存储在集合中的每个信号对象更新其所有参数。
DoEasy 函数库中的其他类(第六十六部分):MQL5.com 信号集合类 DoEasy 函数库中的其他类(第六十六部分):MQL5.com 信号集合类
在本文中,我将针对 MQL5.com 信号服务创建信号集合类,拥有能够管理信号的函数。 此外,我将改进“市场深度”快照对象类,来显示 DOM 的总买卖量。