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

量化交易吧 /  量化策略 帖子:3353279 新帖:18

通用的之字转向指标

我是游客发表于:4 月 17 日 15:41回复(1)

目录

  • 简介
  • 之字转向指标的特性
  • 绘制之字转向的不同方法
  • 基于最高价/最低价的简单之字转向指标
  • 基于收盘价的简单之字转向指标
  • 开始创建通用的之字转向指标
  • 用于源数据的类
  • 用于方向的类
  • 用于绘图的类
  • 整合三个类
  • 价格图表的变化
  • 价格的变化
  • 从EA交易中调用
  • 结论
  • 附件

简介

之字转向指标(图1)是在 MetaTrader 5 用户中最流行的指标之一,现今已经开发出了多种多样的之字转向指标。然而,它们其中的一些非常慢,这使得无法把它们用于创建EA交易。其他一些经常出错,这让用它们作观察都非常困难。对于那些运行速度快而且没有错误的指标,在使用它们开发EA交易或者另外的指标时使用还比较复杂,出现这种情况是因为展开和解释之字转向指标的数据还不是那么容易。


图 1. 之字转向(ZigZag)指标

本文探讨了绘制之字转向指标的需求,以及绘制它的不同方法,得出结论并获得了一种统一的算法,这种算法将用于作为创建通用指标的基础,这使得可以通过属性窗口来选择不同的之字转向类型。

在指标的开发中将使用面向对象的编程。将会为绘制不同阶段的之字转向指标创建多个基类,对每个阶段将创建多个子类。把基类和子类分开,这样可以使创建不同的新的之字转向类型变得尽可能简单。

除了绘制之字转向本身,本文还将致力于使用得到的指标来开发其他的指标和EA交易,当前的任务是确保可以从之字转向指标中取得数据,并且可以把它作为其他算法的一部分,不会很复杂和耗时。

之字转向指标的特性

之字转向(图1)指标是一条连接局部高价和低价之间的折线,初学者可能马上想到: 能够在底部买入而在顶部卖出就好了!这种想法看起来当然很诱人,但是之字转向只有在历史中才看起来那样诱人,在现实中,情况是有些不一样的。很明显,只要后面有几个柱,就可能形成与之前比较的新高或者新低,图2显示了一种情形,当时指标的最后阶段停止了构造(改变),价格反转并且走向相反的方向(向上)。

 
图 2. 之字转向方向向下,而价格反转向上

但是,又过了几个柱,价格下跌了(图 3), 之字转向的最后一段又继续向下了。

  
图 3. 价格继续下跌,而最后一段之字转向恢复了它的造型

这一次,指标达到了它的最小值,但是这只能在几个柱之后再被确认 (图 4).

 
图 4. 之字转向指标又用了10个柱来画出新的向上的段落,这样才形成了底部


之字转向的这些特点在图5中很好地做了解释。彩色点指出了之前的顶部或者底部构成时的柱,指标会在用蓝色点标出的柱开始绘出新的向上线段,而在用红色点标出的柱开始绘出新的向下的线段。


 图5,红色点和蓝色点指出了之字转向开始出现时的柱   

尽管有这样的特征,之字转向指标并没有失去它的流行和吸引力,至少,它被大量应用在图表的可视化分析中,帮助过滤噪音并且侦测价格变化的主要方向。在更加实用的应用程序中,该指标可以用于侦测支撑/阻力水平或者用于识别模式。并且,它可以被用于绘制趋势线,就像任何其他用于技术分析的图形工具一样,与斐波那契水平线、扇形线等等类似。这里无法列出交易者复杂使用之字转向指标的所有方法。

绘制之字转向的不同方法

显然,之字转向有两种状态: 它的方向向上或者向下。当线的方向是向上时 — 监视价格看是否有新高出现, 而当线的方向是向下时 — 等待新低的出现。还需要监视看是否满足条件指示有方向的改变,所以,为了画出一条之字转向,需要做的是:

  1. 获取源数据; 
  2. 构建线形改变方向的条件公式;  
  3. 监视价格新高和新低的出现。

源数据可以是单独一个序列 (例如,柱的收盘价) 或者两个序列 (例如,柱的最高价和最低价),如果使用了一个数据序列,它可以不仅使用收盘价,而且可以是任何指标,震荡指标或者移动平均。当根据指标数据绘制之字转向时,也可能使用两个数据序列: 一个是基于柱的最高价的指标数据,而另一个是基于柱的最低价的数据。 

方向改变的条件是最重要的一点,它定义了之字转向的不同类型。这些条件可能有很大的不同。例如,这样的条件可以是根据当前柱与n个柱的最高价/最低价做比较而构成,换句话说,如果当前柱的源数据的值是最近n个柱的最大值或者最小值,这就定义了之字转向的方向。这个原则应用在经典的之字转向指标中。另一个方法 — 根据从固定最大值或者最小值回滚的大小。回滚的大小可以以点数衡量 (如果源序列为价格) 或者是约定的单位 (如果是某些指标)。可以使用的方法并不局限于这两种,也可以使用任何指标判断方向 — 如随机震荡指标, ADX, 等等。如果随机震荡指标在50以上,之字转向指标就指向上方,如果它低于50 - 就指向下方。现在,根据 ADX 判断方向:如果 PDI 线在 MDI 线上方,之字转向线就指向上方,如果 PDI 低于 MDI,就指向下方。 

