MQL5 酷宝书 - 移动通道交易信号(基础篇)
📘

MQL5 酷宝书 - 移动通道交易信号(基础篇)

第 1/3 篇

移动通道信号的起点

MT5 客户端内置的「移动通道交易信号」属于趋势跟随类指标信号,其逻辑基于价格与动态通道边界的相对位置生成多空提示。该示例最早发布于 2016 年 9 月 26 日,至今在官方代码库累计浏览 4271 次、被用户收藏 9 次,说明这类基础通道策略仍有稳定查阅需求。 对外汇与贵金属交易者而言,这类信号本质是高波动品种上的概率型辅助,并非确定性入口。开 MT5 后可在「导航器—信号」中直接加载该模块,观察其在 EURUSD 的 H1 周期上如何标记通道突破,再决定是否接入自己的风控参数。

「从移动通道到信号策略的切入」

前面那篇讲等距通道绘制的文章,用的是「等距通道」工具和面向对象写法把移动通道画了出来。本篇不重复造图,而是盯住这类通道能吐出的信号,尝试拼一套可交易的策略逻辑。 MQL5 社区里已有若干文章调用标准库现成模块生成信号,本文想补的缺口是:把通道类信号接进你自己的标准类体系,而不是每次都从头写。 对新手最友好的路径是从简到繁——先跑通一个只认通道突破的基础策略,再逐步挂上过滤和仓位规则。外汇与贵金属杠杆高、滑点跳空频繁,任何通道信号都只是概率倾向,实盘前务必在 MT5 策略测试器用历史数据验证。

◍ 用指标缓存区替代图形对象画通道

在 MT5 里跑 EA 优化时,图表上根本不会渲染任何图形对象——测试器不是图表本身。官方限制写得很死:测试/优化期间引用创建对象的属性,智能交易程序只会拿到零值,只有可视测试模式例外。所以若想让通道逻辑能回测、能批量优化,就不能靠 ObjectCreate 那套,得把分形和通道都塞进指标缓存区。 等距通道指标就是按这个思路写的:第一块算分形缓存区,第二块算通道边界缓存区。注意代码里第二块不在第一次调用时就激活,而是等下一次 OnCalculate 进来才跑——这样分形缓存区已经填好,通道边界才不会引用到空数据。 分形集合由 CFractalSet 类管理,其中 Calculate 方法负责把分形点转成通道缓存区数值,类定义在 CFractalPoint.mqh 里。下面这段是 OnCalculate 里分形计算部分的真实代码,黄色高亮块就是分形模块。 别在优化里依赖画图对象 EA 优化时图表对象属性全返回 0,通道若靠图形对象绘制,回测结果等同废测。改走指标缓存区,才能在 MT5 策略测试器里拿到可验证的分形与通道数值。

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

分形通道的增量计算入口

在自定义指标的主计算函数里,MT5 会用 prev_calculated 告知上一次已经处理过的柱数。当 prev_calculated 大于 0,说明不是首次加载,此时若全局分形设置对象 gFracSet 尚未初始化,就必须先调用 Init 补齐参数,否则后续计算无从谈起。 Init 接收的入参直接决定分形识别逻辑:InpPrevFracNum 控制回溯的历史分形数量,InpBarsBeside 与 InpBarsBetween 约束相邻及中间 K 线条件,InpRelevantPoint 选定参考点类型,InpLineWidth 和 InpToLog 则分别管画线粗细与日志开关。任一参数非法都会让 Init 返回 false,代码随即打印错误并 return 0,指标在该图表停止刷新。 初始化通过后,gFracSet.Calculate 把上下分形缓冲、时间数组以及三条通道缓冲一并传入,完成本周期的通道边界推算。最后 return rates_total,把当前总柱数交还 MT5,下一次 OnCalculate 调用时 prev_calculated 就会等于这个值,实现只算新数据的增量更新。 开 MT5 把这段贴进指标源码,故意把 InpBarsBetween 设成负数,能立刻在专家日志看到「分形设置初始化错误!」并确认指标不绘制——这是验证参数校验是否生效的最快办法。外汇与贵金属市场波动剧烈,此类通道仅作概率参考,实盘须自担高风险。

