MQL5:创建自己的指标·进阶篇
📊

MQL5:创建自己的指标·进阶篇

(2/3)· 用 MQL5 向导生成 TSI 框架后,OnCalculate 与缓冲区组织才是真正动手写算法的起点

实战向 第 2/3 篇
很多人以为 MQL5 向导点完 Done 就拥有了可用指标,其实那只是一层空壳。缓冲区没接对、OnCalculate 没写算法,图表上连一条线都跑不出来。把向导当终点,是自定义指标最常见的误操作。

「改写 OnCalculate 的第一种形式」

OnCalculate() 是指标在 MT5 里响应重算事件的主入口,新订单到达、品种历史更新都会触发它。主计算逻辑必须塞进这个函数,辅助活儿可以拆到别的子函数里再被它调用。 MQL5 向导默认生成的是第二种形式,把开高低收、成交量、点差、周期开盘时间全暴露出来。但我们做单数据源指标只用第一种形式:只接一个 price[] 数组。这样改写后,指标既能挂在价格上,也能基于别的指标输出再算一层。 price[] 里装什么看参数选项卡:选 Close 就是收盘价;选 Typical Price 则每根 K 线变成 (高+低+收)/3。rates_total 是数组长度,循环里控边界离不了它;索引从 0 起、过去在左最新在右,price[rates_total-1] 永远是刚走完的那根。 下面这段是最简第一种形式骨架,返回值会原样变成下次调用的 prev_calculated,新手常漏看这点导致重复计算。

MQL5 / C++
class="type">int OnCalculate(class="kw">const class="type">int rates_total,      class=class="str">"cmt">// price[]数组大小;
                   class="kw">const class="type">int prev_calculated, class=class="str">"cmt">// 上次调用计算后的价格柱的数量
                   class="kw">const class="type">int begin,           class=class="str">"cmt">// price[]数组开始计算的索引
                   class="kw">const class="type">class="kw">double& price[])     class=class="str">"cmt">// 指标计算的依据数组
  {
class=class="str">"cmt">//---
class=class="str">"cmt">//--- 返回值会作为下一次调用的 prev_calculated 参数调用
   class="kw">return(rates_total);
  }

给 TSI 挂上六个隐藏缓冲层

图表上最终只画出一条 TSI 主线,但底层要先算清楚动量与其绝对值的双重平滑。按公式推导,除已显示的 TSIBuffer 外,还需 6 个双精度数组承接中间量:MTMBuffer 存 mtm,AbsMTMBuffer 存mtm,EMA_MTMBuffer 与 EMA2_MTMBuffer 分别存 EMA(mtm,r) 和 EMA(EMA(mtm,r),s),EMA_AbsMTMBuffer 与 EMA2_AbsMTMBuffer 对应绝对值的两次平滑。

这些数组必须在全局声明,并在 OnInit 里用 SetIndexBuffer 绑定。注意 indicator_buffers 要设成 7——1 个绘图数据加 6 个计算缓冲,漏改这个数 MT5 会直接报初始化失败。 中间数组统一标 INDICATOR_CALCULATIONS,它们不进副图画线,只留作逐步推导。下面这段代码把缓冲映射全列出来了,复制进 MQ5 文件就能编译过: 别把缓冲数写死成 6 新手常只数自己加的数组,忘了主图线也占一个坑。indicator_buffers 等于 7 是硬约束,外汇和贵金属行情跳空多,缓冲错位会导致历史回算从 begin 索引就乱掉,指标形态可能完全失真,这类品种交易本身高风险,验证前先开模拟盘跑一遍。

