MQL5 酷宝书 - 移动通道交易信号·进阶篇
📈

MQL5 酷宝书 - 移动通道交易信号·进阶篇

(2/3)·通道画得出却测不了?这篇把图形对象换成指标缓冲,让信号能进优化器

含代码示例实战向 第 2/3 篇
很多人把移动通道画在图表上就以为策略完成了,结果一进优化器信号全变零。测试模式下图形对象根本不渲染,靠对象属性读价等于空跑。本篇换条路:用指标缓冲区承载通道,信号才进得了回测。

「等距通道信号的初始化与方向判定」

在自定义信号类里,通道上下边界和新建标志都走成员方法取数:Upper 读缓冲区索引 2,Lower 读 3,NewChannel 读 4,分别对应等距通道指标的边界与事件位。 Direction 方法先处理首个通道的过滤逻辑。若 m_on_start 为 false 且历史分形数 m_prev_frac_num 等于 3,会比对 m_last_ch_time 与当前柱时间,相同则返回 0 屏蔽首根信号,避免 EA 加载瞬间误触发。 实际计算时取 StartIndex 作为基准,用长度为 2 的数组装前一根与当前实际柱的上下边界。任一边界为 0 直接 return 0,说明通道尚未在对应柱上成型。 下面这段是核心取数结构,复制进 MT5 的 CSignalEquidChannel 类即可验证边界读取是否正常。

MQL5 / C++
class="kw">protected:
  class="type">bool              InitCustomIndicator(CIndicators *indicators);
  class="type">class="kw">double            Upper(class="type">int ind) {class="kw">return(m_equi_chs.GetData(class="num">2,ind));}
  class="type">class="kw">double            Lower(class="type">int ind) {class="kw">return(m_equi_chs.GetData(class="num">3,ind));}
  class="type">class="kw">double            NewChannel(class="type">int ind) {class="kw">return(m_equi_chs.GetData(class="num">4,ind));}
  };
class="type">class="kw">double CSignalEquidChannel::Direction(class="type">void)
  {
  class="type">class="kw">double result=class="num">0.;
  class="type">class="kw">datetime last_bar_time=this.Time(class="num">0);
  class="type">bool is_new_channel=(this.NewChannel(class="num">0)>class="num">0.);
  if(!m_on_start)
    if(m_prev_frac_num==class="num">3)
      {
      class="kw">static class="type">class="kw">datetime last_ch_time=class="num">0;
      if(is_new_channel)
        {
        last_ch_time=last_bar_time;
        if(m_last_ch_time==class="num">0)
          m_last_ch_time=last_ch_time;
        }
      if(m_last_ch_time==last_bar_time)
        class="kw">return class="num">0.;
      else
        m_on_start=true;
      }
  class="type">int actual_bar_idx=this.StartIndex();
  class="type">class="kw">double upper_vals[class="num">2],lower_vals[class="num">2];
  ArrayInitialize(upper_vals,class="num">0.);
  ArrayInitialize(lower_vals,class="num">0.);
  for(class="type">int idx=ArraySize(upper_vals)-class="num">1,jdx=class="num">0;idx>=class="num">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]==class="num">0.) || (lower_vals[jdx]==class="num">0.))
      class="kw">return class="num">0.;
    }
  class="type">class="kw">double curr_high_pr,curr_low_pr;

◍ 等距通道里的边界命中与多空权重

等距通道信号类的核心,是先抓当前 K 线的高低价,再据此把上下边界的冗余区算出来。下面这段逻辑里,上边界存进 m_upper_zone[0](内部冗余)和 m_upper_zone[1](外部),下边界反之,全部走 NormalizePrice 对齐报价精度。 边界算完有一道硬校验:若 m_upper_zone[0] <= m_lower_zone[0],说明上下内部冗余区已经交叠,函数直接 return 0.,不再给信号。这是通道收敛到极致时的熔断,实盘里黄金 5 分钟图常在第 20~30 根通道线后出现。 多空判定分两个独立方法。LongCondition 只在 m_base_low_price>0 且最低价落进下边界区间 [m_lower_zone[1], m_lower_zone[0]] 时才返回 m_pattern_0;ShortCondition 镜像处理上边界。最终信号值 = m_weight*(LongCondition()-ShortCondition()),权重把两边强度拉成净方向。 别把冗余区当摆设:m_pnt_in 和 m_pnt_out 若设成 0,边界就是一根线,毛刺触发会陡增;EURUSD 上把 m_pnt_out 设 2 倍点差往往更稳。

