English Русский Español Deutsch 日本語 Português
旗形形态

旗形形态

MetaTrader 5示例 | 25 九月 2017, 09:07
12 150 0
Dmitry Fedoseev
Dmitry Fedoseev

目录

旗形形态的基本特性,就如同它的名字所关联的,是一种明显的垂直方向价格变化("旗杆")伴着随后的水平方向的价格盘整所构成的水平旗形(图 1).


图 1. 旗形

在技术分析相关的书籍和网站中,旗形形态经常与三角旗形形态做横向比较,与旗形不同,三角旗形是三角形形状 (图 2), 这也是为什么在一些技术分析资源中,旗形形态与三角形态一起分析。


图 2. 三角旗形

可能看起来三角旗形和三角形只是相同形态的不同名称,在一些技术分析书籍中,例如 Tomas Bulkowski 的“图表形态百科全书”中, 这两种形态是分别独立描述的。而且书中还描述了楔形形态,它和水平绘制的三角形是类似的,它的狭窄部分在左边,而在右方价格变化的范围更大。


图 3. 楔形

除了楔形形态,还有扩张三角形,以及各种长方形形态都类似于旗形。所以,应该有明确的规则来区分三角旗形和三角形,区分楔形和扩张三角形,以及区分旗形和水平形态。这个问题将会在文章中首先做探讨,然后,我们将创建指标来搜索所有这些形态。 

三角旗形和三角形的差别

让我们探讨一下三角旗形和三角形的区别,以及其他上述形状类似的形态的区别:

  • 长方形形态和旗形;
  • 三角形和三角旗形;
  • 扩张三角形和楔形.

长方形形态,三角形和扩张三角形都属于一类,而旗形、三角旗形和楔形属于另一类,

第一类的模式都是由价格反转点构成的(图4),所以可以使用之字转向(ZigZag)指标来搜索它们。


图 4. 形态: a — 水平形态, b — 三角形, c — 扩张三角形.  
形态显示价格有上涨的期待(用于买入).

第二类的模式是由填充它们区域的柱构成的,当然,在图表上很难看到明显形成这样的图形,但是重点是有一个柱被邻近的柱大幅覆盖,而形成了形态。 


图 5. 形态: a — 旗形, b — 三角形, c — 楔形.
形态显示了价格可能会上涨 (用于买入).

现在我们已经确定了类别和它们的基本差别,让我们单独探讨每个形态。 

水平形态

让我们从第一类的形态开始讨论,确定这样形态的简便方法是使用之字转向指标,这类形态的第一个是水平形态, 它对应着第二种类的旗形形态。然而,在大多数技术分析相关资料中,水平形态也被称为旗形形态。

为了形成水平形态,价格应当有明显的垂直方向变化,然后再在相同的价格水平上形成至少双顶和双底。价格也可能形成三顶和三底(图 6), 或者更多。所以,指标的一个参数就是确定构成形态的顶部和底部的数量。


图 6. 水平形态: a — 由两个顶部/底部构成,b — 由三个顶部/底部构成.
形态显示出价格可能上涨 (适合买入) 

在形态中并不要求顶部和底部的边缘是水平的,它们必须是平行的,所以指标将有一个或者多个参数来选择形态的倾斜方向: 水平方向,向上倾斜,向下倾斜 (图 7).


图 7. a — 水平方向形态, b — 向下倾斜的形态, c — 向上倾斜的形态. 
形态显示价格可能上涨 (适合买入)

当然,这样向上或者向下倾斜的形态不能称为水平形态,但是它们在原则上与水平形态很接近,

当价格突破了顶部构成的水平时,形态就结束了。

 
图 8. 形态结束并建立一个
买入仓位: a — 对于水平形态,
b — 对于向下倾斜的形态 

向上倾斜形态进场的时刻将不考虑顶部的倾斜,而会使用最后一个顶部的水平价格 (图 9).


图 9. 确定向上倾斜形态的入场点 (买入)

在向下倾斜形态中也可以使用水平级别的简单变化,所以指标还将有个变量用于选择级别的类型而不论形态的类型。

水平形态的目标是由形态形成之前垂直方向价格变化的大小来决定的,价格的距离应当等于它形成之前的价格的距离(图10)。


图 10. 确定目标。L1 是在进入形态之前价格的距离,
它应当等于退出形态的距离L2。 

因为形态的上下边界是平行的,我们可以使用一个简单方法来确定目标: 我们可以根据价格和第一个形态顶部的距离来使用相同的距离设置最近的底部(图 11)。


图 11. 确定目标的简单方法。价格距离构成第一个顶部之间的距离L1
等于最后一个底部和目标的距离L2


收敛三角形

收敛三角形形态和水平形态有少许不同,唯一的不同是构成形态的之字转向片段必须是连续收敛的 (图 12)。


 图 12. 收敛三角形片段 3-4 必须比
片段 1-2 小, 而片段 5-6 必须比 3-4 小
 

其余的条件与水平形态类似: 三角形的水平位置或者向上/向下倾斜,在突破了由最近两个顶部或者底部水平级别的阻力位时进场,目标水平的计算也类似。


扩张三角形

上面的收敛三角形原则可以应用于扩张三角形,唯一的区别是构成形态的之字转向片段在增大(图 13)。


 图 13. 扩张三角形. 片段 3-4 必须大于
片段 1-2, 而片段 5-6 必须大于 3-4
 

根据这三种形态如此的类似,我们可以为搜索它们创建一个通用指标。  


用于搜索水平形态和三角形的通用指标

为了创建指标,我们将使用 iUniZigZagSW 指标,它来自通用的之字转向指标这篇文章。还需要以下另外的文件: CSorceData.mqh, CZZDirection.mqh 和 CZZDraw.mqh。这些文件以及iUniZigZagSW.mq5 文件可以从通用之字转向指标文章的附件中下载。下载文档,再解压并把 MQL5 文件夹复制到终端数据文件夹中,在复制之后, ZigZag 文件夹将会出现在 MQL5/Indicators 中,包含了几个文件 (包括 iUniZigZagSW.mq5), 还有另一个 ZigZag 文件夹,CSorceData.mqh, CZZDirection.mqh 和 CZZDraw.mqh 文件将出现在 MQL5/Includes 文件夹中。在复制文件之后,重新启动终端来编译指标或者在 MetaEditor 中逐个编译它们。在终端的图表中运行 iUniZigZagSW 指标已确保它能正常工作。

沃尔夫波形文章中,在创建指标的中间步骤中保存了iWolfeWaves_Step_1.mq5 文件,它实现了使用 iCustom() 函数访问 iUniZigZagSW.mq5 指标,另外,在它内部还有一个数组保存了所有之字转向的顶部和底部。下载‘沃尔夫波形’文章的附件, 把它解压缩, 复制iWolfeWaves_Step_1.mq5 到 MQL5/Indicators, 把它重命名为 "iHorizontalFormation" 并在 MetaEditor 中打开它,所有使用指标来侦测水平形态的工作都将在这个文件中进行。可能需要在这个文件中修改 iUniZigZagSW 指标的路径,为了检查这一点,要编译指标并尝试在图表上运行它。如果出现了 "载入指标出错" 的消息窗口, 就要在 OnInit() 中找到对 iCustom() 的调用, 并且把指标名称从"iUniZigZagSW"改为"ZigZags\\iUniZigZagSW"。在这样修改之后,再次编译指标,并确保它可以在图表上正确运行。在这个阶段,指标不会画出任何东西。

在这里讨论的整个搜索形态的过程可以分为几个独立任务:

  1. 确定在形成形态之前的价格变化的数值,
  2. 确定形态的形状,
  3. 确定形态的斜度,
  4. 形态生成结束: 在形态形成之后或者等到突破某一水平。
  5. 确定目标。

每个任务(除了第一个)都将提供集中方案选项,这将有助于我们创建一个通用指标来识别所有这三种形态。我们将能够在指标参数窗口中使用枚举的下拉列表在选项中作切换, 

选择形状(形态类型)的枚举:

