外汇EA编写教程:带有图形界面的通用通道

内容

概论

在以前的 文章 里曾描述过利用图形界面创建通用振荡器。在本文中, 我们创建了一个非常有趣、便利和有用的指标, 可以大大简化并加快图表分析。除振荡器外, 还有其它类型的技术分析指标, 也与振荡器一样有趣。它们包括趋势、波动率和交易量指标, 以及可划分为不同类别的其它指标。在本文中, 我们将研究通用通道指标的创建。

我以前发表的有关通用振荡器的文章相当复杂, 它们是针对有经验的程序员而非初学者。由于本文的主题偏向创建通用振荡器, 所以我们在此不会赘述一般问题。我们将在通用振荡器的基础上创建一个通用通道。因此, 即便程序员新人也可以通过修改所提供的素材来创建其通用指标, 而无需分析利用图形界面创建通用指标的所有细微之处。

尽管通用指标相似, 但仍存在重大的根本差异。所有通道指标显示为三条线, 包括中心, 顶部和底部线。中心线的绘图原理与移动平均线相似, 而移动均线指标主要用于绘制通道。顶部线和底部线的位置距中心线距离相等。距离的确定可以按照点为单位, 作为价格百分比 (包络指标), 使用标准偏差值 (布林带) 或 ATR 值 (Keltner 通道)。因此, 通道指标将使用两个独立的模块构建:

  1. 中心线计算模块
  2. 通道宽度定义模块 (或绘制通道边界)

有各种不同类型的通道, 如 Donchian 通道 (价格通道)。通常它以创建边界线 (价格范围) 开始, 之后计算中心线的数值 (在范围中间)。然而, 此通道也可以通过上述方案来构建: 即首先我们绘制一条中心线, 这条中心线被定义为价格区间的中间价, 然后我们在等于价格范围一半的距离上绘制通道边界。当然, 这比通常通道需要更多的计算。然而, 由于本文的主要目的是创建一个通用指标, 我们可以允许一些例外, 而此方法将增加中心线与边界的可能组合的数量。例如, 我们将能够创建一个在价格通道中带有中心线的指标, 边界位于标准偏差的距离, 如布林带等。 

中心线的类型

用于中心线的各种移动平均线。我们来定义它们的类型和参数的数量。所有将在指标中使用的中心线变体如表 1 所示。

表 1. 中心线的类型

标准
函数
名称 参数
iAMA 自适应移动均线 1. int ama_period — AMA 周期
2. int fast_ma_period — 快速移动均线周期
3. int slow_ma_period — 慢速移动均线周期
4. int ama_shift — 指标的水平偏移
5. ENUM_APPLIED_PRICE  applied_price — 处理所用价格类型 
iDEMA 双重指数移动均线 1. int ma_period — 均化周期
2. int ma_shift — 指标的水平偏移
3. ENUM_APPLIED_PRICE  applied_price — 价格类型
iFrAMA 分形自适应移动均线 1. int ma_period — 均化周期
2. int ma_shift — 指标的水平偏移
3. ENUM_APPLIED_PRICE  applied_price — 价格类型
iMA 移动均线 1. int ma_period — 均化周期
2. int ma_shift — 指标的水平偏移
3. ENUM_MA_METHOD ma_method — 平滑类型
4. ENUM_APPLIED_PRICE applied_price — 价格类型
iTEMA 三重指数移动均线 1. int ma_period — 均化周期
2. int ma_shift — 指标的水平偏移
3. ENUM_APPLIED_PRICE  applied_price — 价格类型
iVIDyA 变可指数动态均线 1. int cmo_period — Chande 动量周期 
2. int ema_period — 平滑因子周期  
3. int ma_shift — 指标的水平偏移 
4. ENUM_APPLIED_PRICE  applied_price — 价格类型
价格通道的中心线 1. int period

基于表 1 “参数” 列的分析, 我们得到所需的最小参数集合 (表 2)。

表 2. 用于计算通道中心线的通用参数集合 

类型 名称
int period1
int period2
int period3
int shift
ENUM_MA_METHOD ma_method 
ENUM_APPLIED_PRICE  price

所有将在指标属性窗口中显示的中心线参数均带 “c_” 前缀。 

边界类型

现在我们来确定通道边界的计算方法 (表 3)。

表 3. 通道宽度计算变体 

标准
函数
名称  参数
iATR 平均真实范围 1. int ma_period — 均化周期
iStdDev  标准偏差 1. int ma_period — 均化周期
2. int ma_shift — 指标的水平偏移
3. ENUM_MA_METHOD — 平滑类型
4. ENUM_APPLIED_PRICE applied_price — 价格类型 
–  按照点数  int width — 宽度的点数 
–  百分比 (在包络线中)  double width — 宽度作为价格百分比   
–  就像在价格通道中  double width — 相对于价格通道实际宽度的缩放比率

根据表 3 的 “参数” 列, 我们得到一组所需的参数 (表 4)。

表 4. 用于计算通道宽度的通用参数集合

类型 名称
int period
int  shift 
ENUM_MA_METHOD  ma_method  
ENUM_APPLIED_PRICE  price
double  width 

我们需要 int 变量用于计算点数, 但它未包括在表 4 中, 因为我们可以使用一个 double 变量代之。这会减少属性窗口中的变量总数。

所有将在指标属性窗口中显示的边界计算参数均带 “w_” 前缀。 

中心线类

中心线基类的基本原理和方法类似于 带有图形界面的通用振荡器 中描述的 CUniOsc 类。因此我们将使用这个类, 并稍微修改它。

在 MQL5/Include 文件夹中, 我们创建 UniChannel 文件夹, 复制 CUniOsc.mqh 文件 (从 Include/UniOsc) 至此, 并将其重命名为 CUniChannel.mqh。在文件中保留基类 (COscUni), 子类 Calculate1 (其全名为 COscUni_Calculate1) 及其子类 COscUni_ATR。其它的类应予以删除。

我们将类重新命名: 将 “COscUni” 片段替换为 “CChannelUni”。为了更换, 使用编辑器功能 (主菜单 — 编辑 — 查找和替换 — 替换)。但不要使用 “全部替换” 按钮。逐一替换实例, 以确保所有替换都正确。

中心线类总是绘制一条实线, 因此我们不需要许多基类方法。删除不必要的方法后, 将会保留以下基类:

class CChannelUniWidth{
   protected:
      int m_handle;      // 指标句柄
      string m_name;     // 指标名称
      string m_label1;   // 缓存区 1 的名称     
      string m_help;     // 指标参数帮助
      double m_width;    // 通道宽度
   public:
  
      // 构造器
      void CChannelUniWidth(){
         m_handle=INVALID_HANDLE;
      }
      
      // 析构器
      void ~CChannelUniWidth(){
         if(m_handle!=INVALID_HANDLE){
            IndicatorRelease(m_handle);
         }
      }
  
      // 从指标的 OnCalculate() 函数里调用的主要方法
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
         return(rates_total);
      }
      
      // 得到加载的指标句柄
      int Handle(){
         return(m_handle);
      }
      
      // 句柄检查方法, 允许检查指标是否已成功加载  
      bool CheckHandle(){
         return(m_handle!=INVALID_HANDLE);
      }
      
      // 得到指标名称
      string Name(){
         return(m_name);
      }    
      // 得到缓存区标签文本
      string Label1(){
         return(m_label1);
      }
      
      // 得到参数提示
      string Help(){
         return(m_help);
      }
};

与第二个缓冲区相关的一切都可以从 Calculate 类中删除。之后只剩下一个 Calculate 方法:

class CChannelUni_Calculate1:public CChannelUni{
   public:
      // 主要方法是从指标的 OnCalculate() 函数调用
      // 前两个参数类似于  
      // 指标 OnCalculate() 函数的前两个参数
      // 第三个参数是指标中心线的缓存区
      int Calculate( const int rates_total,    
                     const int prev_calculated,
                     double & buffer0[]
      ){
        
         // 确定复制元素的数量
        
         int cnt;
        
         if(prev_calculated==0){
            cnt=rates_total;
         }
         else{
            cnt=rates_total-prev_calculated+1;
         }  
        
         // 复制数据到指标缓存区
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
        
         return(rates_total);
      }
    };

我们来利用 iMA 指标编写子类。来自文件的 CChannelUni_ATR 类应重命名为 CChannelUni_MA。我们替换调用指标, 并删除我们不需要的部分。作为结果, 我们得到如下类:

class CChannelUni_MA:public CChannelUni_Calculate1{
   public:
   // 构造器
   // 前两个参数对于所有子类均相同
   // 之后是加载指标的参数 
   void CChannelUni_MA( bool use_default,
                        bool keep_previous,
                        int & ma_period,
                        int & ma_shift,
                        long & ma_method,
                        long & ma_price){
      if(use_default){ // 使用选择的省缺值
         if(keep_previous){
            // 之前使用的参数没有修改
            if(ma_period==-1)ma_period=14;
            if(ma_shift==-1)ma_shift=0;
            if(ma_method==-1)ma_method=MODE_SMA;
            if(ma_price==-1)ma_price=PRICE_CLOSE;
         }
         else{
            ma_period=14;
            ma_shift=0;
            ma_method=MODE_SMA;
            ma_price=PRICE_CLOSE;            
         }      
      }    
      
      // 加载指标
      m_handle=iMA(Symbol(),Period(),ma_period,ma_shift,(ENUM_MA_METHOD)ma_method,(ENUM_APPLIED_PRICE)ma_price);
      
      // 指标名称形成一条线
      m_name=StringFormat( "iMA(%i,%i,%s,%s)",
                           ma_period,
                           ma_shift,        
                           EnumToString((ENUM_MA_METHOD)ma_method),              
                           EnumToString((ENUM_APPLIED_PRICE)ma_price)
                        );
      
      // 缓冲区名称的字符串
      m_label1=m_name;
      // 参数提示
      m_help=StringFormat( "ma_period - c_Period1(%i), "+
                           "ma_shift - c_Shift(%i), "+
                           "ma_method - c_Method(%s)"+
                           "ma_price - c_Price(%s)",
                           ma_period,
                           ma_shift,
                           EnumToString((ENUM_MA_METHOD)ma_method),
                           EnumToString((ENUM_APPLIED_PRICE)ma_price)
                           );
   }
    };

我们来看一下如何在变量 m_name 和 mlabel1 中形成字符串。在子窗口中绘制的指标的名称(m_name 变量) 显示在子窗口的左上角。通道将显示在价格图表上, 其名称不可见, 因此我们将为 m_label 变量分配与 m_name 变量相同的详细名称, 以便当我们将鼠标悬浮在中央通道上时显示在弹出框里。 

创建所有其它标准指标的类都类似于 iMA。一个例外是价格通道。由于价格通道不包括在标准终端指标集合中, 我们需要计算它。有两种可能的选择:

  1. 创建 Calculate 类型的子类并在其中执行计算
  2. 创建一个附加的指示符并通过 iCustom 函数调用它
两个选项可以同等使用。在第一种情况下, 文章中创建的指标将依赖较少的文件, 但相同的计算需要多次执行 (首先我们确定通道边界以便计算中心线, 然后我们再次确定通道边界以便判断通道宽度)。在第二种情况下, 不会重复计算。此外, 我们还可以获得额外的、可独立使用的完整价格通道指标。      

以下附件包含 CUniChannel.mqh 文件, 其中包含所有其它指标的子类和 iPriceChannel 指标。iPriceChannel 指标的中心线数据位于缓冲区 0 中。如果有人需要进一步修改类用于任何其它指标, 其所需数据位于非零缓冲区, 则需要创建另一个 Calculate 子类或为基类中的缓冲区索引创建一个变量, 并在子类的构造函数中为它分配所需的数值。   

用于计算宽度和绘制通道的类

让我们再次使用 CUniChannel 作为我们基类的基础。保存指标中心线数值的缓冲区和两个保存通道边界的缓冲区将在方法中用计算出的数值填充, 并将被传递给 Calculate 类的方法。与 CUniChannel 对比, 此处我们将分别为每个边界计算选项创建 Calculate 子类。这些子类将加载指标, 并在其中形成指标和缓冲区的名称。我们还需要稍微修改基类: 为通道宽度添加一个变量 – 变量的值将通过子类构造函数设置。

我们来将 CUniChannel.mqh 文件保存为 CUniChannelWidth.mqh 并对其进行修改。首先我们删除所有的子类, 只剩下基类和 Calculate。将 CChannelUni 重命名为 CChannelUniWidth (不要忘记子类中的构造函数, 析构函数和父类名称也需要更改)。结果文件如下:

class CChannelUniWidth{
   protected:
      int m_handle;           // 指标句柄
      string m_name;          // 指标名称
      string m_label1;        // 缓冲区 1 名称     
      string m_help;          // 指标参数提示
      double m_width;         // 通道宽度
   public:
  
      // 构造器
      void CChannelUniWidth(){
         m_handle=INVALID_HANDLE;
      }
      
      // 析构器
      void ~CChannelUniWidth(){
         if(m_handle!=INVALID_HANDLE){
            IndicatorRelease(m_handle);
         }
      }
  
      // 从指标的 OnCalculate() 函数里调用主方法
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
         return(rates_total);
      }
      
      // 得到加载指标的句柄
      int Handle(){
         return(m_handle);
      }
      
      // 句柄检查方法, 能够检查指标是否已加载成功  
      bool CheckHandle(){
         return(m_handle!=INVALID_HANDLE);
      }
      
      // 得到指标名称
      string Name(){
         return(m_name);
      }    
      // 得到缓冲区的标签文本
      string Label1(){
         return(m_label1);
      }
      
      // 得到参数提示
      string Help(){
         return(m_help);
      }
};
  

我们将 CChannelUni_Calculate 类重命名为 CChannelUni_Calculate_ATR, 并向其内添加一个构造函数。构造函数可以从通用振荡器的 COscUni_ATR 类中获取, 但是我们需要重新命名它, 将宽度参数添加其内。还有哪些需要修改的: 我们需要添加指标和缓冲区名称生成。最后, 基于 ATR 计算边界的类如下:

class CChannelUni_Calculate_ATR:public CChannelUniWidth{
   public:
      // 构造器
      // 开始两个是所有子类的标准参数 
      // 紧随加载指标的参数
      // 最后一个参数是通道宽度
      void CChannelUni_Calculate_ATR(bool use_default,
                                     bool keep_previous,
                                     int & ma_period,
                                     double & ch_width){
         if(use_default){ // 使用选择的省缺值
            if(keep_previous){ // 以前使用的参数无需修改
               if(ma_period==-1)ma_period=14;
               if(ch_width==-1)ch_width=2;
            }
            else{
               ma_period=14;
               ch_width=2;
            }      
         } 
         
         // 保存计算方法中用到的宽度参数  
         m_width=ch_width; 
         // 加载指标
         m_handle=iATR(Symbol(),Period(),ma_period);
         // 指标名形成的线
         m_name=StringFormat("ATR(%i)",ma_period);
         // 缓存区名称字符串
         m_label1=m_name;
         // 参数提示 
         m_help=StringFormat("ma_period - Period1(%i)",ma_period); // 提示   
      }   
      
      // 从指标的 OnCalculate() 函数调用的主函数
      // 前两个参数类似于
      // OnCalculate() 函数的前两个参数
      // 之后传递指标缓存区 
      int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
      
         // 计算开始定义
         
         int start;
         
         if(prev_calculated==0){
            start=0; 
         }
         else{ 
            start=prev_calculated-1;
         }  
         // 计算的主循环和缓存区填充 
         for(int i=start;i<rates_total;i++){
            
            // 得到指标数据用于计算柱线
            double tmp[1];
            if(CopyBuffer(m_handle,0,rates_total-i-1,1,tmp)<=0){
               return(0);
            }
            
            // 乘以宽度参数
            tmp[0]*=m_width;   
            // 计算上下边界的值
            bufferUpper[i]=bufferCentral[i]+tmp[0];
            bufferLower[i]=bufferCentral[i]-tmp[0];
         }   
         
         return(rates_total);
      }
        };

请注意, 主循环内仅复制一根柱线的 ATR 指标值。此变体比之将数值序列复制到缓冲区慢得多。但是这种方法可以为我们节省一个指标缓冲区, 当手工将指标挂载到图表时, 速度损失才可见。但延迟十分之几秒对于用户无所谓。在策略测试器中, 测试开始时, 图表上可以看到少量的柱线, 因此在复制每根柱线的数据时所丧失的时间并不明显。

一些通道宽度计算选项不需要额外的指标 — 例如, 如果宽度设置为点, 或包络通道。在此情况下, 我们将把 0 分配给基类的 m_handle 变量 (它与 INVALID_HANDLE 值不同)。

以下附件包含 CUniChannelWidth.mqh 文件, 其中包含其它通道计算选项的子类。   

创建通用通道指标

现在我们已经准备好上述类, 我们可以创建一个通用的通道标, 尽管还缺乏图形界面。

让我们在编辑器中创建新的 iUniChannel 自定义指标。在 MQL5 向导中创建指标时, 请选择以下函数: OnCalculate(…,open,high,low,close), OnTimer, OnChartEvent, 创建 Line 类型的三个缓冲区。

我们需要创建两个枚举来选择中心线和通道类型。枚举将位于 UniChannelDefines.mqh 文件中。根据表 1 和表 3 创建枚举:

// 中心线类型枚举
enum ECType{
   UniCh_C_AMA,
   UniCh_C_DEMA,
   UniCh_C_FrAMA,
   UniCh_C_MA,
   UniCh_C_TEMA,
   UniCh_C_VIDyA,
   UniCh_C_PrCh
};
// 边界线类型枚举
enum EWType{
   UniCh_W_ATR,
   UniCh_W_StdDev,
   UniCh_W_Points,
   UniCh_W_Percents,
   UniCh_W_PrCh
            };

