下载MetaTrader 5

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

13 九月 2013, 07:28
Nikolay Kositsin
2
1 743

简介

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

遗憾的是,大部分人是人到中年才开始接触金融和股票市场,所以学习起来并不容易。在本文中,我将帮助大家克服这一理解 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 客户端交互的全部内容。当然,该主题相比我们已讨论的部分要宽泛得多,本文的目标是帮着初学者理解基本原理,因此相关细节请参见文档。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/37

附加的文件 |
sma.mq5 (1.74 KB)
sma_.mq5 (2.73 KB)
最近评论 | 前往讨论 (2)
okwh
okwh | 17 9月 2013 在 03:27

计算机翻译的?凑合吧。

和MQL4相比,指标数组下标从过去计算,是固定的,不是动态的.  不知道连续运行一周、一月时会怎么样?数据都在内存?? 若都在内存,岂不是会越运行越慢? 

 

但是,在  https://www.mql5.com/zh/articles/31中又说:

  1. “将元素的索引设置为序列,这和 MQL4 中是一样的”- 这意味着什么?在我们的示例中,我们使用元素索引作为时间序列。换言之,当前柱(尚未形成)始终具有索引 [0],上一个(已经形成)具有索引 [1],等等。

 所以要小心,到底用的是哪种情况。MQL4的例子大多使用的指标数组以当前为0, MQL5的例子大多使用的指标数组下标以过去为0。

enbo lu
enbo lu | 17 9月 2013 在 06:24
DxdCn:

计算机翻译的?凑合吧。

和MQL4相比,指标数组下标从过去计算,是固定的,不是动态的.  不知道连续运行一周、一月时会怎么样?数据都在内存?? 若都在内存,岂不是会越运行越慢? 

 

但是,在  https://www.mql5.com/zh/articles/31中又说:

  1. “将元素的索引设置为序列,这和 MQL4 中是一样的”- 这意味着什么?在我们的示例中,我们使用元素索引作为时间序列。换言之,当前柱(尚未形成)始终具有索引 [0],上一个(已经形成)具有索引 [1],等等。

 所以要小心,到底用的是哪种情况。MQL4的例子大多使用的指标数组以当前为0, MQL5的例子大多使用的指标数组下标以过去为0。

MQL5中要使用ArraySetAsSeries()函数,将动态数组(向索引大的方向分配内存存储最新值)转换成时间序列形式,即,最新的数据存储在索引0的位置,次新的数据存储在索引为1的位置,以此类推,最新的数据永远存储在索引0位置。

用copybuffer()函数读取,从索引为0的位置开始向后读取数组中的元素,读取多少个自己定义就行。

 

在 MQL5 中使用对象指针 在 MQL5 中使用对象指针

默认情况下,MQL5 中的所有对象都通过引用传递,但还有使用对象指针的可能性。然而,由于对象可能没有初始化,我们必须执行指针检查。在这种情况下,MQL5程序可能会因为关键性错误而终止并卸载。自动创建的对象不会引起此类错误,因此就此意义而言它们十分安全。通过本文,我们将理解对象引用和对象指针之间的差异,并思考如何编写使用指针的安全代码。

自适应交易系统以及它们在 MetaTrader 5 客户端中的运用 自适应交易系统以及它们在 MetaTrader 5 客户端中的运用

本文推荐一种由很多策略组成的自适应系统,每种策略执行其自己的虚拟交易操作。实际交易依据当时最赚钱策略的信号进行。归功于使用面向对象的方法、标准库中用于处理数据的类和交易类,系统的架构看起来很简单并且可扩展;现在,您可以轻松地创建和分析包含数以百计的交易策略的自适应系统。

EA 交易中采用OnTrade() 函数处理交易事件 EA 交易中采用OnTrade() 函数处理交易事件

MQL5提供了海量的创新,其中就包括使用各种类型的事件(计时器事件、交易事件、自定义事件等)。有了处理事件的能力,您就能够创建全新类型的自动与半自动交易程序。我们会在本文中一起学习交易事件,并针对OnTrade()函数编写一些处理交易事件的代码。

利用 MQL5 云网络加速计算 利用 MQL5 云网络加速计算

您的家用电脑是几核的?优化一项交易策略,您可以运用多少计算机?我们在此展示如何利用MQL5云网络,点击鼠标即可获取遍及全球的计算能力,并通过这种方式加速计算。每过去一年,时间就是金钱这句话都会成为更被热议的话题,我们不能承受重要运算几十小时甚或几天的等候。