enum EPatternType{
   PatternTapered,
   PatternRectangular,
   PatternExpanding
};

属性窗口中对应的变量:

input EPatternType         Pattern        =  PatternRectangular;

这个参数可以用来选择形态的形状: PatternTapered — 收敛三角形, PatternRectangular - 长方形, PatternExpanding - 扩张三角形.

用于选择形态倾斜方向的枚举:

enum EInclineType{
   InclineAlong,
   InclineHorizontally,
   InclineAgainst
};

属性窗口中对应的变量:

input EInclineType         Incline        =  InclineHorizontally; 

这个参数可以选择形态的倾斜方向: InclineAlong — 在期望的方向上继续延伸 (对于买入就是上涨,对于卖出就是下跌), InclineHorizontally — 没有倾斜, InclineAgainst — 于期望的方向上相反。

用于选择形态结束方法的枚举:

enum EEndType{
   Immediately,
   OneLastVertex,
   TwoLastVertices
};

属性窗口中对应的变量:

input EEndType             CompletionType =  Immediately;

可以使用以下的选项: 在形态完成后立即, OneLastVertex — 在突破了形态最近的顶部水平后, TwoLastVertices — 在突破了形态中由两个顶部构成的水平之后.

用于选择目标计算选项的枚举:

enum ETargetType{
   FromVertexToVertex,
   OneVertex,
   TwoVertices
};

属性窗口中对应的变量:

input ETargetType          Target         =  OneVertex;

可以使用以下的选项: FromVertexToVertex (图 11), OneVertex (图 10), TwoVertices, 使用形态中的两个初始底部 (参见图 14).


图 14. 三顶的形态。目标确定选项是 TwoVertices,
形态完成方法是 OneLastVertex.

当形态完成方法设为 Immediately 的时候, Target 参数就是无效的,因为只能使用 FromVertexToVertex 。对于另外两种形态完成选项 (OneLastVertex 和 TwoLastVertices), 所有三种 CompletionType 选项的不同组合都是可能的。请注意下面的特性: 如果选择了 OneVertex 或者 TwoVertices 选项来确定目标, 一个或者两个第一底部(图14中的点2, 或者点2和点4)来用于确定目标, 而突破水平是根据最近一个或者两个顶部来确定的 (图14中的点5或者点3和点5). 如果使用了双顶的形态,将使用点3或者点1和点3。

为了解决任务1, 我们需要一个参数来确定形态之前价格变化的大小:

input double               K1             =  1.5;

片段1-2的高度(参见图14)用作形态的基础(它的参考大小), 而所有对大小的检查都是相对它的。K1 参数确定了片段0-1的高度应该比片段1-2高多少倍。

为了确定形态的形状,我们使用 K2 参数:

input double               K2             =  0.25;

参数值越小,形态的高度在它的长度中就越稳定。对于三角形形态 (扩张和收敛), 有一个增加参数用来搜索最明显的三角形。

为了确定形态的倾斜方向,我们使用 K3 参数:

input double               K3             =  0.25;

参数值越小,就意味着指标将搜索水平位置的形态,要搜索倾斜的形态,就应该在K2参数中指定较大的数值,它可以找到明显倾斜的形态。

最后,还有一个主要的参数:

input int                  N              =  2;

N 参数确定了形态定点的数量。 

结果,我们就有了以下的外部参数集 (还有 ZigZag 的参数):

input EPatternType         Pattern        =  PatternRectangular;
input EInclineType         Incline        =  InclineHorizontally;     
input double               K1             =  1.5;
input double               K2             =  0.25;
input double               K3             =  0.25;
input int                  N              =  2;
input EEndType             CompletionType =  Immediately;
input ETargetType          Target         =  OneVertex;

使用 N 参数,我们计算在确定形态时需要多少个之字转向点。首先,我们声明一个全局变量:

int RequiredCount;

在 OnInit() 函数中,我们计算它的值:

RequiredCount=N*2+2;

2*N 是构成形态的顶点的数量 (N 个顶部和 N 个底部). 另外一个顶点确定了之前价格的变化,而额外的一个点是新的之字转向片段(没有在计算中使用)的最后一个点。

更多的操作将在 OnTick() 函数中进行,新的代码将会加在住指标循环的最后。如果之字转向点的数量足够并且只有在它方向改变时,才将检查形态形成的条件,价格和水平将在每个之字转向变化时作检查:

if(CurCount>=RequiredCount){
   if(CurDir!=PreDir){      
      // 检查条件

   }
   // 检查价格和水平级别

} 

首先,我们计算基础数值,也就是片段 1-2 的高度(参见图14),这个值将在检查所有形态形成条件的时候使用。然后,检查任务1的条件,也就是之前的价格变化

int li=CurCount-RequiredCount;                                            // 初始形态点在 PeackTrough 数组中的索引
double base=MathAbs(PeackTrough[li+1].Val-PeackTrough[li+2].Val);        // 基础数值
double l1=MathAbs(PeackTrough[li+1].Val-PeackTrough[li].Val);             // 片段 1-2 的高度
   if(l1>=base*K1){                                                       // 检查之前价格变化的大小
        // 其它的检查

   }

进一步的检查将依赖于之字转向的最后片段是指向上涨还是下跌。

if(CurDir==1){              // 最后的之字转向片段指向上涨
   // 检查上涨方向的条件  
             
}
else if(CurDir==-1){        // 最后的之字转向片段指向下跌
   // 检查下跌方向的条件

}

让我们讨论上涨方向条件的检查:

if(CheckForm(li,base) && CheckInclineForBuy(li,base)){      // 检查形态和方向
   if(CompletionType==Immediately){ 
      // 画出指标箭头
      UpArrowBuffer[i]=low[i];
      // 画出目标点
      UpDotBuffer[i]=PeackTrough[CurCount-1].Val+l1;
   }
   else{
      // 设置突破水平参数
      SetLevelParameters(1);
      // 设置目标参数
      SetTarget(1,li);
   }
} 

形态形成条件是使用两个函数作检查的: CheckForm() 来检查形态的形状,而 CheckInclineForBuy() 来检查倾斜度。如果成功检查了形状和倾斜,那么就根据形态完成的类型,在图表上画出箭头或者目标点,或者突破水平的参数。

CheckForm() 函数。形态中第一个点在 PeackTrough 数组中的索引基础数值 'base' 会传给函数,这里是函数的代码:

bool CheckForm(int li,double base){               
   switch(Pattern){
      case PatternTapered: 
         // 收敛
         return(CheckFormTapered(li,base));
      break;               
      case PatternRectangular: 
         // 长方形
         return(CheckFormRectangular(li,base));
      break;
      case PatternExpanding: 
         // 扩张
         return(CheckFormExpanding(li,base));
      break;
   }
   return(true);
}

在函数中,会根据 Pattern 参数的值调用适当的函数: 对于收敛三角形调用 CheckFormTapered(), 对于长方形形态调用 CheckFormRectangular(), 对于扩张三角形调用 CheckFormExpanding()。

CheckFormTapered() 函数:

bool CheckFormTapered(int li,double base){
   // 从1开始循环,不检查第一个片段,
   // 但是所有随后的片段都会相对它做检查 
   for(int i=1;i<N;i++){ 
      // 计算形态中下一个顶部点的索引
      int j=li+1+i*2;
      // 下一个片段的数值 
      double lv=MathAbs(PeackTrough[j].Val-PeackTrough[j+1].Val);
      // 前一个片段的数值
      double lp=MathAbs(PeackTrough[j-2].Val-PeackTrough[j-1].Val);
      // 前一个片段应该更大,否则
      // 函数返回 false   
      if(!(lp-lv>K2*base)){
         return(false);
      }
   } 
   return(true);
}

在函数中,构成形态的之字转向片段在一个循环中做检查,而每个后面的片段必须小于前一个。

CheckFormExpanding() 函数类似,只有一点区别:

if(!(lv-lp>K2*base)){
   return(false);
}

为了达成这个条件,每个后面的片段必须比前一个片段要大。

