MQL5 酷宝书 - 移动通道交易信号

Denis Kirichenko | 26 九月, 2016


概论

之前的文章 «MQL5 酷宝书 - 移动通道编程» 描述了绘制等距通道的方法, 此通道经常称为移动通道。为了解决此任务, 使用了 «等距通道» 工具和 OOP 能力。

本文将专注于信号, 它可以使用这些通道来识别。我们来尝试创建基于这些信号的交易策略。

在 MQL5 上还有多篇发表的文章, 它们描述了调用 标准库 里现成的模块来生成交易信号。希望本文能够补充材料, 并拓宽用户的 标准类范围。

那些刚接触此策略的人, 很欢迎能够从简单到复杂地了解这些材料。首先, 创建一个基本策略, 然后在可能的情况下扩充它, 令其更加复杂。


1. 等距通道指标

在之前有关移动通道的文章中, 智能交易程序通过创建图形对象绘制通道自身。一方面, 这种方法可简化程序员的任务, 但在另一方面, 渲染某些事情是不可能的。例如, 如果 EA 工作于优化模式, 那么它不能在图表上检测任何图形对象, 因为它毕竟不是图表。根据测试期间的 限制:


测试时的图形对象

在测试/优化期间不会绘制图形对象。因此, 在测试/优化过程中引用创建对象的属性时, 智能交易程序将获得零值。

此限制不会应用在可视测试模式。


因此, 将使用不同的方法, 创建反映分形和实际通道两者的指标。

这个指标称为 等距通道。它基本上由两大块组成。首先计算分形缓存区, 其次 — 通道缓存区。

此处是 Calculate 事件 处理器的代码。

//+------------------------------------------------------------------+
//| 自定义指标迭代函数                                                |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//--- 如果前次调用没有柱线
   if(prev_calculated==0)
     {
      //--- 缓存区清零
      ArrayInitialize(gUpFractalsBuffer,0.);
      ArrayInitialize(gDnFractalsBuffer,0.);
      ArrayInitialize(gUpperBuffer,0.);
      ArrayInitialize(gLowerBuffer,0.);
      ArrayInitialize(gNewChannelBuffer,0.);
     }
//--- 计算分形 [开始]
   int startBar,lastBar;
//---
   if(rates_total<gMinRequiredBars)
     {
      Print("没有足够的计算数据");
      return 0;
     }
//---
   if(prev_calculated<gMinRequiredBars)
      startBar=gLeftSide;
   else
      startBar=rates_total-gMinRequiredBars;
//---
   lastBar=rates_total-gRightSide;
   for(int bar_idx=startBar; bar_idx<lastBar && !IsStopped(); bar_idx++)
     {
      //---
      if(isUpFractal(bar_idx,gMaxSide,high))
         gUpFractalsBuffer[bar_idx]=high[bar_idx];
      else
         gUpFractalsBuffer[bar_idx]=0.0;
      //---
      if(isDnFractal(bar_idx,gMaxSide,low))
         gDnFractalsBuffer[bar_idx]=low[bar_idx];
      else
         gDnFractalsBuffer[bar_idx]=0.0;
     }
//--- 计算分形 [结束]

//--- 计算通道边界 [开始]
   if(prev_calculated>0)
     {
      //--- 如果设置尚未初始化
      if(!gFracSet.IsInit())
         if(!gFracSet.Init(
            InpPrevFracNum,
            InpBarsBeside,
            InpBarsBetween,
            InpRelevantPoint,
            InpLineWidth,
            InpToLog
            ))
           {
            Print("分形设置初始化错误!");
            return 0;
           }
      //--- 计算
      gFracSet.Calculate(gUpFractalsBuffer,gDnFractalsBuffer,time,
                         gUpperBuffer,gLowerBuffer,
                         gNewChannelBuffer
                         );
     }
//--- 计算通道边界 [结束]

//--- 返回 prev_calculated 的数值用于下次调用
   return rates_total;
  }

计算分形缓存区数值的模块以 黄色 加亮, 而计算通道缓存区的模块 — 为 绿色。很容易就能注意到, 第二块的激活并非在第一块, 而只在下一次调用处理程序时。这种 第二模块 的实现可以得到填满的分形缓存区。

现在, 有关分形点集合的几句话 — CFractalSet 对象。由于通道显示方法的改变, 也有必要修改 CFractalSet 类。关键的方法是 CFractalSet::Calculate, 它计算指标的通道缓存区。代码在 CFractalPoint.mqh 中提供。


现在有了一个基础 — 来自等距通道的信号提供者。指标的操作在视频里显示。

2. 基本 策略

所以, 让我们从简单的事情开始, 并在 OOP 的帮助下修改和改进。我们有一些基本的策略。

策略将会考虑相当简单的交易规则。入场将在通道的边界进行。当价格触及下边界将开多头仓位, 当它接触到上边界 - 空头仓位。图例. 1 显示价格触及下边界, 所以机器人买入一定交易量。交易价位 (止损和止盈) 会按照固定大小自动放置。如果此处有开仓, 重复的入场信号将被忽略。

图例.1 入场信号

图例.1 入场信号


还值得一提的是 标准库 已经成长了许多。它 已经 包括了许多现成的类可以使用。首先, 我们尝试 «连接» 到信号类 CExpertSignal。根据文档, 它是创建交易信号生成器的 基类

这个类已被相当准确地命名。这不是 CTradeSignal 也不是 CSignal, 是设计用于 EA 代码而命名的信号类 — CExpertSignal

我不会纠缠于它的内容。文章 «MQL5 向导: 如何创建交易信号模块» 包含了信号类方法的详细描述。


2.1 CSignalEquidChannel 信号类

所以, 派生的信号类如下:

//+------------------------------------------------------------------+
//| 类 CSignalEquidChannel                                           |
//| 目的: 交易信号类                                                  |
//|          基于等距通道。                                           |
//| 派生自 CExpertSignal 类。                                         |
//+------------------------------------------------------------------+
class CSignalEquidChannel : public CExpertSignal
  {
protected:
   CiCustom          m_equi_chs;          // 指标对象 "EquidistantChannels"   
   //--- 可调整参数
   int               m_prev_frac_num;     // 之前的分形
   bool              m_to_plot_fracs;     // 显示分形?
   int               m_bars_beside;       // 分形分立左/右的柱线
   int               m_bars_between;      // 过渡柱线
   ENUM_RELEVANT_EXTREMUM m_relevant_pnt; // 相关点
   int               m_line_width;        // 线宽
   bool              m_to_log;            // 保持日志?
   double            m_pnt_in;            // 内部冗余, pips
   double            m_pnt_out;           // 外部冗余, pips
   bool              m_on_start;          // 启动时的信号标志
   //--- 计算
   double            m_base_low_price;    // 基准最低价
   double            m_base_high_price;   // 基准最高价
   double            m_upper_zone[2];     // 上部区域: [0]-内部冗余, [1]-外部  
   double            m_lower_zone[2];     // 下部区域
   datetime          m_last_ch_time;      // 最后一个通道的发生时间
   //--- 行情模型的 "权重" (0-100)
   int               m_pattern_0;         //  "触及通道的下边界 - 买入, 上边界 - 卖出"

   //--- === 方法 === --- 
public:
   //--- 构造器/析构器
   void              CSignalEquidChannel(void);
   void             ~CSignalEquidChannel(void){};
   //--- 可调整参数的赋值方法
   void              PrevFracNum(int _prev_frac_num)   {m_prev_frac_num=_prev_frac_num;}
   void              ToPlotFracs(bool _to_plot)        {m_to_plot_fracs=_to_plot;}
   void              BarsBeside(int _bars_beside)      {m_bars_beside=_bars_beside;}
   void              BarsBetween(int _bars_between)    {m_bars_between=_bars_between;}
   void              RelevantPoint(ENUM_RELEVANT_EXTREMUM _pnt) {m_relevant_pnt=_pnt;}
   void              LineWidth(int _line_wid)          {m_line_width=_line_wid;}
   void              ToLog(bool _to_log)               {m_to_log=_to_log;}
   void              PointsOutside(double _out_pnt)    {m_pnt_out=_out_pnt;}
   void              PointsInside(double _in_pnt)      {m_pnt_in=_in_pnt;}
   void              SignalOnStart(bool _on_start)     {m_on_start=_on_start;}
   //--- 调整行情模型的 "权重" 方法
   void              Pattern_0(int _val) {m_pattern_0=_val;}
   //--- 赋值验证方法
   virtual bool      ValidationSettings(void);
   //--- 创建指标和时间序列的方法
   virtual bool      InitIndicators(CIndicators *indicators);
   //--- 检查行情模型是否已生成的方法
   virtual int       LongCondition(void);
   virtual int       ShortCondition(void);
   virtual double    Direction(void);
   //---
protected:
   //--- 指标初始化方法
   bool              InitCustomIndicator(CIndicators *indicators);
   //- 获取通道上边界值的方法
   double            Upper(int ind) {return(m_equi_chs.GetData(2,ind));}
   //- 获取通道下边界值的方法
   double            Lower(int ind) {return(m_equi_chs.GetData(3,ind));}
   //- 获取通道发生标志的方法
   double            NewChannel(int ind) {return(m_equi_chs.GetData(4,ind));}
  };
//+------------------------------------------------------------------+

有些细微差别需要注意。

这个类中的主要信号发生器是等距通道设置。而且它是当前版本仅有的一个。至此, 根本不会有任何其它的。反之, 这个类包含一个可操纵自定义类型技术指标的 CiCustom

基本模型用作信号模型: "触及通道的下边界 — 买入, 上边界 — 卖出"。由于针尖触摸精度, 所以说, 除了最可能的事件, 它还使用可调边界的缓存区。外部冗余参数 m_pnt_out 确定价格可以超出通道边界多远, 而内部冗余参数 m_pnt_in — 价格能够停在距边界多远。此逻辑十分简单。如果价格已经非常接近, 或者略微超过边界, 则假设价格触及通道边界。图例.2 示意性展示一个缓存区当价格从下面进入, 价格与边界触发模型。

图例.2 触发基本信号模型

图例.2 触发基本信号模型


参数数组 m_upper_zone[2] 是通道的上边界轮廓, 而 m_lower_zone[2] — 下边界。

例中 $1,11552 所在价位作为通道上边界 (红线)。$1,11452 所在价位负责缓存区下边界的下限, 而 $1,11702 — 负责上边界。因此, 外部冗余大小是 150 点, 且内部为 100 点。价格显示为蓝色曲线。

参数 m_on_start 允许忽略机器人在图表上运行后的第一个通道信号, 在此情况下, 一个通道已经绘就。如果该标志被复位, 机器人只在下一个通道工作, 并且不会处理当前交易信号。

参数 m_base_low_pricem_base_high_price 保存实际柱线的最低价和最高价数值。这是考虑到如果在零号柱线的每次报价到来时执行交易, 或是只有新柱线出现时才会在前一根柱线上交易。

现在, 有关方法的几句话。在此应注意的是, 开发者提供了足够广泛的行动自由, 因为大约有一半的方法是虚构的。这意味着子类的行为可以出于必要来实现。

我们从 Direction() 方法开始, 它量化评估潜在的交易方向:

//+------------------------------------------------------------------+
//| 判断 "权重" 方向                                                  |
//+------------------------------------------------------------------+
double CSignalEquidChannel::Direction(void)
  {
   double result=0.;
//--- 新通道外观
   datetime last_bar_time=this.Time(0);
   bool is_new_channel=(this.NewChannel(0)>0.);
//--- 是否忽略第一个通道信号
   if(!m_on_start)
      //--- 初始化期间第一个通道是否如常显示
      if(m_prev_frac_num==3)
        {
         static datetime last_ch_time=0;
         //--- 是否新通道出现
         if(is_new_channel)
           {
            last_ch_time=last_bar_time;
            //--- 是否首次启动
            if(m_last_ch_time==0)
               //--- 当第一个通道出现时保存柱线时间
               m_last_ch_time=last_ch_time;
           }
         //--- 如果时间匹配
         if(m_last_ch_time==last_ch_time)
            return 0.;
         else
         //--- 清除标志
            m_on_start=true;
        }
//--- 实际柱线索引
   int actual_bar_idx=this.StartIndex();
//--- 设置边界
   double upper_vals[2],lower_vals[2]; // [0]-前一根实际柱线, [1]-实际柱线
   ArrayInitialize(upper_vals,0.);
   ArrayInitialize(lower_vals,0.);
   for(int idx=ArraySize(upper_vals)-1,jdx=0;idx>=0;idx--,jdx++)
     {
      upper_vals[jdx]=this.Upper(actual_bar_idx+idx);
      lower_vals[jdx]=this.Lower(actual_bar_idx+idx);
      if((upper_vals[jdx]==0.) || (lower_vals[jdx]==0.))
         return 0.;
     }
//--- 获取价格
   double curr_high_pr,curr_low_pr;
   curr_high_pr=this.High(actual_bar_idx);
   curr_low_pr=this.Low(actual_bar_idx);
//--- 如果得到价格
   if(curr_high_pr!=EMPTY_VALUE)
      if(curr_low_pr!=EMPTY_VALUE)
        {
         //--- 保存价格
         m_base_low_price=curr_low_pr;
         m_base_high_price=curr_high_pr;
         //--- 定义缓存区域价格
         //--- 上边界区域: [0]-内部冗余, [1]-外部 
         this.m_upper_zone[0]=upper_vals[1]-m_pnt_in;
         this.m_upper_zone[1]=upper_vals[1]+m_pnt_out;
         //--- 下边界区域: [0]-内部冗余, [1]-外部 
         this.m_lower_zone[0]=lower_vals[1]+m_pnt_in;
         this.m_lower_zone[1]=lower_vals[1]-m_pnt_out;
         //--- 规范化
         for(int jdx=0;jdx<ArraySize(m_lower_zone);jdx++)
           {
            this.m_lower_zone[jdx]=m_symbol.NormalizePrice(m_lower_zone[jdx]);
            this.m_upper_zone[jdx]=m_symbol.NormalizePrice(m_upper_zone[jdx]);
           }
         //--- 检查区域收敛
         if(this.m_upper_zone[0]<=this.m_lower_zone[0])
            return 0.;
         //--- 结果
         result=m_weight*(this.LongCondition()-this.ShortCondition());
        }
//---
   return result;
  }
//+------------------------------------------------------------------+

方法实体内的 第一块 检查是否有必要忽略图表上的第一个通道, 如果它存在的话。

第二块获取当前价格并确定缓冲区域。这是为检查每个区域的收敛。如果通道太窄或缓冲区域太宽, 则纯数学上可能性存在价格同时进入两个区域。所以, 这种情况应进行处理。

目标线用 蓝色 加亮。在此, 如果有的话, 获取量化评估的交易方向。

现在, 我们研究 LongCondition() 方法。

//+------------------------------------------------------------------+
//| 检查买入条件                                                      |
//+------------------------------------------------------------------+
int CSignalEquidChannel::LongCondition(void)
  {
   int result=0;
//--- 如果设置了最低价格
   if(m_base_low_price>0.)
      //--- 如果最低价格位于下边界
      if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1]))
        {
         if(IS_PATTERN_USAGE(0))
            result=m_pattern_0;
        }
//---
   return result;
  }
//+------------------------------------------------------------------+

对于买入, 检查价格是否进入 下边界 缓存区域。如果是的话, 检查激活行情模型的权限。更多 "IS_PATTERN_USAGE(k)" 类型的结构详情可在 «基于自定义指标的交易信号生成器» 一文里找到。

方法 ShortCondition() 的操作与上述类似。关注点仅在于 上边界 缓存区域。

//+------------------------------------------------------------------+
//| 检查卖出条件                                                      |
//+------------------------------------------------------------------+
int CSignalEquidistantChannel::ShortCondition(void)
  {
   int result=0;
//--- 如果设置了最高价格
   if(m_base_high_price>0.)
      //--- 如果最高价格位于上边界
      if((m_base_high_price>=m_upper_zone[0]) && (m_base_high_price<=m_upper_zone[1]))
        {
         if(IS_PATTERN_USAGE(0))
            result=m_pattern_0;       
        }
//---
   return result;
  }
//+------------------------------------------------------------------+

该类使用 InitCustomIndicator() 方法初始化一个自定义指标:

//+------------------------------------------------------------------+
//| 初始化自定义指标                                                  |
//+------------------------------------------------------------------+
bool CSignalEquidChannel::InitCustomIndicator(CIndicators *indicators)
  {
//--- 添加对象到集合
   if(!indicators.Add(GetPointer(m_equi_chs)))
     {
      PrintFormat(__FUNCTION__+": 添加对象错误");
      return false;
     }
//--- 指定指标参数
   MqlParam parameters[8];
   parameters[0].type=TYPE_STRING;
   parameters[0].string_value="EquidistantChannels.ex5";
   parameters[1].type=TYPE_INT;
   parameters[1].integer_value=m_prev_frac_num;   // 1) 之前分形
   parameters[2].type=TYPE_BOOL;
   parameters[2].integer_value=m_to_plot_fracs;   // 2) 显示分形?
   parameters[3].type=TYPE_INT;
   parameters[3].integer_value=m_bars_beside;     // 3) 分形分立左/右的柱线
   parameters[4].type=TYPE_INT;
   parameters[4].integer_value=m_bars_between;    // 4) 过渡柱线
   parameters[5].type=TYPE_INT;
   parameters[5].integer_value=m_relevant_pnt;    // 5) 相关点
   parameters[6].type=TYPE_INT;
   parameters[6].integer_value=m_line_width;    // 6) 线宽
   parameters[7].type=TYPE_BOOL;
   parameters[7].integer_value=m_to_log;          // 7) 保持日志?

//--- 对象初始化
   if(!m_equi_chs.Create(m_symbol.Name(),_Period,IND_CUSTOM,8,parameters))
     {
      PrintFormat(__FUNCTION__+": 对象初始化错误");
      return false;
     }
//--- 缓存区编号
   if(!m_equi_chs.NumBuffers(5))
      return false;
//--- ok
   return true;
  }
//+------------------------------------------------------------------+

参数数组中的第一个值应为指标名字的字符串。

该类还应包含一个虚构 ValidationSettings() 方法。它调用祖辈的类似方法, 并检查通道指标的参数是否设置正确。还有获取自定义指标相应缓冲器数值的服务方法。

至此, 这就是与派生的信号类有关的一切。


2.2 CEquidChannelExpert 交易策略的类

基本理念的实现将需要编写一个派生自 CExpert 标准类的类。在当前步骤, 代码要尽可能地紧凑, 因为, 事实上, 它只需要修改主处理器的行为 — Processing() 方法。它是虚构的, 赋予了编写任何策略的机会。

//+------------------------------------------------------------------+
//| 类 CEquidChannelExpert。                                         |
//| 目的: 基于等距通道交易的 EA 类。                                   |
//| 派生自 CExper 类。                                                |
//+------------------------------------------------------------------+
class CEquidChannelExpert : public CExpert
  {
   //--- === 数据成员 === --- 
private:


   //--- === 方法 === --- 
public:
   //--- 构造器/析构器
   void              CEquidChannelExpert(void){};
   void             ~CEquidChannelExpert(void){};

protected:
   virtual bool      Processing(void);
  };
//+------------------------------------------------------------------+

此处是方法本身。

//+------------------------------------------------------------------+
//| 主模块                                                           |
//+------------------------------------------------------------------+
bool CEquidChannelExpert::Processing(void)
  {
//--- 计算方向
   m_signal.SetDirection();
//--- 检查是否开仓
   if(!this.SelectPosition())
     {
      //--- 开仓模块
      if(this.CheckOpen())
         return true;
     }
//--- 是否没有交易操作
   return false;
  }

所有的一切都十分简单。首先, 交易信号评估可能的交易方向, 之后检查存在的仓位。如果无持仓, 看来这是一个开仓的机会。如果有持仓, 则离开。

基本策略的代码在 BaseChannelsTrader.mq5 文件里实现。



基本策略的操作示例在视频里呈现。


图例.3 基本策略在 2013-2015 间的结果。

图例.3 基本策略在 2013-2015 间的结果。

结果是在策略测试器中针对 EURUSD 品种的小时周期运行的。在余额图上可以看到, 在确定的间隔内基本策略的操作依照 "锯齿理论": 一笔无利交易随后跟一笔盈利交易。在测试时, 自定义参数值在 base_signal.set 文件里提供。它还包含通道参数, 其值将在所有版本的策略里保持不变。

此处及以下, 使用 "基于真实分时的每笔分时" 测试模式。

实质上, 有 2 种方式来提高策略的交易性能。首先是优化, 在于选择最大化盈利的参数值组合, 等等。其次的方法是关注发掘影响 EA 性能的因素。如果第一种方法与交易策略的逻辑变化无关联, 则第二种不可或缺。

在下一节中, 基本策略会为了追求性能因素而编辑。


3. 性能因素

关于配置的几句话。策略的所有文件放在 project 下的单一文件夹里也许将是便利的。所以, 基本策略的实现位于 Base 子目录 (图例.4) 等。

图例.4 通道策略的项目文件夹层次示例

图例.4 通道策略的项目文件夹层次示例

此外, 假设每一个新的因素都是新的阶段, 来修改构成 EA 代码的源文件。

3.1使用尾随

开始之前, 建议添加尾随功能到策略中。作为 CTrailingFixedPips 类的一个对象, 可以 在固定 "距离" (单位点数) 上维护已开仓位。这可既可以是止损价位, 也可以是止盈价位。若要禁用尾随止盈, 在相应参数里设置零值 (InpProfitLevelPips)。

在代码中进行以下更改:

在智能程序 ChannelsTrader1.mq5 文件里添加一组自定义参数:

//---
sinput string Info_trailing="+===-- 尾随 --====+"; // +===-- 尾随 --====+
input int InpStopLevelPips=30;          // 止损价位, pips
input int InpProfitLevelPips=50;        // 止盈价位, pips

在初始化模块, 编写创建 CTrailingFixedPips 类型的对象, 在策略里包含它并设置尾随参数。

//--- 尾随对象
   CTrailingFixedPips *trailing=new CTrailingFixedPips;
   if(trailing==NULL)
     {
      //---错误
      printf(__FUNCTION__+": 创建尾随错误");
      myChannelExpert.Deinit();
      return(INIT_FAILED);
     }
//--- 添加尾随对象
   if(!myChannelExpert.InitTrailing(trailing))
     {
      //---错误
      PrintFormat(__FUNCTION__+": 尾随初始错误");
      myChannelExpert.Deinit();
      return INIT_FAILED;
     }
//--- 尾随参数
   trailing.StopLevel(InpStopLevelPips);
   trailing.ProfitLevel(InpProfitLevelPips);

由于将使用尾随, 也有必要在 EquidistantChannelExpert1.mqh 文件里修改主要的 CEquidChannelExpert::Processing() 方法。

//+------------------------------------------------------------------+
//| 主模块                                                           |
//+------------------------------------------------------------------+
bool CEquidChannelExpert::Processing(void)
  {
//--- 计算方向
   m_signal.SetDirection();
//--- 如果无持仓
   if(!this.SelectPosition())
     {
      //--- 开仓模块
      if(this.CheckOpen())
         return true;
     }
//--- 如果存在仓位
   else
     {
      //--- 价差仓位修改可行性
      if(this.CheckTrailingStop())
         return true;
     }
//--- 是否没有交易操作
   return false;
  }

就是这样。尾随已被添加。更新的策略文件均位于一个单独的 ChannelsTrader1 子文件夹。

我们来检查创新是否对有效性产生任何影响。

因此, 在策略测试器的优化模式下, 以相同历史间隔和参数值, 多次运行基本策略。止损和止盈参数已调整:

 变量 开始步进
停止
止损价位, pips
0
10
100
止盈价位, pips
0
10
150

优化结果可以在 ReportOptimizer-signal1.xml 文件里找到。图例 Fig.5 里示意最佳运行结果, 止损价位 = 0, 以及止盈价位 = 150。

图例.5 使用尾随后策略在 2013-2015 间的结果。

图例.5 使用尾随后策略在 2013-2015 间的结果。

很容易就可注意到最后一张插图类似于图例 3。因此可以说, 在此数值范围内使用尾随并未改善结果。


3.2通道类型

有一个假设, 通道类型影响性能结果。一般的看法是这样的: 最好在下降通道时卖出, 并在升势中买入。如果通道横盘 (无倾向), 则可依据双边界交易。

枚举 ENUM_CHANNEL_TYPE 定义通道类型:

//+------------------------------------------------------------------+
//| 通道类型                                                          |
//+------------------------------------------------------------------+
enum ENUM_CHANNEL_TYPE
  {
   CHANNEL_TYPE_ASCENDING=0,  // 升势
   CHANNEL_TYPE_DESCENDING=1, // 降势
   CHANNEL_TYPE_FLAT=2,       // 横盘
  };
//+------------------------------------------------------------------+

在 EA 源文件 ChannelsTrader2.mq5 的初始化模块里 定义搜索通道类型的容易参数。

//--- 过滤参数
   filter0.PointsInside(_Point*InpPipsInside);
   filter0.PointsOutside(_Point*InpPipsOutside);
   filter0.TypeTolerance(_Point*InpTypePips);
   filter0.PrevFracNum(InpPrevFracNum);
   ...

此参数控制价格点数的变化速度。假设它等于 7 个点。之后, 如果通道的每根柱线 "成长" 6 个点, 这不足以认为是升势。然后, 它将被简单地认定为横盘 (无倾向)。

在信号源文件 SignalEquidChannel2.mqh 的 Direction() 方法里添加通道类型的标识。

//--- 如果通道是新的
   if(is_new_channel)
     {
      m_ch_type=CHANNEL_TYPE_FLAT;                // 行盘 (无倾向) 通道
      //--- 如果类型的冗余已设置
      if(m_ch_type_tol!=EMPTY_VALUE)
        {
         //--- 通道类型
         //--- 变化速度
         double pr_speed_pnt=m_symbol.NormalizePrice(upper_vals[1]-upper_vals[0]);
         //--- 如果速度足够
         if(MathAbs(pr_speed_pnt)>m_ch_type_tol)
           {
            if(pr_speed_pnt>0.)
               m_ch_type=CHANNEL_TYPE_ASCENDING;  // 升势通道
            else
               m_ch_type=CHANNEL_TYPE_DESCENDING; // 降势通道
           }
        }
     }

最初, 通道被认为是横盘 - 即不上升也不下降。如果标识通道类型的 冗余参数 未设置数值, 那么它不可确定 变化速度

 买入条件将会包括非降势通道 检查

//+------------------------------------------------------------------+
//| 检查买入条件                                                      |
//+------------------------------------------------------------------+
int CSignalEquidChannel::LongCondition(void)
  {
   int result=0;
//--- 如果设置了最低价格
   if(m_base_low_price>0.)
      //--- 如果通道非降势
      if(m_ch_type!=CHANNEL_TYPE_DESCENDING)
         //--- 如果最低价格位于下边界
         if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1]))
           {
            if(IS_PATTERN_USAGE(0))
               result=m_pattern_0;
           }
//---
   return result;
  }
//+------------------------------------------------------------------+

在卖出条件里也有类似的非升势通道检查。

在 EquidistantChannelExpert2.mqh 文件中的主要 CEquidChannelExpert::Processing() 方法与基本版本中的相同, 因为尾随已被排除。

检查这个因素的有效性。只有一个参数要优化。

 变量 开始步进
停止
类型冗余, pips
0
5
150

优化结果可以在 ReportOptimizer-signal2.xml 文件里找到。最佳运行结果示于图例.6。

图例.6 使用通道类型后策略在 2013-2015 间的结果。

图例.6 使用通道类型后策略在 2013-2015 间的结果。


很容易就注意到, 策略测试结果稍好于基本策略的结果。事实证明, 给定参数的基准值, 像通道类型般的过滤器会影响最终结果。 


3.3通道宽度

似乎通道宽度可以影响策略类型本身。如果通道变窄, 那么当突破它的边界时, 将可能继续朝突破方向交易, 而非反向。这是突破策略的结果。如果通道变宽, 可以基于其边界交易。这是反弹策略。当前的策略则是 — 基于通道边界执行交易。

显然, 在此需要一条判断通道是窄或宽的准则。为了不走极端, 建议介于两者之间增加冗余, 考虑如何在分析通道时既不窄, 也不宽。作为结果, 需要 2 条准则:

  1. 狭窄通道的足够宽度;
  2. 宽阔通道的足够宽度。

如果通道与两者均不符, 那么克制入场就是明智选择。

图例.7 通道宽度, 图解

图例.7 通道宽度, 图解

应当注意的是, 判断通道宽度时的几何问题。由于图表数轴存在不同计量数值。因此, 测量 AB 和 CD 线段的长度很容易。但计算 CE 段 (图例.7) 就有问题了。

所选最简单的方法进行规范化, 虽然也许有争议, 且不是最准确的。公式如下:

CE 长度 ≃ CD 长度 / (1.0 + 通道速度)

通道宽度使用 ENUM_CHANNEL_WIDTH_TYPE 枚举度量:

//+------------------------------------------------------------------+
//| 通道宽度                                                          |
//+------------------------------------------------------------------+
enum ENUM_CHANNEL_WIDTH_TYPE
  {
   CHANNEL_WIDTH_NARROW=0,   // 窄
   CHANNEL_WIDTH_MID=1,      // 平均
   CHANNEL_WIDTH_BROAD=2,    // 宽
  };

在智能程序 ChannelsTrader3.mq5 源文件的 "Channels" 自定义参数组里加入通道宽度准则。

//---
sinput string Info_channels="+===-- 通道 --====+"; // +===-- 通道 --====+
input int InpPipsInside=100;            // 内部冗余, pips
input int InpPipsOutside=150;           // 外部冗余, pips
input int InpNarrowPips=250;            // 窄通道, pips
input int InpBroadPips=1200;            // 宽通道, pips
...

如果窄通道的标准比宽通道的数值更大, 将产生初始化错误。

//--- 过滤参数
   filter0.PointsInside(_Point*InpPipsInside);
   filter0.PointsOutside(_Point*InpPipsOutside);
   if(InpNarrowPips>=InpBroadPips)
     {
      PrintFormat(__FUNCTION__+": 指定的窄通道和边界数值错误");
      return INIT_FAILED;
     }
   filter0.NarrowTolerance(_Point*InpNarrowPips);
   filter0.BroadTolerance(_Point*InpBroadPips);

通道宽度等级的判断时刻位于 Direction() 方法实体之中。

//--- 通道宽度 
   m_ch_width=CHANNEL_WIDTH_MID;               // 平均
   double ch_width_pnt=((upper_vals[1]-lower_vals[1])/(1.0+pr_speed_pnt));
//--- 如果指定了窄通道准则
   if(m_ch_narrow_tol!=EMPTY_VALUE)
      if(ch_width_pnt<=m_ch_narrow_tol)
         m_ch_width=CHANNEL_WIDTH_NARROW;      // 窄      
//--- 如果指定了宽通道准则
   if(m_ch_narrow_tol!=EMPTY_VALUE)
      if(ch_width_pnt>=m_ch_broad_tol)
         m_ch_width=CHANNEL_WIDTH_BROAD;       // 宽 

起初, 考虑通道是平均的。在此之后, 检查它是窄或宽。

也有必要修改用来确定交易方向的方法。因此, 对于买入条件, 看上去如下方式:

//+------------------------------------------------------------------+
//| 检查买入条件                                                      |
//+------------------------------------------------------------------+
int CSignalEquidChannel::LongCondition(void)
  {
   int result=0;
//--- 如果通道较窄 - 突破上边界时交易
   if(m_ch_width==CHANNEL_WIDTH_NARROW)
     {
      //--- 如果设置了最高价格
      if(m_base_high_price>0.)
         //--- 如果最高价格位于上边界
         if(m_base_high_price>=m_upper_zone[1])
           {
            if(IS_PATTERN_USAGE(0))
               result=m_pattern_0;
           }
     }
//--- 或者通道较宽 - 在下边界反弹时交易
   else if(m_ch_width==CHANNEL_WIDTH_BROAD)
     {
      //--- 如果设置了最低价格
      if(m_base_low_price>0.)
         //--- 如果最低价格位于下边界
         if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1]))
           {
            if(IS_PATTERN_USAGE(0))
               result=m_pattern_0;
           }
     }
//---
   return result;
  }
//+------------------------------------------------------------------+

该方法由两大块组成。第一块 检查窄通道内的突破交易机会。注意, 在当前变形中, 价格到达上边界缓存区域的顶部时才会认定为突破。第二块 检查价格是否已经进入下边界缓存区域, 以便反弹策略发挥作用。

检查卖出机会的方法 — ShortCondition() — 通过类比创建。

在 EquidistantChannelExpert3.mqh 文件里的主要方法 CEquidChannelExpert::Processing() 保留不变。

有 2 个参数要进行优化。

 变量 开始步进
停止
窄通道, pips
100
20
250
宽通道, pips
350
50
1250

优化结果可以在 ReportOptimizer-signal3.xml 文件里找到。最佳运行结果示于图例.8。

图例.8 考虑通道宽度后策略在 2013-2015 间的结果。

图例.8 考虑通道宽度后策略 在 2013-2015 间的结果。


也许, 这是以上所有描述的因素中影响最大的。余额曲线现在有一个更明显的方向。


3.4边际线止损和止盈价位

如果交易目标原本由止损和止盈的形式表现, 那么现在有能力依据当前策略条件进行调整。简言之, 如果一个通道在图表上经动态调整形成确定的角度, 止损和止盈价位应与通道边界联动。

出于便利添加了一对模型。现在它们看起来像这样:

//--- 行情模型的 "权重" (0-100)
   int               m_pattern_0;         //  "自通道边界反弹" 模式
   int               m_pattern_1;         //  "通道边界突破" 模式
   int               m_pattern_2;         //  "新通道" 模式

以前的版本只有一个, 且它负责检查价格触及通道的任何边界。现在, 反弹和突破模式将有所区别。现在还有第三种模式 — 新通道模式。当出现新通道, 且在前一个通道时已有开仓的情况下, 需要它。如果模式被触发, 则平仓。

买入条件, 看上去如下方式:

//+------------------------------------------------------------------+
//| 检查买入条件                                                      |
//+------------------------------------------------------------------+
int CSignalEquidChannel::LongCondition(void)
  {
   int result=0;
   bool is_position=PositionSelect(m_symbol.Name());
//--- 如果通道较窄 - 突破上边界时交易
   if(m_ch_width_type==CHANNEL_WIDTH_NARROW)
     {
      //--- 如果设置了最高价格
      if(m_base_high_price>0.)
         //--- 如果最高价格位于上边界
         if(m_base_high_price>=m_upper_zone[1])
           {
            if(IS_PATTERN_USAGE(1))
              {
               result=m_pattern_1;
               //--- 如果无持仓
               if(!is_position)
                  //--- 输出到日志
                  if(m_to_log)
                    {
                     Print("\触发 \"通道边界突破\" 买入模式。");
                     PrintFormat("最高价: %0."+IntegerToString(m_symbol.Digits())+"f",m_base_high_price);
                     PrintFormat("触发价格: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[1]);
                    }
              }
           }
     }
//--- 或者如果通道较宽或是平均 - 在下边界反弹时交易
   else
     {
      //--- 如果设置了最低价格
      if(m_base_low_price>0.)
         //--- 如果最低价格位于下边界
         if((m_base_low_price<=m_lower_zone[0]) && (m_base_low_price>=m_lower_zone[1]))
           {
            if(IS_PATTERN_USAGE(0))
              {
               result=m_pattern_0;
               //--- 如果无持仓
               if(!is_position)
                  //--- 输出到日志
                  if(m_to_log)
                    {
                     Print("\n触发 \"通道边界反弹\" 买入模式。");
                     PrintFormat("最低价: %0."+IntegerToString(m_symbol.Digits())+"f",m_base_low_price);
                     PrintFormat("区域上边际: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[0]);
                     PrintFormat("区域下边际: %0."+IntegerToString(m_symbol.Digits())+"f",m_upper_zone[1]);
                    }
              }
           }
     }
//---
   return result;
  }
//+------------------------------------------------------------------+

现在检查卖出条件:

//+------------------------------------------------------------------+
//| 检查平多头仓位条件                                                |
//+------------------------------------------------------------------+
bool CSignalEquidChannel::CheckCloseLong(double &price) const
  {
   bool to_close_long=true;
   int result=0;
   if(IS_PATTERN_USAGE(2))
      result=m_pattern_2;
   if(result>=m_threshold_close)
     {
      if(m_is_new_channel)
         //--- 如果多仓已平
         if(to_close_long)
           {
            price=NormalizeDouble(m_symbol.Bid(),m_symbol.Digits());
            //--- 输出到日志
            if(m_to_log)
              {
               Print("\触发 \"新通道\" 平多仓模式。");
               PrintFormat("平仓价格: %0."+IntegerToString(m_symbol.Digits())+"f",price);
              }
           }
     }
//---
   return to_close_long;
  }
//+------------------------------------------------------------------+

对于空头仓位, 平仓条件是相同的。


现在, 关于尾随的几句话。为其编写了单独的 CTrailingEquidChannel 类, 其父类是 CExpertTrailing 类。

//+------------------------------------------------------------------+
//| 类 CTrailingEquidChannel。                                       |
//| 目的: 基于等距通道尾随停止类。                                     |
//|       派生自类 CExpertTrailing。                                  |
//+------------------------------------------------------------------+
class CTrailingEquidChannel : public CExpertTrailing
  {
protected:
   double            m_sl_distance;       // 止损距离
   double            m_tp_distance;       // 止盈距离
   double            m_upper_val;         // 上边界
   double            m_lower_val;         // 下边界
   ENUM_CHANNEL_WIDTH_TYPE m_ch_wid_type; // 通道宽度类型
   //---
public:
   void              CTrailingEquidChannel(void);
   void             ~CTrailingEquidChannel(void){};
   //--- 保护数据的初始化方法
   void              SetTradeLevels(double _sl_distance,double _tp_distance);
   //---
   virtual bool      CheckTrailingStopLong(CPositionInfo *position,double &sl,double &tp);
   virtual bool      CheckTrailingStopShort(CPositionInfo *position,double &sl,double &tp);
   //---
   bool              RefreshData(const CSignalEquidChannel *_ptr_ch_signal);
  };
//+------------------------------------------------------------------+

从通道信号获取信息的方法已 红色 加亮。

检查多、空仓位尾随可能性的方法已采用多态重定义 - OOP 的基本原理。

为了让尾随类能够接收实际通道的时间和价格目标, 有必要与 CSignalEquidChannel 信号类创建绑定。它已在 CEquidChannelExpert 类中以常量指针方式实现。这种方法可以获取来自信号的所有必要信息, 无需冒危险去改变信号参数本身。

//+------------------------------------------------------------------+
//| 类 CEquidChannelExpert。                                         |
//| 目的: 基于等距通道交易的 EA 类。                                   |
//| 派生自 CExper 类。                                                |
//+------------------------------------------------------------------+
class CEquidChannelExpert : public CExpert
  {
   //--- === 数据成员 === --- 
private:
   const CSignalEquidChannel *m_ptr_ch_signal;

   //--- === 方法 === --- 
public:
   //--- 构造器/析构器
   void              CEquidChannelExpert(void);
   void             ~CEquidChannelExpert(void);
   //--- 指向通道信号对象
   void              EquidChannelSignal(const CSignalEquidChannel *_ptr_ch_signal){m_ptr_ch_signal=_ptr_ch_signal;};
   const CSignalEquidChannel *EquidChannelSignal(void) const {return m_ptr_ch_signal;};

protected:
   virtual bool      Processing(void);
   //--- 交易平仓检查
   virtual bool      CheckClose(void);
   virtual bool      CheckCloseLong(void);
   virtual bool      CheckCloseShort(void);
   //--- 尾随停止检查
   virtual bool      CheckTrailingStop(void);
   virtual bool      CheckTrailingStopLong(void);
   virtual bool      CheckTrailingStopShort(void);
  };
//+------------------------------------------------------------------+

负责平仓和尾随的方法已在智能程序里重定义。

在 EquidistantChannelExpert4.mqh 文件里的主要方法 CEquidChannelExpert::Processing() 如下所见:

//+------------------------------------------------------------------+
//| 主模块                                                           |
//+------------------------------------------------------------------+
bool CEquidChannelExpert::Processing(void)
  {
//--- 计算方向
   m_signal.SetDirection();
//--- 如果无持仓
   if(!this.SelectPosition())
     {
      //--- 开仓模块
      if(this.CheckOpen())
         return true;
     }
//--- 如果存在仓位
   else
     {
      if(!this.CheckClose())
        {
         //--- 价差仓位修改可行性
         if(this.CheckTrailingStop())
            return true;
         //---
         return false;
        }
      else
        {
         return true;
        }
     }
//--- 是否没有交易操作
   return false;
  }
//+------------------------------------------------------------------+

这些参数将被优化:
 变量 开始步进
停止
止损, points
25
5
75
止盈, points50
5
200

优化结果可以在 ReportOptimizer-signal4.xml 文件里找到。最佳运行结果示于图例.9。

图例.9 考虑通道边际线级别后策略在 2013-2015 间的结果。

图例.9 考虑通道边际线级别后策略在 2013-2015 间的结果。


很明显, 这个因素 — 边际线价位 — 不能提升性能。


结论

本文表述的过程, 是开发和实现基于移动通道发送信号的类。每个信号版本均随带交易策略和测试结果。

应强调的是, 使用固定值设置等距通道已贯穿整篇文章。因此, 哪个因素是否有效的结论仅对于指定的值为真。

依然有其它的方法来改善性能结果。本文介绍的是寻找这种可能性的工作部分。