请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3364680 新帖:1

针对初学者的 MQL 5 中的自定义指标

外汇工厂发表于:4 月 17 日 16:25回复(1)

简介

深刻理解任何知识学科(无论是数学、音乐还是编程等)的基础是对其基础的学习。如果从很小的时候起就开始相似的学习则再好不过,这样对于基础的理解要容易得多,并且理解具体而全面。

遗憾的是,大部分人是人到中年才开始接触金融和股票市场,所以学习起来并不容易。在本文中,我将帮助大家克服这一理解 MQL5 和为 MetaTrader 5 客户端编写自定义指标的最初障碍。

作为简单示例的 SMA 指标

学习事物最有效且最合理的方式是实际问题的解决方案。既然我们讨论的是自定义指标,我们将从学习简单指标开始,简单指标包含在 MQL5 中展示指标操作的基础方面的一些代码。

作为示例,我们会探讨最有名的技术分析指标 - 简单移动平均线 (SMA)。它的计算简单:

SMA = SUM (CLOSE (i),MAPeriod) / MAPeriod

其中:

  • SUM - 值的总和;
  • CLOSE (i) - 第 i 个柱的收盘价;
  • MAPeriod - 求平均的柱的数量(平均周期)。

以下是不包括任何额外功能的该指标的代码:

//+------------------------------------------------------------------+
//|                                                          SMA.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
#property indicator_type1   DRAW_LINE
#property indicator_color1  Red
  
input int MAPeriod = 13;
input int MAShift = 0; 
  
double ExtLineBuffer[]; 
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                |
//+------------------------------------------------------------------+  
void OnInit()
  {
   SetIndexBuffer(0, ExtLineBuffer, INDICATOR_DATA);
   PlotIndexSetInteger(0, PLOT_SHIFT, MAShift);
   PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, MAPeriod - 1);
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[])
  {
   if (rates_total < MAPeriod - 1)
    return(0);
    
   int first, bar, iii;
   double Sum, SMA;
   
   if (prev_calculated == 0)
    first = MAPeriod - 1 + begin;
   else first = prev_calculated - 1;

   for(bar = first; bar < rates_total; bar++)
    {
     Sum = 0.0;
     for(iii = 0; iii < MAPeriod; iii++)
      Sum += price[bar - iii];
     
     SMA = Sum / MAPeriod;
      
     ExtLineBuffer[bar] = SMA;
    }
     
   return(rates_total);
  }
//+------------------------------------------------------------------+

在 MetaTrader 5 客户端中的运行结果如下:

首先,我们需要考虑两件事情 - 一方面,每个代码串的目的,另一方面,程序代码和客户端的交互。

使用注释

初看指标代码,眼睛按如下所示捕捉对象:

//+------------------------------------------------------------------+
//|                                                          SMA.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                |
//+------------------------------------------------------------------+  
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+

必须要注意到,它们只是注释,和代码没有直接的关系,它们为代码的可读性设计而生,用于显示代码某些部分的特定语义内容。当然,我们可以将注释从代码中去除,而不会对代码的进一步简化有任何的损害,但这样一来代码将在理解上失去简洁性。在我们的示例中,我们使用单行注释,这类注释起始于字符对 "//" 并以换行符作为结束标志。

显而易见,作者可以在注释中写下所有必要内容,以帮助在一段时间后理解该代码。在我们的示例中,注释的第一部分关乎指标名称和作者信息,第二和第三部分用于区分函数OnInit()和OnCalculate()。末尾的最后一行只是关闭程序代码。

SMA 代码的结构

因此,如我们所见,指标的整个代码可分为 3 个部分:

1. 在全局层面编写的未包含在括号中的代码,位于第一和第二段注释之间。
2. OnInit() 函数的说明。

3. OnCalculate() 函数的说明。

必须要注意的是,函数在编程中的意义比在数学中要宽泛得多。例如,在编程语言中,数学函数总是接收一些输入参数并返回计算值,此外,MQL5 中的函数还可以执行一些字符操作、交易操作、文件操作等。

事实上,任何用 MQL5 编写的指标总是具有最小用户编写部分集,该部分的注释是个别的并基于创建指标的功能。

除了这些组分,最小函数集可包含其他 MQL5 函数的说明 - OnDeInit():