CheckFormRectangular() 函数:

bool CheckFormRectangular(int li,double base){   
   // 在除了第一个片段之外的所有片段做循环      
   for(int i=1;i<N;i++){
      // 计算下一个顶部的索引 
      int j=li+1+i*2; 
      // 计算下一个片段的大小
      double lv=MathAbs(PeackTrough[j].Val-PeackTrough[j+1].Val);
      // 片段应该和基础数值相差不大 
      if(MathAbs(lv-base)>K2*base){
         return(false); 
      }
   }
   return(true);
}

在这个函数中,每个片段都和基础数值作比较,如果差别很大,函数就返回 false。

如果形状检查成功,就检查它的倾斜度。CheckInclineForBuy() 函数:

bool CheckInclineForBuy(int li,double base){                 
   switch(Incline){
      case InclineAlong:
         // 在价格移动方向上的倾斜
         return(CheckInclineUp(li,base));
      break;
      case InclineHorizontally:
         // 没有倾斜
         return(CheckInclineHorizontally(li,base));
      break;                     
      case InclineAgainst:
         // 倾斜方向与价格移动方向相反
         return(CheckInclineDn(li,base));
      break;
   } 
   return(true);
}  


用于卖出的倾斜检查函数只有两行不同:

bool CheckInclineForSell(int li,double base){                 
   switch(Incline){
      case InclineAlong:
         // 在价格移动方向上的倾斜
         return(CheckInclineDn(li,base));
      break;
      case InclineHorizontally:
         // 没有倾斜
         return(CheckInclineHorizontally(li,base));
      break;                     
      case InclineAgainst:
         // 倾斜方向与价格移动方向相反
         return(CheckInclineUp(li,base));
      break;
   } 
   return(true);
} 

如果 Incline 等于 InclineAlong (与移动方向相同), 买入就调用 CheckInclineUp() 而卖出则调用 CheckInclineDn()。如果 Incline = InclineAgainst 就相反。

CheckInclineUp(), 该函数用于检查形态的向上倾斜:

bool CheckInclineUp(int li,double base){   
   // 在除了第一个片段之外的所有片段做循环      
   for(int v=1;v<N;v++){
      // 计算下一个顶部的索引
      int vi=li+1+v*2;
      // 计算下一个之字转向片段的中部
      double mc=(PeackTrough[vi].Val+PeackTrough[vi+1].Val)/2;
      // 计算前一个之字转向片段的中部
      double mp=(PeackTrough[vi-2].Val+PeackTrough[vi-1].Val)/2;
      // 下一个片段应该比前一个更高
      if(!(mc>mp+base*K3)){
         return(false);
      }
   }
   return(true);
} 

所有的之字转向片段都在函数中作检查: 计算每个片段的中部,并且与前一个片段的中部作比较,每个片段应该高于它前一个片段 base*K3 的数值。

CheckInclineDn() 函数有一点差别:

if(!(mc<mp-base*K3)){
   return(false);
}

为了满足这个条件,每个后续片段必须比前一个要小。

CheckInclineHorizontally() 函数:

bool CheckInclineHorizontally(int li,double base){ 
   // 基础片段的中点
   double mb=(PeackTrough[li+1].Val+PeackTrough[li+2].Val)/2;        
   for(int v=1;v<N;v++){
      // 下一个顶部的索引
      int vi=li+1+v*2;
      // 下一个片段的中点
      double mc=(PeackTrough[vi].Val+PeackTrough[vi+1].Val)/2;
      // 下一个片段的中点 
      // 和基础片段的中点不能偏移太多
      if(MathAbs(mc-mb)>base*K3){
         return(false);
      }
   }                  
   return(true);
}

如果检查形状和倾斜成功,就执行以下的代码部分:

if(CompletionType==Immediately){                    // 立即入场
   UpArrowBuffer[i]=low[i];
   UpDotBuffer[i]=PeackTrough[CurCount-1].Val+l1;
}
else{                                               // 等待水平的突破
   SetLevelParameters(1);
   SetTarget(1,li);
}

如果形态结束设为 Immediately, 指标就画出箭头并设置目标点,在其它情况下就使用 SetLevelParameters() 函数来设置突破水平,而使用 SetTarget() 函数来设置目标。

SetLevelParameters() 函数:

void SetLevelParameters(int dir){
   CurLevel.dir=dir;   
   switch(CompletionType){
      case OneLastVertex:                            // 根据一个点
          CurLevel.v=PeackTrough[CurCount-3].Val;
      break;
      case TwoLastVertices:                          // 根据两个点
         CurLevel.x1=PeackTrough[CurCount-5].Bar;
         CurLevel.y1=PeackTrough[CurCount-5].Val;
         CurLevel.x2=PeackTrough[CurCount-3].Bar;
         CurLevel.y2=PeackTrough[CurCount-3].Val;
      break;
   }
} 

在 SetLevelParameters() 函数中,SLevelParameters 结构用于保存水平参数:

struct SLevelParameters{
   int x1;
   double y1;
   int x2;
   double y2;       // 从 x1 到 y2 - 倾斜水平参数
   double v;        // 水平级别的数值
   int dir;         // 方向
   double target;   // 目标
   // 用于计算倾斜水平数值的方法
   double y3(int x3){
      if(CompletionType==TwoLastVertices){
            return(y1+(x3-x1)*(y2-y1)/(x2-x1));
      }
      else{
         return(v);
      }
   }
   // 用于初始化或者重置参数的方法
   void Init(){
      x1=0;
      y1=0;
      x2=0;
      y2=0;
      v=0;
      dir=0;   
   }
};

该结构包含了线形参数的栏位: x1, y1, x2, y2; 栏位 'v' 用于水平级别数值; 'd' 是形态的方向; 'target' 是目标。目标可以设为价格水平 (当使用 FromVertexToVertex 时) 或者是突破水平的数值 (当使用 OneVertex 或者 TwoVertices 时)。y3() 方法用于计算倾斜水平的数值,Init() 方法用于初始化或者重置数值,

如果所有的形态生成条件都满足,就调用 SetLevelParameter() 函数。根据所选水平的类型 (水平或者倾斜),倾斜水平参数(栏位 x1, y1, x2, y2) 或者一个水平级别数值 'v' 会在这个函数中设置。在 y3() 方法中, 水平数值是使用 x1, y1, x2, y2 栏位或者返回的 'v' 栏位数值计算的。

在指标中声明了两个 SLevelParameters 类型的变量:

SLevelParameters CurLevel;
SLevelParameters PreLevel;

这对变量的使用和变量 CurCount-PreCount 以及 CurDir-PreDir 类似, 变量的数值在指标初始计算时会重置 (代码部分位于 OnTick() 函数的开头):

int start;

if(prev_calculated==0){           // 第一次计算全部柱
   start=1;      
   CurCount=0;
   PreCount=0;
   CurDir=0;
   PreDir=0;  
   CurLevel.Init();    
   CurLevel.Init();
   LastTime=0;
}
else{                           // 计算新柱和生成的柱
   start=prev_calculated-1;
}


在每个柱的计算中,这些变量的数值会有变化(代码位于指标循环的开始):

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

目标参数是通过 SetTarget() 函数调用的:

void SetTarget(int dir,int li){
   switch(Target){
      case FromVertexToVertex:
         //  '从顶点到顶点'的版本
         if(dir==1){
            CurLevel.target=PeackTrough[CurCount-1].Val+(PeackTrough[li+1].Val-PeackTrough[li].Val);
         }
         else if(dir==-1){
            CurLevel.target=PeackTrough[CurCount-1].Val-(PeackTrough[li].Val-PeackTrough[li+1].Val);
         }
      break;
      case OneVertex:
         // 使用一个顶点
         CurLevel.target=MathAbs(PeackTrough[li].Val-PeackTrough[li+2].Val);
      break;
      case TwoVertices:
         // 使用两个顶点
         SetTwoVerticesTarget(dir,li);
      break;
   }
}

