下载MetaTrader 5

沃尔夫波形 (Wolfe Waves)

23 六月 2017, 10:36
Dmitry Fedoseev
1
9 475

内容

概述

沃尔夫波形 (Wolfe Wave) 是 Bill Wolfe 发现并描述的图形分析形态。此图案看起来像一个三角形或楔形 (沃尔夫称之为 '上升的楔子'), 并具有一些特殊的细微差别。比尔·沃尔夫 (Bill Wolfe) 提出的图形化方法可以检测到一种形态, 根据此形态可以找到入场的时刻和方向, 并且还有益于预测价格应达到的目标, 以及达到目标的时间。

在本文中, 我们将详细研究沃尔夫波形的检测和解释规则。我们将根据 通用之字折线 文章中的之字折线指标, 创建自动检测并显示波形的指标。我们还将根据结果指标创建一个简单的专家交易系统。此 EA 将允许我们测试指标绩效, 并得到比尔·沃尔夫所提出的图形分析的第一印象, 之后会在本文中讨论。 

检测沃尔夫波形的规则

我们来研究买入示例中的沃尔夫波形 (图例.1)。价格形成两个连续下降的低峰 (蓝线, 点 1 和 3), 以及两个连续下降的高峰 (点 2 和 4)。在点 4 逆转并形成高峰之后, 价格继续下滑。一旦价格触及点 1 — 点 3 的延长线, 就进行买入操作 (点 5)。 


图例. 1. 买入沃尔夫波形。蓝线是价格, 红线是形成的检测目标。在点 5 执行入场, 目标是点 7

1—3 和 2—4 延长线的交汇点 6 则为检测目标的到达时间。目标价位 (点 7) 的定义是 1—4 延长线与通过点 6 绘制的垂直线的交点。此方法不提供止损计算算法, 通常建议是您自行决定使用止损。以上是沃尔夫书中讲述的波形检测规则。 

当开发本文的指标时, 还会发现更多的规则。

  1.  点 3 必须远低于点 1, 以下条件应予以检查:

    v3<v1-d1

    此处:

    • v3 — 点 3 的价位;
    • v1 — 点 1 的价位;
    • d1 — 点 1 和 点 2 (线段 1''-2'') 之间的垂直距离乘以 K1 (K1 是属性窗口的参数, 其默省缺值为 0.1)。

  2. 检测目标的 1—4 延长线必须向上, 即点 4 必须远高于 点 1。以下条件应予以检查:

    v4>v1+d1;

    v4 — 点 4 的价位。 

  3. 点 4 必须远低于点 2, 以下条件应予以检查:

    v4<v2-d2;

    此处 v2 是点 2 的价位, d2 是点 2 和点 3 (线段 2''-3'') 之间的垂直距离乘以 K2 (K2 是属性窗口的参数, 其默省缺值为 0.1)。

  4. 检测目标到达时间的 2-4 与 1-3 延长线必须交汇点于右侧, 所以 2-2' 的高度必须远高于 4-4' 的高度。在此必须执行以下检查:

    h2-h4>K3*h2;

    此处 h2 是线段 2-2' 的高度, h4 是线段 4-4' 的高度, K3 是比率 (K3 是属性窗口的参数, 其默省缺值为 0.1)。

这些规则声明并未假定绝对正确。以后我们在指标的创建过程中还要详细描述。基于这些素材, 您可以根据自己的想法调整代码。 

选择使用的之字折线

开始之前, 我们下载 附件, 它包含许多 通用之字折线 一文中的多种版本之字折线指标。我们需要从中选择一个在我们的文章中使用。我们不使用 iUniZigZagPrice 和 iUniZigZagPriceSW, 它们设计时基于图表上运行的其它指标进行计算, 因此它们仅对视觉分析有用。其它指标似乎更有趣。它们当中的每一个都可用来创建专家交易系统。此外, 我们不会使用 iCloseZigZag 和 iHighLowZigZag, 它们只是如何创建之字折线的初始示例。剩下两个版本, 即 iUniZigZag 和 iUniZigZagSW。在子窗口中工作的 iUniZigZagSW 指标更适合我们, 因为它提供了更广泛的功能。附件中也包含 iUniZigZagSWEvents 指标, 并展示了使用 iCustom() 函数访问 iUniZigZagSW 指标的示例。我们将使用此变体, 因为它将允许我们使用 iUniZigZagSW 指标的所有可能性, 且可另行将沃尔夫波形的检测代码与之字折线代码分离。

iUniZigZagSWEvents 指标显示在价格图表上, 四个缓冲区用于绘制指标: 两个带箭头的缓冲区, 另外两个画点。这些就是我们检测定沃尔夫波形所需要的。箭头将指示形态识别位置, 点则用于目标。我们的指标将用到 图形对象, 特别是 趋势线 绘制波形和构型以检测目标。如果您将其绘制为线段而非延伸射线, 那么它会是显示不同构型的非常方便的工具。  

除了检测入场时刻和方向外, 沃尔夫波形也用于预测目标。所以, 当使用 iUniZigZagSW 时会出现困难。指标带有 SrcSelect 参数, 可以根据所绘制的之字折线选择分析数据的来源。可以选择以下四个选项之一:

  • Src_HighLow — 按照最高价和最低价;
  • Src_Close — 按照收盘价;
  • Src_RSI — 按照 RSI 指标;
  • Src_MA — 按照移动均线。

基于我们现正创建的指标创建专家交易系统。此即为什么如果我们利用价格来构建之字折线, 那么预测的目标就可以用来放置止盈位。在图表上显示目标没有任何问题。但是如果使用RSI (SrcSelect=Src_RSI) 计算之字折线, 则预测目标将是 RSI 指标, 而非价格。所以, 一旦 RSI 指标达到目标值, 我们就需要市价平仓, 而不可能在图表上显示目标价格和附加构型。

当使用基于价格 (Src_HighLow 或 Src_Close) 绘制的之字折线时, 目标价格和附加构型将显示在图表上。在所有其它情况下, 只会显示一个箭头, 表示已发现的结构及其方向。目标值仍然在适相应的价格缓冲区中提供 (为了能够让专家交易系统以市价平仓来应对任何其它目的), 但不会显示。

很有可能在实践中, 当指标达到目标价位时, 市价平仓的想法无法实现。大多数指标的值在一定范围内变化, 目标结果可能在此范围之外。但是, 在任何情况下, 缓冲区都将包含目标值。

收集关于之字折线峰值的数据

我们现在开始创建指标。我们在编辑器中打开 iUniZigZagSWEvents, 文件并将其保存为 iWolfeWaves。我们将操控这个指标。

直接访问所有的之字折线峰值非常方便 — 在此情况下, 我们不必每次都在历史中搜索它们。我们来创建一个数组保存数值。现在, 每当之字折线改变方向时, 一个新的元素将被添加到数组中。如果指标简单地延伸最后一个线段 (更新极值), 则数组的最后一个元素将被更新。

对于每个峰值, 我们将保存峰值、方向和所在柱线的索引 (索引从左到右)。为此目的, 我们将使用一个含有三个字段的 结构:

struct SPeackTrough{
   double   Val; // 峰值
   int      Dir; // 方向
   int      Bar; // 柱线索引
};

我们来创建一个这些结构的数组:

SPeackTrough PeackTrough[];

如果之字折线仅仅基于最高价和最低价 (SrcSelect = Src_HighLow), 当方向改变的情况下将数组递增就足够了, 设置数值并用指标最后延伸的一段更新最后的元素。基于收盘价 (SrcSelect = Src_Close) 或任何其它指标数据的之字折线更难办。在柱线形成期间, 方向改变, 之字折线可以返回原来的状态 (即当前柱线开盘之前)。这意味着对于相同柱线的每次重新计算, 峰值数组需要返回到前一根柱线的初始状态。如果我们经常更改数组大小, 这可能会减慢指标的运行。因此, 我们来引入一个附加的变量, 所用数组大小将被保存其内。必要时, 数组将以块为单位进行修改, 只允许尺寸增长。重新计算同一根柱线之前, 我们要返回此变量的初始值。

