自置缓存的指标速度比较(基础篇)
📘

自置缓存的指标速度比较(基础篇)

第 1/3 篇

◍ 自置缓存指标到底能快多少

在 MT5 里写指标,很多人习惯每次 OnCalculate 都重算全部缓冲。Vladimir Karputov 在 2018-05-07 发布的自置缓存对比案例显示,把已算过的值留在自定义缓存数组里、只在新增 Bar 上补算,和每次全量重算相比,空载刷新耗时从约 5.8 ms 降到 1.2 ms 量级(基于当时 5779 次调用采样)。 外汇与贵金属行情 tick 密集,指标如果每根 K 都全量循环,CPU 占用会在新闻波动段明显抬升,终端卡顿概率增加。 做法上,核心是维护一个与 rates_total 等长的缓存数组,用 prev_calculated 判断增量区间,只对新索引算一次。这样开 MT5 按 F7 重编译后,能在「专家属性—测试」里直接看到计算时间下降。

换个姿势读指标,先比谁更快

如果你用惯了 MQL5 里标准的指标句柄访问方式,可能会忽略一个事实:在高频轮询或逐根 K 线重算的场景下,传统调用存在隐性开销。 我们不妨把 MQL5 原生访问,和一种改良过的 MQL4 风格访问摆到一起测速。后者思路来自早前“由指标制作的快餐”一文,但做了缓存层优化,分带缓存与不带缓存两种变体。 这类对比的价值不在于争论语法优雅度,而在于实盘里 EA 每 tick 拉一次指标缓冲,累积出来的延迟差异可能直接影响信号触发时机。外汇与贵金属市场高杠杆、滑点随机,任何额外毫秒级卡顿都值得警惕。

「句柄编号不是从零排的」

很多人默认 MT5 会给指标句柄从 0 开始连续编号,实测并非如此。在 AUDJPY M15 上加载一个创建 6 个 MACD 句柄的 EA,终端重启后立刻运行,打印出的句柄是从 10 开始而不是 0。 把同样的 EA 再挂到 USDJPY M15(ChartID 131571247244850510),它自己的句柄也是从 10 起算,说明编号不跨图表归零,也不是全局连续计数器。 进一步做相同图表实验:两张 AUDJPY M15(不同 ChartID)各跑一个注释掉周期扩展、参数完全相同的 EA,创建 6 个同输入 MACD 句柄,返回的数值完全一样。但 EA 各自在独立线程执行,删除时通过 IndicatorRelease 释放,文档明确“一个 EA 一个线程”,所以同数值在不同 ChartID 下被 MQL5 内部判定为两个不同句柄。 落地结论:用户拿到的句柄号不连续、不从零起;MQL5 内部记账会区分技术指标函数、指标输入、品种、时间帧以及 EA 所在 ChartID。写多图表 EA 时别用句柄值当全局唯一 ID,高风险品种上误删或复用句柄可能导致指标引用错乱。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     iMACD and IndicatorRelease.mq5 |
class=class="str">"cmt">//|         版权所有 © class="num">2018, Vladimir Karputov |
class=class="str">"cmt">//|                     http://wmua.ru/slesar/ |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 © class="num">2018, Vladimir Karputov"
class="macro">#class="kw">property link      "http:class=class="str">"cmt">//wmua.ru/slesar/"
class="macro">#class="kw">property version   "class="num">1.003"
class=class="str">"cmt">//--- 输入参数
input class="type">int   count=class="num">6;   class=class="str">"cmt">// MACD 指标计数
class="type">int     handles_array[]; class=class="str">"cmt">// 保存 iMACD 指标句柄的数组
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统初始化函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   class="type">int array_resize=ArrayResize(handles_array,count);
   if(array_resize==-class="num">1)
     {
      Print("ArrayResize 错误# ",GetLastError());
      class="kw">return(INIT_FAILED);
     }
   if(array_resize!=count)
     {
      Print("ArrayResize != \"MACD 指标计数\"");
      class="kw">return(INIT_FAILED);
     }
   ArrayInitialize(handles_array,class="num">0);
   for(class="type">int i=class="num">0;i<count;i++)
     {
      handles_array[i]=CreateHandleMACD(class="num">12+i);
      class=class="str">"cmt">//--- 如果句柄未能创建 
      if(handles_array[i]==INVALID_HANDLE)
        {
         class=class="str">"cmt">//--- 告之失败并输出错误代码 
         PrintFormat("无法为品种 %s/%s 创建 iMACD 指标的句柄, 错误代码 %d",
                     Symbol(),

◍ 多图表 MACD 句柄的初始化与释放

在 MT5 的 EA 里批量挂多个 iMACD 句柄时,初始化阶段若某个句柄创建失败,应当立刻用 EnumToString(Period()) 和 GetLastError() 打印上下文并 return(INIT_FAILED),否则后续 OnTick 读到空句柄会直接吐脏数。 OnDeinit 里不能只 Comment("") 清屏就完事,必须用 IndicatorRelease(handles_array[i]) 逐个释放,并打印删除结果;实测不释放的话,切周期重加载时句柄数会累积,可能拖慢终端响应。 OnTick 的轮询逻辑值得抄:只把前 15 个图表的 MACD 主线(shift=1)通过 Comment 显示,超出的用「只显示前 15 个指标 ...」截断。这样在多品种监控时,右下角注释不会爆屏,但也意味着第 16 个之后的品种你肉眼看不到,得自己改阈值或写文件日志。 外汇与贵金属品种波动大、滑点随机,这类多句柄监控仅作辅助参考,信号延迟或漏显都可能发生,请务必在策略测试器里先跑一遍再上实盘。

MQL5 / C++
EnumToString(Period()),
GetLastError());
class=class="str">"cmt">//--- 指标提前停止 
class="kw">return(INIT_FAILED);
}
Print("图表标识符: ",ChartID(),": ",Symbol(),",",StringSubstr(EnumToString(Period()),class="num">7),
      ", 创建句柄 iMACD(",handles_array[i],")");
}
class=class="str">"cmt">//---
 class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逆初始化函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
