开发基于振荡器的之字折线 (ZigZag) 指标。 执行需求规范的示例

19 七月 2018, 08:22
Dmitry Fedoseev
0
2 447

内容

概述

文章《订购指标时如何准备需求规范》带有 需求规范 样本,可基于各种振荡器开发之字折线指标。 在本文中,我将向您展示如何逐步实施此任务。

基于振荡器的之字折线指标。

在继续阅读之前,请务必单击上面的链接来学习规范。  

一般规范分析

在首次阅读时可以看到开发指标的 主要需求

  1. 开发过程要分阶段进行。
  2. 确保实现指标的最佳性能。
  3. 指标还拥有图形界面。

之字折线算法。 之字折线的构造算法与标准算法不同。

  1. 之字折线并非在局部极值形成时改变方向,而是在振荡器数值越过超买/超卖级别时才改变方向。 
  2. 之字折线基于价格图表。 相应地,新的极值由价格数据决定。

因此,必须要注意由此产生的某些特征。 

  1. 指标的高点/低点可能与价格的最高价/最低价不对应。 所以,当之字折线改变方向时,必须检查新价格的最高价/最低价是否比振荡器跨入超买/超卖区域 (图例 1) 更早出现。



    图例 1. WPR 离开超买区域发生在箭头 1 所标记的柱线中,
    然而,新的之字折线段落只绘制到箭头 2 所标记的柱线

  2. 由于之字折线的方向变化由振荡器定义,因此其数值随柱线形成而变化。 因此,之字折线能够改变方向,但当柱线形成后,也许方向改变会取消。 在此情况下,有必要确保指标的正确操作。
  3. 出于新的极值由价格图表 (按最高/最低价格) 定义,因此在柱线形成时不能取消新形成的高点/低点。 不过,之字折线可能会在具有新极值的柱线上逆转。 在此情况下,新的高点/低点会被取消 (图例 2)。


     
    图例 2. 1 — 之字折线顶点是新柱线形成的高点。
    2 — 之字折线逆转,而之前检测到的高点则被取消

    当然,由于 MetaTrader 5 拥有彩色具有之字折线绘图样式,会令本案例含糊不清。 它允许垂直在绘制之字折线段落时不将其顶点向左移动 (预判的最大值)。 然而,这样绘图无法为我们提供独立绘制两个之字折线段落 (垂直和相邻倾斜) 的能力。 另外,这种绘制之字折线的方式并不常见,且规范没有明确要求应用此方法。 因此,默认情况下会选择最常用的选项。

显示。 之字折线的显示有其自身的特性。

  1. 除了显示指标本身外,价格图表还应标记指标跨入超买 (位于柱线最高价的黄点) 和超卖 (位于柱线最低价的绿点) 区域的柱线。 
  2. 依据之字折线顶端和底端的相互位置来揭示形态。 由之字折线段落形成的形态应有不同的颜色。 事实证明这是最大的问题。 首先,我们不仅需要在检测到形态的柱线上做标记,还要更改历史记录中若干之字折线段落的颜色。 其次,当柱线形成时,之字折线逆转和新极值可能会被取消。 因此,在计算柱线之前,我们需要清除可能已标记形态 (应返回到中性颜色) 的之字折线区域。 第三,形态 (包括相反指向的形态) 也许会重叠。 因此,在清理和绘制之字折线段落时,我们不应该破坏之前已检测到形态的颜色 (图例 3)。



    图例 3. 重叠形态

    我们来考察图例 3 中显示的之字折线段落。 段落 1-4 构成上行趋势形态。 这意味着,段落 1 应该是蓝色的。 但它已被纳入下行形态,因此,它被涂成红色。 当段落 6 出现时,形成另一个向上形态 (段落 3-6)。 由于之字折线段落在每根柱线计算之前应该收到它们的初始颜色,在此情况下,我们只需要清除段落 5 和 6,因为段落 3 和 4 已经涉及到另一个形态。

    替代解决方案是改变每个新形态中所有段落的颜色。 然而,这会令指标在历史上的信息量较少。 所以,我们选择了第一个选项,尽管它要复杂得多。

图形界面。 图形界面的需求相对简单。 控件集合是恒定的,它不需要依据所选的振荡器而变化。 仅用到了两个数字参数 (超买/超卖水平),并且它们的设置对于所有振荡器是相同的。

在理解了任务的所有功能之后,我们可以开始开发指标。 在每个开发阶段结束时,我们在附件中设置相应的文件名。 如果您在进一步阅读中对于所添加代码的顺序难以理解的话,我们建议您在编辑器中打开相应阶段的文件并进行检查。

阶段 1 — 开发之字折线