MQL5 / C++
class="macro">#class="kw">property indicator_buffers class="num">7
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_buffers class="num">7
class="macro">#class="kw">property indicator_plots  class="num">1
class=class="str">"cmt">//---- TSI 绘图属性
class="macro">#class="kw">property indicator_label1  "TSI"
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_color1  Blue
class="macro">#class="kw">property indicator_style1  STYLE_SOLID
class="macro">#class="kw">property indicator_width1  class="num">1
class=class="str">"cmt">//--- 输入参数
class="kw">input class="type">int      r=class="num">25;
class="kw">input class="type">int      s=class="num">13;
class=class="str">"cmt">//--- 指标缓冲区
class="type">class="kw">double        TSIBuffer[];
class="type">class="kw">double        MTMBuffer[];
class="type">class="kw">double        AbsMTMBuffer[];
class="type">class="kw">double        EMA_MTMBuffer[];
class="type">class="kw">double        EMA2_MTMBuffer[];
class="type">class="kw">double        EMA_AbsMTMBuffer[];
class="type">class="kw">double        EMA2_AbsMTMBuffer[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓冲区映射关系
   SetIndexBuffer(class="num">0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(class="num">1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(class="num">2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(class="num">3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(class="num">4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(class="num">5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(class="num">6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
class=class="str">"cmt">//---
   class="kw">return(class="num">0);
  }
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=class="str">"cmt">// price[]数组大小;
                 class="kw">const class="type">int prev_calculated,class=class="str">"cmt">// 次调用计算后的价格柱的数量;
                 class="kw">const class="type">int begin,          class=class="str">"cmt">// price[]数组开始计算的索引; 
                 class="kw">const class="type">class="kw">double& price[])    class=class="str">"cmt">// 指标计算的依据数组;
  {
class=class="str">"cmt">//---
class=class="str">"cmt">//--- 返回值会作为下一次调用的 prev_calculated 参数调用
   class="kw">return(rates_total);
  }

◍ 差值缓冲与指数均线的落地做法

MTM 指标的中间层先把逐根 K 线的价格变动算出来:从索引 1 到 rates_total-1 遍历价格序列,把 price[i]-price[i-1] 写进 MTMBuffer[],把它的绝对值写进 AbsMTMBuffer[]。这两步只是搬运差值,真正麻烦的是怎么在数组上求指数平均。 MQL5 标准库里没有「直接对数组算 MA」的内置函数,但 Include 目录下的 MovingAverages.mqh 提供了现成实现,路径通常是终端安装目录/MQL5/Include/MovingAverages.mqh。它支持 SMA、EMA、SMMA、LWMA 四种经典算法,自己重写容易出错,引用库更稳。 实际只要 ExponentialMAOnBuffer() 一个函数:它吃原始数组、吐出均值到目标数组。下面这段代码就是差值计算和引库的最小可用骨架,开 MT5 建个自定义指标把缓冲区和 include 补上就能跑。

MQL5 / C++
class=class="str">"cmt">//--- 计算 mtm 和 |mtm| 的数值
  for(class="type">int i=class="num">1;i<rates_total;i++)
    {
      MTMBuffer[i]=price[i]-price[i-class="num">1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
    }
class="macro">#include <MovingAverages.mqh>

「两组均线函数与缓冲区的延迟起点」

MovingAverages.mqh 里八个平滑函数分两组:SimpleMA、ExponentialMA、SmoothedMA、LinearWeightedMA 只在指定位置返回单次均值,适合零散取数但不适合循环写数组;另一组带 OnBuffer 后缀的函数直接把 price[] 算好的均线填进 buffer[],并且认 AS_SERIES 索引方向,循环里跑更省事。 带 OnBuffer 的函数参数里 rates_total、prev_calculated、begin 跟 OnCalculate 同义。begin 标的是源数组里‘有意义数据’的起始下标——比如 MTMBuffer[0] 没定义,真实数据从 1 开始,所以 begin 得传 1。 均线填进输出数组时自带迟滞:period=10 时,结果从 begin+period-1 也就是 begin+9 才出现。次要指数均线若主周期 r、次周期 s,且从索引 r 起算,那 EMA2_xxxBuffer 里的值就从 r+s-1 起有数,前面都是空。 别把正态当圣经:用 OnBuffer 系做双层 EMA 时,循环算指标值一定要从 r+s-1 起步,否则前半段除以未初始化缓冲会出野值。下面这段就是双层指数平均后算 TSI 的核心,F5 编译进 MT5 能直接看线。

MQL5 / C++
class=class="str">"cmt">//--- 计算数组的首要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         class="num">1,   class=class="str">"cmt">// 索引, 开始从哪个数据开始平滑计算 
                         r,   class=class="str">"cmt">// 指数平均的周期
                         MTMBuffer,        class=class="str">"cmt">// 计算平均的缓冲区
                         EMA_MTMBuffer);   class=class="str">"cmt">// 存储计算结果的缓冲区
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         class="num">1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);
class=class="str">"cmt">//--- 计算数组的次要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
class=class="str">"cmt">//--- 现在计算指标的数值
   for(class="type">int i=r+s-class="num">1;i<rates_total;i++)
     {
      TSIBuffer[i]=class="num">100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }

用 prev_calculated 掐掉重复算力

写出的指标能跑和跑得顺是两回事。OnCalculate() 里若每次都把 MTMBuffer[] 和 AbsMTMBuffer[] 从头算一遍,碰到 price[] 长度冲到几万甚至上百万根 K 线时,CPU 资源会被无谓的重复运算吃满,跟处理器强弱关系不大。 破法在 prev_calculated:它是上次 OnCalculate() return 的值,首次调用恒为 0,此时全量填充缓冲区;后续调用只需从 prev_calculated-1 的索引接着算最后几根。下面这段是优化后的核心骨架,注意 start 的切换逻辑。

MQL5 / C++
class="type">int OnCalculate(class="kw">const class="type">int rates_total,    class=class="str">"cmt">// price[]数组大小;
                 class="kw">const class="type">int prev_calculated,class=class="str">"cmt">// 次调用计算后的价格柱的数量;
                 class="kw">const class="type">int begin,          class=class="str">"cmt">// price[]数组开始计算的索引; 
                 class="kw">const class="type">class="kw">double &price[])    class=class="str">"cmt">// 指标计算的依据数组;
  {
class=class="str">"cmt">//--- 计算 mtm 和 |mtm| 的数值
   MTMBuffer[class="num">0]=class="num">0.0;
   AbsMTMBuffer[class="num">0]=class="num">0.0;
   for(class="type">int i=class="num">1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-class="num">1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
class=class="str">"cmt">//--- 计算数组的首要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         class="num">1,   class=class="str">"cmt">// 索引, 开始从哪个数据开始平滑计算 
                         r,   class=class="str">"cmt">// 指数平均的周期
                         MTMBuffer,        class=class="str">"cmt">// 计算平均的缓冲区
                         EMA_MTMBuffer);  class=class="str">"cmt">// 存储计算结果的缓冲区
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         class="num">1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);
class=class="str">"cmt">//--- 计算数组的次要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
class=class="str">"cmt">//--- 现在计算指标的数值
   for(class="type">int i=r+s-class="num">1;i<rates_total;i++)
     {
      TSIBuffer[i]=class="num">100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
class=class="str">"cmt">//---返回值会作为下一次调用的 prev_calculated 参数调用
   class="kw">return(rates_total);
  }
class=class="str">"cmt">//--- 如果这是第一次调用 
   if(prev_calculated==class="num">0)
     {
      class=class="str">"cmt">//--- 把索引0的数据设为0
     MTMBuffer[class="num">0]=class="num">0.0;
     AbsMTMBuffer[class="num">0]=class="num">0.0;
     }
class=class="str">"cmt">//--- 计算 mtm 和 |mtm| 的数值
   class="type">int start;
   if(prev_calculated==class="num">0) start=class="num">1;    class=class="str">"cmt">// 设置从 MTMBuffer[] 和 AbsMTMBuffer[] 的索引1开始计算填写数据
   else start=prev_calculated-class="num">1;      class=class="str">"cmt">// 设置从上一次计算的最后一个索引开始计算填写数据 
   for(class="type">int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-class="num">1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
逐行拆:函数头四个参数里 prev_calculated 就是上轮返回的柱数;首轮 prev_calculated==0 时把 start 置 1,从第一根差分开始;否则 start=prev_calculated-1,只补算新进来的柱。EMA 系列调用不用动,因为 ExponentialMAOnBuffer() 内部已按最优写过,只需对 TSIBuffer[] 沿用同一招。 return(rates_total) 把当前总柱数交还终端,下轮变成 prev_calculated 传入。这样无论行情跑多久,指标缓冲区的重算量都被锁在新增栏位上,外汇与贵金属品种在高波动时段可能频繁推送新 tick,这种写法能明显降低卡顿概率。

MQL5 / C++
class="type">int OnCalculate(class="kw">const class="type">int rates_total,    class=class="str">"cmt">// price[]数组大小;
                 class="kw">const class="type">int prev_calculated,class=class="str">"cmt">// 次调用计算后的价格柱的数量;
                 class="kw">const class="type">int begin,          class=class="str">"cmt">// price[]数组开始计算的索引; 
                 class="kw">const class="type">class="kw">double &price[])    class=class="str">"cmt">// 指标计算的依据数组;
  {
class=class="str">"cmt">//--- 计算 mtm 和 |mtm| 的数值
   MTMBuffer[class="num">0]=class="num">0.0;
   AbsMTMBuffer[class="num">0]=class="num">0.0;
   for(class="type">int i=class="num">1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-class="num">1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
class=class="str">"cmt">//--- 计算数组的首要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         class="num">1,   class=class="str">"cmt">// 索引, 开始从哪个数据开始平滑计算 
                         r,   class=class="str">"cmt">// 指数平均的周期
                         MTMBuffer,        class=class="str">"cmt">// 计算平均的缓冲区
                         EMA_MTMBuffer);  class=class="str">"cmt">// 存储计算结果的缓冲区
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         class="num">1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);
class=class="str">"cmt">//--- 计算数组的次要移动平均
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
class=class="str">"cmt">//--- 现在计算指标的数值
   for(class="type">int i=r+s-class="num">1;i<rates_total;i++)
     {
      TSIBuffer[i]=class="num">100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
class=class="str">"cmt">//---返回值会作为下一次调用的 prev_calculated 参数调用
   class="kw">return(rates_total);
  }
class=class="str">"cmt">//--- 如果这是第一次调用 
   if(prev_calculated==class="num">0)
     {
      class=class="str">"cmt">//--- 把索引0的数据设为0
     MTMBuffer[class="num">0]=class="num">0.0;
     AbsMTMBuffer[class="num">0]=class="num">0.0;
     }
class=class="str">"cmt">//--- 计算 mtm 和 |mtm| 的数值
   class="type">int start;
   if(prev_calculated==class="num">0) start=class="num">1;    class=class="str">"cmt">// 设置从 MTMBuffer[] 和 AbsMTMBuffer[] 的索引1开始计算填写数据
   else start=prev_calculated-class="num">1;      class=class="str">"cmt">// 设置从上一次计算的最后一个索引开始计算填写数据 
   for(class="type">int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-class="num">1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

◍ 指标数值的首次与循环填充

在自定义指标的 OnCalculate 收尾阶段,真正给缓冲区写值的逻辑很直接:先用 prev_calculated 判断是不是第一次跑。若为 0,就把 start 设成 r+s-1,也就是前导平滑周期耗尽后的第一根可计算 K 线,避免拿空数据做除法。 之后一个从 start 到 rates_total 的 for 循环,把每根 K 线的 TSI 值算出来写进 TSIBuffer:用 100 乘以双重平滑动量 EMA2_MTMBuffer[i],再除以双重平滑绝对动量 EMA2_AbsMTMBuffer[i]。这个 100 的缩放系数让 TSI 落在近似 -100 到 100 的区间,方便在 MT5 副图直接看背离。 函数最后 return rates_total,这个值会被 MT5 在下一次 tick 时当作 prev_calculated 传回来,从而跳过已算过的柱子只补新值。外汇与贵金属波动受杠杆与突发事件影响大,TSI 仅作动量概率参考,实盘前请在策略测试器用对应品种 Tick 数据验证计算起点是否如预期。

MQL5 / C++
class=class="str">"cmt">//--- 现在计算指标的数值
  if(prev_calculated==class="num">0) start=r+s-class="num">1; class=class="str">"cmt">// 第一次计算的开始位置
  for(class="type">int i=start;i<rates_total;i++)
    {
      TSIBuffer[i]=class="num">100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
    }
class=class="str">"cmt">//--- 返回值会作为下一次调用的 prev_calculated 参数调用
  class="kw">return(rates_total);
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 TSI 双平滑后的趋势与超买超卖区,你不必每次手动编译调试。

常见问题

不一定相等。缓冲区数量可多于绘图数量,多余缓冲区常作为辅助数组存放中间计算结果,绘图只绑定其中需要可视化的部分。
倾向写成 input int 参数,方便在指标加载时调整而不重新编译;宏定义会固化周期,不利于多品种测试。
它们决定本次调用需要计算的新柱数量。忽略 prev_calculated 会造成重复计算或数组越界,是自定义指标卡顿的主因之一。
可以。将编译好的 ex5 放入对应终端 Indicators 目录,小布盯盘对应品种页会读取其缓冲并在副图渲染,无需额外配置。
指数平滑起始段样本不足,概率上前 r+s 根柱偏差较大,常见做法是起始索引跳过或做种子值填充,避免误判信号。