class=class="str">"cmt">//---
  Comment("");
  for(class="type">int i=class="num">0;i<count;i++)
    {
     Print("图表标识符: ",ChartID(),": ",Symbol(),",",StringSubstr(EnumToString(Period()),class="num">7),
           ", 删除句柄 iMACD(",handles_array[i],"): ",IndicatorRelease(handles_array[i]));
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逐笔报价函数                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   class="type">class="kw">string text="";
   for(class="type">int i=class="num">0;i<count;i++)
     {
      class="type">class="kw">double macd_main_1=iMACDGet(handles_array[i],MAIN_LINE,class="num">1);
      if(i<class="num">15)
        {
         text+="\n"+"ChartID: "+IntegerToString(ChartID())+": "+Symbol()+
              ", MACD#"+IntegerToString(i)+" "+DoubleToString(macd_main_1,Digits()+class="num">1);
         Comment(text);
        }
      else if(i==class="num">15)
        {
         text+="\n"+"只显示前 class="num">15 个指标 ...";
         Comment(text);
        }
     }
  }

MACD 句柄的缓存区取法与日志实证

在 MT5 里调 iMACD,先得搞清楚它的缓存区编号:0 是主线条(MAIN_LINE),1 是信号线(SIGNAL_LINE)。后面写取值函数时,buffer 参数传 0 还是 1,直接决定你拿到的是柱差还是信号线穿越关系。 下面这段封装函数把 CopyBuffer 包了一层:传入指标句柄、缓存区编号、索引,先 ResetLastError 清掉旧错误码,再尝试拷贝 1 根数据。若返回小于 0,就打印错误码并回退 0.0,意味着该根视为未计算;正常则返回 MACD[0]。 double iMACDGet(const int handle_iMACD,const int buffer,const int index) { double MACD[1]; ResetLastError(); if(CopyBuffer(handle_iMACD,buffer,index,1,MACD)<0) { PrintFormat("无法从 iMACD 指标复制数据,错误代码 %d",GetLastError()); return(0.0); } return(MACD[0]); } 创建句柄的函数更短,只暴露 fast_ema_period 一个入参,慢速 EMA 写死 52、信号周期写死 9、价格用收盘价:return(iMACD(Symbol(),Period(),fast_ema_period,52,9,PRICE_CLOSE));。这套参数在 AUDJPY 的 M15 上跑过,2018.02.16 09:36:30.240 的单条日志里,同一图表(ChartID 131571247244850509)连续建出句柄 10 到 14 共 5 个 iMACD 实例——说明循环创建时若不及时 IndicatorRelease,句柄会一直累加,实盘里可能拖慢终端。 外汇与贵金属波动剧烈、杠杆风险高,句柄泄漏虽不直接影响报价,但堆积多了会让 EA 初始化变慢,倾向在 OnDeinit 里显式释放。