我们在 MetaEditor 中创建新的 OscZigZagStep1 自定义指标。 添加一个外部变量以便在代码中留出变量的位置。 在事件处理程序选择窗口中,选择第一个选项 — OnCalculate(...,open,high,low,close),不需要其它的处理程序。 在显示参数窗口中创建两个缓冲区。 我们为第一个缓冲区命名 "HighLow",类型 — 彩色箭头和两种颜色: 金色和柠檬绿色 第二个命名为 "ZigZag",类型 — 彩色线段和三种颜色: 灰色,矢车菊蓝色和红色 (图例 4)。


图例 4. 在指标开发向导窗口中选择显示参数

由于彩色点与柱线相关,因此首先绘制它们 (更接近柱线),然后绘制之字折线更合乎逻辑。 因此,缓冲区按此顺序排列。

单击完成后,指示文件将出现在编辑器中。 首先,我们应该删除其中过多的颜色样本来调整 indicator_color1 的属性值。 那么 indicator_color1 属性的指示线应具有以下外观:

#property indicator_color1  clrGold,clrLimeGreen

应以相同的方式调整 indicator_color2 的属性 (应保留三种颜色)。

在自动创建的外部参数中查找字符串:

input int      Input1;

删除它并替换为声明 WPR 指标参数的变量:

input int         WPRperiod   =  14;
input double      WPRmax      =  -20;
input double      WPRmin      =  -80;

下面,我们声明句柄的变量:

int h;

在 OnInit() 函数的最开始下载指标:

h=iWPR(Symbol(),Period(),WPRperiod);
if(h==INVALID_HANDLE){
   Print("无法加载指标");
   return(INIT_FAILED);
}  

释放 OnDeinit() 函数中的句柄:

void OnDeinit(const int reason){
   if(h!=INVALID_HANDLE){
      IndicatorRelease(h);
   }
}  

使用指标开发向导时,我们已经创建了显示缓冲区,但我们还需要辅助缓冲区。 例如,我们现在需要保存振荡器数值的缓冲区。 将 indicator_buffers 加 1 (用 5 替换 4):

#property indicator_buffers 5

声明另一个保存振荡器数值缓冲区的数组:

double         wpr[];

在 OnInit() 函数中指派此数组用于指标缓冲区的中间计算。 代码添加在 OnInit() 函数的最末尾:

SetIndexBuffer(4,wpr,INDICATOR_CALCULATIONS); 

进入 OnCalculate() 函数,编写计算柱线计数范围的标准代码,并将 WPR 振荡器数值复制到缓冲区:

int start;

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

if(CopyBuffer(h,0,0,rates_total-start,wpr)==-1){
   return(0);
}

现在,我们可以编写标准指标循环并显示振荡器跨入超买/超卖区域的点位:

for(int i=start;i<rates_total;i++){
   HighLowBuffer[i]=EMPTY_VALUE;
   if(wpr[i]>WPRmax){
      HighLowBuffer[i]=high[i];
      HighLowColors[i]=0;
   }
   else if(wpr[i]<WPRmin){
      HighLowBuffer[i]=low[i];
      HighLowColors[i]=1;      
   }      
}

在此阶段,指标可以挂载到图表。 我们挂载标准的 WPR,并确认一切顺利 (图例 5)。


图例 5. 在价格图表上显示超买/超卖区域

我们继续开发之字折线。 我们将需要几个辅助缓冲区: 一个用于当前指标方向,而另外两个保存之字折线最后顶端和底端的柱线索引:

double         dir[]; // 方向
double         lhb[]; // 最后的顶端柱线索引
double         llb[]; // 最后的底端柱线索引

由于我们添加了三个缓冲区,我们需要在定义指标缓冲区数量的属性里提升:

#property indicator_buffers 8

通过 SetIndexBuffer() 函数来应用 OnInit() 函数中声明的新数组:

SetIndexBuffer(5,dir,INDICATOR_CALCULATIONS);  
SetIndexBuffer(6,lhb,INDICATOR_CALCULATIONS);   
SetIndexBuffer(7,llb,INDICATOR_CALCULATIONS);    

此代码应附加在 OnInit() 函数中已存在的最后一次 SetIndexBuffer() 函数调用之后。

现在,我们要考虑一个重点。 若令彩色线段类型的缓冲区正常工作,请确保为其设置空值 0。 否则,将显示下面显示的指示线而非之字折线:


图例 6. 如果彩色线段类型缓冲区的值不为空,则之字折线显示不正确

若要设置空值,请在 OnInit() 函数的最末尾 'return' 字符串之前添加以下字符串:

PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0); 

请注意,第一个参数等于 1。 在此情况下,这是显示缓冲区群中的索引,即 1 对应于 ZigZagBuffer[] 缓冲区。

OnInit() 函数已准备就绪。 现在,我们来关注 OnCalculate() 函数并继续在标准指标循环中编写代码。

