MQL5酷客宝典 - 滑动通道编程·进阶篇
(2/3)· 从等距通道五种点序分类到 CFractalPoint 类封装,把 Victor 策略的通道规则写成可扩展代码
分形通道类的内存布局与初始化入口
在 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 数据验证通道重绘频率,再决定是否接入下单逻辑。
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 中展开。
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 历史数据里跑一遍验证。
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 线。
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 跑不同周期验证。
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">// ... } } }