为 FromVertexToVertex 要计算一个价格值。对于 OneVertex, 价格变化的数值是从突破水平到目标,赋值到 'target' 栏位。在 SetTwoVerticesTarget() 函数中计算 SetTwoVerticesTarget :

void SetTwoVerticesTarget(int dir,int li){
   // 形态中初始线的坐标
   //  - 从底部到顶部  
   double x11=PeackTrough[li].Bar;
   double y11=PeackTrough[li].Val;
   double x12=PeackTrough[li+1].Bar;
   double y12=PeackTrough[li+1].Val;
   // 通过两个底部画线用于买入的坐标
   // 或者通过两个顶部画线用于卖出
   double x21=PeackTrough[li+2].Bar;
   double y21=PeackTrough[li+2].Val;
   double x22=PeackTrough[li+4].Bar;
   double y22=PeackTrough[li+4].Val;
   // 线的交叉点的数值
   double t=TwoLinesCrossY(x11,y11,x12,y12,x21,y21,x22,y22);
   // 根据方向设置目标数值
   if(dir==1){
      CurLevel.target=t-PeackTrough[li].Val;
   }
   else if(dir==-1){
      CurLevel.target=PeackTrough[li].Val-t;         
   }
}

对于 SetTwoVerticesTarget 版本, 'target' 栏位的赋值为从突破水平到目标的价格移动,与 OneVertex 类似。

让我们探讨价格和水平跟踪是如何进行的 (CompletionType 不等于 Immediately):

// 使用水平级别
if(CompletionType!=Immediately){
   // 之字转向有变化
   if(PeackTrough[CurCount-1].Bar==i){
      if(CurLevel.dir==1){                // 等待向上突破
         // 把水平数值赋予 cl 变量
         double cl=CurLevel.y3(i); 
         // 之字转向突破了水平级别
         if(PeackTrough[CurCount-1].Val>cl){
            // 设置向上箭头
            UpArrowBuffer[i]=low[i];
            // 设置目标点
            if(Target==FromVertexToVertex){
               // 'target' 栏位中的价格
               UpDotBuffer[i]=CurLevel.target;                        
            }
            else{
               // 与 'target' 栏位中水平的距离
               UpDotBuffer[i]=cl+CurLevel.target;
            }
            // 清零 'dir' 栏位以停止跟踪水平
            CurLevel.dir=0;
         }
      }
      else if(CurLevel.dir==-1){         // 等待向下突破
         // 把水平数值赋予 cl 变量
         double cl=CurLevel.y3(i);
         // 之字转向突破了水平级别
         if(PeackTrough[CurCount-1].Val<cl){
            // 设置向下箭头
            DnArrowBuffer[i]=low[i];
            // 设置目标点
            if(Target==FromVertexToVertex){
               // 'target' 栏位中的价格
               DnDotBuffer[i]=CurLevel.target;
            }
            else{                     
               // 与 'target' 栏位中水平的距离
               DnDotBuffer[i]=cl-CurLevel.target;
            }
            // 清零 'dir' 栏位以停止跟踪水平
            CurLevel.dir=0;
         }         
      }         
   }
} 

这个检查是在每次之字转向有变化的时候进行的,所有的之字转向峰值都保存在 PeackTrough 数组中,改变是由最新之字转向点对当前柱索引变化来确定的:

if(PeackTrough[CurCount-1].Bar==i){

当前的水平数值使用 y3() 方法来计算:

double cl=CurLevel.y3(i); 

要检查最新的之字转向片段是否突破了这一水平:

if(PeackTrough[CurCount-1].Val>cl){

如果水平被突破,指标就画出箭头并加上目标点,'target' 栏位可能包含目标的价格数值,在这种情况下直接使用这个数值。它也可能包含和目标的距离,在这种情况下目标是根据当前价格水平数值来计算的: 

if(Target==FromVertexToVertex){
   UpDotBuffer[i]=CurLevel.target;                        
}
else{
   UpDotBuffer[i]=cl+CurLevel.target;
}

最后,'dir' 栏位要重置来停止跟踪价格,直到下一个形态出现:

CurLevel.dir=0;

现在,指标的创建就结束了,一些它运行的片段显示在图15中。


图 15. iHorizontalFormation 指标的信号

指标中还加入了一个提醒的功能,可用的指标放在下面的附件中,文件名为 iHorizontalFormation。


用于搜索旗形,三角旗形和楔形的通用指标

现在,我们将创建用于搜索第二类形态的指标,它们的形状是由柱形填充在形态区域内而构成的,形态起始于一个大的价格变化,在这种情况下,它从一根长柱开始。我们使用一个大周期的 ATR 指标来确定长柱,如果一个柱的影线大小超过了ATR值乘以一个系数,就认为它是长的,所以,我们需要外部参数来用作ATR周期数和系数:

input int                  ATRPeriod            =  50;
input double               K1                   =  3;

让我们声明一个全局指标变量来用作ATR句柄:

int h;

在 OnInit() 函数中,载入 ATR 指标并取得它句柄的值:

h=iATR(Symbol(),Period(),ATRPeriod);
if(h==INVALID_HANDLE){
   Alert("载入指标出错");
   return(INIT_FAILED);
}

在指标主循环中取得ATR的数值:

double atr[1];
if(CopyBuffer(h,0,rates_total-i-1,1,atr)==-1){
   return(0);
}

使用ATR的值检查柱的大小,如果柱的影线大小超过了由系数设置的阈值,我们就需要确定期望的价格变化方向,方向是由颜色(根据开盘价和收盘价)来确定的,如果收盘价格高于开盘价,就期待价格的上涨变化,如果收盘价低于开盘价,就期待价格的下跌变化。

if(high[i]-low[i]>atr[0]*K1){    // 长柱形
   if(close[i]>open[i]){         // 柱为上涨方向
      Cur.Whait=1;
      Cur.Count=0;
      Cur.Bar=i;
   }
   else if(close[i]<open[i]){    // 柱为下跌方向
      Cur.Whait=-1;   
      Cur.Count=0;
      Cur.Bar=i;
   }
}


当柱的大小和方向条件符合时,Cur 结构的栏位值就设为对应的数值: 期待的方向在 Whait 栏位中设置 (1 上涨, -1 下跌), 并且重设 Count 栏位,它被赋值为0,这个栏位将用于计算形态中的柱数。形态中初始(长)柱的索引保存在 'Bar' 栏位中。

让我们分析一下 Cur 结构,该结构有三个栏位,还有一个 Init() 方法来快速重置所有的栏位:

struct SCurPre{
   int Whait;
   int Count;
   int Bar;
   void Init(){
      Whait=0;
      Count=0;
      Bar=0;
   }
};

在 OnTick() 函数的开始,声明了两个这种类型的静态变量以及一个datetime类型的变量:

static datetime LastTime=0;   
static SCurPre Cur;          
static SCurPre Pre;

然后我们要计算从指标开始计算的第一个柱的索引,还要初始化 Cur 和 Pre 变量:

int start=0;

if(prev_calculated==0){           // 第一次计算指标
   
   start=1;      
   
   Cur.Init();
   Pre.Init();             
     
   LastTime=0;      
}
else{                             // 计算新柱和正在生成的柱
   start=prev_calculated-1;
}

Cur 和 Pre 变量的数值移动到主指标循环的开始:

if(time[i]>LastTime){       // 第一次计算一个柱
   LastTime=time[i];
   Pre=Cur;              
}
else{                      // 重新计算一个柱
   Cur=Pre;
}  

这个方法和变量在沃尔夫波形一文中有详细描述 (PreCount 和 CurCount 变量)。在本文中,当创建 iHorizontalFormation 指标时会使用 (以 Cur 和 Pre 为前缀的变量),

如果 Cur.Count 变量不为0, 指标就继续检验用于侦测形态的条件,计算构成形态的柱的数量,而增加 CurCount 变量。长柱之后的第一个柱会跳过,而以下的检查从第三个柱开始进行:

if(Cur.Whait!=0){
   Cur.Count++;            // 计算柱数
   if(Cur.Count>=3){
      // 进一步检查

   }
}

作进一步检查的主要指示是柱形的覆盖 (图 16).

图 16. 两个柱的相互覆盖 L 定义为
最高价的最小值和最低价的最大值之间的差

覆盖是使用两个柱来计算的,等于最高价的最小值和最低价的最大值之间的差。 

Overlapping=MathMin(high[i],high[i-1])-MathMax(low[i],low[i-1]);

不会检查初始柱的覆盖,所以覆盖检查是从第三个柱开始而不是第二个柱开始的,

两个柱的覆盖必须超过一个阈值,如果这个数值是使用点数设置的,指标的工作将非常依赖于时段,因为在不同的时段中,这个参数值将差别很大。为了不依赖于时段,让我们使用两个柱的最长影线作为基础值来进行柱的检查:

double PreSize=MathMax(high[i-1]-low[i-1],high[i]-low[i]);

检查柱的覆盖值:

if(!(Overlapping>=PreSize*MinOverlapping))

如果两个柱没有覆盖,就认为一系列连续覆盖柱就算结束,在这种情况下,我们用一行代码检查柱数:

if(Cur.Count-2>=MinCount){
   // 进一步检查
}
Cur.Whait=0;

如果一行中的柱数超过了 MinCount 变量的值,就进行另外的检查,否则对形态形成的等待就终止,把 CurCount 变量清零。在上面的代码中,当检查条件时,要从 CurCount 变量中减去2,也就是说,在覆盖条件中,第一个长柱和结束的柱没有考虑在内。

MinOverlapping 和 MinCount 是指标的外部变量:

input double               MinOverlapping       =  0.4;
input int                  MinCount             =  5;

当满足覆盖条件的柱数条件满足后,我们就继续进一步检查: 形态的形状和倾斜。首先,我们确定找到覆盖柱序列的参数:  

double AverSize,AverBias,AverSizeDif;
PatternParameters(high,low,i-1,Cur.Count-2,AverSize,AverBias,AverSizeDif);

参数是在 PatternParameters() 函数中确定的,并且以引用方式返回,变量有 AverSize, AverBias, AverSizeDif。平均柱的大小在 AverSize 中返回, 平均柱中心的偏移在 AverBias 中返回, 而邻近柱的平均差在 AverSizeDif 中返回。让我们在 PatternParameters() 函数中详细探讨以理解这些参数是如何计算的:

void PatternParameters( const double & high[],
                        const double & low[],
                        int i,
                        int CurCnt,
                        double & AverSize,
                        double & AverBias,
                        double & AverSizeDif
){
            
   // 平均柱大小            
   AverSize=high[i-CurCnt]-low[i-CurCnt];
   // 平均柱偏移
   AverBias=0;
   // 两个邻近柱偏差的平均值
   AverSizeDif=0;
   
   for(int k=i-CurCnt+1;k<i;k++){      // 序列中除了第一个柱的所有柱
      // 平均大小
      AverSize+=high[k]-low[k];
      // 平均偏移
      double mc=(high[k]+low[k])/2;
      double mp=(high[k-1]+low[k-1])/2;
      AverBias+=(mc-mp);
      // 平均大小的偏差
      double sc=(high[k]-low[k]);
      double sp=(high[k-1]-low[k-1]);
      AverSizeDif+=(sc-sp);               
      
   }
   
   // 总数除以数量
   AverSize/=CurCnt;
   AverBias/=(CurCnt-1);
   AverSizeDif/=(CurCnt-1); 
} 

以下数据传递给函数: 两个箭头 '高' 和 '低', 覆盖结束的柱的索引, 序列的长度以及三个用于返回值的变量,数值是在 for 循环中计算的。因为 AverBias 和 AverDiff 是为两个邻近的柱计算的,序列中的第一个柱会被跳过:

for(int k=i-CurCnt+1;k<i;k++)

所以,在循环之前,AverBias 和 AverDiff 要被重置,而 AverSize 变量的值要设为根据循环中被跳过柱来计算,

柱的大小在循环中被加到 AverSize:

AverSize+=high[k]-low[k];

对于 AverBias (偏移), 计算柱的中点,然后计算它们之间的差,这些差在结果中汇总:

double mc=(high[k]+low[k])/2;
double mp=(high[k-1]+low[k-1])/2;
AverBias+=(mc-mp);

对于 AverSizeDif, 计算的是邻近柱的大小和它们之间的差,差的结果也会被汇总:

double sc=(high[k]-low[k]);
double sp=(high[k-1]-low[k-1]);
AverSizeDif+=(sc-sp);    

在循环之后,所有总的值要除以汇总数值的数量:

AverSize/=CurCnt;
AverBias/=(CurCnt-1);
AverSizeDif/=(CurCnt-1); 

在计算参数之后,要检查形态的形状,这个检查不依赖于期望价格变化的方向。形状是使用三个函数检查的: FormTapered() 用于检查收敛三角形 (三角旗形), FormHorizontal() 用于检查长方形 (旗形), FormExpanding() 用于检查扩张形状 (楔形):

if(   FormTapered(AverSizeDif,AverSize) ||
      FormHorizontal(AverSizeDif,AverSize) ||
      FormExpanding(AverSizeDif,AverSize)
){ 
   // 检查方向
}

iHorizontalFormation 指标设置只允许选择三种类型中的一种,而三种类型可以独立使用。这是因为条件很少能够满足,而且交易信号也少见。指标参数中有三个变量使得可以启用/禁用这些形态中的每一个,另外,每个形状都在属性窗口中提供了一个系数:

input bool                 FormTapered          =  true;
input double               FormTaperedK         =  0.05;
input bool                 FormRectangular      =  true;
input double               FormRectangularK     =  0.33;
input bool                 FormExpanding        =  true;
input double               FormExpandingK       =  0.05;

 让我们分析一下形状检查函数。FormTapered() 函数:

bool FormTapered(double AverDif, double AverSize){
   return(FormTapered && AverDif<-FormTaperedK*AverSize);
}

如果柱大小的平均差小于负的阈值,则柱的大小可以认为是减小的,这对应了形态的收敛形状:

FormHorizontal() 函数:

bool FormHorizontal(double AverDif, double AverSize){
   return(FormRectangular && MathAbs(AverDif)<FormRectangularK*AverSize);
}

如果柱大小的平均差小于阈值,则柱的大小可以认为是相等的,这对应了形态的长方形形状:

FormExpanding() 函数:

bool FormExpanding(double AverDif, double AverSize){
   return(FormExpanding && AverDif>FormExpandingK*AverSize);
}

在这个函数中,与收敛形态相反,柱大小的平均差应当超过正的阈值,这对应了增长的柱以及扩张形状。

如果形状检查成功完成,就检查形态的倾斜。这个检查会根据期待价格移动方向,CheckInclineForBuy() 用于上涨的方向, CheckInclineForSell() 用于下跌的方向:

if(Cur.Whait==1){
   if(CheckInclineForBuy(AverBias/AverSize)){
      // 再检查上涨方向

   }
}
else if(Cur.Whait==-1){
   if(CheckInclineForSell(AverBias/AverSize)){   
      // 再检查下跌方向

   }
}

倾斜检查的选项是独立启用的,和形状检查的选项类似,在属性窗口中有相应的变量,在属性窗口中对每种倾斜都有一个独立的系数:

input bool                 InclineAlong         =  true;
input double               InclineAlongK        =  0.1;
input bool                 InclineHorizontal    =  true;
input double               InclineHorizontalK   =  0.1;
input bool                 InclineAgainst       =  true;
input double               InclineAgainstK      =  0.1;


CheckInclineForBuy() 函数:

bool CheckInclineForBuy(double Val){
   return(  (InclineAlong && Val>InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val<-InclineAgainstK)
   );
}   

相对柱的 AverBias/AverSize 偏移值会被传递给这个函数,如果它高于正的阈值,形态就被认为是向上倾斜的;如果它小于负的阈值,则倾斜就是向下的。如果不考虑符号,数值在阈值之内,就认为形态是水平的:

bool CheckInclineForBuy(double Val){
   return(  (InclineAlong && Val>InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val<-InclineAgainstK)
   );
}   

对于下跌方向也类似:

bool CheckInclineForSell(double Val){
   return(  (InclineAlong && Val<-InclineAlongK) || 
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) || 
            (InclineAgainst && Val>InclineAgainstK)
   );
}  

下载,下跌倾斜对应着价格移动的方向,而上涨倾斜是相反的方向。

最后的检查是完成柱的方向,最后的检查有两种可能: 结束柱的方向和形态方向相同或者相反,在属性窗口中的下述参数使得可以在最后的检查中启用这两种方式:

input bool                 EnterAlong           =  true;
input bool                 EnterAgainst         =  true;

检查上涨方向是按以下方式进行的:

if((EnterAlong && close[i]>open[i]) || (EnterAgainst && close[i]<open[i])){
   Label1Buffer[i]=low[i];
   Label3Buffer[i]=close[i]+(high[Cur.Bar]-low[Cur.Bar]);
}

如果选择了 EnterAlong 而柱是上涨方向的,或者选择了 EnterAgainst 而柱是下跌方向的,指标就会画出箭头和目标点,目标距离大小等于初始大柱的大小。

对于下跌方向也类似:

if((EnterAlong && close[i]<open[i]) || (EnterAgainst && close[i]>open[i])){
   Label2Buffer[i]=high[i];                  
   Label4Buffer[i]=close[i]-(high[Cur.Bar]-low[Cur.Bar]);
}

该指标应该可以说是完成了,有可用的指标带有提醒功能,位于下面的附件中,文件名为 iFlag。 


测试指标

测试指标有效性的最简单方便的方法就是在策略测试器中运行一个EA交易,在沃尔夫波形文章中,作者创建了一个简单的EA交易,我们可以稍微改动该EA,并且用它来测试本文中创建的指标。iHorizontalFormation 和 iFlag 指标缓冲区的索引对应了 iWolfeWaves 指标缓冲区的索引,所以,我们只需要改变 EA 交易的外部参数以及 iCustom() 调用。

还有另一种测试指标的有趣方法,可以直接评估它们的效果: 一个测试指标。在另外的指标中模拟根据柱指标的箭头进行交易,而在图表中显示净值和余额线。

创建一个测试指标的最简单和明显的方法就是使用 iCustom() 函数,但是这种方法有很大的缺点: 主指标在价格图表窗口中画出箭头,而测试指标画出的净值和余额曲线显示在一个子窗口中。所以,我们需要在图表上使用相同参数运行两个指标,晚些时候如果您需要改变参数,就需要为两个指标都这样做,很不方便。

另一个 variant is to make the tester indicator draw arrows as graphical objects on the chart.

第三个方法是使用ChartIndicatorAdd()函数,这个函数可以在图表上附加另一个指标,在这种情况下,当您每次改变主指标参数的时候,您将需要找到图表上的另外的测试指标,把它删除并重新使用新的参数重新开始,这是一个可接受和方便的选项。

然而另外还有第四个方法, 它与第三个方法相比不会更方便,但是它在实现的过程中更加简单。此外,我们可以创建一个通用的测试期指标,并把它们稍微修改之后就可以用于 iHorizontalFormation 指标和 iFlag 指标,

修改后的 iHorizontalFormation 和 iFlag 是和创建外部可用ID的时候修改了。 

input int                  ID             =  1;

然后,我们需要一个短的指标名称并使用这个参数来在 OnInit 函数中设置短的指标名称。

string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

段名称包含了指标文件名称,"-" 符号和 ID 变量的数值。测试指标将可以找到主指标,并且使用它的短名称来取得他的句柄。

iHorizontalFormation 指标是基于 ZigZag 的,可以使用高价-低价,关闭或者其它指标。. 当使用 high-low 计算时, 在图表上画出的箭头不会从图表上消失。并且,如果我们使用这个指标来交易,我们可以在当前正在生成的柱上分析它的信号。在其它情况下,当使用收盘价和其它指标计算之字转向时,箭头将在已经生成的柱上做跟踪。所以,我们需要通知测试指标它应当在哪个柱上检查箭头。

iHorizontalFormation 和 iFlag 指标会画出目标点,可以用于设置获利。但是对于 iHorizontalFormation 它只有在之字转向使用价格计算的时候才有可能,所以我们需要通知测试指标,它是应当使用目标点还是额外的获利和止损参数。第一个想法是使用一个全局变量来传递数据, 但是 MetaTrader 5 终端有以下的特点: 当指标参数改变时,指标会有新的实例,以新的句柄载入,而之前的实例会从内存中立即退出。所以,如果我们返回指标的外部参数,新载入和指标的计算就不会进行,也就是说 OnInit() 函数不会被执行,并且 prev_calculated 变量将不会重置,结果,全局变量将不会得到新的数值。 

测试指标需要的参数将使用指标缓冲区传递,一个缓冲区元素就足够担当此任务了。我们使用已有元素,也就是第一个和最左边的元素,我们需要传递两个数值,它们其中的一个确定测试指标是应当在已经完成的柱上还是在正在出现的柱上分析主指标,第二个数值设置了是否应当使用目标点来做仓位的获利。在OnCalculate() if prev_calculate=0 处加入了以下代码:

int ForTester=0;       // 一个用于此数值的变量
if(!(SrcSelect==Src_HighLow)){
   // 在已经完成的柱上工作
   ForTester+=10;
}   
if(!(SrcSelect==Src_HighLow || SrcSelect==Src_Close)){
   // 可以使用目标点
   ForTester+=1;
}     
UpArrowBuffer[0]=ForTester;  

使用数组元素,我们需要传递两个不大于10的数值,这就是为什么我们要把它们其中的一个乘以10再加上第二个。因为传递的数字数值只可能是0或者1,我们可以使用一个二进制数,但是传递的数据数量很少,所以就不需要节约字节了。

应当在 iFlag 指标中做出类似的修改,在 OnInit() 函数中:

string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);


