作为技术分析工具的 MTF 指标·进阶篇
(2/3)·在M1到M15的快节奏里,高周期信号若不在手边,决策混乱只是时间问题
◍ 多均线缓冲区的预检与拷贝逻辑
在 MT5 自定义指标里同时挂三条不同周期的均线,第一道关卡是先确认主图报价柱数够不够算。代码用 ExtBarsMinimum 做下限判断,若 rates_total 小于这个值直接 return(0),避免后续用残缺数据画出误导性的交叉信号。外汇与贵金属市场跳空频繁,这种预检能减少假信号触发概率。 接下来对三个指标句柄分别调 BarsCalculated,只要对应周期不为 0 且已算柱数跟不上 rates_total,就打印具体差了几根并退出。实战中若终端刚加载或切换周期,常看到类似 'ExtHandle1 的数据没有全部计算 (120 bars)' 的日志,说明后台还在追算历史。 拷贝环节用 to_copy 控制增量:首次或异常时整段重拷,常规刷新只补 rates_total-prev_calculated 根,并多拷一根保边缘连续。每次 CopyBuffer 前都插 IsStopped() 拦截,用户移除指标时能立刻断流不卡线程。把下面这段直接塞进你的 OnCalculate 开头,跑 EURUSD 的 M15 就能验证三缓冲是否同步。
class="kw">const class="type">int &spread[]) { class=class="str">"cmt">//--- 检查报价总数 if(rates_total<ExtBarsMinimum) class="kw">return(class="num">0); class=class="str">"cmt">// 用于计算的柱数不够 class=class="str">"cmt">//--- 没有计算所有的数据 class="type">int calculated=BarsCalculated(ExtHandle1); if(calculated<rates_total&&period1!=class="num">0) { Print("ExtHandle1 的数据没有全部计算 (",calculated,"bars ). Error",GetLastError()); class="kw">return(class="num">0); } calculated=BarsCalculated(ExtHandle2); if(calculated<rates_total&&period2!=class="num">0) { Print("ExtHandle2 的数据没有全部计算 (",calculated,"bars ). Error",GetLastError()); class="kw">return(class="num">0); } calculated=BarsCalculated(ExtHandle3); if(calculated<rates_total&&period3!=class="num">0) { Print("ExtHandle3 的数据没有全部计算 (",calculated,"bars ). Error",GetLastError()); class="kw">return(class="num">0); } class=class="str">"cmt">//--- 我们可以复制部分数据 class="type">int to_copy; if(prev_calculated>rates_total || prev_calculated<class="num">0) to_copy=rates_total; else { to_copy=rates_total-prev_calculated; if(prev_calculated>class="num">0) to_copy++; } class=class="str">"cmt">//---- 取得 ma 缓冲区 if(IsStopped()) class="kw">return(class="num">0); class=class="str">"cmt">//检查停止标志 if(period1!=class="num">0) if(CopyBuffer(ExtHandle1,class="num">0,class="num">0,to_copy,ExtBuf1)<=class="num">0) { Print("取得 ExtHandle1 失败!错误 ",GetLastError()); class="kw">return(class="num">0); } if(IsStopped()) class="kw">return(class="num">0); class=class="str">"cmt">//检查停止标志 if(period2!=class="num">0) if(CopyBuffer(ExtHandle2,class="num">0,class="num">0,to_copy,ExtBuf2)<=class="num">0) { Print("取得 ExtHandle2 失败!错误 ",GetLastError()); class="kw">return(class="num">0); } if(IsStopped()) class="kw">return(class="num">0); class=class="str">"cmt">//检查停止标志 if(period3!=class="num">0) if(CopyBuffer(ExtHandle3,class="num">0,class="num">0,to_copy,ExtBuf3)<=class="num">0) { Print("取得 ExtHandle3 失败!错误 ",GetLastError()); class="kw">return(class="num">0); } class=class="str">"cmt">//--- 返回 prev_calculated 的值用于下次调用 class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+
「跨周期取数别再靠 iClose 那套老接口」
MT5 里早就没有 iClose()、iHigh() 这类 MQL4 习惯函数了,取序列统一走 CopyClose()、CopyOpen()、CopyTime() 等;自定义指标和 MA、CCI、MACD 则靠 CopyBuffer() 把计算缓冲拉出来。要做 M1 到 MN1 全周期覆盖,光时间框架就有 26 档,若再叠加多个品种,调用次数会成倍膨胀。 多数信息类指标其实只需最近 2 根柱,没必要每次复制整段历史。把 Copy 类调用封装成独立函数重复用,能压住代码长度。下面这段 _iClose 就是按 symbol+周期+偏移取收盘价的最小实现: 低周期的历史柱数量远多于高周期,这是跨周期计算最容易踩的坑。比如 M1 一年产生的 bar 数可能是 MN1 的上千倍,若统一按高周期柱数去拉低周期数据,内存和耗时都会爆。务实做法是用最小公共柱数做上限,或逐周期单独定深度。 图形型 MTF 指标还要处理「未闭合 bar 的线在跳」的问题。M5 由 5 根 M1 组成,M15 一根蜡烛要等 3 根 M5 走完才固定,直接按索引映射会出台阶。用 _iTime 按开启时间反查柱号,再给末根 bar 加插值权重,振荡指标(WPR/CCI)的线形才接得顺。 这类系统虽吃资源,但能补策略测试器不能同时看多周期图的短板。以三重滤网为例:一筛 W1+M30 跑 MACD(12,26,1) 与 EMA13,二筛 D1+M5 跑 Stochastic(5,3,3),三筛 H4+M1 挂单——原生测试只给分页标签,用 MTF 工具重写的 EA 能在同一画面追线,缺陷一眼可辨。外汇与贵金属杠杆高,跨周期信号仅作概率参考,实盘前务必在 MT5 历史数据自测。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double _iClose(class="type">class="kw">string symbol,class="type">int tf,class="type">int index) { if(index < class="num">0) class="kw">return(-class="num">1); class="type">class="kw">double buf[]; ENUM_TIMEFRAMES timeframe=TFMigrate(tf); if(CopyClose(symbol,timeframe, index, class="num">1, buf)>class="num">0) class="kw">return(buf[class="num">0]); else class="kw">return(-class="num">1); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double _iWPR(class="type">class="kw">string symbol, class="type">int tf, class="type">int period, class="type">int shift) { ENUM_TIMEFRAMES timeframe=TFMigrate(tf); class="type">int handle=iWPR(symbol,timeframe,period); if(handle<class="num">0) { Print("iWPR 对象没有创建: 错误 ",GetLastError()); class="kw">return(-class="num">1); } else class="kw">return(_CopyBuffer(handle,shift)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double _CopyBuffer(class="type">int handle,class="type">int shift) { class="type">class="kw">double buf[]; if(CopyBuffer(handle,class="num">0,shift,class="num">1,buf)>class="num">0) class="kw">return(buf[class="num">0]); class="kw">return(EMPTY_VALUE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double _CopyBuffer(class="type">int handle,class="type">int index,class="type">int shift) { class="type">class="kw">double buf[]; class="kw">switch(index) {
把旧周期整数映射成 MT5 枚举
老版指标常把周期写成整数(1、5、15…),直接塞进 iMA 之类的新接口会编译报错。下面这段 TFMigrate 函数就是做整数到 ENUM_TIMEFRAMES 的桥接,覆盖从当前周期到月线的常用档位。 switch 里 0 映射 PERIOD_CURRENT,1 对应 M1,5 对应 M5,60 对应 H1,1440 对应 D1,43200 对应 MN1;中间还补了 M2、M3、M4、M6、M10、M12 这些非标准分钟周期,避免老脚本在非常规周期上崩。 前面那段 _CopyBuffer 的 case 0~4 则是按缓冲区索引取指标线:CopyBuffer(handle,0~4,shift,1,buf) 成功就返回 buf[0],否则落到 EMPTY_VALUE。五个线索引对应主缓冲加四个附加缓冲,改指标时数错索引会直接拿到空值。 外汇和贵金属杠杆高、滑点随机,这类周期映射函数只解决兼容性,不预示任何方向;跑之前先在策略测试器用 2023 年 XAUUSD 的 M5 数据验证一遍返回值。
case class="num">0: if(CopyBuffer(handle,class="num">0,shift,class="num">1,buf)>class="num">0) class="kw">return(buf[class="num">0]); break; case class="num">1: if(CopyBuffer(handle,class="num">1,shift,class="num">1,buf)>class="num">0) class="kw">return(buf[class="num">0]); break; case class="num">2: if(CopyBuffer(handle,class="num">2,shift,class="num">1,buf)>class="num">0) class="kw">return(buf[class="num">0]); break; case class="num">3: if(CopyBuffer(handle,class="num">3,shift,class="num">1,buf)>class="num">0) class="kw">return(buf[class="num">0]); break; case class="num">4: if(CopyBuffer(handle,class="num">4,shift,class="num">1,buf)>class="num">0) class="kw">return(buf[class="num">0]); break; class="kw">default: break; } class="kw">return(EMPTY_VALUE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">return(_CopyBuffer(handle,shift) class="kw">return(_CopyBuffer(handle,class="num">0,shift) class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_TIMEFRAMES TFMigrate(class="type">int tf) { class="kw">switch(tf) { case class="num">0: class="kw">return(PERIOD_CURRENT); case class="num">1: class="kw">return(PERIOD_M1); case class="num">5: class="kw">return(PERIOD_M5); case class="num">15: class="kw">return(PERIOD_M15); case class="num">30: class="kw">return(PERIOD_M30); case class="num">60: class="kw">return(PERIOD_H1); case class="num">240: class="kw">return(PERIOD_H4); case class="num">1440: class="kw">return(PERIOD_D1); case class="num">10080: class="kw">return(PERIOD_W1); case class="num">43200: class="kw">return(PERIOD_MN1); case class="num">2: class="kw">return(PERIOD_M2); case class="num">3: class="kw">return(PERIOD_M3); case class="num">4: class="kw">return(PERIOD_M4); case class="num">6: class="kw">return(PERIOD_M6); case class="num">10: class="kw">return(PERIOD_M10); case class="num">12: class="kw">return(PERIOD_M12);
◍ 把数字周期码映射回图表周期
这段 switch 把 MT5 内部用的整数周期码转成可读的 PERIOD 常量:20 对应 M20,16385 起是 H1 到 H12 的序列(16385、16386、16387…16396 间隔多为 1 或 2),16408 是 D1,32769 是 W1,49153 是 MN1,未命中则回退 PERIOD_CURRENT。写跨周期指标时,直接用这些数字当参数容易踩坑,走一遍映射函数能让日志可读。 指标头设定了主图窗口画线、1 个 buffer、1 个 plot,DRAW_LINE 蓝色宽 1;输入参数里 tf=5(即 M5 基准)、maPeriod=13、Shift=0、MODE_SMA 配 PRICE_CLOSE,并限制 Bars_Calculated=500 根。开 MT5 把这段塞进 OnInit 前的周期解析,能避免把 16388 误认为 H6 这类低级 bug。 外汇与贵金属跨周期调用本身不放大风险,但 H12/D1 级别信号在消息面跳空时可能失效,验证前先用策略测试器跑 500 根历史柱看缓冲对齐。
case class="num">20: class="kw">return(PERIOD_M20); case class="num">16385: class="kw">return(PERIOD_H1); case class="num">16386: class="kw">return(PERIOD_H2); case class="num">16387: class="kw">return(PERIOD_H3); case class="num">16388: class="kw">return(PERIOD_H4); case class="num">16390: class="kw">return(PERIOD_H6); case class="num">16392: class="kw">return(PERIOD_H8); case class="num">16396: class="kw">return(PERIOD_H12); case class="num">16408: class="kw">return(PERIOD_D1); case class="num">32769: class="kw">return(PERIOD_W1); case class="num">49153: class="kw">return(PERIOD_MN1); class="kw">default: class="kw">return(PERIOD_CURRENT); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//---- 指标设置 class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#class="kw">property indicator_plots class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_color1 Blue class="macro">#class="kw">property indicator_width1 class="num">1 class=class="str">"cmt">//---- 输入参数 class="kw">input ENUM_TIMEFRAMES tf = class="num">5; class=class="str">"cmt">// 时间框架 class="kw">input class="type">int maPeriod = class="num">13; class=class="str">"cmt">// MA 周期数 class="kw">input class="type">int Shift = class="num">0; class=class="str">"cmt">// 偏移 class="kw">input ENUM_MA_METHOD InpMAMethod = MODE_SMA; class=class="str">"cmt">// 移动平均方法 class="kw">input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_CLOSE; class=class="str">"cmt">// 使用的价格 class="kw">input class="type">int Bars_Calculated = class="num">500; class=class="str">"cmt">//---- 指标缓冲区 class="type">class="kw">double ExtMA[]; class=class="str">"cmt">//---- 移动平均的 句柄 class="type">int MA_Handle; class=class="str">"cmt">//--- 所需计算的最小柱数 class="type">int ExtBarsMinimum; ENUM_TIMEFRAMES _tf; class="type">int pf; class=class="str">"cmt">//--- 我们将保留移动平均值指标中的数值 class="type">int bars_calculated=class="num">0; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { _tf=tf; ENUM_TIMEFRAMES timeframe;
「跨周期均线的绘制偏移与句柄初始化」
在 MT5 自定义指标里做跨周期 MA,最容易被忽略的是 PLOT_SHIFT 和 PLOT_DRAW_BEGIN 的换算。若外部传入时间框架 _tf 小于等于当前图表周期 timeframe,代码会强制把 _tf 设为当前周期,避免向下取数据出错;随后用 pf = MathFloor(_tf/timeframe) 算出周期系数,例如当前是 M5、_tf 是 M15,pf 就是 3,draw_begin 和 draw_shift 都要乘这个系数。 缓冲区映射和绘图属性设置必须落在 OnInit 里。SetIndexBuffer 把 ExtMA 绑到 0 号线,PlotIndexSetInteger 写 PLOT_DRAW_BEGIN 时减掉一个 pf,是为了让高周期均线在视觉上与本周期 K 线对齐;PlotIndexSetString 拼出的标签如 MA(15 50) 会直接显示在数据窗口。 MA_Handle 通过 iMA(NULL,_tf,maPeriod,0,...) 拿句柄,失败就 Print 错误并 return INIT_FAILED,这一步不处理会导致 OnCalculate 里取到空序列。ExtBarsMinimum 设为 draw_begin+draw_shift,OnCalculate 开头若 rates_total 小于它直接 return 0,实盘外汇或贵金属波动下少一根柱都可能让计算越界,属于高风险的边界遗漏。 下方代码给出 OnInit 尾部到 OnCalculate 开头的片段,逐行注释已译成中文,开 MT5 新建指标把这段粘进对应函数即可验证偏移效果。
class="type">int draw_shift=Shift;class=class="str">"cmt">// 初始值 PLOT_SHIFT class="type">int draw_begin=maPeriod;class=class="str">"cmt">// 初始值 PLOT_DRAW_BEGIN class=class="str">"cmt">//--- timeframe=_Period; if(_tf<=timeframe)_tf=timeframe;class=class="str">"cmt">// 如果时间框架小于或者等于当前, 把它设为 PERIOD_CURRENT pf=(class="type">int)MathFloor(_tf/timeframe);class=class="str">"cmt">// 计算用于 PLOT_DRAW_BEGIN, PLOT_SHIFT 和计算柱数的系数. draw_begin=maPeriod*pf;class=class="str">"cmt">// 计算 PLOT_DRAW_BEGIN draw_shift=Shift*pf;class=class="str">"cmt">// 计算 PLOT_SHIFT class=class="str">"cmt">//---- 指标缓冲区映射 SetIndexBuffer(class="num">0,ExtMA,INDICATOR_DATA); class=class="str">"cmt">//--- PlotIndexSetInteger(class="num">0,PLOT_DRAW_BEGIN,draw_begin-pf); class=class="str">"cmt">//设置开始画的第一个柱的索引 PlotIndexSetInteger(class="num">0,PLOT_SHIFT,draw_shift); class=class="str">"cmt">//设置画线的偏移 PlotIndexSetString(class="num">0,PLOT_LABEL,"MA("+class="type">class="kw">string(tf)+" "+class="type">class="kw">string(maPeriod)+")");class=class="str">"cmt">//用于数据窗口的名称 class=class="str">"cmt">//--- MA_Handle=iMA(NULL,_tf,maPeriod,class="num">0,InpMAMethod,InpAppliedPrice); class=class="str">"cmt">//取得 MA 的句柄 if(MA_Handle==INVALID_HANDLE) { Print("获取 MA 句柄失败!错误 ",GetLastError()); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- 设置精确度 IndicatorSetInteger(INDICATOR_DIGITS,_Digits); class=class="str">"cmt">//--- 所需计算的最小柱数 ExtBarsMinimum=draw_begin+draw_shift;class=class="str">"cmt">// 计算所需的最小柱数 class=class="str">"cmt">//--- 初始化结束 class="kw">return(INIT_SUCCEEDED); } 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(rates_total<ExtBarsMinimum) class="kw">return(class="num">0); class=class="str">"cmt">// 用于计算的柱数不够 class="type">int limit; class=class="str">"cmt">//--- 对数组元素使用时间序列索引 ArraySetAsSeries(time,true);
把跨周期 MA 缓冲接进主循环的实战写法
把移动平均线的句柄数据搬进当前指标数组,第一步是把目标数组设为时间序列方向:ArraySetAsSeries(ExtMA,true) 让最新柱落在索引 0,避免后面取价时正负号打架。 重新计算边界的判断很关键:当 prev_calculated 大于 rates_total、小于等于 0 或外部计算标记变更时,limit 直接拉到 rates_total-ExtBarsMinimum-1,相当于全量重算;否则只从 rates_total-prev_calculated+pf+1 往前补新柱,省掉重复劳动。若 Bars_Calculated 非零,再用 MathMin 掐一道上限。 主循环里用 _CopyBuffer 按时间索引取数,这个函数内部走 CopyBuffer(handle,0,start_time,1,buf),成功返回 buf[0],失败给 EMPTY_VALUE。外汇与贵金属波动跳空频繁,CopyBuffer 取不到值会静默返回空值,实盘前应在 MT5 用不同品种测一遍返回率。 Interpolate 开关控制插值:开启时,用 _iTime 找上一级周期对齐时刻,按相邻柱数 n 算 factor=1.0/n,再把中间柱用线性权重 ExtMA[i+k]=k*factor*ExtMA[i+n]+(1.0-k*factor)*ExtMA[i] 补全。这样小周期图上也能画出大周期 MA 的连续轨迹,黄金 M15 加载 H1 均线时肉眼可见断层被填平。 尾部那组 include 和 compTF 枚举不是装饰:Trade.mqh 等库留给下单模块,compTF 的 A/B/C/E 四档把 m1-m5-m15 到 h1-h4-d1 的组合固化下来,后面多周期确认信号直接传枚举值即可,不用每次硬写周期常量。
ArraySetAsSeries(ExtMA,true); class=class="str">"cmt">//--- 侦测起始位置 class=class="str">"cmt">//--- 计算所需复制的数据数量 class=class="str">"cmt">//--- 以及限制重新计算的柱的起始索引 if(prev_calculated>rates_total || prev_calculated<=class="num">0|| calculated!=bars_calculated)class=class="str">"cmt">// 检查计算指标的起始点 { limit=rates_total-ExtBarsMinimum-class="num">1; class=class="str">"cmt">// 用于计算所有柱的起始索引 } else { limit=(rates_total-prev_calculated)+pf+class="num">1; class=class="str">"cmt">// 用于计算新柱的起始索引 } if(Bars_Calculated!=class="num">0) limit=MathMin(Bars_Calculated,limit); class=class="str">"cmt">//--- 主循环 for(class="type">int i=limit;i>=class="num">0 && !IsStopped();i--) { ExtMA[i]=_CopyBuffer(MA_Handle,time[i]); } class=class="str">"cmt">//--- bars_calculated=calculated; class=class="str">"cmt">//+--------- CopyBuffer MA 句柄 ----------------------------------+ class="type">class="kw">double _CopyBuffer(class="type">int handle,class="type">class="kw">datetime start_time) { class="type">class="kw">double buf[]; if(CopyBuffer(handle,class="num">0,start_time,class="num">1,buf)>class="num">0) class="kw">return(buf[class="num">0]); class="kw">return(EMPTY_VALUE); } class=class="str">"cmt">//+-------------------- END -----------------------------------------+ class="kw">input class="type">bool Interpolate = true; class=class="str">"cmt">//--- 主循环 for(class="type">int i=limit;i>=class="num">0 && !IsStopped();i--) { class="type">int n; class="type">class="kw">datetime t=time[i]; ExtMA[i]=_CopyBuffer(MA_Handle,t); if(!Interpolate) class="kw">continue; class=class="str">"cmt">//--- class="type">class="kw">datetime times= _iTime(t); for(n = class="num">1; i+n<rates_total && time[i+n]>= times; n++) class="kw">continue; class="type">class="kw">double factor=class="num">1.0/n; for(class="type">int k=class="num">1; k<n; k++) ExtMA[i+k]=k*factor*ExtMA[i+n]+(class="num">1.0-k*factor)*ExtMA[i]; } class=class="str">"cmt">//--- class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">datetime _iTime(class="type">class="kw">datetime start_time) { if(start_time < class="num">0) class="kw">return(-class="num">1); class="type">class="kw">datetime Arr[]; if(CopyTime(NULL,_tf, start_time, class="num">1, Arr)>class="num">0) class="kw">return(Arr[class="num">0]); else class="kw">return(-class="num">1); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define EXPERT_MAGIC class="num">1234502 class=class="str">"cmt">//--- class="macro">#include <Trade\Trade.mqh> class="macro">#include <Trade\SymbolInfo.mqh> class="macro">#include <Trade\PositionInfo.mqh> class="macro">#include <Trade\AccountInfo.mqh> class="macro">#include <Trade\OrderInfo.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ enum compTF { A, class=class="str">"cmt">//m1-m5-m15 B, class=class="str">"cmt">//m5-m15-h1 C, class=class="str">"cmt">//m15-h1-h4 E class=class="str">"cmt">//h1-h4-d1 };