//+------------------------------------------------------------------+
//| 自定义指标去初始化函数                                              |
//+------------------------------------------------------------------+  
void OnDeinit(const int reason)
  {

  }

在我们的示例中,这不是必须的,因此未包含在此。

SMA 和 MetaTrader 客户端的交互

配合打开的 SMA.mq5 在 MetaEditor 中按下 "Compile"(编译)键后,现在我们来讨论得到的编译文件 SMA.ex5 的工作。必要要注意的是,含扩展名 .mq5 的文本文件只是文本格式的源代码,必须在编译后才能用于客户端中。

将该指标从 Navigator(导航器)窗口附加至图表后,MetaTrader 将执行指标的第一部分代码。之后,MetaTrader 将调用函数 OnInit() 以执行该函数一次,然后对每个新的订单号(新报价到来后)调用 OnCalculate() 函数并执行该函数的代码。如果 OnDeInit() 出现在指标中,MetaTrader 将在从图表中分离指标或在时间表改变后调用该函数。

经此解释,指标的所有部分的意义和目的一目了然。处于全局层面的代码的第一部分有一些简单运算符,这些运算符会在指标启动后执行一次。除此之外,该部分还包含了对变量的声明,这些变量在指标的所有程序块中“可见”,并且在指标位于图表上时记住变量的值。

执行一次的常量和函数应位于 OnInit() 函数内部,因为将它们放在函数OnCalculate() 的程序块中是不明智的。指标的计算代码可为每个柱计算指标的值,应放置在函数OnCalculate()内。

用于在指标从图表移除后删除图表无用数据(如有)的程序应放置在OnDeInit() 内。例如,必须删除指标创建的图形对象。

经过上述说明的铺垫,我们已准备好详细考察在前文中讨论的指标的代码。

SMA 指标的程序代码

代码行的第一组起始于运算符 #property,该运算符可用于指定指标设置的其他参数。可能程序属性的完整列表可在MQL5 文档中找到。如必要,可以编写指标的额外属性。我们的示例包含 5 行代码,各行代码的目的在注释中指出:

//---- 这个指标会在主窗口中绘图
#property indicator_chart_window
//---- 一个缓冲区用于指标的计算和绘图
#property indicator_buffers 1
//---- 只有一个绘图 
#property indicator_plots   1
//---- 指标绘成线形
#property indicator_type1   DRAW_LINE
//---- 指标线的颜色是红色 
#property indicator_color1  Red 

注意,代码行的末尾没有分号 (";")。原因如下:事实上,在我们的示例中,这些是以另一种方式表示的常量定义。

我们的简单移动平均线仅有 2 个参数,用户可更改这些参数 - 它是指标沿时间轴的平均周期和水平平移(以柱为单位)。由于在更深一层的两行代码行中声明,这两个参数应声明为指标的输入变量:

//---- 指标输入参数
input int MAPeriod = 13; //平均周期
nput int MAShift = 0; //水平转换 (柱数)

请注意,在声明这些输入参数后还有注释,且这些注释将作为输入参数的名称在指标的 "Properties"(属性)窗口中可见:

在我们的示例中,这些名称相比指标的变量名称要更为清楚。因此,这些注释应该是简单的。

最后一行没有括号的代码行是动态数组 ExtLineBuffer[] 的声明。

//---- 动态数组的声明
//将被用于指标缓冲区
double ExtLineBuffer[];  

因为下述的几个原因,它被声明为全局变量。

首先,该数组应转换为指标缓冲区,它在 OnInit() 函数的程序块中实施。其次,指标缓冲区本身将在 OnCalculate() 函数内部使用。再次,该数组将存储指标的值,这些值将用于在图表上绘制曲线。由于该数组声明为全局变量的事实,它对指标的所有程序块均可用,并会一直存储自身的值直至指标从图表分离。

OnInit() 函数的内容仅通过 3 个运算符表示,这些运算符是 MetaTrader 客户端的内置函数。

第一个函数的调用将一维动态数组 ExtLineBuffer[]分配给第零个指标缓冲区。带不同输入参数值的另外两个函数的调用可用于将指标沿价格轴平移,以及指定从编号为 MAPeriod 的柱开始的指标绘图。

