MQL5 酷宝书 - 移动通道交易信号·进阶篇
(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 类即可验证边界读取是否正常。
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 倍点差往往更稳。
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 不开单,上线前务必在策略测试器跑一次初始化日志。
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 还没写具体触发条件。外汇与贵金属杠杆高,实盘前务必在策略测试器用历史数据验证信号方向计算是否如预期。
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,图例显示该因子未提升性能。外汇与贵金属杠杆品种下,这类回测结论仅代表历史样本概率,实盘仍属高风险。
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 倍,能压住一部分噪声。
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">//--- 如果设置了最低价格