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

量化交易吧 /  数理科学 帖子:3364670 新帖:28

沃尔夫波形 (Wolfe W*es)

我就是英雄发表于:4 月 17 日 15:25回复(1)


内容

  • 概述

  • 检测沃尔夫波形的规则

  • 选择使用的之字折线

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

  • 一点几何

  • 检测波形

  • 绘制波形和目标

  • 删除图形对象

  • 警报功能

  • 专家交易系统

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

  • 结论

  • 附件

概述

沃尔夫波形 (Wolfe W*e) 是 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, 文件并将其保存为 iWolfeW*es。我们将操控这个指标。

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

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

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; // 设置新数值}

 

指标可以在现阶段得以保存, 它可作为开发各种定义之字折线形态的指标的基础。在以下附件中, 指标名称为 "iWolfeW*es_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(_DrawW*es){ // 绘制结构                        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(_DrawW*es){ // 绘制结构   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(_DrawW*es){
                        // 用其它颜色绘制                        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' 高于点 4double 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   DrawW*es       =  true;             // 启用波形和对象绘制input color  BuyColor        =  clrAqua;          // 买入波形颜色input color  SellColor       =  clrRed;           // 卖出波形颜色input int    W*esWidth      =  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() 函数里应强制禁用绘图 (DrawW*es 变量)。为此目的, 我们声明使用一个附加变量, 替代 DrawW*es:

bool _DrawW*es;

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

if(SrcSelect==Src_HighLow || SrcSelect==Src_Close){
   _DrawW*es=DrawW*es;
}else{
   _DrawW*es=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,W*esWidth);
   fObjTrend(prefix+"23",time[i2],v2,time[i3],v3,col,W*esWidth);   
   fObjTrend(prefix+"34",time[i3],v3,time[i4],v4,col,W*esWidth);
   fObjTrend(prefix+"45",time[i4],v4,time[i5],v5,col,W*esWidth);

   // 绘制构型   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,W*esWidth);
fObjTrend(prefix+"23",time[i2],v2,time[i3],v3,col,W*esWidth);   
fObjTrend(prefix+"34",time[i3],v3,time[i4],v4,col,W*esWidth);
fObjTrend(prefix+"45",time[i4],v4,time[i5],v5,col,W*esWidth);

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

// 绘制构型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(_DrawW*es){
   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(_DrawW*es){
   ChartRedraw(0);
}

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

专家交易系统

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

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

我们在编辑器中创建一个新的专家交易系统, 并将其命名为 eWolfeW*es。从指标中复制外部参数并将其添加到 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 _DrawW*es;if(MQLInfoInteger(MQL_VISUAL_MODE)){
   _DrawW*es=DrawW*es;
}else{
   _DrawW*es=false;
}

 

当 iWolfeW*es 指标由 iCustom() 函数调用时, 变量 _DrawW*es 将用于替代 DrawW*es。调用指标并检查是否成功加载:

h=iCustom(  Symbol(),
            Period(),
            "iWolfeW*es",
            Alerts,
            SrcSelect,
            DirSelect,
            RSIPeriod,
            RSIPrice,
            MAPeriod,
            MAShift,
            MAMethod,
            MAPrice,
            CCIPeriod,
            CCIPrice,
            ZZPeriod,
            K1,
            K2,
            K3,
            _DrawW*es,
            BuyColor,
            SellColor,
            W*esWidth,
            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 _DrawW*es;
   
   if(MQLInfoInteger(MQL_VISUAL_MODE)){
      _DrawW*es=DrawW*es;
   }
   else{
      _DrawW*es=false;
   }

   // 加载指标   h=iCustom(  Symbol(),
               Period(),
               "iWolfeW*es",
               Alerts,
               SrcSelect,
               DirSelect,
               RSIPeriod,
               RSIPrice,
               MAPeriod,
               MAShift,
               MAMethod,
               MAPrice,
               CCIPeriod,
               CCIPrice,
               ZZPeriod,
               K1,
               K2,
               K3,
               _DrawW*es,
               BuyColor,
               SellColor,
               W*esWidth,
               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];
   }
}

 

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

我们来测试由此产生的专家交易系统。如果您在测试后将指标加载到图表上, 您可以看到 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/iWolfeW*es_Step_1.mq5

  • Indicators/iWolfeW*es.mq5

  • Experts/eWolfeW*es.mq5 

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

  • Indicators/iUniZigZagSW.mq5

  • Include/CSorceData.mqh

  • Include/CZZDirection.mqh>

  • Include/CZZDraw.mqh




全部回复

0/140

量化课程

    移动端课程