我们将使用两个变量来存储数组大小。在一个变量中, 存储前一根柱线时的数组大小。当前计算的柱线时的大小将存储在第二个变量当中:

int PreCount; // 前一根柱线时 PeackTrough 数组的大小
int CurCount; // 当前计算柱线时 PeackTrough 数组的大小

在柱线形成并计算完成后, 或计算历史柱线之后, CurCount 变量的值应转移到 PreCount 变量。然后, 在每次计算新形成的柱线之前, 我们要把数值从 PreCount 移动到 CurCount。只有 CurCount 变量将会用于所有的计算。PreCount 变量只是辅助。关于柱线结构完毕的信息只能在下一根柱线开盘 (或计算切换到历史中的下一根柱线) 时才知道。新柱线的出现将由时间决定: 如果柱线时间已经改变, 则会出现一根新柱线 (或计算历史中的下一根柱线已经开始)。需要一个辅助变量以便确定新的柱线:

datetime LastTime;

PreCount, LastCount 和 LastTime 是指标的全局变量。但是它们也可以在 OnCalculate() 指标函数里声明为静态变量。 

我们转进到 OnCalculate() 函数。基于 prev_calculated 的值, 判断是第一次执行指标计算还是仅计算新的柱线。0 意即全部计算。在此情况下, 变量 PreCount, CurCount 和 LastTime 需要初始化。以下代码位于 OnCalculte() 函数的最上面, 定义计算的柱线范围, 并初始化辅助变量:

int start; // 起始计算的柱线的索引变量

if(prev_calculated==0){ // 计算全部的柱线
   start=1;
   CurCount=0;   
   PreCount=0;
   LastTime=0;
}
else{ // 计算新柱线
   start=prev_calculated-1;
}

现在我们来处理标准的指标循环。在一开始, 我们要将变量 PreCount, CurCount 中的数值组织转移:

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

   if(time[i]>LastTime){ // 计算新 (下一次) 柱线
      LastTime=time[i];
      PreCount=CurCount;
      PreDir=CurDir;
   }
   else{ // 柱线重新计算
      CurCount=PreCount;
      CurDir=PreDir;
   }


在所有的计算当中, 仅适用 CurCount 变量, 而 PreCount is 仅设计用于维护当前的 CurCount 值。在新柱线开盘伊始, CurCount 首先包含前一根柱线计算后获得的数值。这就是为什么我们要把这个数值移动到 PreCount。在新柱线计算之后, CurCount 的数值可以改变。但是, 我们只能在下一根柱线的开盘时才能确定该值是最终的。这就是为什么在重新计算同一柱线的情况下, PreCount 变量的数值被放置到 CurCount。

主要指标循环应包含取自 iUniZigZagSWEvents 指标的以下代码:

UpArrowBuffer[i]=EMPTY_VALUE;
DnArrowBuffer[i]=EMPTY_VALUE;

UpDotBuffer[i]=EMPTY_VALUE;
DnDotBuffer[i]=EMPTY_VALUE;      

// 方向

double dir[2];
if(CopyBuffer(handle,3,rates_total-i-1,2,dir)<=0){
   return(0);
}
if(dir[0]==1 && dir[1]==-1){
   DnArrowBuffer[i]=high[i];
   c++;

}
else if(dir[0]==-1 && dir[1]==1){
   UpArrowBuffer[i]=low[i];
   c++;
}

// 新的最高价

double lhb[2];
if(CopyBuffer(handle,4,rates_total-i-1,2,lhb)<=0){
   return(0);
}
if(lhb[0]!=lhb[1]){
   UpDotBuffer[i]=high[i];
}

// 新的最低价

double llb[2];
if(CopyBuffer(handle,5,rates_total-i-1,2,llb)<=0){
   return(0);
}
if(llb[0]!=llb[1]){
   DnDotBuffer[i]=low[i];
}  

绘制箭头的代码部分将不会被使用, 所以我们来删除它。

在操作期间, 由于需要最后一个线段来检测点 5 (见图例.1), 指标将监控之字折线的每次变化, 包括方向改变和最后线段的每次延伸。我们将使用上述片段中的部分代码, 这与绘制新极值有关。

若要监控之字折线的方向并确定其变化, 我们将需要几个类似于 CurCount 和 PreCount 的变量: PreDir 和 CurDir:

int PreDir; // 前一根柱线的之字折线方向
int CurDir; // 当前柱线的之字折线方向

在 OnCalculate() 中它们即可以是全局的, 也可是静态的。在指标计算伊始, 我们还需要初始化这些变量, 并在柱线计算之后移动数值, 类似于 PreCount 和 CurCount。以下是 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[])
  {

   int start; // 用于计算起始柱线的变量
   
   if(prev_calculated==0){ // 全部计算 
      start=1; 
      CurCount=0;
      PreCount=0;
      CurDir=0;
      PreDir=0;      
      LastTime=0;
   }
   else{ // 仅计算新的柱线
      start=prev_calculated-1;
   }

   // 指标主循环
   for(int i=start;i<rates_total;i++){
   
      if(time[i]>LastTime){ // 新柱线
         LastTime=time[i];
         PreCount=CurCount;
         PreDir=CurDir;
      }
      else{ // 柱线重新计算
         CurCount=PreCount;
         CurDir=PreDir;
      }

      // 清除绘制箭头和点的缓冲区
      
      UpArrowBuffer[i]=EMPTY_VALUE;
      DnArrowBuffer[i]=EMPTY_VALUE;
      
      UpDotBuffer[i]=EMPTY_VALUE;
      DnDotBuffer[i]=EMPTY_VALUE;    
      
      // 辅助变量
      
      double hval[1];
      double lval[1];
      
      double zz[1];
      
      // 新的最高价
      
      double lhb[2];
      // 接收缓冲区的两个元素, 内带新的最高价的柱线索引
      if(CopyBuffer(handle,4,rates_total-i-1,2,lhb)<=0){ 
         return(0);
      }
      if(lhb[0]!=lhb[1]){ // 这是新的最高价
         // 获取最高价格 (或基于之字折线计算的数据)
         if(CopyBuffer(handle,0,rates_total-i-1,1,hval)<=0){
            return(0);
         }      
         if(CurDir==1){ // 已知的最后向上方向
            // 更新有关最后一个之字折线拐点的信息
            RefreshLast(i,hval[0]);
         }
         else{ // 之字折线方向已改变
               // 加入新的数值
            AddNew(i,hval[0],1);
         }
         // 此处, 我们将添加检查条件以便识别向下的沃尔夫波形  
      }
      
      // 新的最低价
      
      double llb[2];
      // 接收缓冲区的两个元素, 内带新的最低价的柱线索引
      if(CopyBuffer(handle,5,rates_total-i-1,2,llb)<=0){
         return(0);
      }
      if(llb[0]!=llb[1]){ // 这是新的最低价
         // 获取最低价格 (或基于之字折线计算的数据)
         if(CopyBuffer(handle,1,rates_total-i-1,1,lval)<=0){
            return(0);
         }         
         if(CurDir==-1){ // 已知的最后向下方向
            // 更新有关最后一个之字折线拐点的信息
            RefreshLast(i,lval[0]);
         }
         else{ // 之字折线方向已改变
            // 加入新的数值
            AddNew(i,lval[0],-1);
         }
         // 此处, 我们将添加检查条件以便识别向上的沃尔夫波形 
      }      
   }   
   return(rates_total);
}

此代码包含 AddNew() 和 RefreshLast() 函数。之字折线变化的柱线索引和新的极值被传递给这两个函数。之字折线的方向也一并传递给 AddNew()。

添加新点的 AddNew() 函数:

void AddNew(int i,double v,int d){
   if(CurCount>=ArraySize(PeackTrough)){ // 数组中没有可用元素
      ArrayResize(PeackTrough,ArraySize(PeackTrough)+1024); // 增加数组大小
   }
   PeackTrough[CurCount].Dir=d; // 设置方向
   PeackTrough[CurCount].Val=v; // 设置数值
   PeackTrough[CurCount].Bar=i; // 设置柱线
   CurCount++; // 使用数组占用元素的数量增加变量
   CurDir=d; // 记忆最后的之字折线方向
} 