中心线类型的枚举称为 ECType, 通道宽度类型的枚举是 EWType。将含有枚举的文件和两个先前创建的含类文件连接到指标:

#include <UniChannel/UniChannelDefines.mqh>
#include <UniChannel/CUniChannel.mqh>
#include <UniChannel/CUniChannelWidth.mqh>
    

现在我们声明两个外部变量来选择中心线类型和通道宽度, 以及根据表 2 和表 4 的参数变量:

// 中心线参数
input ECType               CentralType   =  UniCh_C_MA;
input int                  c_Period1     =  5;
input int                  c_Period2     =  10;
input int                  c_Period3     =  15;
input int                  c_Shift       =  0;
input ENUM_MA_METHOD       c_Method      =  MODE_SMA;
input ENUM_APPLIED_PRICE   c_Price       =  PRICE_CLOSE;
// 边界参数
input EWType               WidthType     =  UniCh_W_StdDev;
input int                  w_Period      =  20;
input int                  w_Shift       =  0;
input ENUM_MA_METHOD       w_Method      =  MODE_SMA;
input ENUM_APPLIED_PRICE   w_Price       =  PRICE_CLOSE;
      input double               w_Width       =  2.0;

我们来声明两个变量, 这些变量现在将是内部的, 且以后将在带有图形界面的指标版本的属性窗口中显示:

bool                 UseDefault  =  false;
      bool                 KeepPrev    =  false;

这些变量的目的在有关通用振荡器的文章中有详细描述: UseDefault 变量启用每个新选择的指标以省缺设置加载模式, 并且 KeepPrev 变量启用切换指标时保留参数的模式。在没有图形界面的指标版本中, 指标从属性窗口加载参数, 因此 UseDefault 值为 false。KeepPrev 也被设置为 false, 因为图形界面不存在, 且没有指标切换。 

在指标初始化期间需要准备参数。就像在通用振荡器中一样, 我们将在一个单独的 PrepareParameters() 函数中准备参数, 但首先我们将创建所有外部变量的副本:

ECType               _CentralType;
int                  _ma_Period1;
int                  _ma_Period2;
int                  _ma_Period3;
int                  _ma_Shift;
long                 _ma_Method;
long                 _ma_Price;
EWType               _WidthType;
int                  _w_Period;
int                  _w_Shift;
long                 _w_Method;
long                 _w_Price;
      double               _w_Width;

然后我们编写参数准备函数:

void PrepareParameters(){
   _CentralType=CentralType;
   _WidthType=WidthType;
  
   if(UseDefault && KeepPrev){
      _c_Period1=-1;
      _c_Period2=-1;
      _c_Period3=-1;
      _c_Shift=0;
      _c_Method=-1;
      _c_Price=-1;
      _w_Period=-1;
      _w_Shift=0;
      _w_Method=-1;
      _w_Price=-1;
      _w_Width=-1;
   }
   else{  
      _c_Period1=c_Period1;
      _c_Period2=c_Period2;
      _c_Period3=c_Period3;
      _c_Shift=c_Shift;
      _c_Method=c_Method;
      _c_Price=c_Price;
      _w_Period=w_Period;
      _w_Shift=w_Shift;
      _w_Method=w_Method;
      _w_Price=w_Price;
      _w_Width=w_Width;
   }
            }

请注意, 如果 UseDefault && KeepPrev 条件满足, 则将所有变量设置为 -1, 并将 0 分配给 Shift 变量, 因为这些变量的值不可从指标对象里设置, 而只能从用户界面 (指标属性窗口或图形界面) 设置。   

准备参数后, 我们可以创建用于计算中心线和通道的对象。在通用振荡器中, LoadOscillator() 函数即用于此目的。此处我们将有两个函数: LoadCentral() 和 LoadWidth(), 但我们首先需要声明指针:

CChannelUni * central;
            CChannelUniWidth * width;

一些指标具有水平移位参数, 但有些指标没有, 尽管所有指标都可以平移。因此, 我们声明一个值为 0 的附加变量 shift0, 并将其传递给类构造函数。平移将通过平移指标缓冲区来执行。

LoadCentral() 函数:

void LoadCentral(){
   switch(_CentralType){ // 根据所选的类型创建一个适当的类
      case UniCh_C_AMA:
         central=new CChannelUni_AMA(  UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       _c_Period2,
                                       _c_Period3,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_DEMA:
         central=new CChannelUni_DEMA( UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_FrAMA:
         central=new CChannelUni_FrAMA(UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_MA:
         central=new CChannelUni_MA(   UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Method,
                                       _c_Price);
      break;
      case UniCh_C_TEMA:
         central=new CChannelUni_TEMA( UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_VIDyA:
         central=new CChannelUni_VIDyA(UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       _c_Period2,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_PrCh:
         central=new CChannelUni_PriceChannel(  UseDefault,
                                                KeepPrev,
                                                _c_Period1);
      break;
   }
}
    

通道宽度计算选项之一 (CChannelUni_Calculate_InPoints 类) 包括以点为单位的参数, 该类提供了根据品种报价中的小数位数调整参数值的可能性。对于调整函数进行操作, 必须在创建对象时将乘数参数传递给类构造函数。对于 2 和 4 位小数的报价, 乘数将等于 1, 对于 3 和 5 位小数的报价为 10。在外部参数中, 我们声明 bool 类型的 Auto5Digits 变量:

input bool                 Auto5Digits   =  true;

如果 Auto5Digits 为 true, 参数将被调整, 若为 false – 数值如常使用。在 Auto5Digits 之下, 我们为乘数再声明一个变量:

int mult;

在 OnInit() 函数的开头, 我们计算 mult 的值:

   if(Auto5Digits && (Digits()==3 || Digits()==5)){
      mult=10; // 参数的点数值将乘以 10
   }
   else{
      mult=1; // 参数的点数值不必调整
         }

现在我们来编写 LoadWidth() 函数:

void LoadWidth(){
   switch(_WidthType){ // 根据所选的类型创建一个适当的类
      case UniCh_W_ATR:
         width=new CChannelUni_Calculate_ATR(UseDefault,KeepPrev,_w_Period,_w_Width);
      break;
      case UniCh_W_StdDev:
         width=new CChannelUni_Calculate_StdDev(UseDefault,KeepPrev,_w_Period,shift0,_w_Method,_w_Price,_w_Width);
      break;
      case UniCh_W_Points:
         width=new CChannelUni_Calculate_InPoints(UseDefault,KeepPrev,_w_Width,mult);
      break;
      case UniCh_W_Percents:
         width=new CChannelUni_Calculate_Envelopes(UseDefault,KeepPrev,_w_Width);
      break;
      case UniCh_W_PrCh:
         width=new CChannelUni_Calculate_PriceChannel(UseDefault,KeepPrev,_w_Period,_w_Width);
      break;
   }
            }

创建每个对象(中心线和宽度) 之后, 我们需要检查它们是否已成功创建。如果对象已创建, 设置指标的短名称, 并使用 Print() 函数在参数上显示帮助信息: 

Print("中心线参数匹配:",central.Help());
      Print("宽度参数匹配:",width.Help());  

缓冲区的标签和移位将使用 SetStyles() 函数设置:

void SetStyles(){
   // 缓冲区名称
   PlotIndexSetString(0,PLOT_LABEL,"中心: "+central.Label1());
   PlotIndexSetString(1,PLOT_LABEL,"上边界: "+width.Label1());  
   PlotIndexSetString(2,PLOT_LABEL,"下边界: "+width.Label1());  
  
   // 缓冲区移位
   PlotIndexSetInteger(0,PLOT_SHIFT,_c_Shift);
   PlotIndexSetInteger(1,PLOT_SHIFT,_w_Shift);
   PlotIndexSetInteger(2,PLOT_SHIFT,_w_Shift);
            }

作为结果, 我们得到以下 OnInit() 函数:

int OnInit(){
  
   // 准备一个乘数来调整参数
   if(Auto5Digits && (Digits()==3 || Digits()==5)){
      mult=10;
   }
   else{
      mult=1;
   }
  
   // 准备参数
   PrepareParameters();
  
   // 加载中心线指标
   LoadCentral();
  
   // 检查中心线是否已成功加载 
   if(!central.CheckHandle()){
      Alert("中心线错误 "+central.Name());
      return(INIT_FAILED);
   }    
  
   // 加载宽度计算指标
   LoadWidth();
  
   // 检查宽度计算指标是否已成功加载
   if(!width.CheckHandle()){
      Alert("宽度错误 "+width.Name());
      return(INIT_FAILED);
   }      
   // 显示参数提示
   Print("中心线参数匹配: "+central.Help());
   Print("宽度参数匹配: "+width.Help());  
  
   // 设置名称
   ShortName="iUniChannel";  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);  
  
   // OnInit 函数的标准部分
   SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
   SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA);
   SetIndexBuffer(2,Label3Buffer,INDICATOR_DATA);
  
   // 设置标签和缓存区平移
   SetStyles();
   return(INIT_SUCCEEDED);
            }

在这一点上, 我们可以假设没有图形界面的指标是完整的。它可以用于测试所有的类和参数的操作, 之后我们可以继续创建一个图形界面。

在测试中, 暴露出指标操作中的一些不便之处。其中一个缺点是单独管理中心线周期和宽度计算周期。当然, 这种控制可以扩大指标的可能性, 但是在某些情况下, 可能需要使用单个参数同时控制两个周期。我们来做一个微小的改变, 令通道宽度周期等于中心线的三个周期之一。使用枚举 (位于 UniChannelDefines.mqh 文件) 中选择四个可用选项之一:

enum ELockTo{
   LockTo_Off,
   LockTo_Period1,
   LockTo_Period2,
   LockTo_Period3
            };

当选择 LockTo_Off 选项时, 单独调整周期。在所有其它情况下, w_Period 参数的值等于中心线的相应周期。我们在 w_Period 变量之后立即声明一个类型为 ELockTo 的变量:

input ELockTo              w_LockPeriod  =  LockTo_Off;

我们在 PrepareParameters() 函数的底部添加以下代码:

switch(w_LockPeriod){ // 依据加锁类型
   case LockTo_Period1:
      _w_Period=_c_Period1;
   break;
   case LockTo_Period2:
      _w_Period=_c_Period2;      
   break;
   case LockTo_Period3:
      _w_Period=_c_Period3;      
   break;
            }

另一个缺点是关于参数合法性的消息显示在 “智能系统” 选项卡的一行中, 其中一部分不会显示在较窄的屏幕上。我们修改代码以便在一列中显示信息。替代 Print, 我们将使用我们自己的 PrintColl() 函数。两个参数传递给这个函数: 标题和一个帮助字符串。在此函数中, 帮助字符串切分为几部分, 单独打印:

void PrintColl(string caption,string message){
   Print(caption); // 打印标题
   string res[];
   // 切分消息
   int cnt=StringSplit(message,',',res);
   // 按部分打印消息
   for(int i=0;i<cnt;i++){
      StringTrimLeft(res[i]);
      Print(res[i]);
   }
            }

相应地, OnInit() 函数中更改了两条帮助打印行:

PrintColl("中心线参数匹配:",central.Help());
            PrintColl("宽度参数匹配:",width.Help());    

现在指标已完全就绪, 附件中的文件名是 “iUniChanhel”。现在我们来创建图形界面。   

创建图形界面类

图形界面将基于通用振荡器的图形界面。将 UniOsc / UniOscGUI.mqh 复制到 UniChannel 文件夹, 并将其名称更改为 UniChannelGUI.mqh。通用通道的图形界面与通用振荡器的界面区别很大, 所以我们需要在这里做很多工作。

主要区别在于通用通道需要独立选择两个指标 (中心线和边界), 所以应该有两个主要指标选择列表。第一个列表应跟随控件来管理中心线参数。然后进入针对边界参数的第二个列表和控件。因此, 第二个列表没有固定的坐标, 它们必须要经过计算。除了两种类型选择列表之外, 表单应始终有两个用于偏移值的输入字段。字段的坐标也不是固定的。另一个重点是选择与 w_LockPeriod 参数对应的变量的列表。在所有需要显示宽度控件组中 w_Period 参数输入字段的情况下, 应显示一个额外的下拉列表。

首先, 我们在 UniChannelGUI.mqh 文件中进行一般修改:

1. 枚举文件的路径:

#include <UniOsc/UniOscDefines.mqh>

需要由以下替换:

#include <UniChannel/UniChannelDefines.mqh>

2. 添加一个带有 ELockTo 枚举值的数组:

ELockTo e_lockto[]={LockTo_Off,LockTo_Period1,LockTo_Period2,LockTo_Period3};
  

3. 使用 ENUM_APPLIED_VOLUME 和 ENUM_STO_PRICE 枚举删除数组。

我们现在继续修改 CUniOscControls 类。  

中心线控件类

1. CUniOscControls 类被重新命名为 CUniChannelCentralControls。

2. 变量 m_volume 和 m_sto_price 的声明应从类中删除。相应地, 我们从 SetPointers(), Hide() 和 Events() 方法中删除与这些控件相关联的所有内容。

3. 添加 m_last_y 变量, 其中将记录组中最后一个控件的 Y 坐标。添加一个接收该变量值的方法 — GetLastY()。我们不再需要 FormHeight() 方法, 所以我们删除它。代之, 我们需要添加 ControlsCount() 方法, 返回子类中的控件数量。该数字将用于计算表单的高度。

此处是结果父类:

class CUniChannelCentralControls{
   protected:
      CSpinInputBox * m_value1; // 周期 1
      CSpinInputBox * m_value2; // 周期 2
      CSpinInputBox * m_value3; // 周期 3
      CComBox * m_price;        // 价格
      CComBox * m_method;       // 方法
      int m_last_y;             // 最后控件的 Y 坐标
   public:
  
   // 得到最后控件的 Y 坐标
   int GetLastY(){
      return(m_last_y);
   }
  
   // 传递对象指针至对象的方法
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CSpinInputBox & value3,
                        CComBox & price,
                        CComBox & method){
      m_value1=GetPointer(value1);
      m_value2=GetPointer(value2);      
      m_value3=GetPointer(value3);            
      m_price=GetPointer(price);
      m_method=GetPointer(method);
   }
  
   // 隐藏控件群
   void Hide(){
      m_value1.Hide();
      m_value2.Hide();
      m_value3.Hide();
      m_price.Hide();
      m_method.Hide();
   }
  
   // 事件处理
   int Event(int id,long lparam,double dparam,string sparam){
      int e1=m_value1.Event(id,lparam,dparam,sparam);
      int e2=m_value2.Event(id,lparam,dparam,sparam);
      int e3=m_value3.Event(id,lparam,dparam,sparam);
      int e4=m_price.Event(id,lparam,dparam,sparam);
      int e5=m_method.Event(id,lparam,dparam,sparam);
      if(e1!=0 || e2!=0 || e3!=0 || e4!=0 || e5!=0){
         return(1);
      }
      return(0);
   }
  
   // 控件初始化方法 (修改标签) 
   virtual void InitControls(){
   }  
  
   // 显示控件群
   virtual void Show(int x,int y){
   }  
  
   // 得到群内控件数量
   virtual int ControlsCount(){
      return(0);
   }      
        };

我们来修改 CUniOscControls_ATR 子类:

1. 将其名称更改为 CUniChannelCentralControls_AMA, 这将是 AMA 指标的类。

2. 遵照 表 1 的 “参数” 列, 我们在 InitControls() 方法中初始化所有控件, 并在 Show() 方法中调用所有控件的 Show() 方法。最后一个控件的值应分配到 m_last_y 变量。

3. 现在我们删除 FormHeight() 方法, 并添加 ControlsCount() 代之。

此处是结果类:

class CUniChannelCentralControls_AMA:public CUniChannelCentralControls{
   void InitControls(){
      // 控件初始化
      m_value1.Init("c_value1",SPIN_BOX_WIDTH,1," ama_period");
      m_value2.Init("c_value2",SPIN_BOX_WIDTH,1," fast_ma_period");      
      m_value3.Init("c_value3",SPIN_BOX_WIDTH,1," slow_ma_period");      
   }
  
   // 显示 
   void Show(int x,int y){
      m_value1.Show(x,y);
      y+=20;
      m_value2.Show(x,y);
      y+=20;
      m_value3.Show(x,y);
      y+=20;
      m_price.Show(x,y);
      m_last_y=y;
   }
  
   // 得到群内控件数量
   int ControlsCount(){
      return(4);
   }
        };

类似地, 我们为中心线使用的其余指标创建类, 并删除所有不必要的振荡器子类。

宽度计算控件的类

基于 CUniChannelCentralControls 类, 我们创建一个用于管理通道宽度参数的类。制作 CUniChannelCentralControls 类的副本, 并将其名称更改为 CUniChannelWidthControls。在这个类中, 我们需要两个输入字段 (周期和宽度), 平均类型和价格的两个标准枚举, 以及 w_LockPeriod 参数枚举。作为结果, 我们得到如下类:

class CUniChannelWidthControls{
   protected:
      CSpinInputBox * m_value1; // 周期
      CSpinInputBox * m_value2; // 宽度
      CComBox * m_price;        // 价格
      CComBox * m_method;       // 方法
      CComBox * m_lockto;       // 锁定类型
      int m_last_y;             // 最后控件的 Y 坐标 
   public:
  
   // 得到最后控件的 Y 坐标
   int GetLastY(){
      return(m_last_y);
   }
  
   // 传递对象指针至对象的方法
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CComBox & price,
                        CComBox & method,
                        CComBox & lockto){
      m_value1=GetPointer(value1);
      m_value2=GetPointer(value2);      
      m_price=GetPointer(price);
      m_method=GetPointer(method);
      m_lockto=GetPointer(lockto);      
   }
  
   // 隐藏控件群
   void Hide(){
      m_value1.Hide();
      m_value2.Hide();
      m_price.Hide();
      m_method.Hide();
   }
  
   // 事件处理
   int Event(int id,long lparam,double dparam,string sparam){
      int e1=m_value1.Event(id,lparam,dparam,sparam);
      int e2=m_value2.Event(id,lparam,dparam,sparam);
      int e4=m_price.Event(id,lparam,dparam,sparam);
      int e5=m_method.Event(id,lparam,dparam,sparam);
      int e6=m_lockto.Event(id,lparam,dparam,sparam);      
      if(e1!=0 || e2!=0 || e4!=0 || e5!=0 || e6){
         return(1);
      }
      return(0);
   }
  
   // 控件初始化方法 (修改标签) 
   virtual void InitControls(){
   }  
  
   // 显示控件群
   virtual void Show(int x,int y){
   }  
  
   // 得到群内控件数量
   virtual int ControlsCount(){
      return(0);
   }    
        };

我们来创建它的子类。与中心线类的主要区别是, 在周期输入字段后, 我们需要为 w_LockPeriod 参数创建一个下拉列表。此处的类使用 ATR 进行宽度计算:

class CUniChannelWidthControls_ATR:public CUniChannelWidthControls{
   void InitControls(){
      // 控件初始化
      m_value1.Init("w_value1",SPIN_BOX_WIDTH,1," period");
   }
  
   // 显示控件群
   void Show(int x,int y){
      m_value1.Show(x,y);
      y+=20;
      m_lockto.Show(x,y);
      m_last_y=y;
   }  
  
   // 得到群内控件数量
   int ControlsCount(){
      return(2);
   }    
        };

通道宽度计算的所有其它变体的类与上述这个类似。 

现在, UniChannelGUI.mqh 文件包含两个基本类的控件, 它们的子类和一个表单类。后者需要进行调整。由于尺寸较大, 使用该文件可能不方便, 因此我们将在其它文件中准备控件类。我们创建 UniChannel/CUniChannelCentralControls.mqh 文件, 并将 CUniChannelCentralControls 类及其所有子类移至此文件。我们还在其内包括其它文件: 

#include <IncGUI_v4.mqh>
    #include <UniChannel/UniChannelDefines.mqh>

常量的定义 FORM_WIDTH, SPIN_BOX_WIDTH, COMBO_BOX_WIDTH 将被移至 UniChannelDefines.mqh 文件中。之后, 可以编译 CUniChannelCentralControls 文件以便检查错误。CUniChannelWidthControls 类也应移至单独的文件里。之后, 利用表单类进行操作会更方便。 

表单类

将两个新创建的文件包含到 UniChannelGUI.mqh 中:

#include <UniChannel/CUniChannelCentralControls.mqh>
    #include <UniChannel/CUniChannelWidthControls.mqh>

将 CUniOscForm 重命名为 CUniChannelForm。在公有部分, 我们删除 CUniOscControls 类型的指针。代之, 我们声明两个指针变量: CUniChannelCentralControls 和 CUniChannelWidthControls, 并确定表单类的其它控件。以下变量将位于公有部分:

CComBox           m_c_cmb_main;  // 中心线选择列表
CSpinInputBox     m_c_value1;    // 周期 1 的输入字段
CSpinInputBox     m_c_value2;    // 周期 2 的输入字段
CSpinInputBox     m_c_value3;    // 周期 3 的输入字段
CComBox           m_c_price;     // 价格选择列表
CComBox           m_c_method;    // 方法选择列表
CSpinInputBox     m_c_shift;     // 平移字段
CComBox           m_w_cmb_main;  // 边界选择列表
CSpinInputBox     m_w_value1;    // 周期输入字段
CSpinInputBox     m_w_value2;    // 宽度输入字段
CComBox           m_w_price;     // 价格选择字段
CComBox           m_w_method;    // 方法选择字段
CComBox           m_w_lockto;    // 锁定选项选择字段     
CSpinInputBox     m_w_shift;     // 平移输入字段          
// 中心线控件的群
CUniChannelCentralControls * m_central_controls;
// 边界控件的群
        CUniChannelWidthControls * m_width_controls;  

在 MainProperties() 方法中, 我们更改 m_Name 和 m_Caption 变量的值, 所有其它变量保持不变:

void MainProperties(){
      m_Name         =  "UniChannelForm";
      m_Width        =  FORM_WIDTH;
      m_Height       =  150;
      m_Type         =  0;
      m_Caption      =  "UniChannel";
      m_Movable      =  true;
      m_Resizable    =  true;
      m_CloseButton  =  true;
        }

在 OnInitEvent() 方法中, 我们使用不变的标签 (对应于所选指标的控件集合) 调用所有控件的 Init() 方法, 并填充下拉列表:

void OnInitEvent(){
   // 不包括在群中的控件初始化
  
   m_c_cmb_main.Init("cb_c_main",COMBO_BOX_WIDTH," 选择中心");
   m_w_cmb_main.Init("cb_w_main",COMBO_BOX_WIDTH," 选择边带");
   m_c_price.Init("c_price",COMBO_BOX_WIDTH," 价格");
   m_c_method.Init("c_method",COMBO_BOX_WIDTH," 方法");
   m_c_shift.Init("c_shift",COMBO_BOX_WIDTH,1," 平移");    
  
   m_w_price.Init("w_price",COMBO_BOX_WIDTH," 价格");
   m_w_method.Init("w_method",COMBO_BOX_WIDTH," 方法");
   m_w_shift.Init("w_shift",COMBO_BOX_WIDTH,1," 平移");    
  
   m_w_lockto.Init("cb_w_lockto",COMBO_BOX_WIDTH," 锁定周期");
   m_w_value2.Init("w_value2",SPIN_BOX_WIDTH,0.001," 宽度");
  
   // 填充下拉列表
  
   for(int i=0;i<ArraySize(e_price);i++){
      m_c_price.AddItem(EnumToString(e_price[i]));
      m_w_price.AddItem(EnumToString(e_price[i]));
   }
   for(int i=0;i<ArraySize(e_method);i++){
      m_c_method.AddItem(EnumToString(e_method[i]));
      m_w_method.AddItem(EnumToString(e_method[i]));
   }            
   for(int i=0;i<ArraySize(e_lockto);i++){
      m_w_lockto.AddItem(EnumToString(e_lockto[i]));            
   }
  
   // 允许使用键盘输入平移值            
   m_c_shift.SetReadOnly(false);
   m_w_shift.SetReadOnly(false);                        
        }  

在OnShowEvent() 方法中, 我们显示控件。在显示元素群之后, 我们获得 Y 坐标, 并根据该坐标显示以下控件:

void OnShowEvent(int aLeft, int aTop){
   m_c_cmb_main.Show(aLeft+10,aTop+10);        // 中心线类型选择列表
   m_central_controls.Show(aLeft+10,aTop+30);  // 中心线参数控件群
   int m_y=m_central_controls.GetLastY();      // 得到最后控件的坐标
   m_c_shift.Show(aLeft+10,m_y+20);            // 平移参数输入字段
   m_w_cmb_main.Show(aLeft+10,m_y+40);         // 通道选择列表
   m_width_controls.Show(aLeft+10,m_y+60);     // 通道参数控件群
   m_y=m_width_controls.GetLastY();            // 得到最后控件的坐标
   m_w_value2.Show(aLeft+10,m_y+20);           // 宽度输入字段
   m_w_shift.Show(aLeft+10,m_y+40);            // 平移参数输入字段
        }

控件可以隐藏在 OnHideEvent() 方法中:

void OnHideEvent(){
   m_c_cmb_main.Hide();       // 中心线类型选择列表     
   m_central_controls.Hide(); // 中心线参数控件群
   m_c_shift.Hide();          // 平移参数输入字段
   m_w_cmb_main.Hide();       // 通道选择字段
   m_width_controls.Hide();   // 通道参数控件群
   m_w_shift.Hide();          // 平移参数输入字段
   m_w_lockto.Hide();         // 周期锁定类型选择
   m_width_controls.Hide();   // 宽度输入字段
    }

在 SetValues() 方法中进行更改。更改方法参数集合, 为方法中的所有控件设置对应于这些参数的值:

void SetValues(int c_value1,
               int c_value2,
               int c_value3,
               long c_method,
               long c_price,
               long c_shift,                    
               int w_value1,
               int w_value2,
               long w_method,
               long w_price,
               long w_lockto,
               long w_shift  
){
   // 中心线输入字段
   m_c_value1.SetValue(c_value1);
   m_c_value2.SetValue(c_value2);      
   m_c_value3.SetValue(c_value3);
   m_c_shift.SetValue(c_shift);        
   // 通道参数输入字段
   m_w_value1.SetValue(w_value1);
   m_w_value2.SetValue(w_value2);        
   m_w_shift.SetValue(w_shift);            
  
   // 显示在平滑方法选择列表中所选的类型
   for(int i=0;i<ArraySize(e_method);i++){
      if(c_method==e_method[i]){
         m_c_method.SetSelectedIndex(i);
      }
      if(w_method==e_method[i]){
         m_w_method.SetSelectedIndex(i);
      }            
   }
  
   // 显示在价格类型选择列表中所选的类型
   for(int i=0;i<ArraySize(e_price);i++){
      if(c_price==e_price[i]){
         m_c_price.SetSelectedIndex(i);
      }
      if(w_price==e_price[i]){
         m_w_price.SetSelectedIndex(i);
      }            
   }
   // 显示所选的通道周期锁定类型
   for(int i=0;i<ArraySize(e_lockto);i++){
      if(w_lockto==e_lockto[i]){
         m_w_lockto.SetSelectedIndex(i);
         break;
      }
   }                    
        }  

替代 SetType(), 我们创建两个方法: SetCentralType() 用于设置中心线类型, SetWidthType() 用于设置边框类型。在每个方法结尾处, 一旦对象创建, 属性将设置为控件, 允许使用键盘输入数值。我们还设置最小允许值, 并调用表单高度计算的私有方法:  

方法 SetCentralType():

void SetCentralType(long type){
   // 如果一个对象已经被创建, 我们需要删除它
   if(CheckPointer(m_central_controls)==POINTER_DYNAMIC){
      delete(m_central_controls);
      m_central_controls=NULL;
   }
   switch((ECType)type){ // 根据所选的类创建一个对象
      case UniCh_C_AMA:
         m_central_controls=new CUniChannelCentralControls_AMA();
      break;
      case UniCh_C_DEMA:
         m_central_controls=new CUniChannelCentralControls_DEMA();            
      break;
      case UniCh_C_FrAMA:
         m_central_controls=new CUniChannelCentralControls_FrAMA();            
      break;
      case UniCh_C_MA:
         m_central_controls=new CUniChannelCentralControls_MA();            
      break;
      case UniCh_C_TEMA:
         m_central_controls=new CUniChannelCentralControls_TEMA();            
      break;
      case UniCh_C_VIDyA:
         m_central_controls=new CUniChannelCentralControls_VIDyA();            
      break;
      case UniCh_C_PrCh:
         m_central_controls=new CUniChannelCentralControls_PrCh();            
      break;
   }    
  
   // 将指针传递给控件对象
   m_central_controls.SetPointers(m_c_value1,m_c_value2,m_c_value3,m_c_price,m_c_method);
   // 控件群的初始化
   m_central_controls.InitControls();
  
   // 允许从键盘输入数值
   m_c_value1.SetReadOnly(false);
   m_c_value2.SetReadOnly(false);
   m_c_value3.SetReadOnly(false);
  
   // 设置最小允许值
   m_c_value1.SetMinValue(1);        
   m_c_value2.SetMinValue(1);
   m_c_value3.SetMinValue(1);            
  
   // 计算表单的高度
   this.SolveHeight();
        }

 方法 SetWidthType():

void SetWidthType(long type){
   // 如果一个对象已经被创建, 我们需要删除它
   if(CheckPointer(m_width_controls)==POINTER_DYNAMIC){
      delete(m_width_controls);
      m_width_controls=NULL;
   }
   switch((EWType)type){ // 根据所选的类创建一个对象
      case UniCh_W_ATR:
         m_width_controls=new CUniChannelWidthControls_ATR();
      break;
      case UniCh_W_StdDev:
         m_width_controls=new CUniChannelWidthControls_StdDev();            
      break;
      case UniCh_W_Points:
         m_width_controls=new CUniChannelWidthControls_InPoints();            
      break;
      case UniCh_W_Percents:
         m_width_controls=new CUniChannelWidthControls_Envelopes();            
      break;
      case UniCh_W_PrCh:
         m_width_controls=new CUniChannelWidthControls_PrCh();                        
      break;
   }    
   // 将指针传递给控件对象
   m_width_controls.SetPointers(m_w_value1,m_w_value2,m_w_price,m_w_method);
   // 控件群初始化
   m_width_controls.InitControls();
  
   // 设置最小允许值
   m_w_value1.SetReadOnly(false);
   m_w_value2.SetReadOnly(false);
  
   // 设置最小允许值
   m_w_value1.SetMinValue(1);        
   m_w_value2.SetMinValue(0);
  
   // 计算表单的高度
   this.SolveHeight();
              
        }  

在 SetCentralType() 和 SetWidthType() 方法的末尾, 调用计算表单高度的 SolveHeight() 方法:

void SolveHeight(){
   // 如果两个对象均存在 (中心线和宽度)
   if(CheckPointer(m_central_controls)==POINTER_DYNAMIC && CheckPointer(m_width_controls)==POINTER_DYNAMIC){
      m_Height=(m_width_controls.ControlsCount()+m_central_controls.ControlsCount()+6)*20+10;
   }      
        }  

我们接着去连接指标和用户图形界面。  

连接指标和图形界面

我们将 iUniChannel indicator 保存为  iUniChannelGUI。类似于 iUniOscGUI 指标, 我们在其属性窗口的最顶层加入一个外部的 UseGUI 参数。之后, 我们添加变量 UseDefault和KeepPrev, 将它们省缺设置为 true, 并在属性窗口中显示它们:

input bool                 UseGUI        =  true;
input bool                 UseDefault    =  true;
  input bool                 KeepPrev      =  true;

包含带有图形界面的文件 (其中包含指标的类文件):

#include <UniChannel/UniChannelGUI.mqh>

在 OnInit() 函数的最底部, 我们添加了一个图形界面引导代码。但在这样做之前, 我们需要带有中心线和边界类型的数组。我们将它们添加到指标的外部参数下方:

// 中心线类型的数组
ECType ctype[]={
   UniCh_C_AMA,
   UniCh_C_DEMA,
   UniCh_C_FrAMA,
   UniCh_C_MA,
   UniCh_C_TEMA,
   UniCh_C_VIDyA,
   UniCh_C_PrCh
};
// 边界类型的数组
EWType wtype[]={
   UniCh_W_ATR,
   UniCh_W_StdDev,
   UniCh_W_Points,
   UniCh_W_Percents,
   UniCh_W_PrCh
};

我们还在此添加了一个指向表单类的指针:

CUniChannelForm * frm;

在 OnInit() 函数的最后, 实现图形界面对象的创建:

if(UseGUI){
  
   // 表单对象的创建和初始化
   frm=new CUniChannelForm();
   frm.Init();
  
   // 辅助变量
   int ind1=0;
   int ind2=0;
  
   // 在中心线类型数组中搜索所选类型的中心线
   for(int i=0;i<ArraySize(ctype);i++){        
      frm.m_c_cmb_main.AddItem(EnumToString(ctype[i]));
      if(ctype[i]==_CentralType){
         ind1=i;
      }
   }
  
   // 在边界类型数组中搜索所选类型的通道边界
   for(int i=0;i<ArraySize(wtype);i++){        
      frm.m_w_cmb_main.AddItem(EnumToString(wtype[i]));
      if(wtype[i]==_WidthType){
         ind2=i;
      }
   }      
  
   // 显示列表中所选的中心线类型
   frm.m_c_cmb_main.SetSelectedIndex(ind1);      
   // 准备与类型相对应的控件
   frm.SetCentralType(_CentralType);
  
   // 显示在列表中所选的边界类型
   frm.m_w_cmb_main.SetSelectedIndex(ind2);      
   frm.SetWidthType(_WidthType);      
  
   // 设置数值
   frm.SetValues(
                  _c_Period1,
                  _c_Period2,
                  _c_Period3,
                  _c_Method,
                  _c_Price,
                  _c_Shift,
                  _w_Period,
                  _w_Width,
                  _w_Method,
                  _w_Price,
                  _w_LockPeriod,
                  _w_Shift
   );
  
   // 设置表单属性
   frm.SetSubWindow(0);
   frm.SetPos(10,30);
   // 显示表单
   frm.Show();
    }    

除了创建表单对象之外, 还会填充指标选择列表, 并为其分配选定的变体。我们还可以在控件中设置所有其它数值。之后, 当指标附加到图表时, 将显示带有控件的窗体 (图例. 1)。


图例. 1. 带有通用通道控件的表单

控件正确显示, 现在我们需要确保表单按钮的操作。OnChartEvent() 函数中处理了六个不同的事件。一些事件的处理是相当复杂的, 因此将使用一个单独的函数:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   // 表单事件
   if(frm.Event(id,lparam,dparam,sparam)==1){
      EventForm();
   }
  
   // 选择中心线类型
   if(frm.m_c_cmb_main.Event(id,lparam,dparam,sparam)==1){
      EventCentralTypeChange();
   }  
  
   // 选择边界类型
   if(frm.m_w_cmb_main.Event(id,lparam,dparam,sparam)==1){
      EventWidthTypeChange();
   }  
  
   // 改变中心线参数
   if(frm.m_central_controls.Event(id,lparam,dparam,sparam)==1){
      EventCentralParametersChange();
   }  
  
   // 改变边界参数
   if(frm.m_width_controls.Event(id,lparam,dparam,sparam)==1){
      EventWidthParametersChange();
   }  
   // 改变平移参数
   if(frm.m_c_shift.Event(id,lparam,dparam,sparam)!=0 ||
      frm.m_w_shift.Event(id,lparam,dparam,sparam)
   ){
      EventShift();
   }    
    }  