在指标循环开始时,在第一个字符串清除 HighLowBudder 缓冲区之后顺移辅助缓冲区数据:

lhb[i]=lhb[i-1];      
llb[i]=llb[i-1];
dir[i]=dir[i-1];

我们在代码中为 dir[] 缓冲区设置方向,其中定义了设置跨越的超买/超卖区域:

if(wpr[i]>WPRmax){
   HighLowBuffer[i]=high[i];
   HighLowColors[i]=0;
   dir[i]=1;
}
else if(wpr[i]<WPRmin){
   HighLowBuffer[i]=low[i];
   HighLowColors[i]=1;      
   dir[i]=-1;
} 

现在,在第一阶段最令人兴奋的事情是构建之字折线。 之字折线的方向已定义。 dir[] 缓冲区包含的数值为: 当抬头向上时 1,在低头向下时 -1。 此外,我们需要定义方向变化的柱线。 之字折线基准由以下切分为 4 条分支的代码组成:

if(dir[i]==1){
   if(dir[i-1]==-1){ 
      // 方向转变上行
   }
   else{
      // 上行走势延续
   }      
}
else if(dir[i]==-1){
   if(dir[i-1]==1){ 
      // 方向转变下行
   }
   else{
      // 下行走势延续
   }      
}

我们考察之字折线方向转为上行及其随后的上行走势。 另外两条分支是对称的。

转为上行方向

1. 我们在最后一个底部到所计算的柱线范围内搜索最大价格值 (底部柱线不包括在范围之内):

if(dir[i]==1){
   if(dir[i-1]==-1){ 
      // 方向转变上行
      // 搜索最大值
      int hb=i;
      for(int j=i;j>llb[i];j--){
         if(high[j]>high[hb]){
            hb=j;
         }
      }
      //...
   }
   else{
      // 上行走势延续
   }      
}

2. 在检测到的柱线上,设置之字折线端点,在 lhb[] 缓冲区中设置柱线索引,并通过 ZigZagColor 缓冲区定义中性颜色: 

ZigZagBuffer[hb]=high[hb];
lhb[i]=hb;            
ZigZagColors[hb]=0;

3. 重新计算此柱线时,可能会发现振荡器的值已经改变,且它不应是一个端点。 因此,我们需要删除它。 通常这是在指标循环开始时通过清除缓冲区来完成:

ZigZagBuffer[i]=0;

但在此情况下,形成的之字折线顶部与所计算的柱线错位了未知数量的柱线 (图例 1)。 因此,我们需要保存新顶点所在的柱线索引以及所计算柱线的时间:

NewDotTime=time[i];
NewDotBar=hb;

在指标全局层次上声明 NewDotTime 和 NewDotBar 变量。

4. 检查 NewDotTime 变量值是否与指标循环开始时计算的柱线相匹配。 如果匹配,删除新的之字折线端点:

if(NewDotTime==time[i]){
   ZigZagBuffer[NewDotBar]=0;  
}

上行走势

我们来考察定义上行走势延续的代码段。 如果柱线的最高价超过之前已固定的之字折线数值,则删除旧端点并设置新端点:

if(high[i]>ZigZagBuffer[(int)lhb[i]]){ 
   // 删除旧端点
   ZigZagBuffer[(int)lhb[i]]=0;
   // 设置新端点
   ZigZagBuffer[i]=high[i];
   ZigZagColors[i]=0;
   lhb[i]=i;
}

在指标循环之初重新计算柱线之前,将指标返回到其原始状态 (返回删除的端点):

ZigZagBuffer[(int)lhb[i]]=high[(int)lhb[i]];
ZigZagBuffer[(int)llb[i]]=low[(int)llb[i]];  

确保使用零值初始化 lhb[] 和 llb[] 缓冲区,以避免指标操作之初的数组超界错误。 此外,我们需要将 NewDotTime 和 NewDotBar 变量清零。 这在计算计数范围时完成:

if(prev_calculated==0){
   start=1;
   lhb[0]=0;
   llb[0]=0;   
   NewDotTime=0; 
}
else{
   start=prev_calculated-1;
}

此刻,指标开发的第一阶段已经结束。 在下面的附件中,此阶段的指标名为 OscZigZagStep1.mq5。

阶段 2 — 检测形态并涂色

若要检测形态,我们需要比较 5 个之字折线的极值端点。 建议不要在每次循环中搜索指标中的所有端点,因为这会降低指标的速度 (指标的迅捷操作是主要的规范需求)。 最好将新出现的极值保存在单独的数组中,以便可以直接且快速地访问它们。   

