MQL5:创建自己的指标

MetaQuotes | 22 八月, 2013

简介

什么是指标?指标是我们希望以便利方式在荧幕上显示的一组计算值。这一组值在程序中以数组表示。因此,创建指标意即编写用于处理数组(价格数组)的算法并将处理结果记录在其他数组(指标值)中。

尽管已有许多已成经典的现成指标,但创建自己的指标的必要性始终存在。我们把使用我们自己的算法创建的这类指标称为自定义指标。本文将探讨如何创建简单的自定义指标。

指标是不同的

指标可以表现为带有颜色的线条或区域,或作为指向输入头寸的有利时刻的特殊标签显示。同时,这些类型还可以相互结合,从而提供了更多的指标类型。我们将采用 William Blau 开发的众所周知的“真实强弱指数”作为指标创建的示例。

真实强弱指数

TSI 指标基于双重平滑动量来确定趋势及超卖/超买区域。指标的数学诠释请见 动量、方向和背离,William Blau。在这里,我们仅涉及计算公式。

TSI(CLOSE,r,s) =100*EMA(EMA(mtm,r),s) / EMA(EMA(|mtm|,r),s)

其中:

从上述公式我们可以得知,有三个参数影响指标计算。它们是时间周期 r 和时间周期 s,以及用于计算的价格类型。在前例中,我们使用收盘价。

MQL5 向导

我们将 TSI 以蓝色线条显示 - 在这里,我们需要启动“MQL5 向导”。首先,我们需要指出我们希望创建的程序的类型 - 自定义指标。接来下,我们应设置程序名、rs 参数以及它们的值。

之后,我们需定义指标在单独的窗口中以蓝色线条显示,并为该线条设置 TSI 标签。

在输入所有初始数据后,按 Done(完成)并获得指标的草稿。

//+------------------------------------------------------------------+
//|                                          True Strength Index.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
//---- TSI 绘图属性
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- 输入参数
input int      r=25;
input int      s=13;
//--- 指标缓冲区
double         TSIBuffer[];
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 指标缓冲区映射关系
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                 |
//+------------------------------------------------------------------+
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[])
  {
//---
//--- 返回值会作为下一次调用的 prev_calculated 参数调用
   return(rates_total);
  }
//+------------------------------------------------------------------+

“MQL5 向导”创建指标头文件,其内规定了指标属性,即:

所有准备工作就绪,现在我们可以着手改进和完善我们的代码。

OnCalculate()

函数 OnCalculate() 是Calculate事件的处理函数,在需要重新计算指标值以及在图表上重新绘制指标时出现。这是新的订单号接收、交易品种历史数据更新等的事件。这就是指标值所有计算的主代码必须位于此函数中的原因。

当然,辅助计算可以通过其他的单独函数实施,但这些函数必须用于 OnCalculate处理函数。

默认情况下,“MQL5 向导”创建 OnCalculate() 的第二种形式,该形式提供对所有时序类型的访问:

而对于我们而言,我们只需要一个数据数组,这就是我们要改写调用的 OnCalculate() 函数的第一种形式的原因。

int OnCalculate (const int rates_total,      // price[]数组大小;
                 const int prev_calculated,  // 上次调用计算后的价格柱的数量
                 const int begin,            // price[]数组开始计算的索引
                 const double& price[])      // 指标计算的依据数组
  {
//---
//--- 返回值会作为下一次调用的 prev_calculated 参数调用
   return(rates_total);
  }  

这就使我们不仅可以进一步将指标应用于价格数据,同时还可以基于其他指标的值来创建指标。

如果我们在 Parameters(参数)选项卡中选择 Close(收盘)(默认提供),则传递至 OnCalculate() 的 price[] 将包含收盘价。如果我们选择,例如,Typical Price(典型价格),price[] 将在每个时间周期中包含(最高价+最低价+收盘价)/3 的价格。