我们来研究所有这些函数。函数 EventForm():

void EventForm(){      
   int win=ChartWindowFind(0,ShortName);  // 判断指标子窗口
   ChartIndicatorDelete(0,win,ShortName); // 删除指标
   ChartRedraw();
    }  

当您通过按下十字按钮关闭表单时, 执行此函数, 以短名称搜索指标窗口, 然后删除指标。 

函数 EventCentralTypeChange():

void EventCentralTypeChange(){    
   // 得到的新类型保存到变量
   _CentralType=ctype[frm.m_c_cmb_main.SelectedIndex()];
  
   // 删除旧对象并创建一个新的
   delete(central);
   LoadCentral(true);
  
   // 检查指标加载
   if(!central.CheckHandle()){
      Alert("当指标加载时错误 "+central.Name());
   }
   // 设置偏移量和缓冲区名称
   SetStyles();
   // 在列表中设置新类型
   frm.SetCentralType(ctype[frm.m_c_cmb_main.SelectedIndex()]);
   // 在表单上更新参数值
   frm.SetValues(
                  _c_Period1,
                  _c_Period2,
                  _c_Period3,
                  _c_Method,
                  _c_Price,
                  _c_Shift,
                  _w_Period,
                  _w_Width,
                  _w_Method,
                  _w_Price,
                  _w_LockPeriod,
                  _w_Shift
   );
   // 刷新表单
   frm.Refresh();
  
   // 启动指标重计算的定时器
   EventSetMillisecondTimer(100);
    }