之字折线极值点的数据将保存在结构数组中。 该结构的字段应包含: 柱线索引,数值,方向和更多布尔类型的字段。 如果极值是形态中的最后一个 (限定之字折线的颜色以便与先前识别的形态区分),则会保存' true'。 描述结构并声明数组:

struct SZZDot{
   int bar;
   double val;
   int dir;
   bool pat;
};

SZZDot ZZDot[];

然后,在四部分代码每一个的末尾添加 AddZZDot() 函数的调用。 该函数在 ZZDot[] 数组中添加新的极值:

if(dir[i]==1){ 
   if(dir[i-1]==-1){          
      //...
      AddZZDot(1,high[hb],hb,i);
   }
   else{ 
      if(high[i]>ZigZagBuffer[(int)lhb[i]]){
         //...
         AddZZDot(1,high[i],i,i);
      }
   }      
}
else if(dir[i]==-1){
   if(dir[i-1]==1){
      //...
      AddZZDot(-1,low[lb],lb,i);
   }
   else{
      if(low[i]<ZigZagBuffer[(int)llb[i]]){
         //...
         AddZZDot(-1,low[i],i,i);
      }
   }      
}

将四个参数 (方向,数值,极值所在柱线索引,以及计算的柱线索引) 传递给 AddZdot() 函数。 我们稍后会考察函数本身。 使用 cnt[] 指标缓冲区保存检测到的极值数量 (AADot[] 数组中已占用元素)。 声明 cnt[] 数组:

double         cnt[];

在 OnInit() 函数中,为它调用 SetIndexBuffer() 函数:

SetIndexBuffer(8,cnt,INDICATOR_CALCULATIONS);  

更改定义缓冲区数量的属性的值:  

#property indicator_buffers 9

在指标循环之初,顺移缓冲区最后一个值:

cnt[i]=cnt[i-1];

我们已经提到,在柱线计算期间检测到的之字折线逆转也许在下一次计算同一柱线时消失。 所以,应删除保存在数组中的极值。 不过,这种删除不是通过缩减数组而是通过降低已占用数组元素数量 (cnt[] 缓冲区) 的计数器来执行的。 这显著提高了指标的速度。

我们来看看 AddZdot() 函数:

void AddZZDot(int d,double v,int b,int i){
   
   int c=(int)cnt[i];

   if(c==0){ 
      // 在指标启动或重新计算完毕期间
      ArrayResize(ZZDot,1024);
      ZZDot[c].dir=d;
      ZZDot[c].val=v;
      ZZDot[c].bar=b;
      ZZDot[c].pat=false;
      cnt[i]=1;
   }
   else{
      if(ZZDot[c-1].dir==d){
         // 更新同向极值
         ZZDot[c-1].val=v;
         ZZDot[c-1].bar=b;         
      }
      else{
         // 添加新的极值
         // 必要时,将数组增加 1024 个元素块
         if(c>=ArraySize(ZZDot)){ 
            ArrayResize(ZZDot,c+1024);
         }
         // 添加新的极值
         ZZDot[c].dir=d;
         ZZDot[c].val=v;
         ZZDot[c].bar=b;
         ZZDot[c].pat=false;
         cnt[i]=c+1;
      }
   }
}

在指标启动或重新计算完毕期间,数组大小设置为 1024。 其初始元素具有极值参数,而极值计数器增加 1。 在后续函数调用期间,将检查数组中最后一个极值的方向。 如果它对应于调用函数的参数,则更新最后一个极值的数据。 如果方向相反,则添加新的极值。 

在分析任务时,我已经解释过,在之字折线逆转期间,相反方向的最后一个极值可以移到更早的柱线 (图例 2)。 所以,在执行之字折线主代码之前,将极值 (预先已知) 设置为 ZZDot 数组最后一个占用元素的数值。 这是在指标循环结束时完成的:

if(cnt[i]>0){
   int ub=(int)cnt[i]-1;
   if(ZZDot[ub].dir==1){
      ZZDot[ub].bar=(int)lhb[i];
      ZZDot[ub].val=high[(int)lhb[i]];
   }
   else{
      ZZDot[ub].bar=(int)llb[i];
      ZZDot[ub].val=low[(int)llb[i]];         
   }
}

现在,如果在计算的柱线上检测到新的极值,则其值将在 ZZDot 数组中更新。 在逆转的情况下,先前已知的极值仍然保留。

在第一次指标计算之前以及执行全部重新计算时,将 cnt[] 数组以初始元素进行初始化:

if(prev_calculated==0){
   //...
   cnt[0]=0;
}
else{
   start=prev_calculated-1;
}

现在我们有了全部之字折线的极值数据,并且能够轻松访问它们,我们将重点放到检测形态及其涂色。 如果至少有 5 个之字折线极值,才有可能:

if(cnt[i]>=5)

计算极值数组中最后一个元素的索引:

int li=(int)cnt[i]-1;

