面向初学者的创建具有多个指标缓冲区的指标

Nikolay Kositsin | 4 十二月, 2013

简介

在我的前作《面向新手的 MQL5 自定义指标》《初学者以 MQL5 实现对数字滤波器的实际实施》中,我重点详述了具有一个指标缓冲区的指标的结构。

显而易见,这种方法可广泛适用于编写自定义指标,但真实交易很难限制在它们的使用范围内,因此,是时候提出更复杂的构建指标代码的方法了。幸运的是,MQL5 的能力是无穷无尽的,唯一可限制它的是我们电脑的 RAM。


代码加倍的 Aroon 指标示例

该指标的公式包含两个组件:上涨指标和下跌指标,在单独的图表窗口中绘制:

BULLS =  (1 - (bar - SHIFT(MAX(HIGH(), AroonPeriod)))/AroonPeriod) * 100
BEARS = (1 - (bar - SHIFT(MIN (LOW (), AroonPeriod)))/AroonPeriod) * 100

其中:

从指标的公式我们可以得出,要构建指标,我们必须只有两个缓冲区,指标的结构与我们在前文中讨论的 SMA_1.mq5 的结构将有细微差异。

实际上,这只不过是同一重复代码,具有不同的指标缓冲区数量。因此,我们在 MetaEditor 中打开该指标的代码,并另存为 Aroon.mq5。在有关指标版权和版本号的前 11 行代码中,我们只需替换指标的名称:

//+------------------------------------------------------------------
//|                                                        Aroon.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------
//---- 版权
#property copyright "2010, MetaQuotes Software Corp."
//---- 作者网站的链接
#property link      "http://www.mql5.com"
//----版本号
#property version   "1.00"

接下来,在第 12 行的代码中,我们需要将指标绘制从基本图表窗口更改为单独窗口:

//---- 在独立窗口中绘制指标
#property indicator_separate_window

由于该指标的值的范围完全不同,它的绘制将在一个单独的窗口中进行。

之后,在接下来的 4 行代码中(一般指标属性),我们将使用的指标缓冲区数量更改为 2:

//---- 使用 2 个缓存
#property indicator_buffers 2
//---- 2 种图形用于绘制 
#property indicator_plots   2

接下来的 10 行代码与从特定的指标缓冲区绘制指标有关,我们必须复制它的标签,然后将所有索引从 1 更改为 2。我们还必须更改指标缓冲区的所有标签:

//+----------------------------------------------+
//| 牛市力量指标参数        |
//+----------------------------------------------+
//----绘制类型 = 线
#property indicator_type1   DRAW_LINE
//---- 绘制颜色 = 灰色
#property indicator_color1  Lime
//----线形 = 实线
#property indicator_style1  STYLE_SOLID
//---- 线宽 = 1
#property indicator_width1  1
//---- BullsAroon 指标的标签
#property indicator_label1  "BullsAroon"
//+----------------------------------------------+
//|  熊市力量指标参数       |
//+----------------------------------------------+
//----绘制类型 = 线
#property indicator_type2   DRAW_LINE
//---- 绘制颜色 = 红色
#property indicator_color2  Red
//----线形 = 实线
#property indicator_style2  STYLE_SOLID
//---- 线宽 = 1
#property indicator_width2  1
//----BearsAroon 指标的标签
#property indicator_label2  "BearsAroon"

该指标使用三个水平位置,其值分别为 30、50 和 70。

为了绘制这些位置,我们需要在指标的代码中添加五行代码。

//+----------------------------------------------+
//| 横向水平                           |
//+----------------------------------------------+
#property indicator_level1 70.0
#property indicator_level2 50.0
#property indicator_level3 30.0
#property indicator_levelcolor Gray
#property indicator_levelstyle STYLE_DASHDOTDOT

相较以前的指标,指标输入参数保持不变,除了标题稍有更改:

//+----------------------------------------------+
//| 指标输入参数                   |
//+----------------------------------------------+
input int AroonPeriod = 9; // 周期 
input int AroonShift = 0// 指标的水平柱形偏移 

 然而现在有两个将用作指标缓冲区的数组,并且它们会具有合适的名称:

//--- 声明用于指标缓存的动态数组
double BullsAroonBuffer[];
double BearsAroonBuffer[]; 

我们在完全相同的事物中继续处理 OnInit() 函数的代码。

首先,我们修改第零个缓冲区的代码:

//--- 设置动态数组 BullsAroonBuffer 作为指标缓存 
SetIndexBuffer(0, BullsAroonBuffer, INDICATOR_DATA);
//--- 指标 1(AroonShift) 的水平偏移
PlotIndexSetInteger(0, PLOT_SHIFT, AroonShift);
//---绘制指标 1(AroonPeriod) 的开始
PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, AroonPeriod);
//--- 数据窗口中显示的标签
PlotIndexSetString(0, PLOT_LABEL, "BearsAroon"); 

之后,将整个代码复制到 Windows 的剪贴板,并将其粘贴到相同代码的后面。

然后在粘贴的代码中,我们将指标缓冲区的编号从 0 改为 1,并更改指标数组的名称以及指标的标签:

//--- 设置动态数组 BullsAroonBuffer 作为指标缓存 
SetIndexBuffer(1, BearsAroonBuffer, INDICATOR_DATA); 
//--- 指标 2(AroonShift) 的水平偏移 
PlotIndexSetInteger(1, PLOT_SHIFT, AroonShift); 
//--- 绘制指标 2(AroonPeriod) 的开始 
PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, AroonPeriod); 
//--- 数据窗口中显示的标签 
PlotIndexSetString(1, PLOT_LABEL, "BullsAroon");  

指标的简称也同样需要稍作更改:

//--- 初始化用于指标简称的变量
string shortname;
StringConcatenate(shortname, "Aroon(", AroonPeriod, ", ", AroonShift, ")"); 

现在,我们考虑绘制指标的精确性。指标的实际范围为从 0 到 100,且该范围一直显示。

在这种情况下,很有可能只使用指标的整数值在图表上绘制。为此,我们用 0 表示小数点后的数字,用于指标绘制:

//---确定指标值的绘制精度 
IndicatorSetInteger(INDICATOR_DIGITS, 0);

在 SMA_1.mq5 指标中,我们使用 OnCalculate() 函数调用的第一种形式。

它不太适用于 Aroon 指标,因为它没有 high[] 和 low[] 价格数组。这些数组在该函数的第二种调用形式中可用。因此,更改函数的函数头是必要的:

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 < AroonPeriod - 1) return(0);
   
//--- 声明本地变量 
int first, bar;
double BULLS, BEARS; 

//--- 主循环的 第一次 (开始位索引) 计算
if (prev_calculated == 0)          // 检查OnCalulate函数的第一次调用
    first = AroonPeriod - 1;       // 所有柱形的开始计算位置  
else first = prev_calculated - 1//新柱形计算的开始位置

然而,计算指标值的算法存在某些问题。问题在于,MQL5 没有内置函数,用于按递减索引的方向为当前柱的周期确定最大值和最小值的索引。

解决问题的方法之一是我们自己编写这些函数。幸运的是,自定义指标的 ZigZag.mq5 指标中已经有了这种函数,位于 "MetaTrader5\MQL5\Indicators\Examples" 文件夹中。

最简单的方法是在 ZigZag.mq5 指标中选择这些函数的代码,将其复制到 Windows 剪贴板,然后粘贴到我们的代码中,例如,位于 OnInit() 函数说明的后面,处于全局层面:

//+------------------------------------------------------------------
//|  搜索最高价柱形的索引                            |
//+------------------------------------------------------------------
int iHighest(const double &array[], // 用于搜索最大元素的索引的数组
             int count,            // 数组中元素的个数(降序), 
             int startPos          //开始位的索引
             )                     
  {
//---+
   int index = startPos;
   
   //---- 检查开始位索引
   if (startPos < 0)
     {
      Print("Incorrect value in the function iHighest, startPos = ", startPos);
      return (0);
     
} 
   //---- 检查 startPos 的值
   if (startPos - count < 0) count = startPos;
    
   double max = array[startPos];
   
   //---- 索引搜索
   for(int i = startPos; i > startPos - count; i--)
     {
      if(array[i] > max)
        {
         index = i;
         max = array[i];
        
}
     
}
//---+ 返回最大值所在柱形的索引
   return(index);
  
}
//+------------------------------------------------------------------
//|  搜索最低价柱形的索引                             |
//+------------------------------------------------------------------
int iLowest(
            const double &array[], // 用于搜索最大元素索引的数组
            int count,            // 数组中元素的个数(降序),
            int startPos          //开始位的索引
            ) 
{
//---+
   int index = startPos;
   
   //--- 检查开始位的索引
   if (startPos < 0)
     {
      Print("Incorrect value in the iLowest function, startPos = ",startPos);
      return(0);
     
}
     
   //--- 检查 startPos 的值
   if (startPos - count < 0) count = startPos;
    
   double min = array[startPos];
   
   //--- 索引搜索
   for(int i = startPos; i > startPos - count; i--)
     {
      if (array[i] < min)
        {
         index = i;
         min = array[i];
        
}
     
}
//---+ 返回最低值的柱形索引
   return(index);
  
}