在此函数中更改中心线指标的类型。首先, 获取所选指标的类型, 然后删除旧对象, 并创建一个新对象。当创建新对象时, 一些参数可以更改 (由于 UseDefault 函数), 因此将调用 SetValues() 方法为控件设置新值, 并刷新显示 (Refresh() 方法)。最后, 启动一个定时器来执行指标重计算。      

EventWidthTypeChange() 函数类似于 EventCentralTypeChange() 函数, 所以我们不会详细研究。 

EventCentralParametersChange() 和 EventWidthParametersChange() 函数为指标参数更改提供响应。这些函数在其核心功能方面是相同的。然而, 在更改参数时, 需要注意周期锁定, 并根据锁定修正参数, 因此函数有其自己的独特功能, 两者均要考虑。

void EventCentralParametersChange(){          
  
   // 该变量表示需要重新启动边界指标
   bool dolock=false;
  
   // 修改周期 1 的数值
   if((int)frm.m_c_value1.Value()>0){
      // 将从控件接收的值分配给变量 
      _c_Period1=(int)frm.m_c_value1.Value();
      // 如果周期 1 与宽度指标的周期相连接
      if(_w_LockPeriod==LockTo_Period1){
         // 我们用宽度指标周期设置为周期 1 的值
         _w_Period=_c_Period1;
         // 在表单上显示它
         frm.m_w_value1.SetValue(_w_Period);
         // 表示第二个指标需要重启
         dolock=true;
      }
   }
  
   // 修改周期 2 的数值类似于周期 1
   if((int)frm.m_c_value2.Value()>0){
      _c_Period2=(int)frm.m_c_value2.Value();
      if(_w_LockPeriod==LockTo_Period2){
         _w_Period=_c_Period2;
         frm.m_w_value1.SetValue(_w_Period);
         dolock=true;
      }        
   }
  
   // 修改周期 3 的数值类似于周期 1
   if((int)frm.m_c_value3.Value()>0){
      _c_Period3=(int)frm.m_c_value3.Value();
      if(_w_LockPeriod==LockTo_Period3){
         _w_Period=_c_Period3;
         frm.m_w_value1.SetValue(_w_Period);
         dolock=true;
      }        
   }
  
   // 修改方法
   if(frm.m_c_method.SelectedIndex()!=-1){
      _c_Method=e_method[frm.m_c_method.SelectedIndex()];
   }
  
   // 修改价格
   if(frm.m_c_price.SelectedIndex()!=-1){
      _c_Price=e_price[frm.m_c_price.SelectedIndex()];
   }
  
   // 删除旧的对象并创建新的
   delete(central);
   LoadCentral(false);
   if(!central.CheckHandle()){
      Alert("Error while loading indicator "+central.Name());
   }  
   // 删除第二个指标的对象并创建新的
   if(dolock){
      delete(width);
      LoadWidth(false);
      if(!width.CheckHandle()){
         Alert("当加载指标时错误 "+width.Name());
      }  
   }  
   // 设置偏移量和缓冲区名称
   SetStyles();
   // 启动指标重计算定时器
   EventSetMillisecondTimer(100);
    }  