若在此极值上没有检测到形态,我们则要设置。 我们要为之字折线返回中性颜色:

ZZDot[li].pat=false;

为之字折线的部分返回初始颜色:

for(int j=0;j<4;j++){
   if(ZZDot[li-j].pat){
      break;
   }
   ZigZagColors[ZZDot[li-j].bar]=0;
}

注意: 一旦发现带有形态的极值,循环就结束了。

检查形态条件:

if(ZZDot[li].dir==1){ // 向上
   if(
      ZZDot[li].val>ZZDot[li-2].val && 
      ZZDot[li-2].val>ZZDot[li-4].val && 
      ZZDot[li-1].val>ZZDot[li-3].val
   ){
      ZZDot[li].pat=true; 
      // 涂色 
   }
}
else{ // 向下
   if( 
      ZZDot[li].val<ZZDot[li-2].val && 
      ZZDot[li-2].val<ZZDot[li-4].val && 
      ZZDot[li-1].val<ZZDot[li-3].val
   ){
      ZZDot[li].pat=true; 		
      // 涂色                 
   }            
}

现在,我们只需要编写涂色代码。 它类似于清理。 对于上行方向:   

for(int j=0;j<4;j++){
   if(j!=0 && ZZDot[li-j].pat){
      break;
   }
   ZigZagColors[ZZDot[li-j].bar]=1;
} 

与清理代码略有不同的是,当 j=0 时,不会退出循环。

指标开发的第二阶段已经完毕。 该指标如下所示: 


图例 7. 第 2 阶段完毕时的指标

在下面的附件中,此阶段的指标名为 OscZigZagStep2.mq5。 

阶段 3 — 添加振荡器

我们来描述一下枚举:

enum EIType{
   WPR,
   CCI,
   Chaikin, 
   RSI,
   Stochastic
};

声明用来选择振荡器的外部变量:

input EIType               Type        =  WPR;

添加其余振荡器的参数:

// CCI
input int                  CCIperiod   =  14;
input ENUM_APPLIED_PRICE   CCIprice    =  PRICE_TYPICAL;
input double               CCImax      =  100;
input double               CCImin      =  -100;
// Chaikin
input int                  CHfperiod   =  3;
input int                  CHsperiod   =  10;
input ENUM_MA_METHOD       CHmethod    =  MODE_EMA;
input ENUM_APPLIED_VOLUME  CHvolume    =  VOLUME_TICK;
input double               CHmax       =  1000;
input double               CHmin       =  -1000;
// RSI
input int                  RSIperiod   =  14;
input ENUM_APPLIED_PRICE   RSIprice    =  PRICE_CLOSE;
input double               RSImax      =  70;
input double               RSImin      =  30;
// Stochastic
input int                  STperiodK   =  5;  
input int                  STperiodD   =  3;
input int                  STperiodS   =  3;
input ENUM_MA_METHOD       STmethod    =  MODE_EMA;
input ENUM_STO_PRICE       STprice     =  STO_LOWHIGH;
input double               STmax       =  80;
input double               STmin       =  20; 

声明级别的变量:

double max,min;

在 OnStart 函数之初选择一个振荡器:

switch(Type){
   case WPR:
      max=WPRmax;
      min=WPRmin;  
      h=iWPR(Symbol(),Period(),WPRperiod);      
   break;
   case CCI:
      max=CCImax;
      min=CCImin;  
      h=iCCI(Symbol(),Period(),CCIperiod,CCIprice);  
   break;      
   case Chaikin:
      max=CHmax;
      min=CHmin;  
      h=iChaikin(Symbol(),Period(),CHfperiod,CHsperiod,CHmethod,CHvolume);  
   break;          
   case RSI:
      max=RSImax;
      min=RSImin;  
      h=iRSI(Symbol(),Period(),RSIperiod,RSIprice);  
   break;   
   case Stochastic:
      max=STmax;
      min=STmin;  
      h=iStochastic(Symbol(),Period(),STperiodK,STperiodD,STperiodS,STmethod,STprice);  
   break; 
}

if(h==INVALID_HANDLE){
   Print("无法加载指标");
   return(INIT_FAILED);
}

在 OnCalculate() 函数中以 max 和 min 变量替换 WPRmax 和 WPmin 变量。 

开发指标的第三阶段业已完成。 现在,我们可以在指标属性窗口中选择振荡器。 在下面的附件中,此阶段的指标名为 OscZigZagStep3.mq5。

阶段 4 — 开发图形界面

IncGUI 库用于开发图形界面。 该函数库是为系列文章《自定义图形控件》专门开发的,它包括三个部分 (部分 1部分 2部分 3)。 最新修订版本的函数库 (IncGUI_v4.mqh) 附带在《拥有图形界面的通用振荡器》一文中。 它也附带于此。 在开始图形界面开发之前,将 IncGUI_v4.mqh 文件复制到终端数据文件夹的 MQL5/Includes 目录。