MQL5 / C++
  curr_high_pr=this.High(actual_bar_idx);
  curr_low_pr=this.Low(actual_bar_idx);
class=class="str">"cmt">//--- 如果得到价格
  if(curr_high_pr!=EMPTY_VALUE)
    if(curr_low_pr!=EMPTY_VALUE)
      {
      class=class="str">"cmt">//--- 保存价格
      m_base_low_price=curr_low_pr;
      m_base_high_price=curr_high_pr;
      class=class="str">"cmt">//--- 定义缓存区域价格
      class=class="str">"cmt">//--- 上边界区域: [class="num">0]-内部冗余, [class="num">1]-外部 
      this.m_upper_zone[class="num">0]=upper_vals[class="num">1]-m_pnt_in;
      this.m_upper_zone[class="num">1]=upper_vals[class="num">1]+m_pnt_out;
      class=class="str">"cmt">//--- 下边界区域: [class="num">0]-内部冗余, [class="num">1]-外部 
      this.m_lower_zone[class="num">0]=lower_vals[class="num">1]+m_pnt_in;
      this.m_lower_zone[class="num">1]=lower_vals[class="num">1]-m_pnt_out;
      class=class="str">"cmt">//--- 规范化
      for(class="type">int jdx=class="num">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]);
        }
      class=class="str">"cmt">//--- 检查区域收敛
      if(this.m_upper_zone[class="num">0]<=this.m_lower_zone[class="num">0])
        class="kw">return class="num">0.;
      class=class="str">"cmt">//--- 结果
      result=m_weight*(this.LongCondition()-this.ShortCondition());
      }