void OnInit()
  {
//----+
//---- 把动态数组 ExtLineBuffer 用作指标的第0个缓冲区
   SetIndexBuffer(0,ExtLineBuffer,INDICATOR_DATA);
//---- 根据MAShift设置绘图中水平轴的基础转换
   PlotIndexSetInteger(0,PLOT_SHIFT,MAShift);
//---- 根据MAPeriod设置绘图柱起点
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,MAPeriod);
//----+
  }

PlotIndexSetInteger() 函数的最后调用传递等于 MAPeriod(通过函数 OnCalculate() 的参数 begin)的值到其他指标,如果其应用至我们的指标的值的话。逻辑很简单,最初的 MaPeriod-1 个柱没有什么需要平均,这就是该指标的绘图无用的原因。然而,需要传递该值以平移其他指标的计算原点。

它不是内置函数的完整列表,内置函数用于自定义指标并可放置在指标的该程序块内。请参见 MQL5 文档了解详细信息。

最后,我们来探讨一下 OnCalculate() 函数的代码。该函数和函数 OnInit() 一样没有任何自定义调用,因为这些函数通过 MetaTrader 客户端调用。由于这个原因,函数的输入参数声明为常量。

int OnCalculate(
                const int rates_total,    // 当前订单下的历史中可用的柱数
                const int prev_calculated,// 前一订单计算后的柱数
                const int begin,          // 第一个柱的索引
                const double &price[]     // 用于计算的价格数组
                )

这些输入参数无法更改,参数值由客户端传递进一步用于该函数的代码中。OnCalculate 输入变量的说明请参见 MQL5 文档。函数 OnCalculate() 使用 return(rates_total) 函数将其值返回客户端。客户端在执行 OnCalculate() 后接收当前订单号的该值并将返回值传递至其他参数 prev_calculated。因此,我们始终可以确定柱索引的范围,并仅为上一订单号之后出现的指标的新值执行计算。

必须注意的是,MetaTrader 客户端中柱的排序是从左至右执行,因此最旧的柱(左边)在图表上具有索引 0,接下来的柱具有索引 1,依此类推。缓冲区 ExtLineBuffer[] 的元素具有相同的排序。

指标 OnCalculate 函数内的简单代码结构是通用的,对于许多技术分析指标而言是非常典型的。因此,接下来我们讨论它的细节。OnCalcualte() 函数的逻辑为:

1. 检查计算所需的柱是否存在。
2. 声明局部变量。
3. 获取用于计算的起始柱的索引。
4. 指标计算的主循环。
5. 使用运算符 return() 将 rates_total 的值返回客户端。

我认为第一项是明确的。例如,如果移动平均线的平均周期等于200,但客户端仅有 100 个柱,则没有必要执行计算,因为没有足够的柱用于计算。因此,我们使用运算符 return 返回 0 至客户端。

//---- 检查出现的柱的数目是否足够用于计算
   if(rates_total<MAPeriod-1+begin)
      return(0);

我们的指标可以应用至其他一些指标的数据,这些指标同样可有一些最小数量的柱用于计算。考虑到这一事实,则常量 begin 的使用是必不可少的。请参见文章将指标应用至其他指标了解详细信息。

在该程序块中声明的局部变量仅对 OnCalculate() 函数内的中间计算是必要的。这些变量在函数调用后从电脑的 RAM 释放。

//---- 声明局部变量 
   int first,bar,iii;
   double Sum,SMA;

必须小心主循环的起始索引(变量first)。在函数的第一次调用时(我们可以通过参数 prev_calculated 的值确定),我们必须为所有的柱执行指标值的计算。对于客户端更多的订单号,我们仅需要为出现的新柱执行计算。它通过 3 行代码行实现:

//---- 计算主循环中 第一的起始索引
   if(prev_calculated==0) // 指标的第一个起点
      first=MAPeriod-1+begin; // 所有柱的起始索引
   else first=prev_calculated-1; // 新柱的起始索引

我们已经讨论了指标重新计算主循环运算符的变量更改的范围。

//---- 计算的主循环
   for(bar=first;bar<rates_total;bar++)