RefreshLast() 函数用来刷新最后一个点:

void RefreshLast(int i,double v){
   PeackTrough[CurCount-1].Bar=i; // 设置新柱线
   PeackTrough[CurCount-1].Val=v; // 设置新数值
} 

指标可以在现阶段得以保存, 它可作为开发各种定义之字折线形态的指标的基础。在以下附件中, 指标名称为 "iWolfeWaves_Step_1"。

一点几何

当识别沃尔夫波形并添加检测目标的形状时, 我们需要一点几何知识。我们分开看这些问题, 并编写函数来解决它​​们。

问题 #1. 一条直线由一对点 x-y 设置, 其中 x 是柱线索引, y 是数值 (价格或指标值)。我们知道第三点的 x 坐标, 我们需要找到这一点在线上的数值 (图例. 2)。


图例. 2. 给定: X1, Y1, X2, Y2, X3. 我们需要找出 Y3。

问题 #1 解决方案。直线沿 X 轴的数值每增加一个单位, 我们检测一次直线沿 Y 轴的增加值:

D=(Y2-Y1)/(X2-X1)

此处 D 是增量, Y1 是点 1 处的价格或指标值, Y2 是点 2 处的价格或指标值, X1 是点 1 处的柱线索引, X2 是点 2 处的柱线索引。  

检测 Y3:

Y3=Y1+(X3-X1)*D

此处 X3 是点 3 处的柱线索引, Y3 是点 3 处搜寻线的值。

我们得到以下函数:

double y3(double x1,double y1,double x2,double y2,double x3){
   return(y1+(x3-x1)*(y2-y1)/(x2-x1));
}

以下参数需要传递给函数:

  • x1 — 点 1 处的柱线索引;
  • y1 — 点 1 处的数值;
  • x2 — 点 2 处的柱线索引;
  • y2 — 点 2 处的数值。

问题 #2. 两条线由两个 x-y 点设定。我们需要找到它们交汇点的 x 坐标 (图例. 3)。在此可能会出现以下问题: 为什么我们选择了 x 坐标? 在任何情况下, 获得 x 坐标之后, 将计算点 3 的 y 坐标 (使用其中一条线的方程)。因此, 我们可以先获得点 3 的 y 坐标, 然后使用方程找到 x 值。  


图例. 3. 两条给定直线。我们需要找到它们的交叉点


首先, 使用两点的坐标, 得到 y=a+b*x 形式的线方程。

我们来进行预计算。直线斜率值 (x 轴的每一单位与 y 轴每一单位的比值):

D1=(Y12-Y11)/(X12-X11)

此处, D1 是第一条线的期望斜率值 (每根柱线的直线值变化), X11 是第一条线点1 处的柱线索引, X12 是第一条线点 2 处的柱线索引, Y11 是第一条线点 1 处的值, Y12 是第一条线点 2 处值。     

第二条线的斜率:

D2=(Y22-Y21)/(X22-X21)

此处, D2 是第二条线的期望斜率值 (每根柱线的直线值变化), X21 是第二条线点1 处的柱线索引, X22 是第二条线点 2 处的柱线索引, Y21 是第二条线点 1 处的值, Y22 是第二条线点 2 处值。

此处是直线方程。线 1 方程:

Y3=Y11+D1*(X3-X11)

此处 Y3 是交汇点 (点 3) 处的直线值, X3 是点 3 处的柱线索引。

线 2 方程:

Y3=Y21+D2*(X3-X21)

在交点处, 线的值相等。所以我们将线 1 方程与线 2 方程划等号:

Y11+D1*(X3-X11)=Y21+D2*(X3-X21);

使用得到的表达式, 我们发现 X3。作为结果, 我们得到用于检测交点 X 坐标的 TwoLinesCrossX() 函数:

double TwoLinesCrossX(double x11,double y11,double x12,double y12,double x21,double y21,double x22,double y22){
   double k2=(y22-y21)/(x22-x21);
   double k1=(y12-y11)/(x12-x11);
   return((y11-y21-k1*x11+k2*x21)/(k2-k1));
}

以下参数需要传递给函数:

  • x11, 第一条线点 1 处的柱线索引
  • y11, 第一条线点 1 处的值
  • x12, 第一条线点 2 处的柱线索引
  • y12, 第一条线点 2 处的值
  • x21, 第二条线点 1 处的柱线索引
  • y21, 第二条线点 1 处的值
  • x22, 第二条线点 2 处的柱线索引
  • y22, 第二条线点 2 处的值

一旦定义了线-线交点处的 x 坐标, 可使用一条线的两点的坐标, 以及解决问题 #1 时获得的函数 y3() 来求解 y 坐标。

如果我们需要首先获得 y 坐标, 则应该转换直线方程, 以便通过 y 来表示 x 坐标。 以下是一条线的方程:

X3=X11+(Y3-Y11)/D1

第二条线的方程:

X3=X21+(Y3-Y21)/D2

将两个表达式划等号:

X11+(Y3-Y11)/D1=X21+(Y3-Y21)/D2

我们基于以上方程找到 Y3。因此, 我们获得了 TwoLinesCrossY() 函数:

double TwoLinesCrossY(double x11,double y11,double x12,double y12,double x21,double y21,double x22,double y22){
   double k2=(x22-x21)/(y22-y21);
   double k1=(x12-x11)/(y12-y11);
   return((x11-x21-k1*y11+k2*y21)/(k2-k1));
}

函数参数与 TwoLinesCrossX() 的相同。 

检测波形

现在我们可以轻松访问所有之字折线峰值和辅助几何函数, 我们可以继续检测沃尔夫波形。我们需要 "捕捉" 最后一个之字折线的线段穿过直线 1-3 (见图例.1) 的时刻, 即点 5。因此, 当每次出现新的之字折线极值时我们要检查沃尔夫波形条件 (方向改变, 以及最后的线段延伸时)。在上面的 OnCalculate() 函数代码中, 所有应检查条件的地方均有详细的注释。从它们那里会调用 CheckDn() 和 CheckUp() 函数。我们来详细研究 CheckUp() 函数:

void CheckUp(int rates_total,const double & low[],const datetime & time[],int i){

   if(CurCount<5 || CurDir!=-1){ 
      // 如果没有足够的峰值, 或之字折线没有指向下方, 无需检查
      return;
   }   
   
   // 用峰值数据准备 short 变量 

   // 峰值数据变量
   double v1=PeackTrough[CurCount-5].Val;
   double v2=PeackTrough[CurCount-4].Val;
   double v3=PeackTrough[CurCount-3].Val;
   double v4=PeackTrough[CurCount-2].Val;
   double v5=PeackTrough[CurCount-1].Val;
   
   // 峰值柱线变量
   int i1=PeackTrough[CurCount-5].Bar;
   int i2=PeackTrough[CurCount-4].Bar;               
   int i3=PeackTrough[CurCount-3].Bar;
   int i4=PeackTrough[CurCount-2].Bar;
   int i5=PeackTrough[CurCount-1].Bar;
                  
   if(CurLastBuySig!=i4){ // 如果在此之字折线结构中未检测到波形
      double d1=K1*(v2-v1); // 相对于峰值 1 的缩进峰值 3 的最小值
      if(v3<v1-d1){ // 峰值 3 明显低于峰值 1
         if(v4>v1+d1){ // 线 1-4 指而向上
            double d2=K2*(v2-v3); // 相对于峰值 2 的缩进峰值 4 的最小值
            if(v4<v2-d2){ // 峰值 4 明显低于峰值 2
               double v5l=y3(i1,v1,i3,v3,i); // 点 5 处的值
               if(v5<v5l){ // 之字折线的最后线段与线 1-3 交叉
                  double v4x=y3(i1,v1,i3,v3,i4); // 点 4' 处的值
                  double v2x=y3(i1,v1,i3,v3,i2); // 点 2' 处的值
                  double h4=v4-v4x; // 线 4-4' 高
                  double h2=v2-v2x; // 线 2-2' 高
                  if(h2-h4>K3*h2){ // 线 1-3 和 2-4 相遇 
                     double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4); // 线 1-3 与 2-4 交汇处的柱线
                     double tv=y3(i1,v1,i4,v4,tb); // 线 1-3 与 2-4 交汇点处的值
                     UpArrowBuffer[i]=low[i]; // 显示向上箭头
                     UpDotBuffer[i]=tv; // 显示目标价位的点
                     CurLastBuySig=i4; // 记住, 在此之字折线配置中已发现形状
                     if(_DrawWaves){ // 绘制结构
                        DrawObjects(BuyColor,BuyTargetColor,v1,v2,v3,v4,v5l,i1,i2,i3,i4,i5,time,i,tb,tv,rates_total);
                     }
                  }
               }
            }
         }
      }
   }
}