我们来逐步开发图形界面。

包含函数库。 制作一份 OscZigZagStep3 指标的副本,并将函数库包含在其中:

#include <IncGUI_v4.mqh>

窗体类。 IncGUI_v4.mqh 文件包含 CFormTemplate 类,它是一类用于开发窗体的模板。 在包含函数库之后立即复制并粘贴到指标文件。 然后将它从 CFormTemplate 重命名为 CForm。

窗体属性。 在 MainProperties() 方法中设置主窗体属性:

m_Name         =  "Form";
m_Width        =  200;
m_Height       =  150;
m_Type         =  2;
m_Caption      =  "ZigZag on Oscillator";
m_Movable      =  true;
m_Resizable    =  true;
m_CloseButton  =  true;
  • m_Name 变量是窗体名称 (构成窗体的所有图形对象的前缀)。
  • m_Width 和 m_Height 变量是窗体的宽度和高度 (以像素为单位)。
  • m_Type 变量是窗体类型。 如果值为 2,则窗体底部将显示关闭按钮。
  • m_Caption 变量是窗体标题。
  • m_Movable 变量是一个可移动窗体。 用于移动的按钮位于窗体的左上角。
  • m_Resizable 变量 — 窗体可以展开/折叠。 相应的按钮位于右上角。
  • m_CloseButton 变量 — 窗体可以关闭。 相应的按钮位于右上角。

控件。 创建窗体控件。 窗体将有两套框架。 一套将含有一组单选按钮,而另一套将含有两个输入字段。 在窗体类的 "public" 部分中输入以下代码:

CFrame m_frm1; // 框架 1
CFrame m_frm2; // 框架 2 
CRadioGroup m_rg; // 一组单选按钮      
CInputBox m_txt_max; // 上边界的文本字段    
CInputBox m_txt_min; // 下边界的文本字段

控件初始化。 在 OnInitEvent() 方法中初始化控件。

初始化第一套框架,宽度/高度为 85/97 像素,"Osc Type" 标题宽度为 44 像素:

m_frm1.Init("frame1",85,97,"Osc Type",44);

一组单选按钮位于此框架中。

第二套框架具有相同的尺寸,"Levels" 标题宽度为 32 像素:

m_frm2.Init("frame2",85,97,"Levels",32);

输入级别的字段位于此框架中。

初始化单选按钮组:

m_rg.Init();

向组中添加单选按钮:

m_rg.AddButton(" WPR",0,0);
m_rg.AddButton(" CCI",0,16);
m_rg.AddButton(" Chaikin",0,32);
m_rg.AddButton(" RSI",0,48);            
m_rg.AddButton(" Stochastik",0,64); 

初始化输入上边界和下边界的文本字段:

m_txt_max.Init("max",45,-1," Max");
m_txt_min.Init("min",45,-1," Min");

两个字段的宽度均为 45 像素,允许文本输入 (第三个参数为 -1),其中一个标签为 "Max",第二个字段为 — "Min"。

显示控件。 在 OnShowEvent() 方法中,调用所有控件的 Show() 方法并在窗体中设置它们的坐标:

m_frm1.Show(aLeft+10,aTop+10);
m_frm2.Show(aLeft+105,aTop+10);
m_rg.Show(aLeft+17,aTop+20);
m_txt_max.Show(aLeft+115,aTop+30);
m_txt_min.Show(aLeft+115,aTop+50);     

隐藏控件。 在 OnHideEvent() 方法中隐藏所有控件:

m_frm1.Hide();
m_frm2.Hide();            
m_rg.Hide();
m_txt_max.Hide();
m_txt_min.Hide(); 

窗体标题。 在选择不同的振荡器时,最好在窗体标题中显示它们的名称,因此在窗体类的 "public" 部分中,我们添加了一个方法来更改标题中的文本:

void SetCaption(string str){
   m_Caption="ZigZag on "+str;
   ObjectSetString(0,m_Name+"_Caption",OBJPROP_TEXT,m_Caption);
}

创建窗体对象。 创建 CForm 类对象:

CForm form;

窗体事件。 为了令窗体和控件响应用户操作,应该从指标的 OnChartEvent() 函数里调用 Event() 方法。 取决于事件的类型,该方法返回不同的值。 窗体关闭对应于 1。 应从图表中删除指标:

if(form.Event(id,lparam,dparam,sparam)==1){
   ChartIndicatorDelete(0,0,MQLInfoString(MQL_PROGRAM_NAME)); 
   ChartRedraw();
}