class=class="str">"cmt">//---
  class="kw">return result;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查买入条件                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalEquidChannel::LongCondition(class="type">void)
  {
  class="type">int result=class="num">0;
class=class="str">"cmt">//--- 如果设置了最低价格
  if(m_base_low_price>class="num">0.)
    class=class="str">"cmt">//--- 如果最低价格位于下边界
    if((m_base_low_price<=m_lower_zone[class="num">0]) && (m_base_low_price>=m_lower_zone[class="num">1]))
      {
      if(IS_PATTERN_USAGE(class="num">0))
        result=m_pattern_0;
      }
class=class="str">"cmt">//---
  class="kw">return result;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检查卖出条件                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalEquidistantChannel::ShortCondition(class="type">void)
  {

等距通道信号的判定与指标挂载

信号类里对最高价的落点做了硬约束:只有当 m_base_high_price 大于 0,且落在 m_upper_zone[0] 到 m_upper_zone[1] 的区间内,才会进一步看模式开关 IS_PATTERN_USAGE(0) 是否成立,成立则返回 m_pattern_0,否则 result 保持初始的 0。 InitCustomIndicator 负责把 EquidistantChannels.ex5 挂进指标集合。参数数组固定长度为 8,依次传入之前分形数、是否画分形、分形左右分立柱线数、过渡柱线数、相关点、线宽、是否写日志——改 m_prev_frac_num 或 m_bars_between 会直接改变通道对历史分形的捕捉密度。 创建时用 m_symbol.Name() 和 _Period 绑定品种与周期,缓冲区的 NumBuffers 写死为 5;若 Add 或 Create 任一步返回失败,函数直接 false 并 PrintFormat 报错,EA 初始化会中断。外汇与贵金属杠杆高,这类自定义指标加载失败常导致实盘 EA 不开单,上线前务必在策略测试器跑一次初始化日志。

MQL5 / C++
class="type">int result=class="num">0;
class=class="str">"cmt">//--- 如果设置了最高价格
if(m_base_high_price>class="num">0.)
   class=class="str">"cmt">//--- 如果最高价格位于上边界
   if((m_base_high_price>=m_upper_zone[class="num">0]) && (m_base_high_price<=m_upper_zone[class="num">1]))
      {
      if(IS_PATTERN_USAGE(class="num">0))
         result=m_pattern_0;      
      }
class=class="str">"cmt">//---
class="kw">return result;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 初始化自定义指标                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSignalEquidChannel::InitCustomIndicator(CIndicators *indicators)
  {
class=class="str">"cmt">//--- 添加对象到集合
   if(!indicators.Add(GetPointer(m_equi_chs)))
     {
      PrintFormat(__FUNCTION__+": 添加对象错误");
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- 指定指标参数
   class="type">MqlParam parameters[class="num">8];
   parameters[class="num">0].type=TYPE_STRING;
   parameters[class="num">0].string_value="EquidistantChannels.ex5";
   parameters[class="num">1].type=TYPE_INT;
   parameters[class="num">1].integer_value=m_prev_frac_num;    class=class="str">"cmt">// class="num">1) 之前分形
   parameters[class="num">2].type=TYPE_BOOL;
   parameters[class="num">2].integer_value=m_to_plot_fracs;    class=class="str">"cmt">// class="num">2) 显示分形?
   parameters[class="num">3].type=TYPE_INT;
   parameters[class="num">3].integer_value=m_bars_beside;      class=class="str">"cmt">// class="num">3) 分形分立左/右的柱线
   parameters[class="num">4].type=TYPE_INT;
   parameters[class="num">4].integer_value=m_bars_between;     class=class="str">"cmt">// class="num">4) 过渡柱线
   parameters[class="num">5].type=TYPE_INT;
   parameters[class="num">5].integer_value=m_relevant_pnt;     class=class="str">"cmt">// class="num">5) 相关点
   parameters[class="num">6].type=TYPE_INT;
   parameters[class="num">6].integer_value=m_line_width;       class=class="str">"cmt">// class="num">6) 线宽
   parameters[class="num">7].type=TYPE_BOOL;
   parameters[class="num">7].integer_value=m_to_log;           class=class="str">"cmt">// class="num">7) 保持日志?
class=class="str">"cmt">//--- 对象初始化
   if(!m_equi_chs.Create(m_symbol.Name(),_Period,IND_CUSTOM,class="num">8,parameters))
     {
      PrintFormat(__FUNCTION__+": 对象初始化错误");
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- 缓存区编号
   if(!m_equi_chs.NumBuffers(class="num">5))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- ok
   class="kw">return true;
  }

「等距通道EA的骨架与开仓判定」

在 MT5 里把标准类库里的 CExpert 继承一层,就能快速搭出自己的策略壳。下面这段类声明只留了最小可用接口:构造析构为空实现,保护方法里挂了一个虚函数 Processing,专门留给具体逻辑重写。 [CODE] class CEquidChannelExpert : public CExpert { //--- === 数据成员 === --- private: //--- === 方法 === --- public: //--- 构造器/析构器 void CEquidChannelExpert(void){}; void ~CEquidChannelExpert(void){}; protected: virtual bool Processing(void); }; [/CODE] Processing 才是每根 K 线真正跑的逻辑。它先调用 m_signal.SetDirection() 刷新信号方向,再用 SelectPosition 看当前是否已有持仓;若无持仓就进 CheckOpen 尝试开仓,命中即返回 true,否则返回 false。 [CODE] bool CEquidChannelExpert::Processing(void) { //--- 计算方向 m_signal.SetDirection(); //--- 检查是否开仓 if(!this.SelectPosition()) { //--- 开仓模块 if(this.CheckOpen()) return true; } //--- 是否没有交易操作 return false; } [/CODE] 开 MT5 把这段塞进 Experts 文件夹编译,你会发现 EA 默认不交易——因为 CheckOpen 还没写具体触发条件。外汇与贵金属杠杆高,实盘前务必在策略测试器用历史数据验证信号方向计算是否如预期。

MQL5 / C++
class CEquidChannelExpert : class="kw">public CExpert
  {
   class=class="str">"cmt">//--- === 数据成员 === --- 
class="kw">private:
   class=class="str">"cmt">//--- === 方法 === --- 
class="kw">public:
   class=class="str">"cmt">//--- 构造器/析构器
   class="type">void                CEquidChannelExpert(class="type">void){};
   class="type">void               ~CEquidChannelExpert(class="type">void){};
class="kw">protected:
   class="kw">virtual class="type">bool       Processing(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 主模块                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CEquidChannelExpert::Processing(class="type">void)
  {
class=class="str">"cmt">//--- 计算方向
   m_signal.SetDirection();
class=class="str">"cmt">//--- 检查是否开仓
   if(!this.SelectPosition())
     {
      class=class="str">"cmt">//--- 开仓模块
      if(this.CheckOpen())
         class="kw">return true;
     }
class=class="str">"cmt">//--- 是否没有交易操作
   class="kw">return class="kw">false;
  }

◍ 尾随与通道特征对策略有效性的实测影响

把策略文件收进单一 project 子目录后,第一个可验证的改动是给 EA 挂上固定点数尾随。CTrailingFixedPips 对象能在止损或止盈侧按输入点数维护仓位;把 InpProfitLevelPips 设 0 即关掉尾随止盈。 在 ChannelsTrader1.mq5 里加参数并在初始化时 new 出尾随对象、绑定到专家类,Processing() 方法同步改完即可。用 2013–2015 历史、止损 0–100 步进 10、止盈 0–150 步进 10 做优化,最佳跑出来是止损 0 / 止盈 150,曲线和基准版几乎重合——尾随在这组数值内未改善结果。 通道类型过滤是第二个因子。用 ENUM_CHANNEL_TYPE 区分升、降、横盘,冗余参数默认 7 点:每根柱线通道成长不足 6 点就判为横盘。只优化类型冗余 0–150 步进 5,结果略好于基准,说明通道方向过滤器在给定参数下确实动了最终结果。 通道宽度的影响最明显。用 CE ≃ CD / (1.0 + 通道速度) 做规范化避开坐标轴量纲差,设窄通道 100–250 步进 20、宽通道 350–1250 步进 50 两条准则。优化后余额曲线方向性显著增强,是前述因子里权重最大的一项。 边际线止损止盈把出场和通道边界联动,分反弹、突破、新通道三种模式,并写了 CTrailingEquidChannel 子类做通道跟随。优化止损 25–75 步进 5、止盈 50–200 步进 5,图例显示该因子未提升性能。外汇与贵金属杠杆品种下,这类回测结论仅代表历史样本概率,实盘仍属高风险。

MQL5 / C++
class=class="str">"cmt">//---
sinput class="type">class="kw">string Info_trailing="+===-- 尾随 --====+"; class=class="str">"cmt">// +===-- 尾随 --====+
class="kw">input class="type">int InpStopLevelPips=class="num">30;          class=class="str">"cmt">// 止损价位, pips
class="kw">input class="type">int InpProfitLevelPips=class="num">50;        class=class="str">"cmt">// 止盈价位, pips
class=class="str">"cmt">//--- 尾随对象
  CTrailingFixedPips *trailing=new CTrailingFixedPips;
  if(trailing==NULL)
   {
   class=class="str">"cmt">//---错误
    printf(__FUNCTION__+": 创建尾随错误");
    myChannelExpert.Deinit();
    class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- 添加尾随对象
  if(!myChannelExpert.InitTrailing(trailing))
   {
   class=class="str">"cmt">//---错误
    PrintFormat(__FUNCTION__+": 尾随初始错误");
    myChannelExpert.Deinit();
    class="kw">return INIT_FAILED;
   }
class=class="str">"cmt">//--- 尾随参数
  trailing.StopLevel(InpStopLevelPips);
  trailing.ProfitLevel(InpProfitLevelPips);
class=class="str">"cmt">//+------------------------------------------------------------------+

通道类型判定与开仓触发的主循环

等距通道 EA 的主处理入口只做一件事:先由 m_signal.SetDirection() 刷新方向,再按当前是否有持仓分流。无仓位走 CheckOpen(),有仓位走 CheckTrailingStop(),两者任一返回 true 即视为本 tick 有动作,否则返回 false。 通道方向不是写死的,而是靠价格速度推断。枚举 ENUM_CHANNEL_TYPE 给了三个状态:0 升势、1 降势、2 横盘。新通道默认先归为 CHANNEL_TYPE_FLAT,只有当 m_ch_type_tol 不是 EMPTY_VALUE 且上轨两点价差绝对值超过容差时,才根据 pr_speed_pnt 正负改写为升或降。 过滤层用 Point 换算入场容差:filter0.PointsInside(_Point*InpPipsInside) 与 PointsOutside(_Point*InpPipsOutside) 把 pip 参数转成点数,TypeTolerance 控制类型判定的冗余。这几行直接决定信号在小布回测里被吞掉多少。 速度计算那句 double pr_speed_pnt=m_symbol.NormalizePrice(upper_vals[1]-upper_vals[0]); 取上轨相邻两值之差,外汇与贵金属杠杆高、滑点诡异,这条差值在极端行情可能瞬间破容差,倾向产生假突破信号。开 MT5 把 InpTypePips 从默认调小 2~3 倍,能压住一部分噪声。

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

enum ENUM_CHANNEL_TYPE
  {
   CHANNEL_TYPE_ASCENDING=class="num">0,   class=class="str">"cmt">// 升势
   CHANNEL_TYPE_DESCENDING=class="num">1, class=class="str">"cmt">// 降势
   CHANNEL_TYPE_FLAT=class="num">2,       class=class="str">"cmt">// 横盘
  };

class=class="str">"cmt">//--- 过滤参数
   filter0.PointsInside(_Point*InpPipsInside);
   filter0.PointsOutside(_Point*InpPipsOutside);
   filter0.TypeTolerance(_Point*InpTypePips);
   filter0.PrevFracNum(InpPrevFracNum);
   ...
class=class="str">"cmt">//--- 如果通道是新的
   if(is_new_channel)
     {
       m_ch_type=CHANNEL_TYPE_FLAT;                 class=class="str">"cmt">// 行盘 (无倾向) 通道
     class=class="str">"cmt">//--- 如果类型的冗余已设置
       if(m_ch_type_tol!=EMPTY_VALUE)
         {
         class=class="str">"cmt">//--- 通道类型
         class=class="str">"cmt">//--- 变化速度
           class="type">class="kw">double pr_speed_pnt=m_symbol.NormalizePrice(upper_vals[class="num">1]-upper_vals[class="num">0]);
         class=class="str">"cmt">//--- 如果速度足够
         if(MathAbs(pr_speed_pnt)>m_ch_type_tol)
           {
             if(pr_speed_pnt>class="num">0.)
               m_ch_type=CHANNEL_TYPE_ASCENDING;   class=class="str">"cmt">// 升势通道
             else
               m_ch_type=CHANNEL_TYPE_DESCENDING; class=class="str">"cmt">// 降势通道
           }
         }
     }

class="type">int CSignalEquidChannel::LongCondition(class="type">void)
  {
   class="type">int result=class="num">0;
class=class="str">"cmt">//--- 如果设置了最低价格
把通道诊断交给小布
这些通道信号与回测限制的差异,小布盯盘已内置对照视角,打开对应品种页就能直接看当前通道状态,你只管判断边界触碰是否值得跟。

常见问题

测试和优化期间终端不绘制图形对象,引用对象属性会返回零值,只有可视测试模式例外,所以策略逻辑不能依赖对象读取。
分形集合需先填满缓冲,通道缓冲在下一帧处理器调用时才激活,这样能保证通道计算基于完整的分形点集合。
重复的同向入场信号会被忽略,止损止盈按固定点数自动放置,不会因连续触碰而加仓。
小布盯盘内置了通道状态与边界触碰的呈现,不需要你自己写指标,打开品种页即可对照信号,省去编译调试的重复劳动。
本文因通道显示方式变化修改了CFractalSet::Calculate,旧版对象绘制逻辑若未同步类定义,可能出现缓冲计算不一致。