In OnCalculate():

Label1Buffer[0]=11;

iFlag 指标可以永远在已经完成的柱上分析,而它的目标点可以永远被使用,所以,不需要计算就可以把数值设为11。

当使用已经完成的柱时,仓位明显在新柱开启的时候建立,但是当在当前出现的柱上进场时,进场价格是不知道的。所以需要在 iHorizontalFormation 中加入另一点修改: 加了一个缓冲区用来画出柱的箭头,在这个缓冲区中指定建立仓位的价格水平。

现在我们将直接操作测试指标了,创建一个新的指标,命名为 iTester 并加上外部参数:

input int                  ID             =  1;
input double               StopLoss_K     =  1;
input bool                 FixedSLTP      =  false;
input int                  StopLoss       =  50;
input int                  TakeProfit     =  50;

在此:

  • ID 是主指标的标识符
  • StopLoss_K 是在使用目标点时,根据获利值来计算止损值使用的系数
  • FixedSLTP 意思是使用 StopLoss 和 TakeProfit 变量,还是使用目标点和 StopLoss_K 变量。

如果测试指标显示在左上角时,不仅显示它的名称,还有它所使用箭头的指标名称,那将非常方便。但是当测试指标还没有被附加到图表时,将会显示测试指标的名称。声明一个全局变量:

string IndName;

在 OnInit() 中, 把测试指标的名称赋给它:

IndName=MQLInfoString(MQL_PROGRAM_NAME);

测试指标建立的每个仓位的信息都保存在一个 SPos 结构的数组中,而 PosCnt 变量是用于计数开启仓位的数量:

struct SPos{
   int dir;
   double price;
   double sl;
   double tp;
   datetime time; 
};
SPos Pos[];
int PosCnt;

交易应该只能在数组中加入一次,使用下面的变量:

datetime LastPosTime;

当在数组中加入一个仓位的时候,会检查 LastPosTime 的时间以及仓位建立时柱的时间。当增加一个仓位时,LastPosTime 变量会设置新的时间。如果柱的时间等于 LastPosTime 的时间, 则仓位已经建立。

