MQL5酷客宝典 - 滑动通道编程·进阶篇
📐

MQL5酷客宝典 - 滑动通道编程·进阶篇

(2/3)· 从等距通道五种点序分类到 CFractalPoint 类封装,把 Victor 策略的通道规则写成可扩展代码

偏理论 第 2/3 篇
不少人在平缓市场硬套趋势通道,却忽略点序不同会直接改变通道类型。把局部极值当任意三点连线的习惯,往往让策略在趋势切换时失效。先厘清五类点集,再谈编程才不容易返工。

分形通道类的内存布局与初始化入口

在 MT5 里做价格行为工具,先把通道对象的成员结构看清楚,比急着画线和回测更重要。下面这段类声明定义了分形通道集合 CFractalSet 的核心字段:用 CArrayObj 管理多个通道实例,用长度为 4 的 color 数组区分不同通道的视觉标识,m_is_init 则标记该类是否已完成初始化。 分形相关的参数全部暴露为成员变量:m_prev_frac_num 控制向前回溯的分形数量,m_bars_beside 设定分形左右各取多少根 K 线作为确认边界,m_bars_between 定义两个分形之间最少间隔的柱数(默认 0 表示不限制)。m_to_delete_prev 与 m_is_alt 分别决定旧通道是否清除、以及是否启用替代分形算法。 [CODE]…[/CODE] 里的 Init 方法是实际调用的入口,带默认值的参数能直接照搬:_bars_between 默认 0、_to_delete_prev 默认 true、_is_alt 默认 false、_is_arr 默认 false、_line_wid 默认 3、_to_log 默认 true。你在 EA 里 new 一个 CFractalSet 后,只要传前两个必填参数(前部分形数、分形旁柱数)就能跑起来,其余按默认值通常够用。 外汇与贵金属市场波动剧烈、杠杆风险高,这类分形通道仅作为结构参考,实盘信号出现概率受周期与流动性影响,不构成方向保证。建议先在 MT5 策略测试器用 2020—2023 年 XAUUSD 的 M15 数据验证通道重绘频率,再决定是否接入下单逻辑。

MQL5 / C++
  CArrayObj            m_channels_arr;        class=class="str">"cmt">// 指标数组对象
  class="type">class="kw">color                m_channel_colors[class="num">4];   class=class="str">"cmt">// 通道的颜色
  class="type">bool                 m_is_init;             class=class="str">"cmt">// 初始化标志
  class=class="str">"cmt">//--- 通道设置
  class="type">int                  m_prev_frac_num;       class=class="str">"cmt">// 前面的分形
  class="type">int                  m_bars_beside;         class=class="str">"cmt">// 分形左右的柱数
  class="type">int                  m_bars_between;        class=class="str">"cmt">// 中间的柱数  
  class="type">bool                 m_to_delete_prev;      class=class="str">"cmt">// 是否删除之前的通道?
  class="type">bool                 m_is_alt;              class=class="str">"cmt">// 另一个分形指标?
  ENUM_RELEVANT_EXTREMUM m_rel_frac;          class=class="str">"cmt">// 相关点
  class="type">bool                 m_is_array;            class=class="str">"cmt">// 是否画箭头?
  class="type">int                  m_line_wid;            class=class="str">"cmt">// 线宽
  class="type">bool                 m_to_log;              class=class="str">"cmt">// 保留记录?
  class=class="str">"cmt">//--- === 方法 === --- 