这样,为第一点和第二点通过组合不同的变化,就能获得各种不同的之字转向。另外,当然还可以使用 RSI 数据用于第一点而根据随机震荡指标来判断方向,以此类推。第三点只对看起来类似于之字转向的指标有必要,尽管绘制选项可以有很大不同。 

因为我们这里的任务是获得一个通用指标,有必要尽可能小心地把算法分成两个部分: 对于所有之字转向都同样的部分 (把它称为共用部分) 以及依赖于之字转向的不同部分 (把它称为个体部分)。个体部分使用源数据填充指标缓冲区: 价格或者指标数据。另一个缓冲区 (决定之字转向线的方向)用1或者-1值来填充。这三个缓冲区传给共用部分,在其中会使用它们绘制指标本身。

为了使过程更加清楚,首先创建一个单独的指标,它基于柱的最高/最低价,并且根据第n个柱的最高/最低价来改变放向。

基于最高价/最低价的简单之字转向指标

在 MetaEditor 中创建一个新指标 (主菜单 — 文件 — 新建或者按下 Ctrl+N). 在指标创建向导中,输入名称“iHighLowZigZag”,创建一个外部参数 "period" (int 类型, 数值为 12), 选择 OnCalculate(...,open,high,low,close) 事件处理函数, 创建一个缓冲区,名称为 "ZigZag" (Section 类型, 使用红色) 以及另外三个缓冲区,名称分别为 "Direction", "LastHighBar" 和 "LastLowBar" (line 类型, 颜色为 none)。

"ZigZag" 缓冲区将用于显示之字转向,其余的缓冲区是辅助的。对于所有的辅助缓冲区,在OnInit()函数中,调用SetIndexBuffer()函数把 INDICATOR_DATA 类型改为 INDICATOR_CALCULATIONS类型。在文件的顶部,修改 indicator_plots 属性的值: 把数值设为 1。在那以后,指标会只画一个 "ZigZag" 缓冲区, 并且指标不会画任何多余的线,但是同时,另外的缓冲区可以用来由 iCustom() 函数做处理。 

首先,开始计算的柱的索引('start'变量)是在OnCalculate()函数中确定的,所以只有在指标开始的时候才会计算所有的柱,以后的计算只在每个新柱上进行。另外,还要初始化缓冲区的元素:

  int start; // 开始计算的柱的索引
  if(prev_calculated==0)
    { // 在启动时
     // 初始化缓冲区元素
     DirectionBuffer[0]=0;
     LastHighBarBuffer[0]=0;
     LastLowBarBuffer[0]=0;
     start=1; // 从初始化之后的元素开始计算
    }
  else
    { // 在运行中
     start=prev_calculated-1;
    }
}

现在,主指标循环:

for(int i=start;i<rates_total;i++)
     {

就像上面描述的那样,为了获得通用性,需要把代码分为之字转向方向的计算以及它的绘制两个部分,这个原则将要遵循。首先写出用于确定方向的代码,为了确定方向,使用ArrayMaximum()和ArrayMinimum()函数。如果在计算的柱上出现了高价或者低价,Direction 数组的元素就被赋值为 1 或者 -1,为了得到之字转向在每个柱的当前方向的信息,在计算方向之前,从 Direction 缓冲区的前一个元素取得数值并赋给当前的元素:

// 取得前一个
// 确定方向数组元素的值
   DirectionBuffer[i]=DirectionBuffer[i-1];

// 计算初始柱的
// ArrayMaximum() 和 ArrayMinimum() 函数值
   int ps=i-period+1;
// 确定最高价和最低价
// 在 'period' 个柱的范围之内
   int hb=ArrayMaximum(high,ps,period);
   int lb=ArrayMinimum(low,ps,period);

// 如果识别到最高价或者最低价
   if(hb==i && lb!=i)
     { // 找到了最高价
      DirectionBuffer[i]=1;
     }
   else if(lb==i && hb!=i)
     { // 找到了最低价
      DirectionBuffer[i]=-1;
     }

请注意代码的最后部分: 它识别最高价或者最低价,检查当前柱上是否同时有最高价并且没有最低价,或者相反:有最低价而没有最高价。有时候,可能会有非常长的柱形,可能同时在上面确定两个方向,在这种情况下,Direction 缓冲区会包含之前确认的方向。

一般来说,在 MetaTrader 5 终端内,可以创建一个之字转向,画出垂直的线段,这使得可以在同一个柱上显示指标方向的两个变化,但是,这样的之字转向类型不在本文讨论范围之内。 

让我们继续在主循环中写代码: 以下片段将用于画出之字转向线,其余两个缓冲区将与 Direction 缓冲区作相同的处理:

LastHighBarBuffer[i]=LastHighBarBuffer[i-1];
LastLowBarBuffer[i]=LastLowBarBuffer[i-1];  

这两个缓冲区将包含之字转向中最近的最高价和最低价柱的索引数据。除了那些需要画出指标的柱的索引,那些缓冲区在从EA交易中调用之字转向指标也是被大量应用的,那样就不需要在循环中迭代所有的柱来找到最近的高点了。

确保清除之字转向指标的缓冲区:

ZigZagBuffer[i]=EMPTY_VALUE;  

必须这样做是因为只有在启动时才会完整计算指标,并且在有一些事件发生时也会完整计算,例如下载历史时。缓冲区可能有旧的数据,这可能会扰乱指标线的外观。  

现在,让我们转到绘图,在此,算法被分为四个分支: 开始新的向上移动,开始新的向下移动,继续向上移动,继续向下移动。为了检查方向值,在计算和前一个柱上使用了 switch 操作符:

switch((int)DirectionBuffer[i])
  {
   case 1:
      switch((int)DirectionBuffer[i-1])
        {
         case 1:
            // 继续向上的移动
            ...
            break;
         case -1:
            // 开始新的向上移动
            ...
            break;
        }
      break;
   case -1:
      switch((int)DirectionBuffer[i-1])
        {
         case -1:
            // 继续向下的移动
            ...
            break;
         case 1:
            // 开始新的向下移动    
            ...
            break;
        }
      break;

剩下的工作是写四个代码块,它们其中的两个将要详细探讨: 开始新的向上移动和继续向上移动。当 Direction 缓冲区的值从-1变成1的时候开始新的向上移动,当出现这种情况时,算法会画出之字转向的一个新点,并保存新方向开始时的柱的索引:

ZigZagBuffer[i]=high[i];
LastHighBarBuffer[i]=i;

继续移动会稍微复杂一些,算法检查当前柱的值是否大于之前之字转向的最高价的值,如果它更大,就要移动到最后线段的末端,也就是删除之前画的点并设置一个新点,在此,算法还会保存绘制新点时柱的信息:

// 继续向上的移动
   if(high[i]>high[(int)LastHighBarBuffer[i]])
     { // 新高
      // 删除旧的之字转向点
      ZigZagBuffer[(int)LastHighBarBuffer[i]]=EMPTY_VALUE;
      // 设置一个新点
      ZigZagBuffer[i]=high[i];
      // 有新高的柱的索引
      LastHighBarBuffer[i]=i;
     }

就是这样了,不要忘记使用反括号关闭循环。剩下的就是在策略测试器的可视化模式下测试指标了,完整可运行的 "iHighLowZigZag" 指标可以在附件中找到。

基于收盘价的简单之字转向指标

让我们把新创建的指标修改一下用来基于收盘价来工作。不需要从头开始了: 把 "iHighLowZigZag" 指标以名称 "iCloseZigZag" 保存,并且替换调用 'high' 和 'low' 的数组,改为调用 'close' 数组。这样看起来似乎工作就完成了,但是测试显示,指标的运行是错误的 (图 6)。

 
图 6. 从基于 High/Low 的指标改为基于 'close' 的之字转向指标运行不正确

让我们看一下为什么会这样,如果在一定范围的柱内,当前柱的最高价构造了一个顶,这个柱会保持在顶部,不论收盘价如何改变,如果由收盘价构造顶部,则收盘价在柱的构建过程中可能改变,而最高价将不复存在。当在同样的方向上确认新高/新低时,会满足删除旧点的条件 - 这就是问题存在的原因。新高会被取消, 新点被删除,但是旧的点也被删除了。所以,有必要恢复旧点的位置。最近极值点的位置信息都包含在缓冲区中: LastHighBarBuffer 和 LastLowBarBuffer,最近的点将会从它们中恢复。在指标的主循环中,在 switch 操作符之前,加上两行代码:

ZigZagBuffer[(int)LastHighBarBuffer[i]]=close[(int)LastHighBarBuffer[i]];
ZigZagBuffer[(int)LastLowBarBuffer[i]]=close[(int)LastLowBarBuffer[i]];  

在这样修改后,指标应该可以正常工作了。结果得到的 "iCloseZigZag" 指标可以在文章的附件中找到。 

开始创建通用的之字转向指标

之字转向指标的通用性将能通过独立解决三项任务得到:
  1. 使用源数据填充缓冲区。将使用两个缓冲区,需要它们是因为它们可以用于填充最高价和最低价。为了获得基于收盘价或者任何其他指标的之字转向,两个缓冲区都必须使用相同的值来填充。 
  2. 根据分析源数据填充 Direction 缓冲区。
  3. 画出之字转向。
每个任务都通过它的独立基类和另外的子类来解决,这使得可以通过指标属性窗口选择各种选项和它们的组合。

用于源数据的类

创建 "CSorceData.mqh" 包含文件,在其中加入 CSorceData 类,它将是父类。它将包含一个虚 'Calculate' 方法,和指标的 OnCalculate() 函数类似,但是有某些改动。有两个额外的数组传给该方法: BufferHigh[] 和 BufferLow[]。这些缓冲区用数据填充,将会在未来计算之字转向时使用。因为不仅是价格,还有任何其它指标的值都可以作为源数据,就有必要控制指标的载入过程。为此,加入 CheckHandle() (类型 bool) 虚方法:

class CSorceData
  {
private:
public:
   virtual int Calculate(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[],
                         double &BufferHigh[],
                         double &BufferLow[])
     {
      return(0);
     }
   virtual bool CheckHandle()
     {
      return(true);
     }

  };

现在创建多个子类,一个是用于最高/最低价的:

class CHighLow:public CSorceData
  {
private:
public:
   int Calculate(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[],
                 double &BufferHigh[],
                 double &BufferLow[])
     {
      int start=0;
      if(prev_calculated!=0)
        {
         start=prev_calculated-1;
        }
      for(int i=start;i<rates_total;i++)
        {
         BufferHigh[i]=high[i];
         BufferLow[i]=low[i];
        }
      return(rates_total);
     }
  };

 另一个是用于收盘价的,它只是在循环内部的代码中有所区别。

for(int i=start;i<rates_total;i++)
  {
   BufferHigh[i]=close[i];
   BufferLow[i]=close[i];
  }

这个类的名称是 "CClose:public CSorceData",CheckHandle() 方法还没有使用,

我们将还要创建一些类用来从指标中获取数据,让我们按照不同数量的参数和位置选择指标 (在价格图表上或者在单独的子窗口中) — RSI 和移动平均。让我们为它们写出类,

为 RSI 创建一个类, 称它为 "CRSI:public CSorceData",在它的私有部分加上一个变量用于指标句柄:

   private:
      int m_handle;

加上构造函数: 会把 RSI 的参数传给它, 指标会在其中载入:

void CRSI(int period,ENUM_APPLIED_PRICE price)
  {
   m_handle=iRSI(Symbol(),Period(),period,price);
  }

现在,CheckHandle() 方法:

bool CheckHandle()
  {
   return(m_handle!=INVALID_HANDLE);
  }

Calculate 方法将不使用循环,而是简单的复制缓冲区:

int to_copy;
   if(prev_calculated==0)
     {
      to_copy=rates_total;
     }
   else
     {
      to_copy=rates_total-prev_calculated;
      to_copy++;
     }

   if(CopyBuffer(m_handle,0,0,to_copy,BufferHigh)<=0)
     {
      return(0);
     }

   if(CopyBuffer(m_handle,0,0,to_copy,BufferLow)<=0)
     {
      return(0);
     }
   return(rates_total);

请注意: 如果复制失败 (调用 CopyBuffer() 函数), 此方法返回 0, 如果复制成功 — rates_total。这样,如果复制失败,指标后面也可以重新计算。 

类似地,为移动平均创建一个类,命名为 "CMA:public CSorceData",差别只是在构造函数中:

void CMA(int period,int shift,ENUM_MA_METHOD method,ENUM_APPLIED_PRICE price)
   {
    m_handle=iMA(Symbol(),Period(),period,shift,method,price);
   }

在这种情况下,Calculate() 方法是完全相同的,但是其它的指标可能有某些不同,特别是缓冲区数量不同的情况。完整功能的 "CSorceData.mqh" 文件可以在附件中找到。另外需要注意的是,它只能暂时作为可运行的,因为未来还会进一步扩展它,为其它指标增加一些新的子方法。

用于方向的类

这个类将位于 "CZZDirection.mqh" 文件中, 基类的名称 — "CZZDirection"。这个类将包含一个 Calculate() 虚方法。传给它的参数是用来确定用于计算的柱的 (rates_total, prev_calculated variables), 还有源数据的缓冲区以及用于方向的缓冲区。就像之前提到的那样,之字转向的指标可以使用一个指标来确定,所以也加入了使用指标的功能。增加了 CheckHandle() 虚方法:

class CZZDirection
  {
private:
public:
   virtual int Calculate(const int rates_total,
                         const int prev_calculated,
                         double &BufferHigh[],
                         double &BufferLow[],
                         double &BufferDirection[])
     {
      return(0);
     }
   virtual bool CheckHandle()
     {
      return(true);
     }
  };

现在让我们写一个子类来确定方向,就像在 "iHighLowZigZag" 指标中一样。使用这个方法来确定方向将需要 "period" 参数,所以,将在私有部分加上 m_period 和带有 peroid 参数的构造函数:

class CNBars:public CZZDirection
  {
private:
   int               m_period;
public:
   void CNBars(int period)
     {
      m_period=period;
     }
   int Calculate(const int rates_total,
                 const int prev_calculated,
                 double &BufferHigh[],
                 double &BufferLow[],
                 double &BufferDirection[]
                 )
     {
      int start;

      if(prev_calculated==0)
        {
         BufferDirection[0]=0;
         start=1;
        }
      else
        {
         start=prev_calculated-1;
        }

      for(int i=start;i<rates_total;i++)
        {

         BufferDirection[i]=BufferDirection[i-1];

         int ps=i-m_period+1;
         int hb=ArrayMaximum(BufferHigh,ps,m_period);
         int lb=ArrayMinimum(BufferLow,ps,m_period);

         if(hb==i && lb!=i)
           { // 找到了最高价
            BufferDirection[i]=1;
           }
         else if(lb==i && hb!=i)
           { // 找到了最低价
            BufferDirection[i]=-1;
           }

        }
      return(rates_total);
     }

将创建另一个子类来基于CCI指标确定方向,CCI 的位置高于0对应着之字转向的向上方向,低于0 - 下降方向:

class CCCIDir:public CZZDirection
   {
private:
    int               m_handle;
public:
    void CCCIDir(int period,ENUM_APPLIED_PRICE price)
      {
       m_handle=iCCI(Symbol(),Period(),period,price);
      }
    bool CheckHandle()
      {
       return(m_handle!=INVALID_HANDLE);
      }
    int Calculate(const int rates_total,
                  const int prev_calculated,
                  double &BufferHigh[],
                  double &BufferLow[],
                  double &BufferDirection[]
                  )
      {
       int start;
       if(prev_calculated==0)
         {
          BufferDirection[0]=0;
          start=1;
         }
       else
         {
          start=prev_calculated-1;
         }

       for(int i=start;i<rates_total;i++)
         {

          BufferDirection[i]=BufferDirection[i-1];

          double buf[1];
          if(CopyBuffer(m_handle,0,rates_total-i-1,1,buf)<=0)return(0);

          if(buf[0]>0)
            {
             BufferDirection[i]=1;
            }
          else if(buf[0]<0)
            {
             BufferDirection[i]=-1;
            }
         }
       return(rates_total);
      }
   };

CCI的参数传给构造函数,然后再载入指标。在创建对象后还要使用 CheckHandle() 方法,主循环检查CCI并填充 BufferDirection 缓冲区,

"CZZDirection.mqh" 文件可以在附件中找到。 

用于绘图的类

画出之字转向有多个选项,它可以画成一根线,可以有颜色,可以在顶端带有点,等等。在本文中,我们将只讨论一种绘图方法,但是还是会创建基类和子类用于未来的开发。类将位于 "CZZDraw.mqh" 文件中, 类的名称 — "CZZDraw"。这个类将含有一个 Calculate() 虚方法,参数和用于方向的类相同。另外,有三个来自之字转向的数组会传给它: BufferLastHighBar (用于保存最近最高价的索引), BufferLastLowBar (用于保存最近最低价的索引), BufferZigZag (之字转向本身)。 

class CZZDraw
  {
private:
public:
   virtual int Calculate(const int rates_total,
                         const int prev_calculated,
                         double &BufferHigh[],
                         double &BufferLow[],
                         double &BufferDirection[],
                         double &BufferLastHighBar[],
                         double &BufferLastLowBar[],
                         double &BufferZigZag[]
                         )
     {
      return(0);
     }
  };
Child class: 
class CSimpleDraw:public CZZDraw
   {
private:
public:
    virtual int Calculate(const int rates_total,
                          const int prev_calculated,
                          double &BufferHigh[],
                          double &BufferLow[],
                          double &BufferDirection[],
                          double &BufferLastHighBar[],
                          double &BufferLastLowBar[],
                          double &BufferZigZag[]
                          )
      {
       int start;
       if(prev_calculated==0)
         {
          BufferLastHighBar[0]=0;
          BufferLastLowBar[0]=0;
          start=1;
         }
       else
         {
          start=prev_calculated-1;
         }

       for(int i=start;i<rates_total;i++)
         {
          BufferLastHighBar[i]=BufferLastHighBar[i-1];
          BufferLastLowBar[i]=BufferLastLowBar[i-1];

          BufferZigZag[i]=EMPTY_VALUE;

          BufferZigZag[(int)BufferLastHighBar[i]]=BufferHigh[(int)BufferLastHighBar[i]];
          BufferZigZag[(int)BufferLastLowBar[i]]=BufferLow[(int)BufferLastLowBar[i]];

          switch((int)BufferDirection[i])
            {
             case 1:
                switch((int)BufferDirection[i-1])
                  {
                   case 1:
                      if(BufferHigh[i]>BufferHigh[(int)BufferLastHighBar[i]])
                        {
                         BufferZigZag[(int)BufferLastHighBar[i]]=EMPTY_VALUE;
                         BufferZigZag[i]=BufferHigh[i];
                         BufferLastHighBar[i]=i;
                        }
                      break;
                   case -1:
                      BufferZigZag[i]=BufferHigh[i];
                      BufferLastHighBar[i]=i;
                      break;
                  }
                break;
             case -1:
                switch((int)BufferDirection[i-1])
                  {
                   case -1:
                      if(BufferLow[i]<BufferLow[(int)BufferLastLowBar[i]])
                        {
                         BufferZigZag[(int)BufferLastLowBar[i]]=EMPTY_VALUE;
                         BufferZigZag[i]=BufferLow[i];
                         BufferLastLowBar[i]=i;
                        }
                      break;
                   case 1:
                      BufferZigZag[i]=BufferLow[i];
                      BufferLastLowBar[i]=i;
                      break;
                  }
                break;
            }
         }
       return(rates_total);
      }
   };
这个类不会详细讨论,因为这些已经在 "基于最高价/最低价的简单之字转向指标" 和 "基于收盘价的简单之字转向指标"中描述过了"CZZDraw.mqh" 文件可以在附件中找到

整合三个类

最终,需要使用以上创建的三个类来开发一个指标了。用于源数据的类使得可以使用价格数据以及RSI指标的数据,它经常工作于子窗口当中。价格数据可以在子窗口中显示,但是 RSI 无法在价格图表上显示。所以,我们将创建一个用于子窗口的指标。 

在 MetaEditor 中创建一个新的指标 (主菜单 — 文件 — 新建或者按下 Ctrl+N). 在指标创建向导中,输入名称 "iUniZigZagSW", 创建一个外部参数 "period" (int 类型, 数值为 12), 选择 OnCalculate(...,open,high,low,close) 事件处理函数, 创建以下的缓冲区: 

名称风格颜色
Hifh线绿色
Low线绿色
ZigZag线段红色
Direction线
LastHighBar线 无 
LastLowBar线
在创建新指标之后,包含三个类文件: 
#include <CSorceData.mqh>
#include <CZZDirection.mqh>
#include <CZZDraw.mqh>

指标应该有参数用于选择源数据的类型和确定方向的类型,为此创建两个枚举:

enum ESorce
  {
   Src_HighLow=0,
   Src_Close=1,
   Src_RSI=2,
   Src_MA=3
  };
enum EDirection
  {
   Dir_NBars=0,
   Dir_CCI=1
  };

创建两个这些类型的外部参数: 

input ESorce      SrcSelect=Src_HighLow;
input EDirection  DirSelect=Dir_NBars;

RSI和MA源数据需要对应的参数,CCI指标也是一样,加上它们:

input int                  RSIPeriod   =  14;
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;
input int                  MAPeriod    =  14;
input int                  MAShift     =  0;
input ENUM_MA_METHOD       MAMethod    =  MODE_SMA;
input ENUM_APPLIED_PRICE   MAPrice     =  PRICE_CLOSE;
input int                  CCIPeriod   =  14;
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL;

用于确定n个柱的趋势的额外参数也要加上:

input int                  ZZPperiod   =  14;

现在有更有趣的事情了 — 对应基类类型的三个指针 (在外部参数下方):

CSorceData * src;
CZZDirection * dir;
CZZDraw * zz;

在 OnInit 函数中,根据 SrcSelect 和 DirSelect 变量载入对应的子类,首先, SrcSelect:

switch(SrcSelect)
  {
   case Src_HighLow:
      src=new CHighLow();
      break;
   case Src_Close:
      src=new CClose();
      break;
   case Src_RSI:
      src=new CRSI(RSIPeriod,RSIPrice);
      break;
   case Src_MA:
      src=new CMA(MAPeriod,MAShift,MAMethod,MAPrice);
      break;
  }

在载入之后,检查句柄:

if(!src.CheckHandle())
  {
   Alert("载入指标出错");
   return(INIT_FAILED);
  }

在那以后,DirSelect:

switch(DirSelect)
  {
   case Dir_NBars:
      dir=new CNBars(ZZPeriod);
      break;
   case Dir_CCI:
      dir=new CCCIDir(CCIPeriod,CCIPrice);
      break;
  }

检查句柄:

if(!dir.CheckHandle())
  {
   Alert("载入指标2出错");
   return(INIT_FAILED);
  }

第三个类:

zz = new CSimpleDraw();

在 OnDeinit() 函数中删除对象:

void OnDeinit(const int reason)
  {
   if(CheckPointer(src)==POINTER_DYNAMIC)
     {
      delete(src);
     }
   if(CheckPointer(dir)==POINTER_DYNAMIC)
     {
      delete(dir);
     }
   if(CheckPointer(zz)==POINTER_DYNAMIC)
     {
      delete(zz);
     }
  }

现在,最后一步 — OnCalculate() 函数。CSorceData 和 CZZDirectione类的 Calculate()方法可以返回0,这样就检查结果。如果有错 (收到0值), 也返回零,在下一个订单时刻会重新完整计算:

int rv;

rv=src.Calculate(rates_total,
                 prev_calculated,
                 time,
                 open,
                 high,
                 low,
                 close,
                 tick_volume,
                 volume,
                 spread,
                 HighBuffer,
                 LowBuffer);

if(rv==0)return(0);

rv=dir.Calculate(rates_total,
                 prev_calculated,
                 HighBuffer,
                 LowBuffer,
                 DirectionBuffer);

if(rv==0)return(0);

zz.Calculate(rates_total,
             prev_calculated,
             HighBuffer,
             LowBuffer,
             DirectionBuffer,
             LastHighBarBuffer,
             LastLowBarBuffer,
             ZigZagBuffer);

return(rates_total);

iUniZigZagSW" 指标可以在附件中找到。

价格图表的变化

得到的指标包含了所有之前创建的内容,包括对应图表以及针对子窗口的源数据,所以它是为子窗口创建的,但是,最好也能够在价格图表上看到之字转向,在这种情况下,就要牺牲 RSI 的数据源了,复制一份指标,并命名为 "iUniZigZag", 把 indicator_separate_window 属性改为 indicator_chart_window, 从 ESorce 枚举中删除 Src_RSI 变量, 把 RSI 相关处理从 OnInit() 函数中删除,就能够得到针对价格图表的指标。完整的 "iUniZigZag" 指标可以在附件中找到。   

价格的变化

可以为 MetaTrader 终端创建这样的指标,它不是基于严格定义的源数据,而是图表上的任何其它指标。当把这样的指标附加到图表或者子窗口中时,把 "Apply to(应用到)" 参数设为 "Previous indicator's data(前一指标数据)" 或者 "First indicator's data(第一个指标的数据)"。让我们修改 "iUniZigZagSW" 指标, 这样它就能够 "放到" 其它指标上了。把指标另存为 "iUniZigZagPriceSW" 并且删除一切与 CSorceData 类相关的内容,修改 OnCalculate 函数的类型,并且在函数的开始部分,写一个循环来发布 HighBuffer 和 LowBuffer 缓冲区中的价格数组数值:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[]
                )
  {
   int start;
   if(prev_calculated==0)
     {
      start=0;
     }
   else
     {
      start=prev_calculated-1;
     }

   for(int i=start;i<rates_total;i++)
     {
      HighBuffer[i]=price[i];
      LowBuffer[i]=price[i];
     }
   int rv;
   rv=dir.Calculate(rates_total,
                    prev_calculated,
                    HighBuffer,
                    LowBuffer,
                    DirectionBuffer);

   if(rv==0)return(0);
   zz.Calculate(rates_total,
                prev_calculated,
                HighBuffer,
                LowBuffer,
                DirectionBuffer,
                LastHighBarBuffer,
                LastLowBarBuffer,
                ZigZagBuffer);
   return(rates_total);
  }

类似地,也可以创建一个基于价格数组在价格图表上工作的指标,为此,只要在"iUniZigZagPriceSW"指标中把 indicator_separate_window 属性改为 indicator_chart_window 就可以了。"iUniZigZagPriceSW" 指标可以在附件中找到。iUniZigZagPrice 指标也可以在那里找到,它是基于价格数组在价格图表上工作的版本。 

从EA交易中调用

一般来说,当从EA交易中调用之字转向指标时,它会在循环中搜索最近的顶部或者底部,迭代柱来在缓冲区中检查用于绘制之字转向的数值,这个过程非常慢。本文中开发的之字转向含有额外的缓冲区,这使得可以快速获取所有所需的数据。DirectionBuffer 缓冲区包含了之字转向最近段的方向数据,LastHighBarBuffer 和 LastLowBarBuffer 缓冲区包含了柱的索引,其中最近的顶部和底部已经记录了。当从一边计数时知道了柱的索引以及柱的数量,就可以计算从另外一边数柱的索引了 (指标从左向右计数的,而 CopyBuffer () 函数是从右向左计数的),使用柱的索引,就可以得到之字转向在这个柱的数值。

以下代码可以用于从指标中获取数据,让我们使用 "iUniZigZagSW" 指标来做试验,在 OnInit() 函数中载入指标:

handle=iCustom(Symbol(),Period(),"iUniZigZagSW",SrcSelect,
               DirSelect,
               RSIPeriod,
               RSIPrice,
               MAPeriod,
               MAShift,
               MAMethod,
               MAPrice,
               CCIPeriod,
               CCIPrice,
               ZZPeriod);
在OnTick()函数中取得方向并在图表的注释中输出它:
   string cs="";
// 方向
   double dir[1];
   if(CopyBuffer(handle,3,0,1,dir)<=0)
     {
      Print("从之字转向中获取数据出错");
      return;
     }
   if(dir[0]==1)
     {
      cs=cs+"方向向上";
     }
   if(dir[0]==-1)
     {
      cs=cs+"方向向下";
     }
   Comment(cs,"\n",GetTickCount());

现在取得最近几个顶部/底部的数值,如果指标线是从下向上的,我们就从 LastHighBarBuffer 缓冲区取得最近顶部的柱的索引,然后用它来计算从右向左计数时柱的索引。这个索引可以用于取得 ZigZagBuffer 缓冲区中的数值,我们可以再进一步: 从 LastLowBarBuffer 缓冲区中取得柱的数值,在其中也就是之字转向的数值。这将是前一个底部的柱的索引,然后可以继续,交换调用 LastHighBarBuffer 和 LastLowBarBuffer 缓冲区来收集指标线中所有顶部/底部的数值,以下是在方向向上时用于取得之字转向中最近两个点的代码示例: 

if(dir[0]==1)
  {
   // 从左开始数最近顶部的柱的索引
   if(CopyBuffer(handle,4,0,1,lhb)<=0)
     {
      Print("从 ZigZag 2中获取数据出错");
      return;
     }
   // 从右开始数柱的索引
   ind=bars-(int)lhb[0]-1;

   // 这个柱的 ZigZag 数值
   if(CopyBuffer(handle,2,ind,1,zz)<=0)
     {
      Print("从 ZigZag 3中获取数据出错");
      return;
     }
   //===
   // 这个顶部之前底部的索引
   if(CopyBuffer(handle,5,ind,1,llb)<=0)
     {
      Print("从 ZigZag 4中获取数据出错");
      return;
     }
   // 当从右开始数时柱的索引
   ind=bars-(int)llb[0]-1;

   // 在索引处柱的 ZigZag 数值
   if(CopyBuffer(handle,2,ind,1,zz1)<=0)
     {
      Print("从 ZigZag 5获取数据出错");
      return;
     }

   cs=cs+"\n"+(string)zz1[0]+" "+(string)zz[0];
  }
else if(dir[0]==-1)
  {

  }

完整的示例可以在附件中的 "eUniZigZagSW" EA交易中找到。此 EA 在图表的注释中输出有关之字转向方向的消息,在第二行上还有两个数字,代表之字转向的最近两个点 (图 7),第三行是由 GetTickCount() 函数返回的数字,是为了清楚看见EA正在工作。

 
图 7. 左上角包含了EA交易的输出信息

当然,指标中最近点的数据可以从 LastHighBarBuffer 和 LastLowBarBuffer 缓冲区中通过它们的第0位置或者第一个柱的数值获得,但是这个实例的重点在于连续地从之字转向点中展开任何数量的数据。

从另外的指标中调用

如果一个指标需要在另外指标的基础上运行,例如之字转向,只要使用iCustom()来调用指标就可以了。但是要复制并修改它,这种方法就要有所调整 (为了修改速度和简单性), 有时候不需要 (如果为了代码复用和扩展性)。本文创建的指标在开发其它指标时可以通过iCustom函数来访问,

在其本身,历史中的之字转向不再会准确,因为在历史中它会重新构建,但是,还有 LastHighBarBuffer 和 LastLowBarBuffer 缓冲区, 它们保存着之字转向中间状态时的数据。为了清晰起见,让我们创建一个指标来在指标线改变方向(DirectionBuffer 缓冲区中数值改变)时画出箭头,并且在之字转向中有新高/新低时(LastHighBarBuffer 和 LastLowBarBuffer缓冲区有数值改变)画出点,这个指标的代码不会详细讨论,它可以在附件中的 "iUniZigZagSWEvents" 中找到。指标的外观在图8中展示。

 
图 8. iUniZigZagSWEvents 指标

结论

因为本文毕竟是教学资料,它并没有提供完整可以使用的解决方案,所有在本文中创建的指标只选择了最小范围的源数据和类型用来确定方向,但是,创建指标的过程是非常详细描述的,所以,在理解了这篇文章以后,您就能够自己创建所有所需的子类了。另外,尝试取得一个完整的通用性指标会有困难,但不会在创建阶段还有使用阶段了。当在用于确定方向的数据源中加入各种指标时,需要在属性窗口中加入这些指标所需的参数。最后,参数的数量如果变得太多,使用这样的指标就不方便了。所以最好使用本文中获得的通用类来创建独立的指标。       

附件

  • iHighLowZigZag.mq5 — 基于最高价/最低价的简单之字转向指标。
  • iCloseZigZag.mq5 — 基于收盘价的之字转向指标。
  • CSorceData.mqh — 用于选择源数据的类。
  • CZZDirection.mqh — 用于决定之字转向方向的类。
  • CZZDraw.mqh — 用于绘制之字转向的类。
  • iUniZigZagSW.mq5 — 用于子窗口的通用之字转向指标。
  • iUniZigZag.mq5 — 用于价格图表的通用之字转向指标。
  • iUniZigZagPriceSW.mq5 — 基于子窗口中价格数组的通用之字转向指标。
  • iUniZigZagPrice.mq5 — 基于价格图标中价格数组的通用之字转向指标。 
  • eUniZigZagSW — 在EA交易中使用iCustom()函数来调用 "iUniZigZagSW" 指标。
  • iUniZigZagSWEvents — 创建另一个指标的实例,使用 iCustom() 函数来调用 "iUniZigZagSW" 指标。  

全部回复

0/140

量化课程

    移动端课程