探索新功能:MQL5 的自定义指标·进阶篇
📊

探索新功能:MQL5 的自定义指标·进阶篇

(2/3)· 接上篇基础结构,深挖缓冲区、数组与 IndicatorCounted 如何决定指标重算效率

含代码示例实战向 第 2/3 篇
很多从 MQL4 切到 MQL5 的人直接套用旧指标写法,结果在 OnCalculate 里重复处理全量历史,CPU 占用莫名飙高。其实新终端的数据访问和计数机制已经变了,不理解就会写出看似能跑但拖累图表响应的指标。

◍ MQL5 怎么把历史数据抓进数组

MQL5 把数据访问直接放在数组层面做,不再像旧框架那样为每个值单独开数组再调 iCustom,计算开销明显降下来。核心动作是用系统函数 CopyBuffer 把指标缓冲区里的数据搬进本地数组,具体参数说明在平台帮助文档里能查到。 指标和静态数组能复制的上限受自身尺寸约束;如果是动态数组,拷贝量超过当前长度时,数组会自动扩容接住数据。下面这组函数覆盖了大部分历史访问场景:CopyRates 取 MqlRates 结构、CopyTime 取开盘时间、CopyOpen/High/Low/Close 分别取对应价格、CopyTickVolume 与 CopyRealVolume 取成交量、CopySpread 取点差。 历史数组没必要做成二维,实战里建议只用动态非指标缓冲区来存。有一个坑没人提前讲:当 CopyBuffer 的复制数量传 0 时,会抛出 4003 错误,所以拷贝元素数至少得是 1。外汇与贵金属行情跳空频繁,复制前最好先确认句柄有效,避免空缓冲导致数组异常。

「缓冲区类别与内存消耗的隐性代价」

MT5 的指标缓冲区在数量上不再设硬上限,开发者不必再为中间计算和群集指标手动规划存储结构。但缓冲区本质占内存:若把历史深度设到约 1,000,000 根 K 线,再把高密集群集指标挂到分钟图,终端吃掉数 Gb 内存并不意外,外汇和贵金属交易者跑多品种监控时要留意这台机器的物理内存余量。 缓冲区按用途分成三类。INDICATOR_DATA 是图上显示且能被 iCustom 读取的主缓冲区,必须最先注册,注册顺序错乱时代码能编译但通过 iCustom 取数可能画出错图;INDICATOR_COLOR_INDEX 紧跟主缓冲区之后注册,存颜色索引;INDICATOR_CALCULATIONS 只存中间结果,图上不显示,也不能被外部读取,强行访问会报 4806(找不到请求的数据)。 绘图样式对缓冲区数有硬要求:DRAW_HISTOGRAM2 和 DRAW_FILLING 各要 2 个,DRAW_CANDLES 与 DRAW_BARS 各要 4 个,其中除 DRAW_FILLING 外都支持多色模拟。对比 MQL4 每种颜色单独开缓冲区,MT5 单个缓冲区最多塞 63 色,能明显省内存,也把趋势分段着色这类逻辑写轻了。 颜色缓冲区有个坑:动态改色时最大颜色数受 #property indicator_colorN 里写的种数限制。比如属性只写两种,即便用 PlotIndexSetInteger 想动态扩到更多也无效,缓冲区直接不显示。要留余量就把初始种数写够,再动态降不升。