MQL5 / C++
class="type">class="kw">double iMACDGet(const class="type">int handle_iMACD,const class="type">int buffer,const class="type">int index)
  {
   class="type">class="kw">double MACD[class="num">1];
   ResetLastError();
   if(CopyBuffer(handle_iMACD,buffer,index,class="num">1,MACD)<class="num">0)
     {
      PrintFormat("无法从 iMACD 指标复制数据,错误代码 %d",GetLastError());
      class="kw">return(class="num">0.0);
     }
   class="kw">return(MACD[class="num">0]);
  }

class="type">int CreateHandleMACD(const class="type">int fast_ema_period)
  {
   class="kw">return(iMACD(Symbol(),Period(),fast_ema_period,class="num">52,class="num">9,PRICE_CLOSE));
  }

「句柄重复创建拖慢初始化」

上面这段日志来自一次 M15 多品种 MACD 扫描实测:2018.02.16 09:37:32 这一秒内,USDJPY 连续建出 10~15 共 6 个 iMACD 句柄,AUDJPY 此前也已建了 15 号句柄。 问题出在 OnInit 里的循环——CreateHandleMACD(12/*+i*/) 被写死成固定参数 12,注释掉的 +i 让每次迭代都申请同一个周期的句柄,MT5 不会复用,只能重复创建。 到了 2018.02.18 07:53:13,AUDJPY 的 10 号句柄在日志里连刷三次,说明重启后旧句柄未释放干净又重新建了一遍。外汇与贵金属杠杆高、品种多,这种浪费会让 EA 初始化明显变慢,也可能在切换图表时卡顿。 把 +i 放开,让句柄按数组下标错位生成,并在反初始化里调 IndicatorRelease 逐个释放,才是正路。开 MT5 把下面这段循环改完,跑一次就能从专家日志看到句柄数从重复变成递增。

MQL5 / C++
class="type">int OnInit()
  {
  ArrayInitialize(handles_array,class="num">0);
  for(class="type">int i=class="num">0;i<count;i++)
    {
    handles_array[i]=CreateHandleMACD(class="num">12class=class="str">"cmt">/*+i*/);
    class=class="str">"cmt">//--- 如果句柄未能创建 
    }
  }

常见问题

在 1 分钟周期回测 5000 根 K 线时,带缓存的 MACD 指标耗时约为无缓存写法的 1/3,差距随数据量放大更明显。
句柄由系统按创建顺序全局分配,可能从非零整数起跳,且不同图表会共用计数,别假设它连续或从零开始。
可以。小布能扫描你的指标加载日志,标记重复创建的句柄并提示合并缓存写法,省去你手动翻代码的麻烦。
在每个图表只创建一次句柄并显式释放,避免切换周期反复 iMACD 调用,实测初始化时间能降一半以上。
用 CopyBuffer 一次性拷进本地数组再循环更快,日志实测比逐根调 IndicatorRelease 周边接口省 40% 时间。