针对初学者的 MQL 5 中的自定义指标·进阶篇
📘

针对初学者的 MQL 5 中的自定义指标·进阶篇

第 2/2 篇

拆开一个 SMA 指标的三段骨架

把一段最基础的 MQL5 指标代码摊开看,整体其实只由三块构成:全局层、OnInit() 与 OnCalculate()。夹在第一段和第二段注释之间的全局代码不包在任何括号里,负责句柄与缓冲区这类全局声明。 这里有个容易混淆的点:MQL5 里“函数”的概念比数学里宽得多。数学函数收参数、返数值;而语言里的函数还能做字符串处理、下单、读写文件等副作用操作,别拿纯数学直觉去读指标源码。 最小可运行指标只需上述用户编写部分,但 MQL5 还允许挂一个 OnDeinit() 做退出清理。我们示例里没强制要求,所以留了空壳。下面这段就是空实现的去初始化函数,reason 参数传退出原因码,函数体目前什么都不做。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标去初始化函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+  
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
  }
在 MT5 里新建一个空白指标,把这段贴进编辑器编译,你会看到它不报错但也不画任何线——这正好验证了“最小函数集”的边界。外汇与贵金属行情高波动,用这类空壳做结构验证时勿直接接实盘信号。

MQL5 / C++
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 造成的头部空白是否符合预期,外汇和贵金属波动属性差异大,直接套用周期可能失真,属高风险操作。

MQL5 / C++
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 写进缓冲区。外汇与贵金属波动剧烈,这类均线只描述已发生均值,对后市方向仅具参考倾向,实盘须自担高风险。

MQL5 / C++
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 上验证不同周期均线的刷新延迟。外汇与贵金属杠杆高,参数回测仅反映历史概率,实盘仍可能快速回撤。

MQL5 / C++
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 参数,比读十遍文档更能摸清客户端怎么对待自定义缓冲区。

常见问题

缓冲区就是用来存每条 K 线对应 SMA 数值的数组,必须在 OnInit 里用 SetIndexBuffer 绑定,否则指标画不出来。
至少包含 OnInit 里设缓冲区和周期参数、OnCalculate 里算均值并填缓冲区、以及把指标绑到主图的价格窗口。
小布可以自动读取你加载的指标数值并比对标准 SMA,提示缓冲区越界或循环写错的地方,省去手动逐根核对。
大概率是 OnCalculate 里没处理前 (period-1) 根 K 线无完整均值的情形,起始索引要从 period-1 开始填值。
常见是循环里把价格取错成收盘价以外的值,或周期参数没和外置输入对齐,建议打印中间值逐根排查。