rates_total 参数指示 price[] 数组的大小;该参数在循环中组织计算时十分有用。price[] 中的元素索引从零开始,方向从过去至未来,即 price[0] 元素包含最旧的值,而 price[rates_total-1] 包含最新的数组值。

组织辅助指标缓冲区

仅有一根线会在图表中显示,即一个指标数组的数据。但在此之前,我们需要组织中间计算。中间数据存储在以 INDICATOR_CALCULATIONS 属性标记的指标数组中。从上述公式可以得知,我们还需要以下附加数组:

  1. 用于值 mtm - 数组 MTMBuffer[];
  2. 用于值 |mtm| - 数组 AbsMTMBuffer[];
  3. 用于 EMA(mtm,r) - 数组 EMA_MTMBuffer[];
  4. 用于 EMA(EMA(mtm,r),s) - 数组 EMA2_MTMBuffer[];
  5. 用于 EMA(|mtm|,r) - 数组 EMA_AbsMTMBuffer[];
  6. 用于 EMA(EMA(|mtm|,r),s) - 数组 EMA2_AbsMTMBuffer[]。

我们总共还需要添加 6 个全局级别的双精度类型数组,并需要将这些数组和指标缓冲区绑定至 OnInit() 函数。切勿忘记标示新的指标缓冲区数量;indicator_buffers 属性必须等于 7(原有的 1 个缓冲区加上添加的 6 个缓冲区)。

#property indicator_buffers 7

现在指标代码如下所示:

#property indicator_separate_window
#property indicator_buffers 7
#property indicator_plots   1
//---- TSI 绘图属性
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- 输入参数
input int      r=25;
input int      s=13;
//--- 指标缓冲区
double         TSIBuffer[];
double         MTMBuffer[];
double         AbsMTMBuffer[];
double         EMA_MTMBuffer[];
double         EMA2_MTMBuffer[];
double         EMA_AbsMTMBuffer[];
double         EMA2_AbsMTMBuffer[];
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                               |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 指标缓冲区映射关系
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                 |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,    // price[]数组大小;
                 const int prev_calculated,// 次调用计算后的价格柱的数量;
                 const int begin,          // price[]数组开始计算的索引; 
                 const double& price[])    // 指标计算的依据数组;
  {
//---
//--- 返回值会作为下一次调用的 prev_calculated 参数调用
   return(rates_total);
  }

中间计算

组织缓冲区 MTMBuffer[] 和 AbsMTMBuffer[] 的值的计算是非常容易的。在循环中,从 price[1] 至 price[rates_total-1] 逐一遍历值,将差值写入一个数组,差值的绝对值写入第二个数组。

//--- 计算 mtm 和 |mtm| 的数值
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

下一阶段是计算这些数组的指数平均线。我们有两种方法可用。其一是写入整个算法,设法不犯错误。其二是使用已经调试并严格用于这些目的的现成函数。

MQL5 中没有内置函数用于通过数组值计算移动平均线,但有一个现成的函数库 MovingAverages.mqh 可用,到该库的完整路径为 terminal_directory/MQL5/Include/MovingAverages.mqh,其中 terminal_directory 是 MetaTrader 5 终端的安装目录。该库是一个引用文件;它包含用于计算移动平均线的函数,计算通过使用以下四个经典方法之一在数组上完成:

要使用这些函数,应在任何 MQL5 程序的标头码中添加以下代码:

#include <MovingAverages.mqh>

我们需要函数 ExponentialMAOnBuffer(),该函数在值数组上计算指数移动平均线,并将平均值记录在另一数组中。

数组平滑函数

引用文件 MovingAverages.mqh 总共包含八个函数,这八个函数可以划分为相同类型的两组函数,每组 4 个函数。第一组函数接收数组并在指定位置返回移动平均线的值:

这些函数用于获取平均线的值,一个数组一次,且没有针对多重调用进行优化。若需要在循环中使用该组的函数(以计算平均线的值并进一步将每个计算值写入数组),则需要组织一个最优算法。