之后,OnCalculate() 函数的代码如下所示:

//+------------------------------------------------------------------
//| 自定义指标迭代函数                                                           |
//+------------------------------------------------------------------
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 < AroonPeriod - 1)
    return(0);
   
   //--- 声明本地变量
   int first, bar;
   double BULLS, BEARS;
   
   //--- 计算开始位的索引
   if (prev_calculated == 0//检查第一个指标值的计算
     first = AroonPeriod - 1; // 所有柱形的开始计算位置

   else first = prev_calculated - 1; // 新柱形计算的开始位置

   //--- 主循环
   for(bar = first; bar < rates_total; bar++)
    {
     //--- 求值
     BULLS = 100 - (bar - iHighest(high, AroonPeriod, bar) + 0.5) * 100 / AroonPeriod;
     BEARS = 100 - (bar - iLowest (low,  AroonPeriod, bar) + 0.5) * 100 / AroonPeriod;

     //--- 用计算得到的值填充指标缓存
     BullsAroonBuffer[bar] = BULLS;
     BearsAroonBuffer[bar] = BEARS;
    
}
//---+     
   return(rates_total);
  
}
//+------------------------------------------------------------------

为了沿轴对称,我稍稍修改了代码,即相对于原始指标添加了值为 0.5 的指标的垂直平移。

下面是该指标在图表上的工作结果:

                                                                              

要从距当前柱不远于 AroonPeriod 的范围内找到具有最大或最小值的元素的位置,我们可以使用 MQL5 的内置 ArrayMaximum()ArrayMinimum() 函数,也可以使用它们搜索极值,但这些函数使用递增顺序执行搜索。

然而,搜索应按照索引的递减顺序进行。对于这种情况,最简单的解决方案是使用 ArraySetAsSeries() 函数更改指标和价格缓冲区中的索引顺序。

但我们还需要在计算循环中更改柱排序的方向以及更改第一个变量计算的算法。

在这种情况下,结果 OnCalculate() 函数将如下所示:

//+------------------------------------------------------------------
//| 自定义指标迭代函数                                                           |
//+------------------------------------------------------------------
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 < AroonPeriod - 1)
    return(0);
    
   //--- 将索引设置为时间序列
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low,  true);
   ArraySetAsSeries(BullsAroonBuffer, true);
   ArraySetAsSeries(BearsAroonBuffer, true);
   
   //--- 声明本地变量
   int limit, bar;
   double BULLS, BEARS;
   
   //--- 计算开始位的索引
   if (prev_calculated == 0)                      // 检查OnCalulate函数的第一次调用
       limit = rates_total - AroonPeriod - 1// 所有柱形的开始计算位置
   else limit = rates_total - prev_calculated; // 新柱形计算的开始位置
   
   //--- 主循环
   for(bar = limit; bar >= 0; bar--)
    {
     //--- 计算指标值
     BULLS = 100 + (bar - ArrayMaximum(high, bar, AroonPeriod) - 0.5) * 100 / AroonPeriod;
     BEARS = 100 + (bar - ArrayMinimum(low,  bar, AroonPeriod) - 0.5) * 100 / AroonPeriod;

     //--- 用计算得到的值填充指标缓存
     BullsAroonBuffer[bar] = BULLS;
     BearsAroonBuffer[bar] = BEARS;
    
}
//----+     
   return(rates_total);
  
}
//+------------------------------------------------------------------

我将变量的名称从 "first" 更改为 "limit",在此示例中后者要更为合适。

在此情形中,主循环的代码与 MQL4 中的类似。因此,这种编写 OnCalculate() 函数的风格可用于以最少的代码更改将指标从 MQL4 转换到 MQL5。


总结

好了,大功告成!我们完成了指标的编写,甚至还包括两个版本。

在正确的情况下,解决这种问题的保守和聪明的做法形成的解决方案不比使用儿童的乐高积木拼装玩具要困难多少。