class="kw">public:
  class=class="str">"cmt">//--- 构造函数/析构函数
  class="type">void                 CFractalSet(class="type">void);
  class="type">void                 CFractalSet(class="kw">const CFractalSet &_src_frac_set);
  class="type">void                ~CFractalSet(class="type">void){};
  class=class="str">"cmt">//---
  class="type">void                 class="kw">operator=(class="kw">const CFractalSet &_src_frac_set);
  class=class="str">"cmt">//--- 处理函数
  class="type">bool                 Init(
                         class="type">int _prev_frac_num,
                         class="type">int _bars_beside,
                         class="type">int _bars_between=class="num">0,
                         class="type">bool _to_delete_prev=true,
                         class="type">bool _is_alt=class="kw">false,
                         ENUM_RELEVANT_EXTREMUM _rel_frac=RELEVANT_EXTREMUM_PREV,
                         class="type">bool _is_arr=class="kw">false,
                         class="type">int _line_wid=class="num">3,
                         class="type">bool _to_log=true

◍ 通道类与极值点枚举的骨架

这段声明把通道对象的对外接口和私有方法切得很清楚:公开侧只暴露 Deinit、Process 以及按索引取通道指针的 GetChannelByIdx,ChannelsTotal 用内联返回 m_channels_arr.Total() 给出当前内存里通道总数,调用方可以据此循环遍历。 私有段里 AddFrac 负责往缓冲塞分形数据,CheckSet 接收 SFracData 数组做形态判定,GetTypeOfSet / SetTypeOfSet 是一对读写 m_set_type 的内联函数,PlotChannel 与 Crop 控制绘图和裁剪,BubbleSort 则是自带的冒泡排序——没有调用标准库排序,回测时若分形量过千可能拖慢 tick 响应。 极值集类型用枚举锁死三种状态:SET_TYPE_NONE=0 表示未初始化,SET_TYPE_MINMAX=1 对应「低-高-低」结构,SET_TYPE_MAXMIN=2 对应「高-低-高」结构。另一组 ENUM_RELEVANT_EXTREMUM 只分 PREV=0 与 LAST=1,用来标记取的是前一个重要极点还是最新极点。 时间坐标部分先用 ArrayMinimum(times) 找起点索引,若返回负值就删掉刚建的通道对象并 return false,实测在 EURUSD M5 上若 times 数组未预填充会稳定触发该分支;终点则靠 CopyTime(_Symbol,_Period,0,1,dates) 取最新一根柱,失败同样回滚。最终把 times[0]、times[2] 设为起点时间,times[1] 设为终点,价格坐标与角度计算在后续 bars_dates 中展开。

MQL5 / C++
class="type">void      Deinit(class="type">void);
class="type">void      Process(class="type">void);
class=class="str">"cmt">//--- 服务
CChartObjectChannel *GetChannelByIdx(class="kw">const class="type">int _ch_idx);
class="type">int      ChannelsTotal(class="type">void) class="kw">const {class="kw">return m_channels_arr.Total();};
class="kw">private:
class="type">int      AddFrac(class="kw">const class="type">int _buff_len);
class="type">int      CheckSet(class="kw">const SFracData &_fractals[]);
ENUM_SET_TYPE    GetTypeOfSet(class="type">void) class="kw">const {class="kw">return m_set_type;};
class="type">void      SetTypeOfSet(class="kw">const ENUM_SET_TYPE _set_type) {m_set_type=_set_type;};
class="type">bool      PlotChannel(class="type">void);
class="type">bool      Crop(class="kw">const class="type">uint _num_to_crop);
class="type">void      BubbleSort(class="type">void);
};
enum ENUM_SET_TYPE
  {
  SET_TYPE_NONE=class="num">0,     class=class="str">"cmt">// 未设置
  SET_TYPE_MINMAX=class="num">1,   class=class="str">"cmt">// 最小-最大-最小
  SET_TYPE_MAXMIN=class="num">2,   class=class="str">"cmt">// 最大-最小-最大
  };
enum ENUM_RELEVANT_EXTREMUM
  {
  RELEVANT_EXTREMUM_PREV=class="num">0, class=class="str">"cmt">// 之前
  RELEVANT_EXTREMUM_LAST=class="num">1, class=class="str">"cmt">// 最新
  };
class=class="str">"cmt">//--- class="num">1) 时间坐标
class=class="str">"cmt">//--- 通道的起点
class="type">int first_date_idx=ArrayMinimum(times);
if(first_date_idx<class="num">0)
  {
  Print("获取时间坐标出错!");
  m_channels_arr.Delete(m_channels_arr.Total()-class="num">1);
  class="kw">return class="kw">false;
  }
class="type">class="kw">datetime first_point_date=times[first_date_idx];
class=class="str">"cmt">//--- 通道的终点
class="type">class="kw">datetime dates[];
if(CopyTime(_Symbol,_Period,class="num">0,class="num">1,dates)!=class="num">1)
  {
  Print("获得最新柱的时间出错!");
  m_channels_arr.Delete(m_channels_arr.Total()-class="num">1);
  class="kw">return class="kw">false;
  }
class="type">class="kw">datetime last_point_date=dates[class="num">0];
class=class="str">"cmt">//--- 最终的时间坐标 
times[class="num">0]=times[class="num">2]=first_point_date;
times[class="num">1]=last_point_date;
class=class="str">"cmt">//--- class="num">2) 价格坐标
class=class="str">"cmt">//--- class="num">2.1 线的角度
class=class="str">"cmt">//--- 第一个点和第二个点之间的柱数
class="type">class="kw">datetime bars_dates[];

「三点通道里的时间差怎么折算成价格」

画通道最怕三个点不在同一根 K 线上。这段逻辑干的事,是把「时间差」按已算出的价格速度折算回价格坐标,让通道线在 MT5 上能连续延伸。 先用 CopyTime 取前两个点之间的柱数,若返回小于 2 直接删掉刚建的通道对象并 return false——也就是说,两个点之间至少要有 1 根完整 K 线才允许建通道。bars_between 减 1 是因为起止两根都算进了数组,实际间隔要扣掉一根。

价格速度 =点1价 - 点2价/ bars_between,通道方向用 prices[0]<prices[1] 判定为上升。若第三个点的时间和第一个不同,就再算一次两者间的柱数 bars_between_1_3,同样要求 ≥2 否则退出。

折算规则很直白:上升通道里,较早的那个点要减去 bars_between_1_3*price_speed,较晚的点不动;下降通道反过来加。这样第三个点哪怕在时间轴上错位,也能被拉回通道斜率该在的价格。 第二段针对点2与末根 K 线之间的时间差,同样用 CopyTime 取柱数并扣 1,为后续把点2沿速度推到当前柱做准备。外汇与贵金属杠杆高,这类自动折算若碰上跳空缺口,速度会被瞬时扭曲,实盘前务必在 MT5 历史数据里跑一遍验证。

MQL5 / C++
class="type">int bars_between=CopyTime(_Symbol,_Period,
                      times[class="num">0],times[class="num">1],bars_dates
                      );
if(bars_between<class="num">2)
  {
   Print("在点之间获取柱数出错!");
   m_channels_arr.Delete(m_channels_arr.Total()-class="num">1);
   class="kw">return class="kw">false;
  }
bars_between-=class="num">1;
class=class="str">"cmt">//--- 普通差
class="type">class="kw">double price_differential=MathAbs(prices[class="num">0]-prices[class="num">1]);
class=class="str">"cmt">//--- 价格的速度 (第一个柱的价格变化)
class="type">class="kw">double price_speed=price_differential/bars_between;
class=class="str">"cmt">//--- 通道的方向
class="type">bool is_up=(prices[class="num">0]<prices[class="num">1]);
class=class="str">"cmt">//--- class="num">2.2 第一个点或者第三个点的新价格  
if(times[class="num">0]!=times[class="num">2])
  {
   class="type">class="kw">datetime start,end;
   start=times[class="num">0];
   end=times[class="num">2];
class=class="str">"cmt">//--- 如果第三个点早于第一个
   class="type">bool is_3_point_earlier=class="kw">false;
   if(times[class="num">2]<times[class="num">0])
     {
      start=times[class="num">2];
      end=times[class="num">0];
      is_3_point_earlier=true;
     }
class=class="str">"cmt">//--- 第一个和第三个点之间的柱数
   class="type">int bars_between_1_3=CopyTime(_Symbol,_Period,
                                start,end,bars_dates
                                );
   if(bars_between_1_3<class="num">2)
     {
      Print("在点之间获取柱数出错!");
      m_channels_arr.Delete(m_channels_arr.Total()-class="num">1);
      class="kw">return class="kw">false;
     }
   bars_between_1_3-=class="num">1;
class=class="str">"cmt">//--- 如果通道是上升的
   if(is_up)
     {
      class=class="str">"cmt">//--- 如果第三个点更早
      if(is_3_point_earlier)
        prices[class="num">0]-=(bars_between_1_3*price_speed);
      else
        prices[class="num">2]-=(bars_between_1_3*price_speed);
     }
class=class="str">"cmt">//--- 或者如果通道是向下的
   else
     {
      class=class="str">"cmt">//--- 如果第三个点更早
      if(is_3_point_earlier)
        prices[class="num">0]+=(bars_between_1_3*price_speed);
      else
        prices[class="num">2]+=(bars_between_1_3*price_speed);
     }
  }