控件事件。 单选按钮组事件会改变指标,而更改跨入字段值的事件应替换超买/超卖级别。 在这两种情况下,指标都会全部重新计算。 

在 OnInit() 函数中选择指标的代码部分将被转移到单独的函数中:

bool LoadIndicator(int aType){
   switch(aType){
      case WPR:
         max=WPRmax;
         min=WPRmin;  
         h=iWPR(Symbol(),Period(),WPRperiod);      
      break;
      case CCI:
         max=CCImax;
         min=CCImin;  
         h=iCCI(Symbol(),Period(),CCIperiod,CCIprice);  
      break;      
      case Chaikin:
         max=CHmax;
         min=CHmin;  
         h=iChaikin(Symbol(),Period(),CHfperiod,CHsperiod,CHmethod,CHvolume);  
      break;          
      case RSI:
         max=RSImax;
         min=RSImin;  
         h=iRSI(Symbol(),Period(),RSIperiod,RSIprice);  
      break;   
      case Stochastic:
         max=STmax;
         min=STmin;  
         h=iStochastic(Symbol(),Period(),STperiodK,STperiodD,STperiodS,STmethod,STprice);  
      break; 
   }
   
   if(h==INVALID_HANDLE){
      Print("无法加载指标");
      return(false);
   }   
   
   return(true);
   
}   

它会从 OnInit() 指标函数 (在一开始) 和单选按钮事件里调用。 在 OnInit() 函数中选择指标之后,立即初始化窗体,设置控件的值并显示窗体:

if(!LoadIndicator(Type)){
   return(INIT_FAILED);
}

form.Init(1);
form.m_rg.SetValue(Type);
form.m_txt_max.SetValue(max);   
form.m_txt_min.SetValue(min);  
form.SetCaption(EnumToString(Type));
form.Show(5,20);

在 OnChartEvent() 函数中处理控件事件。 用于更改指标的单选按钮事件:

if(form.m_rg.Event(id,lparam,dparam,sparam)==1){
   
   if(h!=INVALID_HANDLE){
      IndicatorRelease(h);
      h=INVALID_HANDLE;
   }      
   
   if(!LoadIndicator(form.m_rg.Value())){
      Alert("无法加载指标");
   }
   
   form.m_txt_max.SetValue(max);   
   form.m_txt_min.SetValue(min);    

   EventSetMillisecondTimer(100);
}

首先,通过 IndicatorRelease() 函数释放指标句柄,然后选择新指标,为输入字段设置新值并启动计时器。 由于在重新计算指标时可能出现数据更新错误,因此必须使用计时器。 在此情况下,您需要重复尝试重新计算,直到成功为止。

修改级别:

if(form.m_txt_max.Event(id,lparam,dparam,sparam)==1 ||
   form.m_txt_min.Event(id,lparam,dparam,sparam)==1
){
   max=form.m_txt_max.ValueDouble();
   min=form.m_txt_min.ValueDouble();      
   EventSetMillisecondTimer(100);
}

输入字段事件为 "min" 和 "max" 变量分配新值,并启动计时器。  

在 OnTimer() 函数中重新计算指标。 如果成功,计时器关闭,指标将照常继续工作 — 依据逐笔报价。 在上述文章《拥有图形界面的通用振荡器》一文中详细阐述了指标重新计算所需的所有动作。 所以,我们在此仅考查有本质差异的部分。 通用振荡器在类方法中计算,且不需要价格数据。 在此,我们需要调用 OnCalculate() 函数并传递包含价格的数组。 声明数组:

datetime time[];
double open[];
double high[];
double low[];
double close[];
long tick_volume[];
long volume[];
int spread[];

获得柱线数量:

int bars=Bars(Symbol(),Period());
      
if(bars<=0){
   return;
}

我们不需要所有价格数据来构建之字折线。 只需要三个数组: '时间','最高价' 和 '最低价'。 这些是我们复制的数组:

if(CopyTime(Symbol(),Period(),0,bars,time)==-1){
   return;
}

if(CopyHigh(Symbol(),Period(),0,bars,high)==-1){
   return;
}      

if(CopyLow(Symbol(),Period(),0,bars,low)==-1){
   return;
} 

在测试指标时,我们遇到了一个问题: 复制的数据数量有时小于自 Bars() 函数获得的柱线数量。 指标缓冲区的大小对应于 Bars() 函数值。 因此,为了正确显示指标,必须增加复制数据的数组,同时将数据附加到它们的末尾:

if(ArraySize(time)<bars){
   int sz=ArraySize(time);
   ArrayResize(time,bars);
   for(int i=sz-1,j=bars-1;i>=0;i--,j--){
      time[j]=time[i];
   }   
}

if(ArraySize(high)<bars){
   int sz=ArraySize(high);
   ArrayResize(high,bars);
   for(int i=sz-1,j=bars-1;i>=0;i--,j--){
      high[j]=high[i];
   }
}      