MQL5 / C++
if(prev_calculated>class="num">0)
   {
      class=class="str">"cmt">//--- 如果设置尚未初始化
      if(!gFracSet.IsInit())
         if(!gFracSet.Init(
               InpPrevFracNum,
               InpBarsBeside,
               InpBarsBetween,
               InpRelevantPoint,
               InpLineWidth,
               InpToLog
               ))
            {
               Print("分形设置初始化错误!");
               class="kw">return class="num">0;
            }
      class=class="str">"cmt">//--- 计算
      gFracSet.Calculate(gUpFractalsBuffer,gDnFractalsBuffer,time,
                  gUpperBuffer,gLowerBuffer,
                  gNewChannelBuffer
                  );
   }
class=class="str">"cmt">//--- 计算通道边界 [结束]
class=class="str">"cmt">//--- 返回 prev_calculated 的数值用于下次调用
   class="kw">return rates_total;
   }

「用等距通道边界做首触开仓」

先把规则压到最简:价格触到等距通道下边界开多、触上边界开空,止损止盈按固定点数挂。若已有同方向持仓,重复信号直接忽略。这套逻辑在 MT5 标准库里能借 CExpertSignal 派生实现,不用从零写信号处理。 信号类 CSignalEquidChannel 的核心只有等距通道一个发生器,靠 CiCustom 接自定义指标。边界不是一根线,而是带缓冲的区:外冗余 m_pnt_out 管价格能越界多远,内冗余 m_pnt_in 管距边界多近算触到。举例上边界红线在 1.11552,缓冲下沿 1.11452、上沿 1.11702,即外冗余 150 点、内冗余 100 点,价格进区即判触边界。 m_on_start 复位时,图表已画好的第一个通道不被处理,EA 只认下一根通道的信号;m_base_low_price / m_base_high_price 存实时柱高低价,决定是零号柱每tick成交还是新柱才在前柱成交。Direction() 先判是否跳过首通道,再取价算两缓冲区,通道过窄或缓冲过宽可能导致价格同入两区,这种情况必须过滤。 LongCondition() 只看价格是否进下边界缓冲且模型权限开;ShortCondition() 对称看上边界。回测用 EURUSD H1、真实分时分笔模式,2013–2015 余额曲线呈锯齿:一笔损跟一笔盈交替,参数存 base_signal.set,通道值全版本不变。 策略类 CEquidChannelExpert 从 CExpert 派生,只需重写虚方法 Processing():先算方向,无仓则开、有仓则退。完整代码在 BaseChannelsTrader.mq5。提升性能无非两条路——调参优化,或改逻辑挖性能因子,后者避不开。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 类 CSignalEquidChannel                                     |
class=class="str">"cmt">//| 目的: 交易信号类                                            |
class=class="str">"cmt">//|    基于等距通道。                                          |
class=class="str">"cmt">//| 派生自 CExpertSignal 类。                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSignalEquidChannel : class="kw">public CExpertSignal
  {
class="kw">protected:
   CiCustom                m_equi_chs;       class=class="str">"cmt">// 指标对象 "EquidistantChannels"
   class=class="str">"cmt">//--- 可调整参数
   class="type">int                     m_prev_frac_num;   class=class="str">"cmt">// 之前的分形
   class="type">bool                    m_to_plot_fracs;   class=class="str">"cmt">// 显示分形?
   class="type">int                     m_bars_beside;     class=class="str">"cmt">// 分形分立左/右的柱线
   class="type">int                     m_bars_between;    class=class="str">"cmt">// 过渡柱线
   ENUM_RELEVANT_EXTREMUM m_relevant_pnt;     class=class="str">"cmt">// 相关点
   class="type">int                     m_line_width;      class=class="str">"cmt">// 线宽
   class="type">bool                    m_to_log;          class=class="str">"cmt">// 保持日志?
   class="type">class="kw">double                  m_pnt_in;          class=class="str">"cmt">// 内部冗余, pips
   class="type">class="kw">double                  m_pnt_out;         class=class="str">"cmt">// 外部冗余, pips

◍ 等距通道信号类的对外接口与可调权重

等距通道信号类把通道边界的判定和参数暴露都收进了一套 C++ 风格接口。m_pattern_0 这个整型成员代表“触及通道下边界买入、上边界卖出”这一模型的权重,取值区间固定为 0–100,调高它意味着该模型在组合信号里的发言权更大。 外部调用方通过一组 setter 注入参数:PrevFracNum 控制回溯的分形数量,PointsOutside 与 PointsInside 分别以点数定义通道外冗余区和内部容差区,SignalOnStart 决定是否在 EA 启动那一刻就产生信号。 下面这段声明展示了类内部状态与公开方法的骨架,开 MT5 新建信号类时可照此补齐实现。 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); LongCondition 与 ShortCondition 返回 int 而非 bool,说明通道信号可能给出 0(无信号)、正数(多头条件满足度)或负数(空头条件满足度)的多级判定。外汇与贵金属市场杠杆高、滑点跳空频繁,这类通道信号仅作概率参考,实盘前务必在策略测试器里用历史数据验证权重配置。