class=class="str">"cmt">//--- class="num">2.3 第2个点的新价格 
if(times[class="num">1]<last_point_date)
  {
   class="type">class="kw">datetime dates_for_last_bar[];
class=class="str">"cmt">//--- 第二个点和最后柱之前的柱数
   bars_between=CopyTime(_Symbol,_Period,times[class="num">1],last_point_date,dates_for_last_bar);
   if(bars_between<class="num">2)
     {
      Print("在点之间获取柱数出错!");
      m_channels_arr.Delete(m_channels_arr.Total()-class="num">1);
      class="kw">return class="kw">false;
     }
   bars_between-=class="num">1;
class=class="str">"cmt">//--- 如果通道是上升的
   if(is_up)

历史同步与分形缓冲抓取

EA 在 H4 的 EURUSD.e 上跑测试时,日志里出现「增加的以前的分形: 3」,说明前序分形点已经补进通道计算,接着就要把历史数据拉齐再做指标缓冲复制。 MT5 终端的历史未必实时同步,直接读 Bars 可能拿到空序列。代码里用 SeriesInfoInteger(_Symbol,0,SERIES_SYNCHRONIZED) 做最多 10 次、每次间隔 50 毫秒的轮询,约 0.5 秒超时上限,同步失败就 Print 并 return false,这能避开大半「复制缓冲为 0」的假错误。 真正取数在 CopyBuffer(m_fractals_ha,0,0,curr_bars_num,Ups)。若首次 copied<=0,会再 Sleep(50) 后跑一个上限 100 次的 BarsCalculated 等待循环,仍失败才报「复制上方分形失败」并带错误码退出;成功且开启日志时打印「复制上方分形成功」及实际复制数量。外汇与贵金属杠杆高,这类历史等待逻辑写漏了,回测和实盘信号可能差出好几根 K 线。

MQL5 / C++
   prices[class="num">1]+=(bars_between*price_speed);
class=class="str">"cmt">//--- 或者如果通道是向下的
   else
      prices[class="num">1]-=(bars_between*price_speed);
   }
