MQL5 酷宝书 - 移动通道交易信号(基础篇)
移动通道信号的起点
MT5 客户端内置的「移动通道交易信号」属于趋势跟随类指标信号,其逻辑基于价格与动态通道边界的相对位置生成多空提示。该示例最早发布于 2016 年 9 月 26 日,至今在官方代码库累计浏览 4271 次、被用户收藏 9 次,说明这类基础通道策略仍有稳定查阅需求。 对外汇与贵金属交易者而言,这类信号本质是高波动品种上的概率型辅助,并非确定性入口。开 MT5 后可在「导航器—信号」中直接加载该模块,观察其在 EURUSD 的 H1 周期上如何标记通道突破,再决定是否接入自己的风控参数。
「从移动通道到信号策略的切入」
前面那篇讲等距通道绘制的文章,用的是「等距通道」工具和面向对象写法把移动通道画了出来。本篇不重复造图,而是盯住这类通道能吐出的信号,尝试拼一套可交易的策略逻辑。 MQL5 社区里已有若干文章调用标准库现成模块生成信号,本文想补的缺口是:把通道类信号接进你自己的标准类体系,而不是每次都从头写。 对新手最友好的路径是从简到繁——先跑通一个只认通道突破的基础策略,再逐步挂上过滤和仓位规则。外汇与贵金属杠杆高、滑点跳空频繁,任何通道信号都只是概率倾向,实盘前务必在 MT5 策略测试器用历史数据验证。
◍ 用指标缓存区替代图形对象画通道
在 MT5 里跑 EA 优化时,图表上根本不会渲染任何图形对象——测试器不是图表本身。官方限制写得很死:测试/优化期间引用创建对象的属性,智能交易程序只会拿到零值,只有可视测试模式例外。所以若想让通道逻辑能回测、能批量优化,就不能靠 ObjectCreate 那套,得把分形和通道都塞进指标缓存区。 等距通道指标就是按这个思路写的:第一块算分形缓存区,第二块算通道边界缓存区。注意代码里第二块不在第一次调用时就激活,而是等下一次 OnCalculate 进来才跑——这样分形缓存区已经填好,通道边界才不会引用到空数据。 分形集合由 CFractalSet 类管理,其中 Calculate 方法负责把分形点转成通道缓存区数值,类定义在 CFractalPoint.mqh 里。下面这段是 OnCalculate 里分形计算部分的真实代码,黄色高亮块就是分形模块。 别在优化里依赖画图对象 EA 优化时图表对象属性全返回 0,通道若靠图形对象绘制,回测结果等同废测。改走指标缓存区,才能在 MT5 策略测试器里拿到可验证的分形与通道数值。
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 设成负数,能立刻在专家日志看到「分形设置初始化错误!」并确认指标不绘制——这是验证参数校验是否生效的最快办法。外汇与贵金属市场波动剧烈,此类通道仅作概率参考,实盘须自担高风险。
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。提升性能无非两条路——调参优化,或改逻辑挖性能因子,后者避不开。
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(无信号)、正数(多头条件满足度)或负数(空头条件满足度)的多级判定。外汇与贵金属市场杠杆高、滑点跳空频繁,这类通道信号仅作概率参考,实盘前务必在策略测试器里用历史数据验证权重配置。
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);