为了检测一个波形, 我们至少需要 5 个之字折线峰值。附加条件: 为了检测稍后转而向上的波形, 折线应指向下方:

if(CurCount<5 || CurDir!=-1){ 
   // 如果没有足够的峰值, 或之字折线没有指向下方, 无需检查
   return;
}   

若要获得峰值数据, 我们可以直接对 PeakTrough 数组进行寻址, 但这样很不方便。使用简短名称的辅助变量更为简单:

// 峰值数据变量
double v1=PeackTrough[CurCount-5].Val;
double v2=PeackTrough[CurCount-4].Val;
double v3=PeackTrough[CurCount-3].Val;
double v4=PeackTrough[CurCount-2].Val;
double v5=PeackTrough[CurCount-1].Val;
   
// 峰值柱线变量
int i1=PeackTrough[CurCount-5].Bar;
int i2=PeackTrough[CurCount-4].Bar;               
int i3=PeackTrough[CurCount-3].Bar;
int i4=PeackTrough[CurCount-2].Bar;
int i5=PeackTrough[CurCount-1].Bar;

如果已经检测到一个波形, 且已设置了一个箭头, 则不再需要使用相同的之字折线配置。通过检查峰值 4 的索引 (最后形成的峰值) 来识别之字折线配置:

if(CurLastBuySig!=i4){ // 如果在此之字折线结构中未检测到波形

若要保存配置 ID 的值, 我们使用一对类似于 CurCount 和 PreCount 的变量。

现在我们直接进行波形检测。我们计算点 3 相对于点 1 的最小偏移值, 点 2 相对于点 1 的位移:

double d1=K1*(v2-v1); // 相对于峰值 1 的缩进峰值 3 的最小值


然后检查点的位移:

if(v3<v1-d1){ // 峰值 3 明显低于峰值 1
   if(v4>v1+d1){ // 线 1-4 指而向上

我们计算相对于点 2 的点 4 缩进的最小值:

double d2=K2*(v2-v3); // 相对于峰值 2 的缩进峰值 4 的最小值

检查点 2 和点 4 的位置:

if(v4<v2-d2){ // 峰值 4 明显低于峰值 2

现在我们来计算位于线 1-3 上的点对应于所计算柱线的值:

double v5l=y3(i1,v1,i3,v3,i); // 点 5 处的值

检查是否触及线 1-3:

if(v5<v5l){ // 之字折线的最后线段与线 1-3 交叉

计算点 4' 和点 2' 的值:

double v4x=y3(i1,v1,i3,v3,i4); // 点 4' 处的值
double v2x=y3(i1,v1,i3,v3,i2); // 点 2' 处的值

计算 4-4' 和 2-2' 的高度:

double h4=v4-v4x; // 线 4-4' 高
double h2=v2-v2x; // 线 2-2' 高

利用这些高度, 检查线 1-3 和 2-4 是否在右侧相遇:

if(h2-h4>K3*h2){ // 线 1-3 和 2-4 相遇 

如果此条件满足, 意味着波形已发现。 

定义目标。首先定义目标柱线:

double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4); // 线 1-3 与 2-4 交汇处的柱线


请注意, "double" 变量用于计算精度。     

目标值:

double tv=y3(i1,v1,i4,v4,tb); // 线 1-3 与 2-4 交汇点处的值

 记住图标并 "记住" 之字折线配置的 ID:

UpDotBuffer[i]=tv; // 显示目标价位的点
CurLastBuySig=i4; // 记住, 在此之字折线配置中已发现形状

最后, 我们绘制检测目标的波形和结构:

if(_DrawWaves){ // 绘制结构
   DrawObjects(BuyColor,BuyTargetColor,v1,v2,v3,v4,v5l,i1,i2,i3,i4,i5,time,i,tb,tv,rates_total);
}

绘制波形和构型, 即 DrawObjects() 函数, 将在本文的另一部分中予以研究。 

向下的波形 (对于卖出) 可由 CheckDn 函数检测, 除了连接方向略有差异外, 与 CheckUp 相同。函数代码以及与 CheckUp() 函数的差异如下:

void CheckDn(int rates_total,const double & high[],const datetime & time[],int i){

   // 没有足够的峰值, 或并非指向上方 
   if(CurCount<5 || CurDir!=1){ 
      return;
   }

   double v1=PeackTrough[CurCount-5].Val;
   double v2=PeackTrough[CurCount-4].Val;
   double v3=PeackTrough[CurCount-3].Val;
   double v4=PeackTrough[CurCount-2].Val;
   double v5=PeackTrough[CurCount-1].Val;
   
   int i1=PeackTrough[CurCount-5].Bar;
   int i2=PeackTrough[CurCount-4].Bar;               
   int i3=PeackTrough[CurCount-3].Bar;
   int i4=PeackTrough[CurCount-2].Bar;
   int i5=PeackTrough[CurCount-1].Bar;
   
   if(CurLastSellSig!=i4){               
      double d1=K1*(v1-v2); // 峰值 v1 高于峰值 v2
      if(v3>v1+d1){ // 峰值 v3 高于峰值 v1
         if(v4<v1-d1){ // 峰值 v4 低于峰值 v1                     
            double d2=K2*(v3-v2); // 峰值 v3 高于峰值 v2                     
            if(v4>v2+d2){ // 峰值 v4 高于峰值 v2  
               double v5l=y3(i1,v1,i3,v3,i);
               if(v5>v5l){ // 之字折线突破线 1-3 向上
                  double v4x=y3(i1,v1,i3,v3,i4);
                  double v2x=y3(i1,v1,i3,v3,i2);
                  double h4=v4x-v4; // 点 4' 高于点 4
                  double h2=v2x-v2; // 点 2' 高于点 2
                  if(h2-h4>K3*h2){   
                     double tb=TwoLinesCrossX(i1,v1,i3,v3,i2,v2,i4,v4);
                     double tv=y3(i1,v1,i4,v4,tb);                              
                     DnArrowBuffer[i]=high[i];
                     DnDotBuffer[i]=tv;
                     CurLastSellSig=i4;   
                     if(_DrawWaves){
                        // 用其它颜色绘制
                        DrawObjects(SellColor,
                                    SellTargetColor,
                                    v1,
                                    v2,
                                    v3,
                                    v4,
                                    v5l,
                                    i1,
                                    i2,
                                    i3,
                                    i4,
                                    i5,
                                    time,
                                    i,
                                    tb,
                                    tv,
                                    rates_total);
                     }
                  }
               }
            }
         }
      }
   }
}

第一个区别是初步检查:

// 没有足够的峰值, 或并非指向上方 
if(CurCount<5 || CurDir!=1){ 
   return;
}

如果没有足够的峰值, 或之字折线指向下方, 则函数操作应该完毕。

对于指向下方, 峰值和波谷改变位置: 点 1, 3 , 5 , 6 位于上方, 点 2, 4, 7 位于下方, 因此公式中某些变量的位置也会发生变化。检测峰值 1, 3 和 1, 4 之间的最小距离:

double d1=K1*(v1-v2); // 峰值 v1 高于峰值 v2

检查峰值 1, 3 的位置:

if(v3>v1+d1){ // 峰值 v3 高于峰值 v1

检查峰值 1, 4 的位置:

if(v4<v1-d1){ // 峰值 v4 低于峰值 v1 

计算峰值 2, 3 之间的最小距离, 并检查它:

double d2=K2*(v3-v2); // 峰值 v3 高于峰值 v2                     
if(v4>v2+d2){ // 峰值 v4 高于峰值 v2  

检查点 5 是否形成 (之字折线突破线 1-3 向上):

if(v5>v5l){ // 之字折线突破线 1-3 向上

计算 2-2' 和 4-4' 高度, 并检查线 1-3 和 2-4 是否在右侧相遇:

double h4=v4x-v4; // 点 4' 高于点 4
double h2=v2x-v2; // 点 2' 高于点 2

波形和构型使用不同的颜色绘制:

// 用其它颜色绘制
DrawObjects(SellColor,
            SellTargetColor,
            v1,
            v2,
            v3,
            v4,
            v5l,
            i1,
            i2,
            i3,
            i4,
            i5,
            time,
            i,
            tb,
            tv,
            rates_total);

绘制波形和目标

所有波形和构型使用单一算法绘制, 所以使用这样的一个 DrawObjects() 函数。元素指向上方和下方会以不同的颜色绘制。为此, 颜色参数 BuyColor 或 SellColor 被传递给函数。波形和定义目标的构型也以不同的颜色绘制, 所以参数 BuyTargetColor 或 SellTargetColor 也传递给函数。这些变量是指标的外部变量, 您可以设置期望的颜色。除了颜色, 还需要更多一些外部参数。以下是所有波形与对象绘制函数所需的附加参数:

input bool   DrawWaves       =  true;             // 启用波形和对象绘制
input color  BuyColor        =  clrAqua;          // 买入波形颜色
input color  SellColor       =  clrRed;           // 卖出波形颜色
input int    WavesWidth      =  2;                // 波形宽度
input bool   DrawTarget      =  true;             // 附加的启用/禁用构型
input int    TargetWidth     =  1;                // 对象宽度
input color  BuyTargetColor  =  clrRoyalBlue;     // 买入对象颜色
input color  SellTargetColor =  clrPaleVioletRed; // 卖出对象颜色

颜色传递之后, 所有峰值柱线的值和索引变量传递给函数。峰值 5 是例外, 为此, 传递已计算的线 1-3 的值, 替代之字折线尾端的值。所有之字折线极点的坐标以柱线索引给出, 而图形对象需要时间, 所以将指向 "time" 数组的指针传递给函数。已计算柱线的索引 — i, 目标柱线索引 — tb, 目标值 — tv, 以及图表之上的柱线总数 — rates_total 一并传递给函数。 

我们已经注意到, 在本文的开头, 只有之字折线采用最高价/最低价 (SrcSelect 设置为 Src_HighLow) 或收盘价 (SrcSelect 设置为 Src_Close) 计算时, 才应绘制波形和对象。所以, 这取决于 SrcSelect 变量, 在 OnInit() 函数里应强制禁用绘图 (DrawWaves 变量)。为此目的, 我们声明使用一个附加变量, 替代 DrawWaves:

bool _DrawWaves;

接下来, 在 OnInit() 函数中, 我们设置 DrawWaves 变量的值, 或将其设置为 false 来禁用它。此外, 为目标绘制缓冲区设置一个不可见的颜色:

if(SrcSelect==Src_HighLow || SrcSelect==Src_Close){
   _DrawWaves=DrawWaves;
}
else{
   _DrawWaves=false;
   PlotIndexSetInteger(2,PLOT_LINE_COLOR,clrNONE);
   PlotIndexSetInteger(3,PLOT_LINE_COLOR,clrNONE);      
}   

我们继续进入 DrawObjects() 函数。首先, 我们提供了完整的函数代码, 然后我们会详细研究它:

void DrawObjects( color col,
                  color tcol,
                  double v1,
                  double v2,
                  double v3,
                  double v4,
                  double v5,
                  int i1,
                  int i2,
                  int i3,
                  int i4,
                  int i5,
                  const datetime & time[],
                  int i,
                  double target_bar,
                  double target_value,
                  int rates_total){

   // 用于图形对象名称的前缀 
   string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time[i])+"_";

   // 绘制波形                   
   fObjTrend(prefix+"12",time[i1],v1,time[i2],v2,col,WavesWidth);
   fObjTrend(prefix+"23",time[i2],v2,time[i3],v3,col,WavesWidth);   
   fObjTrend(prefix+"34",time[i3],v3,time[i4],v4,col,WavesWidth);
   fObjTrend(prefix+"45",time[i4],v4,time[i5],v5,col,WavesWidth);

   // 绘制构型
   if(DrawTarget){   
    
      datetime TargetTime;
      
      // 获取整数型的目标柱线索引 
      int tbc=(int)MathCeil(target_bar);
      
      if(tbc<rates_total){ // 目标位于图表上存在的柱线之内
         TargetTime=time[tbc];
      }
      else{ // 目标位于未来
         TargetTime=time[rates_total-1]+(tbc-rates_total+1)*PeriodSeconds();
      }
      
      // 计算目标所在柱线处的值
      double tv13=y3(i1,v1,i3,v3,tbc);   
      double tv24=y3(i2,v2,i4,v4,tbc);  
      double tv14=y3(i1,v1,i4,v4,tbc); 

      // 构型

      fObjTrend(prefix+"13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth);   
      fObjTrend(prefix+"24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth);  
      fObjTrend(prefix+"14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth);
      
      // 目标价位水平线
      fObjTrend(prefix+"67",TargetTime,tv24,TargetTime,tv14,tcol,TargetWidth);   
      // 目标价位垂直线 
      fObjTrend(prefix+"7h",time[i],target_value,TargetTime,target_value,tcol,TargetWidth);      
   }
}

所有绘图使用若干趋势线进行, 为此首先形成名称的公用前缀:

// 用于图形对象名称的前缀 
string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time[i])+"_";

然后绘制波形, 所有峰值的坐标传递给函数:

// 绘制波形                   
fObjTrend(prefix+"12",time[i1],v1,time[i2],v2,col,WavesWidth);
fObjTrend(prefix+"23",time[i2],v2,time[i3],v3,col,WavesWidth);   
fObjTrend(prefix+"34",time[i3],v3,time[i4],v4,col,WavesWidth);
fObjTrend(prefix+"45",time[i4],v4,time[i5],v5,col,WavesWidth);

绘制定义目标的结构。检查是否启用了绘图:

// 绘制构型
if(DrawTarget){  

如果已启用, 则绘制结构。当在历史数据上显示指标时, 目标柱线最有可能出现在已存在的柱线上, 但如果最近出现的柱线上检测到波形, 目标可能出现在将来, 即最后一根柱线的右侧。因此, 我们需要两个计算目标柱线时间的变体。为此目的我们声明一个变量:

datetime TargetTime;

target_bar 变量有一个分数值, 因此我们将其提升到最接近的整数:

// 获取整数型的目标柱线索引 
int tbc=(int)MathCeil(target_bar);

之后我们使用得到的 tbc 变量。在此, 我们可以使用 MathFloor() 函数, 并获得最近的较低的整数。这不会影响最终的结果, 因为构型只有一个提示性的目的。当使用 MathCeil(), 线 1-3 和 2-4 的端点必然在目标柱线附近相交, 且构型看起来更自然。

我们来检测目标抵达的时间。如果目标位于现存的柱线之一, 我们只需要计算目标柱线的索引, 并从 'time' 数组中获取时间。如果目标是在最后一根柱线的右侧, 那么我们要检测目标距离最后一根柱线多少根柱线, 并计算时间:

if(tbc<rates_total){ // 目标位于图表上存在的柱线之内
   TargetTime=time[tbc];
}
else{ // 目标位于未来
   TargetTime=time[rates_total-1]+(tbc-rates_total+1)*PeriodSeconds();
}

我们来计算所有直线 (1-3, 2-4 和 1-4) 在目标柱线处的值:

// 计算目标所在柱线处的值
double tv13=y3(i1,v1,i3,v3,tbc);   
double tv24=y3(i2,v2,i4,v4,tbc);  
double tv14=y3(i1,v1,i4,v4,tbc); 

尽管事实上早前计算的目标值已被传递到函数 (target_value 变量), 它仍然会重新为线 2-4 计算新的构型。这与事实相连, 替代来自 target_bar 变量的确切值, 我们使用来自 tbc 变量的值, 该值比 target_bar 稍大。经过这些计算, 我们确保在确切的 target_bar 坐标上, 直线将在 target_value 价位准确相交。

我们使用计算出值绘制直线:

fObjTrend(prefix+"13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth);   
fObjTrend(prefix+"24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth);  
fObjTrend(prefix+"14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth);

使用辅助 fObjTrend() 函数绘制直线:

void fObjTrend(   string  aObjName,
                  datetime aTime_1,
                  double   aPrice_1,
                  datetime aTime_2,
                  double   aPrice_2,
                  color    aColor      =  clrRed,  
                  color    aWidth      =  1,                
                  bool     aRay_1      =  false,
                  bool     aRay_2      =  false,
                  string   aText       =  "",
                  int      aWindow     =  0,                  
                  color    aStyle      =  0,
                  int      aChartID    =  0,
                  bool     aBack       =  false,
                  bool     aSelectable =  false,
                  bool     aSelected   =  false,
                  long     aTimeFrames =  OBJ_ALL_PERIODS
               ){
   ObjectCreate(aChartID,aObjName,OBJ_TREND,aWindow,aTime_1,aPrice_1,aTime_2,aPrice_2);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_BACK,aBack);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_COLOR,aColor);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTABLE,aSelectable);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTED,aSelected);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_TIMEFRAMES,aTimeFrames);
   ObjectSetString(aChartID,aObjName,OBJPROP_TEXT,aText);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_WIDTH,aWidth);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_STYLE,aStyle);
   ObjectSetInteger(aChartID,aObjName,OBJPROP_RAY_LEFT,aRay_1);   
   ObjectSetInteger(aChartID,aObjName,OBJPROP_RAY_RIGHT,aRay_2);
   ObjectMove(aChartID,aObjName,0,aTime_1,aPrice_1);
   ObjectMove(aChartID,aObjName,1,aTime_2,aPrice_2);   
}

它是一个通用函数, 也可用于快速创建趋势线并设置其所有参数。函数参数如表 1 所述。最频繁变化的参数在表的开头 (5 个必需参数), 其余的是可选的, 您可以选择不将其传递给函数。这一变体使得函数的使用非常便利。

表 1. 函数 fObjTrend() 的参数

参数 目的
string aObjName 对象名
datetime aTime_1 第一个锚点的时间
double aPrice_1 第一个锚点的价位
datetime aTime_2 第二个锚点的时间
double aPrice_2 第二个锚点的价位
color aColor 颜色
color aWidth 宽度
bool aRay_1 自第一个锚点的延伸线
bool aRay_2 自第二个锚点的延伸线
string aText 提示文本
int aWindow 子窗口
color aStyle 线型
int aChartID 图表 ID
bool aBack 绘制在背景上
bool aSelectable 对象可选
bool aSelected 对象选中
long aTimeFrames 绘制直线的时间帧

现在我们需要绘制两条额外的直线: 目标柱线上的垂直线, 和目标价位上的一条水平线:

fObjTrend(prefix+"67",TargetTime,tv24,TargetTime,tv14,tcol,TargetWidth);   
fObjTrend(prefix+"7h",time[i],target_value,TargetTime,target_value,tcol,TargetWidth);  

结果就是, 我们得到了波形和构型的图像:


图例. 4. 沃尔夫波形和检测买入交易目标的构型

删除图形对象

当使用基于收盘价 (SrcSelect = Src_Close) 或基于另一个指标的之字折线时, 形状也许会在柱线形成中不时出现或消失。为此目的, 在主要指标循环开始时清除箭头和点的缓冲区: 

UpArrowBuffer[i]=EMPTY_VALUE;
DnArrowBuffer[i]=EMPTY_VALUE;
      
UpDotBuffer[i]=EMPTY_VALUE;
DnDotBuffer[i]=EMPTY_VALUE;  

图形对象删除也应该在循环开始时执行。如果启用了绘制波形和构型, 则在指标循环开始时调用 DeleteObjects() 函数:

if(_DrawWaves){
   DeleteObjects(time[i]);
}  

函数 DeleteObjects() 代码:

void DeleteObjects(datetime time){
   string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time)+"_";
   ObjectDelete(0,prefix+"12");
   ObjectDelete(0,prefix+"23");
   ObjectDelete(0,prefix+"34");
   ObjectDelete(0,prefix+"45");
   ObjectDelete(0,prefix+"13");
   ObjectDelete(0,prefix+"24"); 
   ObjectDelete(0,prefix+"14");    
   ObjectDelete(0,prefix+"67"); 
   ObjectDelete(0,prefix+"7h");    
}

已计算柱线的时间传递给函数。在此函数中, 所有与已计算柱线相对应名称的图形对象均将被删除。

从图表中删除指标时, 我们需要删除由指标创建的所有图形对象。ObjectsDeleteAll() 函数从 DeInit() 函数中调用, 当指标操作完成时, 它会自动执行。指标的名称也被用作所有图形对象的前缀, 作为第二个参数传递给函数。这可确保只有属于指标的图形对象才会被删除:

void OnDeinit(const int reason){
   ObjectsDeleteAll(0,MQLInfoString(MQL_PROGRAM_NAME));
   ChartRedraw(0);
}  

警报功能

我们来添加一个警报功能, 通知每个新出现的箭头。功能类似于 "具有图形界面的通用趋势" 中描述的通用趋势指标使用的功能。 

警报功能允许您跟踪正在成形的柱线 (适用于最高价-最低价为基准的之字折线) 或已成形的柱线 (适合基于收盘价或其它指标的之字折线) 上出现的箭头。我们来创建一个 用于选择警报类型的枚举:

enum EAlerts{
   Alerts_off=0,  // 禁用警报
   Alerts_Bar0=1, // 形成中柱线
   Alerts_Bar1=2  // 完成的柱线
};

将变量添加到属性窗口:

input EAlerts              Alerts         =  Alerts_off;

警报功能代码作为单独的 CheckAlerts() 函数提供。图表上的柱线数和时间数组传递到此函数:

void CheckAlerts(int rates_total,const datetime & time[]){
   if(Alerts!=Alerts_off){ // 启用警报
      static datetime tm0=0; // 一个变量, 最后一次买入警报的柱线时间
      static datetime tm1=0; // 一个变量, 最后一次卖出警报的柱线时间
      if(tm0==0){ // 第一次函数执行
         // 变量初始化
         tm0=time[rates_total-1];
         tm1=time[rates_total-1];
      }
      string mes=""; // 消息变量

      // 有一个向上箭头, 最后一根柱线没有警报
      if(UpArrowBuffer[rates_total-Alerts]!=EMPTY_VALUE && 
         tm0!=time[rates_total-1]
      ){
         tm0=time[rates_total-1]; // 记住最后的警报时间
         mes=mes+" buy"; // 形成一条消息
      }

      // 有一个向下箭头, 最后一根柱线没有警报
      if(DnArrowBuffer[rates_total-Alerts]!=EMPTY_VALUE && 
         tm1!=time[rates_total-1]
      ){
         tm1=time[rates_total-1]; // 记住最后的警报时间
         mes=mes+" sell"; // 形成一条消息
      } 
      if(mes!=""){ // 有一条消息
         // 打开消息窗口
         Alert(MQLInfoString(MQL_PROGRAM_NAME)+"("+Symbol()+","+IntegerToString(PeriodSeconds()/60)+"):"+mes);
      }        
   }   
}

主循环后之后, 在 OnCalculate() 函数末尾调用 CheckAlerts() 函数。OnCalculate() 函数结束时还调用图表刷新功能, 以便加速波形和构型的绘制:

if(_DrawWaves){
   ChartRedraw(0);
}

现在指标创建已经完成。它被称为 iWolfeWaves, 并在文章的附件中提供。

专家交易系统

我们已创建了一个相当复杂的指标。我们来尝试确保它不仅可以在静态历史上正常工作, 还可以评估图形分析研究方法的有效性。为此, 我们创建一个简单的专家交易系统。

EA 应能遵照指标的所有信号开仓。这将使我们能够评估其有效性。因此, 它将工作于对冲账户, 且没有持仓数量的限制。

我们在编辑器中创建一个新的专家交易系统, 并将其命名为 eWolfeWaves。从指标中复制外部参数并将其添加到 EA 文件中。下面我们添加额外的参数来确定止损和止盈:

input double               StopLoss_K     =  1;      // 止损比率
input bool                 FixedSLTP      =  false;  // 固定止损和止盈
input int                  StopLoss       =  50;     // 固定止损值
input int                  TakeProfit     =  50;     // 固定止盈值

这些参数将允许我们选择两个 SL 和 TP 选项之一。

如果 FixedSLTP=false, 则使用 StopLoss_K 变量。在此情况下, 止盈基于指标值设置 — 在目标点代表的价位上, 止损使用 StopLoss_K 系数按照止盈的比例进行计算。SL 和 TP 定义选项仅适用于基于价格的之字折线: 使用最高价-最低价或收盘价 (SrcSelect 设置为 Src_HighLow 或 Src_Close)。

如果 FixedSLTP=true, 则使用 StopLoss 和 TakeProfit 变量。它可用于基于指标和价格的之字折线。   

我们在 OnInit() 函数中检查帐户类型。如果帐户不允许对冲, EA 操作将终止:

if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){
   Print("此非对冲账户");
   return(INIT_FAILED);
}

如果专家交易系统未处于可视测试模式下, 那么我们禁止绘制波形和构型:

bool _DrawWaves;

if(MQLInfoInteger(MQL_VISUAL_MODE)){
   _DrawWaves=DrawWaves;
}
else{
   _DrawWaves=false;
}

当 iWolfeWaves 指标由 iCustom() 函数调用时, 变量 _DrawWaves 将用于替代 DrawWaves。调用指标并检查是否成功加载:

h=iCustom(  Symbol(),
            Period(),
            "iWolfeWaves",
            Alerts,
            SrcSelect,
            DirSelect,
            RSIPeriod,
            RSIPrice,
            MAPeriod,
            MAShift,
            MAMethod,
            MAPrice,
            CCIPeriod,
            CCIPrice,
            ZZPeriod,
            K1,
            K2,
            K3,
            _DrawWaves,
            BuyColor,
            SellColor,
            WavesWidth,
            DrawTarget,
            TargetWidth,
            BuyTargetColor,
            SellTargetColor);
            
if(h==INVALID_HANDLE){
   Print("不能加载指标");
   return(INIT_FAILED);
}

如果指标加载失败, EA 操作应停止。

当使用基于最高价-最低价的指标时, 箭头不会消失, 因此 EA 可以在不完整的当前柱线上操作。在所有其它情况下, EA 应检查第一个完整柱线上的指示箭头。为此目的, 我们使用 EA 的全局变量 "Shift":

int Shift;

根据之字折线类型设置所需的值: 

if(SrcSelect==Src_HighLow){
   Shift=0;
}
else{
   Shift=1;
}

以下是 OnInit() 函数的整体代码:

int OnInit(){

   // 检查账户类型
   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){
      Print("此非对冲账户");
      return(INIT_FAILED);
   }

   // 禁止绘制波形和构型

   bool _DrawWaves;
   
   if(MQLInfoInteger(MQL_VISUAL_MODE)){
      _DrawWaves=DrawWaves;
   }
   else{
      _DrawWaves=false;
   }

   // 加载指标
   h=iCustom(  Symbol(),
               Period(),
               "iWolfeWaves",
               Alerts,
               SrcSelect,
               DirSelect,
               RSIPeriod,
               RSIPrice,
               MAPeriod,
               MAShift,
               MAMethod,
               MAPrice,
               CCIPeriod,
               CCIPrice,
               ZZPeriod,
               K1,
               K2,
               K3,
               _DrawWaves,
               BuyColor,
               SellColor,
               WavesWidth,
               DrawTarget,
               TargetWidth,
               BuyTargetColor,
               SellTargetColor);
       
   // 检查指标是否已成功加载         
   if(h==INVALID_HANDLE){
      Print("不能加载指标");
      return(INIT_FAILED);
   }
   
   // 定义 EA 应在哪根柱线上检查指示箭头
   if(SrcSelect==Src_HighLow){
      Shift=0;
   }
   else{
      Shift=1;
   }

   return(INIT_SUCCEEDED);
}

我们来转入 OnTick() 函数。EA 必须能够与柱线和即时报价操作。我们添加变量, 保存形成中柱线和最后一根已处理柱线的时间 (变量在 OnTick() 函数中声明):

datetime tm[1];     // 形成中柱线的时间
static datetime lt; // 最后已处理柱线的时间

获取最后 (形成中) 柱线的时间:

if(CopyTime(Symbol(),Period(),0,1,tm)<=0)return;

检查柱线时间:

if(Shift==0 || tm[0]!=lt){

如果 Shift==0, 则 EA 在每次即时报价时操作。否则, 如果 lt 变量不等于形成中柱线的时间 (每根柱线计算一次)。 

声明辅助变量并获取指标值:

double tp,sl; // 用于计算止损和止盈的变量

double buf_buy[1];         // 买入箭头
double buf_sell[1];        // 卖出箭头

double buf_buy_target[1];  // 买入目标
double buf_sell_target[1]; // 卖出目标

if(CopyBuffer(h,0,Shift,1,buf_buy)<=0)return;
if(CopyBuffer(h,1,Shift,1,buf_sell)<=0)return;
if(CopyBuffer(h,2,Shift,1,buf_buy_target)<=0)return;
if(CopyBuffer(h,3,Shift,1,buf_sell_target)<=0)return;

如果有交易信号, 计算止损和止盈, 并开仓:

// 有一个箭头, 此柱线上没有开仓
if(buf_buy[0]!=EMPTY_VALUE && LastBuyTime!=tm[0]){
   // 止损和止盈
   if(FixedSLTP){
      tp=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*TakeProfit;
      sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)-_Point*StopLoss;            
   }
   else{
      tp=NormalizeDouble(buf_buy_target[0],_Digits);
      double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      sl=NormalizeDouble(ask-StopLoss_K*(tp-ask),_Digits);         
   }
   // 开仓
   if(!trade.Buy(0.1,Symbol(),0,sl,tp))return;
   // "记住" 最后开仓时间
   LastBuyTime=tm[0];
}

如果使用固定 SL 和 TP (FixedStopLoss=true), 止盈位的计算是 TakeProfit 变量的值乘以 _Point 加上开仓价 (买入时的采购价)。若要计算止损, 从开仓价里减去 StopLoss 变量的值乘以 _Point。计算之后, 结果值要使用 NormalizeDouble() 函数进行规整化, 小数位要与品种报价的小数点后位数相同 (此数字可使用 _Digits 变量获取)。

如果 SL 和 TP 不是固定的, 我们首先检测止盈的值, 然后计算止损。如果开仓失败, 则 OnTick() 函数将被终止, 下一此即时报价来临时再尝试开仓。只要存在指标信号, 即在一根柱线中, 将始终进行尝试。如果开仓成功, 当前柱线的时间将被分配给 LastBuyTime 变量, 以避免在同一根柱线上重复开仓 (当使用即时报价, 即 Shift = 0) 时。LastBuyTime 是专家交易系统的全局变量。

卖出类似于买入, 仅有几个修正:

// 有一个箭头, 这根柱线尚未开仓
if(buf_sell[0]!=EMPTY_VALUE && LastSellTime!=tm[0]){
   // 止损和止盈
   if(FixedSLTP){
      tp=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*TakeProfit;
      sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)+_Point*StopLoss;            
   }
   else{
      tp=NormalizeDouble(buf_sell_target[0],_Digits);
      double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
      sl=NormalizeDouble(bid+StopLoss_K*(bid-tp),_Digits);         
   }
   // 开仓
   if(!trade.Sell(0.1,Symbol(),0,sl,tp))return; 
   // "记住" 最后开仓时间
   LastSellTime=tm[0];        
}  

对于卖出交易, 使用 LastSellTime 而不是 LastBuyTime 变量, 并且使用供给价计算止损/止盈位。

最后, 形成中的柱线时间分配给 lt 变量, 以防止 EA 在同一根柱线上执行任何操作 (如果 EA 设置为在每根柱线上操作, 即Shift = 1)。以下是 OnTick() 函数的整体代码:

void OnTick(){
   
   datetime tm[1]; // 形成中柱线的时间
   static datetime lt; // 最后已处理柱线的时间
   
   // 复制时间
   if(CopyTime(Symbol(),Period(),0,1,tm)<=0)return;
   
   if(Shift==0 || tm[0]!=lt){ // 检查 EA 操作是否为每根柱线

      double tp,sl; // 用于计算止损和止盈的变量

      double buf_buy[1];         // 买入箭头
      double buf_sell[1];        // 卖出箭头
      
      double buf_buy_target[1];  // 买入目标
      double buf_sell_target[1]; // 卖出目标     
      
      if(CopyBuffer(h,0,Shift,1,buf_buy)<=0)return;
      if(CopyBuffer(h,1,Shift,1,buf_sell)<=0)return;
      if(CopyBuffer(h,2,Shift,1,buf_buy_target)<=0)return;
      if(CopyBuffer(h,3,Shift,1,buf_sell_target)<=0)return;

      // 有一个箭头, 此柱线上没有开仓
      if(buf_buy[0]!=EMPTY_VALUE && LastBuyTime!=tm[0]){
         // 止损和止盈
         if(FixedSLTP){
            tp=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*TakeProfit;
            sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)-_Point*StopLoss;            
         }
         else{
            tp=NormalizeDouble(buf_buy_target[0],_Digits);
            double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
            sl=NormalizeDouble(ask-StopLoss_K*(tp-ask),_Digits);         
         }
         // 开仓
         if(!trade.Buy(0.1,Symbol(),0,sl,tp))return;
         // "记住" 最后开仓时间
         LastBuyTime=tm[0];
      }
      
      // 有一个箭头, 这根柱线尚未开仓
      if(buf_sell[0]!=EMPTY_VALUE && LastSellTime!=tm[0]){
         // 止损和止盈
         if(FixedSLTP){
            tp=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*TakeProfit;
            sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)+_Point*StopLoss;            
         }
         else{
            tp=NormalizeDouble(buf_sell_target[0],_Digits);
            double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
            sl=NormalizeDouble(bid+StopLoss_K*(bid-tp),_Digits);         
         }
         // 开仓
         if(!trade.Sell(0.1,Symbol(),0,sl,tp))return; 
         // "记住" 最后开仓时间
         LastSellTime=tm[0];        
      }      
      
      lt=tm[0];
   }
}

专家交易系统代码可在附带的 eWolfeWaves 文件中找到。 

我们来测试由此产生的专家交易系统。如果您在测试后将指标加载到图表上, 您可以看到 EA 在每个箭头处入场, 无论是否有持仓 (图例, 5)。


图例. 5. 专家交易系统在每个指标箭头处入场

当然, 我们首先对交易指标的有效性感兴趣。使用整个 EURUSD H1 历史数据, 省缺设置, EA 的测试结果如图例. 6 所示。


图例. 6. 在整个 EURUSD H1 历史数据上的 EA 测试结果

在测试区间的开始, 有一个显著的下降, 这可能与初期的历史数据质量较低有关。此后, 大约从 1991 年开始, 稳步增长的时期开始。总的来说, 测试结果是积极的, 即使未经优化和额外的检查。

更多来自比尔·沃尔夫所著书籍的提示

除了波形检测规则, 比尔·沃尔夫提供了一些提示, 他称之为 "心理和技术论调"。最重要的技术论调之一是监测即时报价的交易量的建议: 它在反转点会减少, 这种减少可能表明将要逆转。第二个建议是跟随趋势线。比尔·沃尔夫 发现的波形走势经常发生在趋势突破之后, 即突破趋势线之后。即, 趋势突破后出现的波形更可靠。第三个建议是监测线 1-4, 特别是点 4, 如果发生任何不可预见的事件, 则要退出: 反向波形, 交易量强劲增长, 或快速获利的情况。

结论

专家交易系统测试的积极结果 (即使使用省缺设置) 表明, 本文中讨论的图形分析方法绝对有效, 可能有兴趣进行进一步的调查。

一些读者可能想改善指标。在此刻, 指标外部参数有三个可变系数: K1, K2, K3。K1 用于检查点 3 相对于点 1 的位置, 以及点 4 相对于点 1 的位置。也许, 最好使用单独的系数进行这些检查。另一方面, 参数数量的增加使优化复杂化, 这已非优化而是提高了过度适应的风险。也许系数 K1 和 K2 的组合可能更好。这将令指标设置更加容易, 更易于理解。另一方面, 最好只保留一个系数。指标代码功能划分清晰, 使其更容易修改。每个人均可尝试以不同的方式修改指标。  

除了使用指标搜索沃尔夫波形, 还可以在创建其它指标来搜索任何其它之字折线形态时, 将其用作模板。您只需要修改 CheckUp 和 CheckDn 函数的代码。最重要的是, 访问之字折线指标值的问题已经解决了。

我要特别提醒变量 CurCount, PreCount 和 LastTime 的技巧。这不仅是本文所分析的狭义问题的解决方案。在开发指标时, 我们经常需要额外的缓冲区保存中间计算过程中获得的辅助值。在每根柱线上, 将缓冲区前一个元素的值移动到缓冲区当前元素之内; 偶尔会改变这个值。一个元素的值用于计算, 而整个缓冲区正是用于此目的。使用两个变量可以显著减少指标使用的内存容量。

附件

本文中创建的指标文件和专家交易系统如下。文件应放在正确的文件夹中。它们应该保存到终端的相同文件夹中。附件中提供以下文件:

  • Indicators/iWolfeWaves_Step_1.mq5
  • Indicators/iWolfeWaves.mq5
  • Experts/eWolfeWaves.mq5 

若要提供上述文件的操作, 您还需要从文章 "通用之字折线" 中下载文件:

  • Indicators/iUniZigZagSW.mq5
  • Include/CSorceData.mqh
  • Include/CZZDirection.mqh>
  • Include/CZZDraw.mqh

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

附加的文件 |
MQL5.zip (8.67 KB)
最近评论 | 前往讨论 (1)
whkh18
whkh18 | 19 7月 2017 在 03:02

感谢分享,给大家介绍一个黄金0.3美金即可回本的经纪商,有需要可以联系用户名  微信whkh18 

跨平台的EA交易: 信号 跨平台的EA交易: 信号

本文讨论了 CSignal 和 CSignals 类,它们将用于创建跨平台的EA交易。它检验了MQL4和MQL5的区别,看它们在评估交易信号时需要怎样特别的数据,这样来确保写出的代码可以兼容两种编译器。

运用人工智能实现的 Thomas DeMark 次序 (TD SEQUENTIAL) 运用人工智能实现的 Thomas DeMark 次序 (TD SEQUENTIAL)

在本文中, 我将告诉您如何把一个非常著名的策略与神经网络合并以便成功交易。这就是运用人工智能系统实现的 Thomas DeMark 次序策略。仅应用了策略的第一部分, 使用设置和交汇信号。

图形界面 X: 在多行文本框中选择文本 (集成构建 13) 图形界面 X: 在多行文本框中选择文本 (集成构建 13)

本文将实现使用各种组合键选择文本, 及删除所选文本的功能, 类似于在其它任意文本编辑器中完成的方式。此外, 我们将继续优化代码, 并为进入函数库演变第二阶段的最后一个过程准备好类, 其中所有控件均作为单独的图像 (画布) 呈现。

DiNapoli 交易系统 DiNapoli 交易系统

本文详述一款由 Joe DiNapoli 开发的基于菲波纳奇等级的交易系统。文中将会解释系统蕴含的思路和主要概念, 并提供了一款简单的指标作为例子, 便于更清晰地理解。