MQL5 / C++
class="type">bool                m_on_start;               class=class="str">"cmt">// 启动时的信号标志
class=class="str">"cmt">//--- 计算
class="type">class="kw">double              m_base_low_price;         class=class="str">"cmt">// 基准最低价
class="type">class="kw">double              m_base_high_price;        class=class="str">"cmt">// 基准最高价
class="type">class="kw">double              m_upper_zone[class="num">2];          class=class="str">"cmt">// 上部区域: [class="num">0]-内部冗余, [class="num">1]-外部  
class="type">class="kw">double              m_lower_zone[class="num">2];          class=class="str">"cmt">// 下部区域
class="type">class="kw">datetime            m_last_ch_time;           class=class="str">"cmt">// 最后一个通道的发生时间
class=class="str">"cmt">//--- 行情模型的 "权重" (class="num">0-class="num">100)
class="type">int                 m_pattern_0;              class=class="str">"cmt">//  "触及通道的下边界 - 买入, 上边界 - 卖出"
class=class="str">"cmt">//--- === 方法 === --- 
class="kw">public:
  class=class="str">"cmt">//--- 构造器/析构器
  class="type">void                CSignalEquidChannel(class="type">void);
  class="type">void               ~CSignalEquidChannel(class="type">void){};
  class=class="str">"cmt">//--- 可调整参数的赋值方法
  class="type">void                PrevFracNum(class="type">int _prev_frac_num)    {m_prev_frac_num=_prev_frac_num;}
  class="type">void                ToPlotFracs(class="type">bool _to_plot)         {m_to_plot_fracs=_to_plot;}
  class="type">void                BarsBeside(class="type">int _bars_beside)       {m_bars_beside=_bars_beside;}
  class="type">void                BarsBetween(class="type">int _bars_between)     {m_bars_between=_bars_between;}
  class="type">void                RelevantPoint(ENUM_RELEVANT_EXTREMUM _pnt) {m_relevant_pnt=_pnt;}
  class="type">void                LineWidth(class="type">int _line_wid)           {m_line_width=_line_wid;}
  class="type">void                ToLog(class="type">bool _to_log)                {m_to_log=_to_log;}
  class="type">void                PointsOutside(class="type">class="kw">double _out_pnt)     {m_pnt_out=_out_pnt;}
  class="type">void                PointsInside(class="type">class="kw">double _in_pnt)       {m_pnt_in=_in_pnt;}
  class="type">void                SignalOnStart(class="type">bool _on_start)      {m_on_start=_on_start;}
  class=class="str">"cmt">//--- 调整行情模型的 "权重" 方法
  class="type">void                Pattern_0(class="type">int _val) {m_pattern_0=_val;}
  class=class="str">"cmt">//--- 赋值验证方法
  class="kw">virtual class="type">bool        ValidationSettings(class="type">void);
  class=class="str">"cmt">//--- 创建指标和时间序列的方法
  class="kw">virtual class="type">bool        InitIndicators(CIndicators *indicators);
  class=class="str">"cmt">//--- 检查行情模型是否已生成的方法
  class="kw">virtual class="type">int         LongCondition(class="type">void);
  class="kw">virtual class="type">int         ShortCondition(class="type">void);
  class="kw">virtual class="type">class="kw">double      Direction(class="type">void);

常见问题

先把通道上下轨算成指标缓存区,再判断价格首次触碰边界,触发条件写进信号类接口即可。
缓存区随K线自动重绘不残留,回测时可被信号逻辑直接读取,避免对象丢失导致的信号漂移。
小布可加载通道信号到对应品种页做AIGC诊断,直接给出历史触发分布与边界首触胜率参考。
在新K线收盘时只更新最近分形点并推演上下轨,不必每tick重算全部历史分形。
权重越高信号类越偏向该通道触发,可能过滤杂波但也会漏掉部分早进场机会,建议逐步微调。