在此函数中, 当三个周期中的任一个变化时, 执行锁定参数的检查。如果使用锁定, 则边界指标的参数被更改, 在表单上对其更新, 并将 true 分配给 dolock 变量。在结尾处, 旧的指针对象被删除, 创建一个新的, 且如果 dolock 变量等于 true, 则边界对象被删除然后再次i被创建。之后, 启动定时器, 等待指标重计算。

void EventWidthParametersChange(){  
      
   // 该变量表示需要重新启动中心线指标
   bool dolock=false;
   // 修改周期
   if((int)frm.m_w_value1.Value()>0){
      // 将接收自控件的数值分配给变量
      _w_Period=(int)frm.m_w_value1.Value();
      // 进行锁定
      // 宽度参数与中心线的第一个周期相连接
      if(_w_LockPeriod==LockTo_Period1){
         // 为中心线指标的变量分配新值 
         _c_Period1=_w_Period;
         // 在表单上更新数值
         frm.m_c_value1.SetValue(_c_Period1);
         // 表示需要重启宽度指标
         dolock=true;
      }
      else if(_w_LockPeriod==LockTo_Period2){ // 如果锁定周期 2
         _c_Period2=_w_Period;
         frm.m_c_value2.SetValue(_c_Period2);
         dolock=true;
      }
      else if(_w_LockPeriod==LockTo_Period3){ // 如果锁定周期 3
         _c_Period3=_w_Period;
         frm.m_c_value3.SetValue(_c_Period3);
         dolock=true;
      }
   }
  
   // 改变通道宽度参数
   if((double)frm.m_w_value2.Value()>0){
      _w_Width=(double)frm.m_w_value2.Value();
   }      
  
   // 修改方法
   if(frm.m_w_method.SelectedIndex()!=-1){
      _w_Method=e_method[frm.m_w_method.SelectedIndex()];
   }
  
   // 修改价格
   if(frm.m_w_price.SelectedIndex()!=-1){
      _w_Price=e_price[frm.m_w_price.SelectedIndex()];
   }
  
   // 在锁定类型选择列表中发生变化的事件 
   if(frm.m_w_lockto.SelectedIndex()>=0){
      // 将接收自控件的值分配给变量
      _w_LockPeriod=e_lockto[frm.m_w_lockto.SelectedIndex()];
      // 如果选择了某些周期的锁定,
      // 其值被复制, 表单被刷新  
      if(_w_LockPeriod==LockTo_Period1){
         _w_Period=_c_Period1;
         frm.m_w_value1.SetValue(_w_Period);
      }
      else if(_w_LockPeriod==LockTo_Period2){
         _w_Period=_c_Period2;
         frm.m_w_value1.SetValue(_w_Period);
      }
      else if(_w_LockPeriod==LockTo_Period3){
         _w_Period=_c_Period3;
         frm.m_w_value1.SetValue(_w_Period);
      }
   }      
   // 删除旧对象并创建新的
   delete(width);
   LoadWidth(false);
   if(!width.CheckHandle()){
      Alert("当加载指标时错误 "+width.Name());
   }
  
   // 删除第二个指标的对象并创建新的
   if(dolock){
      delete(central);
      LoadCentral(false);
      if(!central.CheckHandle()){
         Alert("当加载指标时错误 "+central.Name());
      }
   }
   // 设置偏移量和缓冲区名称
   SetStyles();
   // 启动指标重计算的定时器
   EventSetMillisecondTimer(100);      
    }    

在此函数中, 更改周期时会检查锁定类型, 如有必要, 更改中心线指标的相应周期。在接收到锁定类型选择列表事件时, 将中心线指标相应变量的值分配给边界指标周期的变量。

平移值变化的事件处理是非常简单的:

void EventShift(){     
   // 在变量中接收新值 
   _c_Shift=(int)frm.m_c_shift.Value();
   _w_Shift=(int)frm.m_w_shift.Value();
   // 设置新样式
   SetStyles();
   // 刷新图表
   ChartRedraw();
    }  

来自控件的数值分配给变量, 调用 SetStyles() 并刷新图表。

在此, 我们的带有图形界面的指标几乎准备就绪了。

在指标测试期间检测到以下瑕疵。启用外部 UseDefault 参数并使用周期锁定时, 锁定失败。它所关联的事实就是, 当加载第二个指标 (宽度指标) 时, 在其构造器中进行参数修改。为了修复这个缺陷, 我不得不修改某些子类的宽度指标。可选参数 “locked” 被添加到 CChannelUni_Calculate_ATR, CChannelUni_Calculate_StdDev 和 CChannelUni_Calculate_PriceChannel 的构造函数中。其省缺值为 false (如果参数没有传递到类, 所有操作都无变化)。当 locked=true 且 use_default=true, 构造函数中的周期参数不会改变 (只要 locked=true)。这是 CChannelUni_Calculate_ATR 类的一部分:

if(use_default){
   if(keep_previous){
      if(ma_period==-1 && !locked)ma_period=14// 改变
      if(ch_width==-1)ch_width=2;
   }
   else{
      if(!locked)ma_period=14// 改变
      ch_width=2;
   }      
    }  

仅当 locked 变量设为 false, 才会将省缺值分配给 ma_period。函数 LoadWidth() 相应改善。在函数伊始计算 ‘Locked’ 的值:

bool Locked=(w_LockPeriod!=LockTo_Off);

之后此变量在对象创建过程中传递给类的构造函数。

就像在通用振荡器中所做的, 在此我们增加了改变配色方案的能力, 并在更改时间帧时提供指标参数的保存。我们不会讨论配色方案的使用, 因为在创建通用振荡器时已经研究过。我们来提供指标参数的保存。

在指标的 OnDeinit() 函数中, 如果由于图表改变而执行了逆初始化, 则我们使用参数值创建图形对象。我们在图表可见性之外创建这些图形对象:

void SaveOrDeleteParameters(const int reason){
   // 如果不是图表改变, 我们应该删除图形对象 
   if(reason!=REASON_CHARTCHANGE){
      ObjectDelete(0,"_CentralType");
      ObjectDelete(0,"_c_Period1");
      ObjectDelete(0,"_c_Period2");
      ObjectDelete(0,"_c_Period3");
      ObjectDelete(0,"_c_Shift");
      ObjectDelete(0,"_c_Method");
      ObjectDelete(0,"_c_Price");
      ObjectDelete(0,"_WidthType");
      ObjectDelete(0,"_w_Period");
      ObjectDelete(0,"_w_LockPeriod");
      ObjectDelete(0,"_w_Shift");
      ObjectDelete(0,"_w_Method");
      ObjectDelete(0,"_w_Price");
      ObjectDelete(0,"_w_Width");      
   }
   else// 在改变图表时, 我们使用参数值创建图形对象
      SaveParameter("_CentralType",(string)_CentralType);
      SaveParameter("_c_Period1",(string)_c_Period1);
      SaveParameter("_c_Period2",(string)_c_Period2);
      SaveParameter("_c_Period3",(string)_c_Period3);
      SaveParameter("_c_Shift",(string)_c_Shift);
      SaveParameter("_c_Method",(string)_c_Method);
      SaveParameter("_c_Price",(string)_c_Price);
      SaveParameter("_WidthType",(string)_WidthType);
      SaveParameter("_w_Period",(string)_w_Period);
      SaveParameter("_w_LockPeriod",(string)_w_LockPeriod);
      SaveParameter("_w_Shift",(string)_w_Shift);
      SaveParameter("_w_Method",(string)_w_Method);
      SaveParameter("_w_Price",(string)_w_Price);
      SaveParameter("_w_Width",(string)_w_Width);        
   }
}
// 保存图形对象中一个参数的辅助函数
void SaveParameter(string name,string value){
   if(ObjectFind(0,name)==-1){
      ObjectCreate(0,name,OBJ_LABEL,0,0,0);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,0);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,-30);
   }
   ObjectSetString(0,name,OBJPROP_TEXT,value);
    }

在 OnInit() 函数中, 调用 PrepareParameters() 函数之后, 我们调用 LoadSavedParameters() 函数:

bool LoadSavedParameters(){
   // 如果所有带参数的对象存在 
   if(ObjectFind(0,"_CentralType")==0 &&
      ObjectFind(0,"_c_Period1")==0 &&
      ObjectFind(0,"_c_Period2")==0 &&
      ObjectFind(0,"_c_Period3")==0 &&
      ObjectFind(0,"_c_Shift")==0 &&
      ObjectFind(0,"_c_Method")==0 &&
      ObjectFind(0,"_c_Price")==0 &&
      ObjectFind(0,"_WidthType")==0 &&
      ObjectFind(0,"_w_Period")==0 &&
      ObjectFind(0,"_w_LockPeriod")==0 &&
      ObjectFind(0,"_w_Shift")==0 &&
      ObjectFind(0,"_w_Method")==0 &&
      ObjectFind(0,"_w_Price")==0 &&
      ObjectFind(0,"_w_Width")==0
   ){
      // 从图形对象中获取数值
      _CentralType=(ECType)ObjectGetString(0,"_CentralType",OBJPROP_TEXT);
      _c_Period1=(int)ObjectGetString(0,"_c_Period1",OBJPROP_TEXT);
      _c_Period2=(int)ObjectGetString(0,"_c_Period2",OBJPROP_TEXT);
      _c_Period3=(int)ObjectGetString(0,"_c_Period3",OBJPROP_TEXT);
      _c_Shift=(int)ObjectGetString(0,"_c_Shift",OBJPROP_TEXT);
      _c_Method=(long)ObjectGetString(0,"_c_Method",OBJPROP_TEXT);
      _c_Price=(long)ObjectGetString(0,"_c_Price",OBJPROP_TEXT);
      _WidthType=(EWType)ObjectGetString(0,"_WidthType",OBJPROP_TEXT);
      _w_Period=(int)ObjectGetString(0,"_w_Period",OBJPROP_TEXT);
      _w_LockPeriod=(long)ObjectGetString(0,"_w_LockPeriod",OBJPROP_TEXT);
      _w_Shift=(int)ObjectGetString(0,"_w_Shift",OBJPROP_TEXT);
      _w_Method=(long)ObjectGetString(0,"_w_Method",OBJPROP_TEXT);
      _w_Price=(long)ObjectGetString(0,"_w_Price",OBJPROP_TEXT);
      _w_Width=(double)ObjectGetString(0,"_w_Width",OBJPROP_TEXT);
      return(true);
   }
   else{
      return(false);
   }
    }  

在此函数中, 执行这些对象是否存在的检查。如果它们确实存在, 则使用这些对象的值, 且函数返回 true。如果函数返回 true, 那么应该使用 false 参数调用 LoadCentral() 和 LoadWidth() 函数 (以防止设置省缺参数)。OnInit() 函数的片段:

bool ChartCange=LoadSavedParameters();
  
    LoadCentral(!ChartCange);  

LoadWidth() 函数的调用方式相同:

LoadWidth(!ChartCange);

现在, 通用通道的创造已全面完成。 

结论

尽管通用振荡器使用了大量的现成代码, 但通用通道的创建仍然需要大量额外的工作。与通用振荡器的主要区别是存在两个独立的单元: 中心线和通道边界。这一困难使工作量增加了近两倍。通用通道包括更复杂的参数变化算法, 而这些是与周期锁定函数相连的。新指标的加载也变得更加复杂, 因为现在使用了两个指标。我们还增加了新的功能 — 在切换时间帧时保存参数。作为结果, 我们创造了一个有益且便利的指标。另外, 这个指标显著地扩展了通道思路的功能, 因为现在您可以单独选择中心线以及构建通道边界的方法。这就提供了大范围的可能组合。通过使用图形界面增加指标应用的速度, 可直观地分析所有这些组合。

附件

本文附带所需文件的可下载存档。文件应置于正确的文件夹中。它们应被保存到终端的相同文件夹中。

 

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

附加的文件 |

files.zip
(93 KB)

 

 


MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!

 

免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。

風險提示

MyFxtops邁投所列信息僅供參考,不構成投資建議,也不代表任何形式的推薦或者誘導行為。MyFxtops邁投非外匯經紀商,不接觸妳的任何資金。 MYFXTOPS不保證客戶盈利,不承擔任何責任。從事外彙和差價合約等金融產品的槓桿交易具有高風險,損失有可能超過本金,請量力而行,入市前需充分了解潛在的風險。過去的交易成績並不代表以後的交易成績。依據各地區法律法規,MyFxtops邁投不向中國大陸、美國、加拿大、朝鮮居民提供服務。

邁投公眾號

聯繫我們

客服QQ:981617007
Email: service@myfxtop.com

MyFxtops 邁投