针对初学者的 MQL 5 中的自定义指标·进阶篇
拆开一个 SMA 指标的三段骨架
把一段最基础的 MQL5 指标代码摊开看,整体其实只由三块构成:全局层、OnInit() 与 OnCalculate()。夹在第一段和第二段注释之间的全局代码不包在任何括号里,负责句柄与缓冲区这类全局声明。 这里有个容易混淆的点:MQL5 里“函数”的概念比数学里宽得多。数学函数收参数、返数值;而语言里的函数还能做字符串处理、下单、读写文件等副作用操作,别拿纯数学直觉去读指标源码。 最小可运行指标只需上述用户编写部分,但 MQL5 还允许挂一个 OnDeinit() 做退出清理。我们示例里没强制要求,所以留了空壳。下面这段就是空实现的去初始化函数,reason 参数传退出原因码,函数体目前什么都不做。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标去初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { }
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标去初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { }
「编译后指标在 MT5 里的运行脉络」
在 MetaEditor 里给 SMA.mq5 按下 Compile,生成的 SMA.ex5 才是 MetaTrader 5 客户端真正能加载的二进制指标文件。.mq5 只是纯文本源码,不编译就拖进图表毫无反应,这是新手最容易卡住的一步。 从 Navigator 把指标挂上图表后,客户端先跑全局层代码一次,随后调用 OnInit() 执行一次初始化,之后每来一个新报价就触发 OnCalculate() 重算。若指标里写了 OnDeInit(),在移除指标或切换周期时会被调用,用来清理图形对象等残留。 全局层适合放声明周期贯穿整个指标运行的变量,它们在整个程序块可见且会记住数值;只该执行一次的逻辑(如常量设定)必须塞进 OnInit(),塞进 OnCalculate() 等于每跳报价都重做一遍,纯属浪费。 外汇与贵金属市场高杠杆、报价跳变频繁,OnCalculate() 里的冗余计算会直接拖慢盯盘终端;理清这三段式生命周期,你才能动手改 SMA 的窗口参数而不把终端跑卡。
◍ SMA 指标的 MT5 代码骨架与缓冲区逻辑
在 MT5 写自定义 SMA,开头用 #property 系列指令声明指标属性:indicator_chart_window 让它画在主图,indicator_buffers 1 和 indicator_plots 1 表明只用 1 个缓冲区和 1 条线,indicator_type1 DRAW_LINE 指定线形,indicator_color1 Red 给红色。注意这些指令行末尾没有分号,它们本质是另一种写法的常量定义,不是可执行语句。 用户可调的参数只有两个:MAPeriod(默认 13,平均周期)和 MAShift(默认 0,水平平移柱数),用 input 修饰后,注释文字会直接显示在指标 Properties 窗口里,所以注释要写得比变量名更直白。 ExtLineBuffer[] 是全局动态数组,声明在 OnInit 之外。原因很实际:它要被 SetIndexBuffer 绑成第 0 号指标缓冲区,在 OnCalculate 里参与计算,还要一直存值供图表重绘,全局化才能跨函数块存活到指标卸载。 OnInit 里三行内置函数就够:SetIndexBuffer(0, ExtLineBuffer, INDICATOR_DATA) 完成绑定;两个 PlotIndexSetInteger 分别管水平平移和绘图起点(PLOT_DRAW_BEGIN 传 MAPeriod,因为前 MAPeriod-1 根柱没东西可平均)。 OnCalculate 的输入参数是 const,由客户端传值不能改。rates_total 是当前可用柱数,prev_calculated 是上轮算过的柱数,begin 是起始索引,price[] 是价格序列。柱排序从左到右索引 0 是最旧。函数末尾 return(rates_total),客户端下次调用就把这个值塞进 prev_calculated,据此只算新柱。 主循环逻辑很典型:先判柱数够不够(比如 MAPeriod=200 但只有 100 根柱就直接 return 0),再定 first 索引——首次调用 prev_calculated=0 全算,之后只从 prev_calculated-1 往后算;循环里对每根 bar 向前累加 MAPeriod 根价格再除周期,得出 SMA 值写进缓冲区。 别把正态当圣经 代码里 MAPeriod 写死 13 只是示例,实盘换品种前先开 MT5 用默认参数加载,看 PLOT_DRAW_BEGIN 造成的头部空白是否符合预期,外汇和贵金属波动属性差异大,直接套用周期可能失真,属高风险操作。
class=class="str">"cmt">//---- 这个指标会在主窗口中绘图 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//---- 一个缓冲区用于指标的计算和绘图 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//---- 只有一个绘图 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//---- 指标绘成线形 class="macro">#class="kw">property indicator_type1 DRAW_LINE class=class="str">"cmt">//---- 指标线的颜色是红色 class="macro">#class="kw">property indicator_color1 Red class=class="str">"cmt">//---- 指标输入参数 class="kw">input class="type">int MAPeriod = class="num">13; class=class="str">"cmt">//平均周期 class="kw">input class="type">int MAShift = class="num">0; class=class="str">"cmt">//水平转换 (柱数) class=class="str">"cmt">//---- 动态数组的声明 class=class="str">"cmt">//将被用于指标缓冲区 class="type">class="kw">double ExtLineBuffer[]; class="type">void OnInit() { class=class="str">"cmt">//---- 把动态数组 ExtLineBuffer 用作指标的第0个缓冲区 SetIndexBuffer(class="num">0,ExtLineBuffer,INDICATOR_DATA); class=class="str">"cmt">//---- 根据MAShift设置绘图中水平轴的基础转换 PlotIndexSetInteger(class="num">0,PLOT_SHIFT,MAShift); class=class="str">"cmt">//---- 根据MAPeriod设置绘图柱起点 PlotIndexSetInteger(class="num">0,PLOT_DRAW_BEGIN,MAPeriod); } class="type">int OnCalculate( class="kw">const class="type">int rates_total, class=class="str">"cmt">// 当前订单下的历史中可用的柱数 class="kw">const class="type">int prev_calculated,class=class="str">"cmt">// 前一订单计算后的柱数 class="kw">const class="type">int begin, class=class="str">"cmt">// 第一个柱的索引 class="kw">const class="type">class="kw">double &price[] class=class="str">"cmt">// 用于计算的价格数组 ) class=class="str">"cmt">//---- 检查出现的柱的数目是否足够用于计算
手搓一个主图 SMA 指标该写的骨架
想在 MT5 主图里挂一条自己的简单移动平均线,核心就是申请一个缓冲区、设好绘图属性,再在 OnCalculate 里把均值算出来写进去。下面这段是可直接编译运行的精简骨架,默认周期 13、偏移 0,线画成红色。 初始化里只做三件事:把动态数组 ExtLineBuffer 绑到第 0 号缓冲区;用 PlotIndexSetInteger 把水平偏移设为 MAShift;把绘图起点设为 MAPeriod,避免前 12 根柱因数据不足画出无效值。 计算函数要先拦掉数据不够的情况——rates_total 小于 MAPeriod-1+begin 时直接 return(0)。首次加载 prev_calculated==0,就从 MAPeriod-1+begin 这根柱开始;之后只处理新柱,first 取 prev_calculated-1,省掉重算历史。 主循环对每根 bar 向内累加 MAPeriod 个 price 求 Sum,再除以 MAPeriod 得 SMA 写进缓冲区。外汇与贵金属波动剧烈,这类均线只描述已发生均值,对后市方向仅具参考倾向,实盘须自担高风险。
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 Red class="kw">input class="type">int MAPeriod = class="num">13; class="kw">input class="type">int MAShift = class="num">0; class="type">class="kw">double ExtLineBuffer[]; class="type">void OnInit() { SetIndexBuffer(class="num">0,ExtLineBuffer,INDICATOR_DATA); PlotIndexSetInteger(class="num">0,PLOT_SHIFT,MAShift); PlotIndexSetInteger(class="num">0,PLOT_DRAW_BEGIN,MAPeriod); } 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">int begin, class="kw">const class="type">class="kw">double &price[]) { if(rates_total<MAPeriod-class="num">1+begin) class="kw">return(class="num">0); class="type">int first,bar,iii; class="type">class="kw">double Sum,SMA; if(prev_calculated==class="num">0) first=MAPeriod-class="num">1+begin; else first=prev_calculated-class="num">1; for(bar=first;bar<rates_total;bar++) { Sum=class="num">0.0; for(iii=class="num">0;iii<MAPeriod;iii++) Sum+=price[bar-iii]; SMA=Sum/MAPeriod; ExtLineBuffer[bar]=SMA; } class="kw">return(rates_total); }
「手写 SMA 指标的核心循环怎么跑」
在 MT5 自定义指标里,OnCalculate 的入参决定了你能在多少根柱上动手:rates_total 是当前历史里可用的柱数,prev_calculated 是上一次调用已经算过的柱数,begin 是有效数据的起始索引,price[] 则是传入的价格序列。 代码里先卡一道门槛:if (rates_total < MAPeriod - 1 + begin) return(0); 意思是历史柱数必须至少比均线周期多 begin 根,否则连第一个有效 SMA 都凑不出来,直接退出避免数组越界。 首次加载时 prev_calculated 为 0,起点 first 设为 MAPeriod-1+begin,也就是从能算出第一根均线的位置开始;后续 tick 进来只需从 prev_calculated-1 接着算新柱,省掉重复劳动。 主循环里对每个 bar 做 MAPeriod 次累加得到 Sum,再除以 MAPeriod 得 SMA,写进 ExtLineBuffer[bar]。把这段直接贴进你的指标文件,改 MAPeriod 就能在 EURUSD 的 M5 上验证不同周期均线的刷新延迟。外汇与贵金属杠杆高,参数回测仅反映历史概率,实盘仍可能快速回撤。
class="kw">const class="type">int rates_total, class=class="str">"cmt">// 当前订单下历史中可用的柱数 class="kw">const class="type">int prev_calculated, class=class="str">"cmt">// 前一订单计算的柱数 class="kw">const class="type">int begin, class=class="str">"cmt">// 第一个柱的索引 class="kw">const class="type">class="kw">double &price[] class=class="str">"cmt">// 用于计算的价格数组 ) { class=class="str">"cmt">//----+ class=class="str">"cmt">//---- 检查出现的柱数是否足够用于计算 if (rates_total < MAPeriod - class="num">1 + begin) class="kw">return(class="num">0); class=class="str">"cmt">//---- 局部变量声明 class="type">int first, bar, iii; class="type">class="kw">double Sum, SMA; class=class="str">"cmt">//---- 主循环中计算第一个起点的索引 if(prev_calculated==class="num">0) class=class="str">"cmt">// 检查是否是第一个起点 first=MAPeriod-class="num">1+begin; class=class="str">"cmt">// 所有柱的起点 else first=prev_calculated-class="num">1; class=class="str">"cmt">// 新柱的起点 class=class="str">"cmt">//----计算的主循环 for(bar = first; bar < rates_total; bar++) { Sum=class="num">0.0; class=class="str">"cmt">//---- 为计算平均做循环累加 for(iii=class="num">0;iii<MAPeriod;iii++) Sum+=price[bar-iii]; class=class="str">"cmt">// 相当于 Sum = Sum + price[bar - iii]; class=class="str">"cmt">//---- 计算平均值 SMA=Sum/MAPeriod; class=class="str">"cmt">//---- 把指标缓冲区元素的值设为我们计算的SMA值 ExtLineBuffer[bar]=SMA; } class=class="str">"cmt">//----+ class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 从指标原理到自行验证的下一步
自定义指标和 MT5 客户端的交互核心,就是 OnInit 里声明缓冲区、OnCalculate 里用 prev_calculated 增量刷新、再用 iCustom 把信号喂给 EA。本文附的 sma.mq5(1.74 KB)和 sma_.mq5(2.73 KB)是两个可直接编译的参照实现,初学者把文件丢进 MQL5/Indicators 目录就能在图表上看到效果。 评论区有个易错点值得记一下:用户 nellypingos 在 2022-02-23 提到,当 rates_total 刚好等于 MA_Period-1 的首次调用,原 if 判断可能跳过计算,这提醒我们复制代码后要在 EURUSD 的 M1 上跑一遍,确认前 MA_Period 根 K 线都有值。 外汇和贵金属杠杆高、滑点随机,任何指标都只是概率辅助;打开 MT5 把 sma_.mq5 加载到 XAUUSD,调一下 MA_Period 参数,比读十遍文档更能摸清客户端怎么对待自定义缓冲区。