为了平掉仓位,也就是说为了计算利润,我们将需要两个另外的变量:

int Closed;
datetime CloseTime;

在一个柱上平仓的利润被赋给 Closed 变量, 而柱的时间被赋给 CloseTime。下一步,我们将详细看到这是如何工作的。

我们已经讨论了所有的辅助变量和 OnInit() 函数,现在我们可以继续到 OnCalculate() 函数,声明以下的辅助变量:

string name;
static int last_handle=-1;
static int shift=0;
static int use_target=0;
int handle=-1;     
int start=2;  

变量的描述:

  • name 将用于取得在ChartIndicatorName()中得到的指标名称;
  • 静态变量 last_handle 将用于保存测试指标的句柄;
  • 静态变量 shiftuse_target 将被用于从测试指标中传递参数;
  • handle 将用于取得在ChartIndicatorGet()中得到的句柄;
  • start 将可以开始指标的计算。

让我们分析一下搜索测试指标的代码,首先,确定价格图表中附加的指标的数量:

int it=ChartIndicatorsTotal(0,0);

使用一个循环:

for(int i=0;i<it;i++){        // 图表上的全部指标
   // 取得下一个指标的名称
   name=ChartIndicatorName(0,0,i);
   // 搜索子字符串 "-"
   int p=StringFindRev(name,"-");
   if(p!=-1){
      // 找到了子字符串,检查标识符的值
      if(StringSubstr(name,p+1,StringLen(name)-p-1)==IntegerToString(ID)){
         // ID 对应,取得句柄
         handle=ChartIndicatorGet(0,0,name);
      }
   }
} 

让我们详细探讨上面的代码部分,'name' 变量赋值为使用 ChartIndicatorName() 函数得到的指标名称,该函数会根据给定的索引返回指标的名称。然后会检查取得的名称与ID是否对应,为此要搜索子字符串"-"是否存在,如果找到了 "-" , 就展开它后面的字符串,如果它与标识符相对应,'handle'变量就通过 ChartIndicatorGet() 函数取得句柄,该函数根据指标名称返回句柄。 

在取得了句柄之后,把它和之前变量 last_handle 中所知的句柄相比较 (这个变量是静态的,也就是说它在 OnCalculate() 函数结束后会保持它的数值):

if(handle!=last_handle){
   if(handle==-1){                    // 没有句柄
      // 设置源名称
      IndicatorSetString(INDICATOR_SHORTNAME,IndName);
      ChartRedraw(0);
      return(0);
   }
   // 检查测试指标的计算是否已经结束
   int bc=BarsCalculated(handle);
   if(bc<=0)return(0);                // 如果没有,函数就中断
   // 使用测试参数复制数据
   double sh[1];
   if(CopyBuffer(handle,0,rates_total-1,1,sh)==-1){
      // 如果复制失败,函数就中断直到
      // 下一个分时
      return(0);
   }
   // 展开独立的参数
   shift=((int)sh[0])/10;              // 已完成或是正在出现的柱
   use_target=((int)sh[0])%10;         // 是否使用目标点?
   last_handle=handle;                 // 保存句柄数值
   // 设置指标名称
   IndicatorSetString(INDICATOR_SHORTNAME,name);
   ChartRedraw(0);
}
else if(prev_calculated!=0){
   // 如果没有新的句柄,只计算新柱, 
   // 还有当前正在形成的柱
   start=prev_calculated-1;
}

如果 'handle' 变量的值不等于 last_handle 的值, 则测试的指标发生了变化,也许它正在被加到图表上,或是它的参数被修改。也可能它已经被从图表上删除。如果指标被从图表上删除,'handle' 变量就等于 -1, 而测试指标被设为默认名称,OnCalculate() 函数就结束。如果 handle 变量为有效的句柄值,就取得了测试参数: 正在形成/已经完成的柱,以及是否允许使用目标点。在 OnCalculate() 的进一步执行过程中, 'handle' 和 'last_handle' 是相等的,会进行 'start' 变量的通常计算,也就是从计算的初始柱开始计算。

默认的 'start' 变量的值是 2,如果需要完整重新计算指标,(当句柄改变或者当 prev_calculated 等于0的时候需要这样做), 有必要重置一些其他的变量:

if(start==2){
   PosCnt=0;
   BalanceBuffer[1]=0;
   EquityBuffer[1]=0;
   LastPosTime=0;
   Closed=0;
   CloseTime=0;      
}

在重置过程中,以下元素会被清零: 开启仓位的数量 PosCnt, 余额和净值指标数组的第一个元素 BalanceBuffer[1]EquityBuffer[1], 最新的仓位时间 LastPosTime, 在同一个柱上平仓的利润Closed 以及平仓时间 ClosedTime

现在,让我们分析主指标循环,下面是它含有注释的完整代码,后面有每行的分析:

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

   // 传入之前知道的余额和净值
   BalanceBuffer[i]=BalanceBuffer[i-1];
   EquityBuffer[i]=EquityBuffer[i-1];

   if(CloseTime!=time[i]){ 
      // 开始新柱的计算
      Closed=0; // 把利润变量清零
      CloseTime=time[i];          
   }

   // 取得测试指标的数据
   double buy[1],sell[1],buy_target[1],sell_target[1],enter[1];
   int ind=rates_total-i-1+shift;
   if(CopyBuffer(last_handle,0,ind,1,buy)==-1 || 
      CopyBuffer(last_handle,1,ind,1,sell)==-1 ||
      CopyBuffer(last_handle,2,ind,1,buy_target)==-1 || 
      CopyBuffer(last_handle,3,ind,1,sell_target)==-1        
   ){
      return(0);
   }
  
   if(shift==0){
      // 如果测试是在正在出现的柱上进行的,取得开盘价格 
      // 要从测试指标的额外缓冲区中取得       
      if(CopyBuffer(last_handle,4,ind,1,enter)==-1){
         return(0);
      } 
   }
   else{
      // 如果测试是在已经完成的柱上进行的,我们使用柱的
      // 开盘价格
      enter[0]=open[i];
   }

   // 买入箭头
   if(buy[0]!=EMPTY_VALUE){
      AddPos(1,enter[0],buy_target[0],spread[i],time[i],use_target);      
   }
   // 卖出箭头
   if(sell[0]!=EMPTY_VALUE){
      AddPos(-1,enter[0],sell_target[0],spread[i],time[i],use_target);       
   }

   // 检查是否需要平仓
   CheckClose(i,high,low,close,spread);

   // 余额线
   BalanceBuffer[i]+=Closed;
   
   // 净值线
   EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);

}

余额值是从之前所知的余额和平仓利润中得到的,为此,要从缓冲区中前面元素中传入之前所知的余额。

// 传入之前所知的余额和净值
BalanceBuffer[i]=BalanceBuffer[i-1];


在每个柱的第一次计算中,这个柱上的平仓利润变量要被清零:

if(CloseTime!=time[i]){ 
   Closed=0;
   CloseTime=time[i];          
}

复制测试指标的数据:

// 取得测试指标的数据
double buy[1],sell[1],buy_target[1],sell_target[1],enter[1];
int ind=rates_total-i-1+shift;
if(CopyBuffer(last_handle,0,ind,1,buy)==-1 || 
   CopyBuffer(last_handle,1,ind,1,sell)==-1 ||
   CopyBuffer(last_handle,2,ind,1,buy_target)==-1 || 
   CopyBuffer(last_handle,3,ind,1,sell_target)==-1        
){
   return(0);
}

箭头缓冲区的数据被复制到 'buy' 和 'sell' 数组中,目标点的数据复制到 'buy_target' 和 'sell_target'。在复制之前,要考虑到 shift 偏移变量来计算柱的索引 'ind',

根据 'shift'的值, 看是复制额外缓冲区或者柱的开盘价:

if(shift==0){
   // 如果测试是在正在出现的柱上进行的,取得开盘价格 
   // 要从测试指标的额外缓冲区中取得       
   if(CopyBuffer(last_handle,4,ind,1,enter)==-1){
      return(0);
   } 
}
else{
   // 如果测试是在已经完成的柱上进行的,我们使用柱的
   // 开盘价格
   enter[0]=open[i];
}

如果在计算的柱上找到了箭头,就通过调用 AddPos() 函数来建立仓位:

// 买入箭头
if(buy[0]!=EMPTY_VALUE){
   AddPos(1,enter[0],buy_target[0],spread[i],time[i],use_target);      
}
// 卖出箭头
if(sell[0]!=EMPTY_VALUE){
   AddPos(-1,enter[0],sell_target[0],spread[i],time[i],use_target);       
}

在 CheckClose() 函数中检查仓位是否需要关闭,如果仓位关闭,利润结果会保存到 Closed 变量中:

// 检查,是否需要平仓
CheckClose(i,high,low,close,spread);

Closed 变量中的利润要加到余额中:

// 余额线
BalanceBuffer[i]+=Closed;

净值是由余额和浮盈共同组成,通过 SolveEquity() 函数计算所得的:

EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);

让我们讨论下面的函数: AddPos(), CheckClose(), SolveEquity(). 以下是每个函数的代码,都带有详细描述。  

AddPos():

void AddPos(int dir, double price,double target,int spread,datetime time,bool use_target){

   if(time<=LastPosTime){
      // 带有 'time' 时间的仓位已经被加过了
      return;
   }
   
   // 数组中没有空闲空间
   if(PosCnt>=ArraySize(Pos)){
      // 数组大小增加32个元素块大小
      ArrayResize(Pos,ArraySize(Pos)+32);
   }
   
   // 保存仓位的方向
   Pos[PosCnt].dir=dir;
   // 仓位建立时间
   Pos[PosCnt].time=time;
   // 仓位的开盘价格
   if(dir==1){
      // 买入的价格
      Pos[PosCnt].price=price+Point()*spread;  
   }
   else{
      // 卖出的价格
      Pos[PosCnt].price=price;  
   }

   // 计算止损和获利
   if(use_target && !FixedSLTP){ 
      // 使用目标点来计算止损
      if(dir==1){
         Pos[PosCnt].tp=target;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price-StopLoss_K*(Pos[PosCnt].tp-Pos[PosCnt].price),Digits());
      }
      else{
         Pos[PosCnt].tp=target+Point()*spread;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price+StopLoss_K*(Pos[PosCnt].price-Pos[PosCnt].tp),Digits());
      }   
   }
   else{
      // 使用 StopLoss 和 TakeProfit 变量来止损
      if(dir==1){
         Pos[PosCnt].tp=Pos[PosCnt].price+Point()*TakeProfit;
         Pos[PosCnt].sl=Pos[PosCnt].price-Point()*StopLoss;
      }
      else{
         Pos[PosCnt].tp=Pos[PosCnt].price-Point()*TakeProfit;
         Pos[PosCnt].sl=Pos[PosCnt].price+Point()*StopLoss;
      }     
   }
   
   PosCnt++;
   
}  

CheckClose() 函数:

void CheckClose(int i,const double & high[],const double & low[],const double & close[],const int & spread[]){
   for(int j=PosCnt-1;j>=0;j--){                                       // 所有仓位
      bool closed=false;                                               // 'false' 值表示仓位依然开启  
      if(Pos[j].dir==1){                                               // 买入
         if(low[i]<=Pos[j].sl){                                        // 价格低于或者等于止损
            // 利润点数
            Closed+=(int)((Pos[j].sl-Pos[j].price)/Point());
            closed=true;                                               // 以j为索引的仓位已经平仓
         }
         else if(high[i]>=Pos[j].tp){                                  // 已经达到了获利
            // 利润点数
            Closed+=(int)((Pos[j].tp-Pos[j].price)/Point());    
            closed=true;                                               // 以j为索引的仓位已经平仓
         }
      }
      else{ // Selling
         if(high[i]+Point()*spread[i]>=Pos[j].sl){                     // 已经达到了止损
            // 利润点数
            Closed+=(int)((Pos[j].price-Pos[j].sl)/Point());
            closed=true;                                               // 以j为索引的仓位已经平仓
         }
         else if(low[i]+Point()*spread[i]<=Pos[j].tp){                 // 价格低于或者等于获利
            // 利润点数
            Closed+=(int)((Pos[j].price-Pos[j].tp)/Point());              
            closed=true;                                               // 以j为索引的仓位已经平仓
         }         
      }
      // 仓位已经关闭,它应当从数组中删除
      if(closed){ 
         int ccnt=PosCnt-j-1;
         if(ccnt>0){
            ArrayCopy(Pos,Pos,j,j+1,ccnt);
         }
         PosCnt--;
      }
   }
}

在 CheckClose() 函数中,所有仓位都保存在 Pos 数组中,检查它们的止损和获利,并与最高价和最低价做比较,如果仓位平仓,它的利润加到 Closed 变量中,随后仓位要从数组中删除。

SolveEquity():

int SolveEquity(int i,const double & close[],const int & spread[]){
   int rv=0;                                // 用于结果的变量
   for(int j=PosCnt-1;j>=0;j--){            // 所有仓位
      if(Pos[j].dir==1){                    // 买入
                                            // 利润
         rv+=(int)((close[i]-Pos[j].price)/Point());
      }
      else{                                // 卖出
         // 利润
         rv+=(int)((Pos[j].price+Point()*spread[i]-close[i])/Point());         
      }
   }
   return(rv);
}  


SolveEquity() 函数计算来自 Pos 数组的所有开启仓位的利润。

我们已经结束了 iTester 指标的分析,可用的 iTester 指标可以在附件中找到。图 17 显示了带有 iHorizontalFormation (箭头) 的图表,而 iTester 位于子窗口中,绿色线显示的是净值,而红色线显示余额。


图 17. iTester 指标 (位于子窗口中), 基于 iHorizontalFormation (图表上的箭头)


结论

在本文中描述的形态侦测方法解决了初始的任务,所以各种形状,例如旗形、三角旗形、三角形和楔形都可以在图表上清楚看到。探讨的方法不仅是可行的而且是绝对正确的。也可能有其他方法来识别相同的形态,例如,您可以使用线性回归,使用最高价和最低价进行独立的计算,然后检查斜率和这些线的聚合/分离。如果我们处理单个子任务,还会有更多的可用方法,可以用来解决侦测形态中的一般难点。尽管这样,在本文中讨论的在创建指标时的价格分析方法对于其他技术分析相关目标也是有所帮助的。 


附件

我们在文章中创建的所有指标都在下面的附件中,包括:

  • iHorizontalFormation
  • iFlag
  • iTester

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/3229

附加的文件 |
files.zip (9.87 KB)
图形界面 XI: 表格单元中的文本编辑框和组合框 (统合构建15) 图形界面 XI: 表格单元中的文本编辑框和组合框 (统合构建15)
在更新的函数库中, 表格控件 (CTable 类) 将补充新的选项。表格单元中的控件阵容得到扩展, 此次添加了文本编辑框和组合框。此外, 此次更新还引入了在运行时调整 MQL 应用程序窗口大小的功能。
在 MetaTrader 5 中创建和测试自定义交易品种 在 MetaTrader 5 中创建和测试自定义交易品种
创建自定义交易品种拓展了开发交易系统和金融市场分析的边界,现在,交易者可以在无限的金融资产工具上绘制图表和测试交易策略了。
使用云存储服务来进行终端之间的数据交换 使用云存储服务来进行终端之间的数据交换
云技术正在变得越来越流行,现在,我们可以选择付费或者免费的存储服务,有没有可能在交易中使用它们呢?本文提出了一种技术,可以使用云存储服务来进行终端之间的数据交换。
深度神经网络 (第 II 部)。制定和选择预测因子 深度神经网络 (第 II 部)。制定和选择预测因子
有关深度神经网络系列的第二篇文章研究当准备模型训练的数据期间预测因子的变换和选择。