MQL5 / C++
class="macro">#class="kw">property indicator_buffers class="num">2      class=class="str">"cmt">// 使用的缓冲区数量
class="macro">#class="kw">property indicator_plots class="num">1        class=class="str">"cmt">// 显示缓冲区数量
class="type">int OnInit()
{
  class=class="str">"cmt">// ...
  SetIndexBuffer(class="num">0, V2, INDICATOR_DATA);
  SetIndexBuffer(class="num">1, V2C,INDICATOR_COLOR_INDEX);
  SetIndexBuffer(class="num">2, V4, INDICATOR_DATA);
  SetIndexBuffer(class="num">3, V4C,INDICATOR_COLOR_INDEX);
  SetIndexBuffer(class="num">4, V1, INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">5, V3, INDICATOR_CALCULATIONS);
  class=class="str">"cmt">// ...
  class="kw">return class="num">0;
}
class="macro">#class="kw">property indicator_color1 clrDarkSalmon, clrDeepSkyBlue class=class="str">"cmt">// 使用两种颜色
class="macro">#class="kw">property indicator_type1   DRAW_COLOR_LINE              class=class="str">"cmt">// 显示类型
class=class="str">"cmt">//---- 缓冲区
class="type">class="kw">double Values[];                 class=class="str">"cmt">// 数值缓冲区
class="type">class="kw">double ValuesPainting[];         class=class="str">"cmt">// 颜色索引缓冲区
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始函数                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
 {
class=class="str">"cmt">//--- 绑定缓冲区
class=class="str">"cmt">//--- 用于做显示缓冲区的数值
  SetIndexBuffer(class="num">0,Values,INDICATOR_DATA);
class=class="str">"cmt">//--- 用于做保存颜色缓冲区的数值
  SetIndexBuffer(class="num">1,ValuesPainting,INDICATOR_COLOR_INDEX);
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=class="str">"cmt">/*...*/)
 {
class=class="str">"cmt">//--- 用于计算的柱数
  class="type">int toCount=(class="type">int)MathMin(rates_total,rates_total-prev_calculated+class="num">1);
class=class="str">"cmt">//--- 尝试复制iRSI指标数据

RSI 缓冲拷贝与双色绘制的坑

在自定义指标里用 CopyBuffer 拉 RSI 句柄数据时,返回值判 -1 是最基本的安全闸。下面这段逻辑先拷数据,失败就打印错误码并 return(0),让下一轮 OnCalculate 带着 prev_calculated 重算,避免脏数据进绘图缓冲。 拷贝成功后,代码从 toCount-2 往 0 倒序扫 Values 数组,拿当前值与上一根 i+1 比:若上一根不是 EMPTY_VALUE 且当前大于上一根,ValuesPainting[i] 标 1,否则标 0。这其实是在给 RSI 主线做上升/非上升的两态着色标记。 着色声明那几行更值得留意:#property indicator_color1 后面跟多色列表时,MT5 会按缓冲索引循环取色。clrDarkSalmon, clrDeepSkyBlue 是两色交替,而用 #define C clrRed 再铺一长串 C 则是强制单色——肉眼看图表时,前者能直接区分 RSI 拐头方向,后者只是偷懒写法。外汇与贵金属波动剧烈,这类视觉辅助仅降低误读概率,不预示任何方向。 开 MT5 自建一个 RSI 着色指标,把 indicator_color1 改成双色,再对照 ValuesPainting 的 1/0 切换,能立刻验证倒序循环里 i+1 越界风险为零(因起点是 toCount-2)。

MQL5 / C++
  if(CopyBuffer(RSIHandle,class="num">0,class="num">0,toCount,Values)==-class="num">1)
    {
      Print("数据复制错误,错误码",GetLastError());
      class=class="str">"cmt">//--- 返回重新计算指标数据
      class="kw">return(class="num">0);
    }
class=class="str">"cmt">//--- 计算颜色部分
  for(class="type">int i=toCount-class="num">2;i>=class="num">0;--i)
    {
      class=class="str">"cmt">//--- 计算第一条线的颜色
      if(Values[i+class="num">1]!=EMPTY_VALUE && Values[i]>Values[i+class="num">1])
        ValuesPainting[i]=class="num">1;
      else
        ValuesPainting[i]=class="num">0;
    }
class=class="str">"cmt">//--- 返回值为下一次调用的 prev_calculated 值
  class="kw">return(rates_total);
  }
class="macro">#class="kw">property indicator_color1 clrDarkSalmon, clrDeepSkyBlue class=class="str">"cmt">// 使用两种颜色
class="macro">#class="kw">property indicator_color1  clrRed, clrRed, clrRed, clrRed, clrRed, clrRed, clrRed, clrRed, class=class="str">"cmt">//…
class="macro">#define C clrRed
class="macro">#class="kw">property indicator_color1  C, C, C, C, C, C, C, C, C, C, C, C, C, C, class=class="str">"cmt">//…

◍ 数组寻址与 CopyBuffer 的坑

直接按下标取数组元素前,先确认该数组的 AsSeries 属性。多维数组和静态数组没法设这个标志;而传给 OnCalculate 的数组可以设。AsSeries 决定索引 0 是代表最新一根 K 还是最旧一根,弄反了整个计算逻辑就歪了。 CopyBuffer 做数据复制时本身不依赖 AsSeries,但不同缓冲区行为不一样。指标缓冲区必须一次性复制完整可用历史深度,漏掉旧数据会导致序列错位。动态和静态(预定义大小)数组的复制方向是从当前向过去走。 函数会自动把动态缓冲区(指标除外)重设成所需大小,静态数组不建议用于复制——尺寸写死容易越界或丢数据。外汇和贵金属行情跳空频繁,缓冲区错位可能让信号失真,属高风险操作。 最稳的做法:给所有存历史数据的缓冲区都显式设 AsSeries;弄清每种缓冲区的结构差异;每次调用后查 _LastError。MT5 里新建指标时,打开「帮助」把 CopyBuffer 和 AsSeries 相关说明通读一遍,比盲抄代码省事。

「用 BarsCalculated 挡掉数据未就绪的坑」

新版指标架构里,IndicatorCounted 的争议基本消失了——它变成 OnCalculate 的上次返回值,终端直接给你 prev_calculated。绝大多数情况下,直接 return rates_total 就够用,也就是把当前所有柱的计数交回去。 但要注意一个现象:如果价格数据在上次调用后发生了变动,比如补历史缺口或重连后重载,prev_calculated 会被终端置为 0。此时若 OnCalculate 返回 0,指标值不会进 DataWindow;想在历史加载或断线重连时看指标并完成全量重算,返回 1 比返回 0 更稳。 对外汇、贵金属这类高波动品种,历史补全常发生在跳空之后,这种返回值的差异会直接影响你盘面看到的第一根重算K线。 BarsCalculated 是另一个实用函数,特别适合吃大量数据的 EA。它最典型的用法是判断依赖指标是否真的算完了:句柄刚建好、首次预计算还没跑完时,CopyBuffer 会报 4806(找不到请求的数据)。下面这段代码演示了如何用 BarsCalculated 轮询等待,再去二次复制。 别把正态当圣经 代码里 for 循环上限写死 1000 次,只是示例量级;实盘若指标极重,可能要按超时毫秒重算,而非死等次数。贵金属多周期共振时,这种等待策略直接决定 EA 会不会在数据空洞上开错单。

MQL5 / C++
<span class="keyword">class="type">int</span> <span class="functions">OnCalculate</span>(<span class="keyword">const</span> <span class="keyword">class="type">int</span> rates_total,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// 数组大小</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">const</span> <span class="keyword">class="type">int</span> prev_calculated,&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// 上一次调用后计算的柱数</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//...)</span>
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">class="kw">return</span> rates_total;
&nbsp;&nbsp;}
<span class="comment">class=class="str">"cmt">//--- 计算需要的柱数</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span> toCount=rates_total-(<span class="keyword">class="type">int</span>)<span class="functions">MathMax</span>(prev_calculated-<span class="number">class="num">1</span>,<span class="number">class="num">0</span>);
<span class="comment">class=class="str">"cmt">//--- 尝试复制iWPR指标数据</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span> copied=<span class="functions">CopyBuffer</span>(WPRHandle,<span class="number">class="num">0</span>,<span class="number">class="num">0</span>,toCount,Values);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> err=<span class="functions">GetLastError</span>();
<span class="comment">class=class="str">"cmt">//--- 检查复制结果</span>
&nbsp;&nbsp; <span class="keyword">if</span>(copied==-<span class="number">class="num">1</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- 如果错误码为4806,说明数据没有被上传</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(err==<span class="number">class="num">4806</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 等待数据上传</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>;i&lt;<span class="number">class="num">1000</span>;++i)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">BarsCalculated</span>(WPRHandle)&gt;<span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 再次尝试复制iWPR指标数据</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; copied=<span class="functions">CopyBuffer</span>(WPRHandle,<span class="number">class="num">0</span>,<span class="number">class="num">0</span>,rates_total,Values);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; err=<span class="functions">GetLastError</span>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="comment">class=class="str">"cmt">//--- 检查复制结果</span>
&nbsp;&nbsp; <span class="keyword">if</span>(copied==-<span class="number">class="num">1</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"读取WPR数值错误,最后错误是 "</span>,err,<span class="class="type">class="kw">string">" 柱数 "</span>,rates_total);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="comment">class=class="str">"cmt">//...</span>
把重算诊断交给小布
这些指标重算瓶颈和缓冲区异常,小布盯盘的 AIGC 已内置诊断,打开对应品种页即可看到调用耗时与警告,你专注策略逻辑就好。

常见问题

标准形接收价格数组与 begin 位置,另一种携带 rates_total 与 prev_calculated,用于精确控制增量计算;需要避免全量重算时选后者。
缓冲区数量本身开销有限,但每个缓冲区的绘图属性与刷新频率叠加后可能拖慢渲染,建议只声明实际使用的缓冲。
MQL5 数组需显式绑定缓冲区且索引方向可配,不像旧版隐式处理,写错方向会拿到反序数据。
目前小布针对标准品种页做重算与资源占用诊断,自编指标若挂在同一图表,其消耗会反映在内置面板里,便于你定位问题段。
MQL5 用 prev_calculated 替代旧版 IndicatorCounted,理解其返回语义才能写出不重复算历史的增量指标。