if(ArraySize(low)<bars){
   int sz=ArraySize(low);
   ArrayResize(low,bars);
   for(int i=sz-1,j=bars-1;i>=0;i--,j--){
      low[j]=low[i];
   }
} 

现在,我们只需要调用 OnCalculate() 函数:

int rv=OnCalculate(
            bars,
            0,
            time,
            open,
            high,
            low,
            close,
            tick_volume,
            volume,
            spread
);

如果 OnCalculte() 函数没有出错,则禁用计时器: 

if(rv!=0){
   ChartRedraw();     
   EventKillTimer();
   form.SetCaption(EnumToString((EIType)form.m_rg.Value()));
}

OnTimer() 函数的完整代码,以及完整的指标可以在附带的 OscZigZagStep4.mq5 文件中看到。

在图表上启动指标时,带有控件的窗体应显示在左上角 (图例 8)。


图例 8. 阶段 4 的图形界面

结束语

我已经完全根据建议的需求规范展示了指标的开发。 然而,执行的准确性并不能定义整个工作。 在我们的案例中,任务是由技术经验丰富的人员准备的,他显然非常了解终端的潜能和指标特性。 但是,有些需求仍要澄清,并应与客户讨论。 这特指之字折线的涂色。

当检测到形态时,若干个之前的之字折线段落被重绘,这也许会破坏我们依据历史记录的分析。 可以将新细节添加到形态中这一事实,令直观分析更加复杂。 实际上,需要这种形态才能制定交易决策。 决策可以在形态出现时或之后制定,但不能提前。 所以,可以建议在柱线上绘制箭头,来替代为之字折线涂色。 另一种解决方案是从检测到形态存在的那一刻到未来,在数根柱线上绘制水平线。  

此外,在开发指标时,其意外和不明显的特征已被揭示出来,而这些在初次读取任务时根本不可能会考虑到,即便在编译时也是如此。 我的意思是,有些情况需要在指标逆转时删除最后一个之字折线的最大值/最小值。 正如文中提到的,您可以使用彩色之字折线缓冲区,但在这种情况下,因为彩色之字折线缓冲区集合有两个数据缓冲区和一个颜色缓冲区,因此很难对它进行涂色。 如果两个数据缓冲区在单根柱线上都有值 (贯穿柱线的垂直线),则两个之字折线段落会分配同一颜色缓冲区中指定的颜色。 可以使用之字折线,而非彩色之字折线缓冲区,并使用图形对象重新绘制之字折线段落,或者简单地设置箭头或圆点。 一般而言,任何任务都需要非常仔细的分析和初步讨论。

附件

这些文件需放在正确的文件夹中。 它们应保存到终端的相同文件夹中。 在 MQL5/Indicators 中, 有这些与指标开发阶段相对应的文件: OscZigZagStep1.mq5, OscZigZagStep2.mq5, ОscZigZagStep3.mq5 和 OscZigZagStep4.mq5。

在 MQL5/Includes 中, 有 IncGUI_v4.mqh 文件,它在 OscZigZagStep4 指标里用于开发图形界面。


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

附加的文件 |
MQL5.zip (87.98 KB)
使用图形界面处理优化结果 使用图形界面处理优化结果

这是处理和分析优化结果想法的续篇,这一次,我们的目标是选择100个最佳的优化结果并且在图形用户界面(GUI)表格中显示它们。用户将可以在优化结果中选择一行而在独立的图表中得到多交易品种余额和回撤图。

强化学习中的随机决策森林 强化学习中的随机决策森林

使用 bagging 的随机森林(Random Forest, RF) 是最强大的机器学习方法之一, 它略微弱于梯度 boosting,这篇文章尝试开发了一个自我学习的交易系统,它会根据与市场的交互经验来做出决策。

社交交易。 可盈利的信号能否变得更好? 社交交易。 可盈利的信号能否变得更好?

大多数订阅者是通过优美的余额曲线和订阅用户数量来选择交易信号。 这就是为什么如今许多提供者只在乎漂亮的统计数据而非信号的真实质量,经常玩弄手数把戏并人为地将余额曲线整理到理想的外观。 本文论述了可靠性准则,以及提供者可用于提高其信号质量的方法。 展现特定信号历史的示例性分析,以及有助于提供者提升盈利并降低风险的方法。

可视化策略构建工具. 无需编程即可创建交易机器人 可视化策略构建工具. 无需编程即可创建交易机器人

本文展示了一个可视化的策略构建工具,它演示了任何用户如何不必编程就能创建交易机器人和相关工具。创建出的 EA 交易是完整功能的,并且可以在策略测试器中测试,通过云计算来优化或者实时运行于图表之上。