主循环中柱的处理按照递增次序 (bar++) 执行,换言之,从左至右,符合自然和正确的方式。在我们的指标中,柱的处理应以其他的方式实施(以相反的顺序)。更好的方法是在指标中使用递增次序。主循环的变量名为“柱”,但许多编程人员更喜欢称之为 "i"。我更倾向于使用前者,因为这样使代码更清楚易读。

在主循环中实施的平均算法很简单。

     {
      Sum=0.0;
       //---- 为做平均循环得到总数
      for(iii=0;iii<MAPeriod;iii++)
         Sum+=price[bar-iii]; // 相当于 Sum = Sum + price[bar - iii]; 
      
      //---- 计算平均数
      SMA=Sum/MAPeriod;

      //---- 把指标缓冲区中的元素值设为我们计算的SMA值
      ExtLineBuffer[bar]=SMA;
     }

在第二个循环中,我们从时间周期较早的柱开始执行价格的累积求和,并使用该平均周期将其划分。作为结果,我们获得 SMA 的最终值。

主循环结束后,OnCalculate 函数从变量 rates_total 返回可用柱的数量。在 OnCalculate() 函数的下一次调用中,该值将由客户端传递至变量 prev_calculated。该值减去 1 后的值将用作主循环的起始索引。

以下是指标的完整源代码,每一代码行都标注有详尽的注释:

//+------------------------------------------------------------------+
//|                                                          SMA.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//---- 指标会在主窗口中绘图
#property indicator_chart_window
//---- 指标的计算和绘图会使用一个缓冲区
#property indicator_buffers 1
//---- 只有一个绘图 
#property indicator_plots   1
//---- 绘图是线形
#property indicator_type1   DRAW_LINE
//---- 指标线颜色是红色 
#property indicator_color1  Red 

//---- 指标输入参数
input int MAPeriod = 13; //平均周期
input int MAShift = 0; //水平转换 (柱数)

//----动态数组定义
//将被用于做指标缓冲区
double ExtLineBuffer[]; 
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                |
//+------------------------------------------------------------------+  
void OnInit()
  {
//----+
//---- 把动态数组 ExtLineBuffer 设为指标的第0个缓冲区
   SetIndexBuffer(0,ExtLineBuffer,INDICATOR_DATA);
//---- 根据MAShift设置绘图和水平轴的转换
   PlotIndexSetInteger(0,PLOT_SHIFT,MAShift);
//---- 根据MAPeriod设置绘图起点
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,MAPeriod);  
//----+
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                  |
//+------------------------------------------------------------------+
int OnCalculate(
                const int rates_total,    // 当前订单下历史中可用的柱数
                const int prev_calculated,// 前一订单计算的柱数
                const int begin,          // 第一个柱的索引
                const double &price[]     // 用于计算的价格数组
                )
  {
//----+   
   //---- 检查出现的柱数是否足够用于计算
   if (rates_total < MAPeriod - 1 + begin)
    return(0);
   
   //---- 局部变量声明 
   int first, bar, iii;
   double Sum, SMA;
   
   //---- 主循环中计算第一个起点的索引
   if(prev_calculated==0) // 检查是否是第一个起点
      first=MAPeriod-1+begin; // 所有柱的起点
   else first=prev_calculated-1; // 新柱的起点

   //----计算的主循环
   for(bar = first; bar < rates_total; bar++)
    {    
      Sum=0.0;
      //---- 为计算平均做循环累加
      for(iii=0;iii<MAPeriod;iii++)
         Sum+=price[bar-iii]; // 相当于 Sum = Sum + price[bar - iii];
         
      //---- 计算平均值
      SMA=Sum/MAPeriod;

      //---- 把指标缓冲区元素的值设为我们计算的SMA值
      ExtLineBuffer[bar]=SMA;
    }
//----+     
   return(rates_total);
  }
//+------------------------------------------------------------------+

这种形式的代码要更易于理解和阅读。

还有另一个特点可用于简化代码的理解。您可以使用空格和空行使代码更加清晰。

总结

这就是有关自定义指标的代码和 MetaTrader 客户端交互的全部内容。当然,该主题相比我们已讨论的部分要宽泛得多,本文的目标是帮着初学者理解基本原理,因此相关细节请参见文档。

全部回复

0/140

达人推荐

量化课程

    移动端课程