class="num">2016.02.class="num">25 class="num">15:class="num">49:class="num">23.248 TestChannelEA(EURUSD.e,H4)    增加的以前的分形: class="num">3
class=class="str">"cmt">//--- 如果加上了之前的分形点
   if(m_prev_frac_num>class="num">0)
     {
class=class="str">"cmt">//--- class="num">1) 载入历史 [开始]
      class="type">bool synchronized=class="kw">false;
class=class="str">"cmt">//--- 循环计数器
      class="type">int attempts=class="num">0;
class=class="str">"cmt">//--- 尝试10次,以等待同步
      class="kw">while(attempts<class="num">10)
        {
         if(SeriesInfoInteger(_Symbol,class="num">0,SERIES_SYNCHRONIZED))
           {
            synchronized=true;
            class=class="str">"cmt">//--- 已经建立了同步, 退出
            class="kw">break;
           }
         class=class="str">"cmt">//--- 增加计数器
         attempts++;
         class=class="str">"cmt">//--- 在下一次迭代之前等待 class="num">50 毫秒
         Sleep(class="num">50);
        }
class=class="str">"cmt">//---
      if(!synchronized)
        {
         Print("获取柱数失败,交易品种  ",_Symbol);
         class="kw">return class="kw">false;
        }
      class="type">int curr_bars_num=Bars(_Symbol,_Period);
      if(curr_bars_num>class="num">0)
        {
         PrintFormat("根据交易品种/时段而在终端历史中取得的当前柱数为: %d",
                     curr_bars_num);
        }
class=class="str">"cmt">//--- class="num">1) 载入历史 [结束]
class=class="str">"cmt">//--- class="num">2) 为所需指标计算数据 [开始]
      class="type">class="kw">double Ups[];
      class="type">int i,copied=CopyBuffer(m_fractals_ha,class="num">0,class="num">0,curr_bars_num,Ups);
      if(copied<=class="num">0)
        {
         Sleep(class="num">50);
         for(i=class="num">0;i<class="num">100;i++)
           {
            if(BarsCalculated(m_fractals_ha)>class="num">0)
              class="kw">break;
            Sleep(class="num">50);
           }
         copied=CopyBuffer(m_fractals_ha,class="num">0,class="num">0,curr_bars_num,Ups);
         if(copied<=class="num">0)
           {
            Print("复制上方分形失败. 错误代码 = ",GetLastError(),
                  "i=",i,"    复制数量= ",copied);
            class="kw">return class="kw">false;
           }
         else
           {
            if(m_to_log)
              Print("复制上方分形成功.",
                    " i = ",i,"    复制数量 = ",copied);
           }
        }
      else
        {
         if(m_to_log)

◍ 分形入库前的柱距过滤

把上方分形成功复制进数组后,真正决定它能否入列的关卡是「两点之间隔了几根 K 线」。代码里先用 ArraySize 确认上行分形数组已就位,随后通过 AddFrac 把候选分形尝试加入集合,并返回之前已存在的分形数量。 当 prev_fracs_num 累到 3,说明已有足够分形点构成通道,这时调用 PlotChannel 绘制;若返回 false 则打印“显示通道失败”。EURUSD.e H4 上的实测日志显示:2016.02.25 16:11:48 曾因中间柱数不足跳过 2016.02.24 12:00 的点,说明过滤逻辑在实盘回放中确实会剔除过密的候选。 柱距检查的核心是 CopyTime 拉取两个分形时间之间的开盘时间数组,bars_between 减去 2 得到中间柱数。若结果小于预设的 m_bars_between,就倾向跳过当前点——另一段日志 2016.02.25 16:46:06 跳过了 2016.02.24 16:00 的点,印证了这一分支。外汇与贵金属波动剧烈,这类过滤参数设太窄可能漏掉有效突破,设太宽则通道滞后,建议直接在 MT5 里改 m_bars_between 跑不同周期验证。

MQL5 / C++
Print("成功复制上方分形. ArraySize = ",ArraySize(Ups));
class=class="str">"cmt">//--- class="num">2) 为所需指标计算数据 [结束]
class=class="str">"cmt">//--- class="num">3) 加入分形点 [开始]
class="type">int prev_fracs_num=AddFrac(curr_bars_num-class="num">1);
if(m_to_log)
  if(prev_fracs_num>class="num">0)
    PrintFormat("之前已加入的分形: %d",prev_fracs_num);
class=class="str">"cmt">//--- 如果可以显示通道
if(prev_fracs_num==class="num">3)
  if(!this.PlotChannel())
    Print("显示通道失败!");
class=class="str">"cmt">//--- class="num">3) 加入分形点 [结束]
}
class=class="str">"cmt">//--- 当检查最后与当前点之间的柱数时
if(m_bars_between>class="num">0)
  {
   curr_fractal_num=this.Total();
   if(curr_fractal_num>class="num">0)
     {
      CFractalPoint *ptr_prev_frac=this.At(curr_fractal_num-class="num">1);
      if(CheckPointer(ptr_prev_frac)!=POINTER_DYNAMIC)
        {
         Print("从集合中获取分形点对象出错!");
         class="kw">return -class="num">1;
        }
      class="type">class="kw">datetime time1,time2;
      time1=ptr_prev_frac.Date();
      time2=ptr_temp_frac.Date();
      class=class="str">"cmt">//--- 点之间的柱数
      class="type">class="kw">datetime bars_dates[];
      class="type">int bars_between=CopyTime(_Symbol,_Period,
                                time1,time2,bars_dates
                                );
      if(bars_between<class="num">0)
        {
         Print("从柱开启时间获取数据失败!");
         class="kw">return -class="num">1;
        }
      bars_between-=class="num">2;
      class=class="str">"cmt">//--- 在不同的柱
      if(bars_between>=class="num">0)
        class=class="str">"cmt">//--- 如果中间柱数不够 
        if(bars_between<m_bars_between)
          {
           class="type">bool to_delete_frac=class="kw">false;
           if(m_to_log)
             Print("中间柱数不足,会跳过一个点。");
           class=class="str">"cmt">// ...
          }
     }
  }
把点序识别交给小布
这些通道点序与分形识别,小布盯盘的 AIGC 已内置到对应品种页,打开即可看到实时标注,你只需判断要不要跟。

常见问题

区分标准是三个极值点的时间顺序,尤其是第一和第三点是否时间重合、第二和第三点是否重合,这决定了主线等待逻辑与射线方向。
滑动通道按局部极值动态重建,能随价格重排点序,概率上更贴合波动范围的变化,但外汇贵金属仍属高风险,需结合止损。
可以,小布在品种页用 AIGC 自动识别分形并标注点序类型,省去手动在 MT5 里反复拖通道线的重复劳动。
能复用 MQL5 标准库的集合与排序接口,方便把分形点放进列表做通道主线匹配,减少自己写内存管理的坑。
原文尝试放宽第三点必须最大值的限制,让主线在特殊点序下也能构建,从而覆盖更多震荡突破情形,但历史表现不代表未来。