第二组函数用于将基于初始值数组的移动平均线的值填入接收数组:

所有指定的函数除数组 buffer[]、price[] 以及 period 平均周期外,均获得 3 个以上的参数,其目的是类同于 OnCalculate() 函数 rates_total、prev_calculated 和 begin 的参数。该组函数可正确处理传递的数组 price[] 和 buffer[],并将索引方向考虑在内(AS_SERIES 标志)。

begin 参数指示源数组的索引,有意义的数据(即需要处理的数据)从此开始。对于 MTMBuffer[] 数组,实际数据从索引 1 开始,因为 MTMBuffer[1]=price[1]-price[0]。MTMBuffer[0] 的值未定义,这就是 begin=1 的原因。

//--- 计算数组的首要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // 索引, 开始从哪个数据开始平滑计算 
                         r,  // 指数平均的周期
                         MTMBuffer,       // 计算平均的缓冲区
                         EMA_MTMBuffer);  // 存储计算结果的缓冲区
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

取平均线时,应考虑时间周期值,因为在输出数组中,计算值的填入带有迟滞,较大的平均周期则该迟滞也较大。例如,如果 period=10,结果数组中的值将起始于 begin+period-1=begin+10-1。在 buffer[] 的进一步调用中,应将其考虑在内,且处理应从索引 begin+period-1 开始。

因此,我们可以从数组 MTMBuffer[] 和 AbsMTMBuffer 轻松获得次要指数平均线。

//--- 计算数组的次要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

当前的 begin 值为 r,因为 begin=1+r-1(r 是主要指数平均线的时间周期,处理从索引 1 开始)。在输出数组 EMA2_MTMBuffer[] 和 EMA2_AbsMTMBuffer[] 中,计算值起始于索引 r+s-1,因为我们从索引 r 开始处理输入数组,且次要指数平均线的时间周期为 s。

所有预计算就绪,现在我们可以计算将会在图表中绘制的指标缓冲区 TSIBuffer[] 的值。

//--- 现在计算指标的数值
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
F5 键编译代码,并在 MetaTrader 5 终端中启动代码。运作良好!

“真实强弱指数”的第一个版本

此时仍有一些问题待解决。

优化计算

事实上,仅仅编写一个可用的指标是远远不够的。如果我们仔细检查 OnCalculate() 的当前实施情况,就会发现它不是最优的。

int OnCalculate (const int rates_total,    // price[]数组大小;
                 const int prev_calculated,// 次调用计算后的价格柱的数量;
                 const int begin,// price[]数组开始计算的索引; 
                 const double &price[]) // 指标计算的依据数组;
  {
//--- 计算 mtm 和 |mtm| 的数值
   MTMBuffer[0]=0.0;
   AbsMTMBuffer[0]=0.0;
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
//--- 计算数组的首要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // 索引, 开始从哪个数据开始平滑计算 
                         r,  // 指数平均的周期
                         MTMBuffer,       // 计算平均的缓冲区
                         EMA_MTMBuffer);  // 存储计算结果的缓冲区
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

//--- 计算数组的次要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
//--- 现在计算指标的数值
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//---返回值会作为下一次调用的 prev_calculated 参数调用
   return(rates_total);
  }

在每个函数的开始,我们在数组 MTMBuffer[] 和 AbsMTMBuffer[] 中计算值。在这种情况下,如果 price[] 的大小达成千上万或甚至百万,不必要的重复计算可能占用 CPU 的所有资源,不论该 CPU 性能如何强劲。

对于组织优化计算,我们使用 prev_calculated 输入参数,其值等于 OnCalculate() 上次调用返回的值。在函数的首次调用中,prev_calculated 的值始终为 0。在此情况下,我们在指标缓冲区中计算所有的值。在下次调用中,我们不必计算整个缓冲区 - 仅需计算最后的值。代码编写如下:

//--- 如果这是第一次调用 
   if(prev_calculated==0)
     {
      //--- 把索引0的数据设为0
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }
//--- 计算 mtm 和 |mtm| 的数值
   int start;
   if(prev_calculated==0) start=1;  // 设置从 MTMBuffer[] 和 AbsMTMBuffer[] 的索引1开始计算填写数据
   else start=prev_calculated-1;    // 设置从上一次计算的最后一个索引开始计算填写数据 
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

EMA_MTMBuffer[]、EMA_AbsMTMBuffer[]、EMA2_MTMBuffer[] 和 EMA2_AbsMTMBuffer[] 的运算块不需要优化计算,因为 ExponentialMAOnBuffer() 已经是以最优方式编写。我们仅需优化 TSIBuffer[] 数组的值的计算。我们使用用于 MTMBuffer[] 的同样方法。

//--- 现在计算指标的数值
   if(prev_calculated==0) start=r+s-1; // 第一次计算的开始位置
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- 返回值会作为下一次调用的 prev_calculated 参数调用
   return(rates_total);

关于优化程序还有最后一点:OnCalculate() 返回 rates_total 的值。这表示 price[] 输入数组的元素数量,该数量用于指标计算。

OnCalculate() 返回值保存在终端内存中,并在下次调用 OnCalculate() 时作为输入参数 prev_calculated 的值传递给函数

这让我们始终知晓 OnCalculate() 的上次调用中输入数组的大小,并从正确的索引开始指标缓冲区的计算而无需不必要的重复计算。

检查输入数据

要使 OnCalculate() 完美运行,我们还需进行以下操作。我们需要检查在其上计算指标值的 price[] 数组。如果数组的大小 (rates_total) 过小,则无需计算 - 我们需要等待,直至下次调用 OnCalculate() 时有足够的数据。

//--- 如果price[]数组太小
  if(rates_total<r+s) return(0); // 不进行计算或者绘图,直接退出
//--- 如果这是第一次调用 
   if(prev_calculated==0)
     {
      //--- 把索引0的数据设为0
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }

由于指数平滑相继两次用于计算“真实强弱指数”,则 price[] 的大小须至少等于或大于时间周期 r 和 s 的和;否则执行将终止,OnCalculate() 返回 0。返回零值意味着指标将不会在图表上绘制,因为并未计算指标的值。

设置表示法

若计算正确,则指标可就绪待用。但如果我们从其他 MQL5 程序调用该指标,则其默认使用 Close(收盘)价格建立。我们也可以使用其他默认价格类型 - 从指标的 indicator_applied_price 属性的 ENUM_APPLIED_PRICE 枚举中指定一个值

例如,若要设置典型价格((最高价+最低价+收盘价)/3)作为价格,则编码如下:

#property indicator_applied_price PRICE_TYPICAL


如果我们仅使用利用 iCustom()IndicatorCreate() 函数的值,则无需进一步的改进。但如果是直接使用,例如在图表上绘制,我们推荐以下额外设置:

这些设置可使用来自自定义指标组中的函数在 OnInit()处理函数中调整。添加新的线条并将指标另存为 True_Strength_Index_ver2.mq5。

//+------------------------------------------------------------------+
//| 自定义指标初始函数                                                 |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 指标缓冲区映射关系
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//--- 设定从哪一个柱开始画指标
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);
   string shortname;
   StringConcatenate(shortname,"TSI(",r,",",s,")");
//--- 设置在数据窗口(DataWindow)中的显示标签
   PlotIndexSetString(0,PLOT_LABEL,shortname);   
//--- 设置单独子窗口或者弹出帮助时候的显示名称
   IndicatorSetString(INDICATOR_SHORTNAME,shortname);
//--- 设置指标数值显示的精确度
   IndicatorSetInteger(INDICATOR_DIGITS,2);
//---
   return(0);
  }

如果我们启动两种版本的指标,然后将图表滚动至开始处,我们会看到所有的差异。



小结

以创建“真实强弱指数”指标为例,我们可以归纳出在 MQL5 中编写任何指标的基本要点: