自置缓存的指标速度比较·进阶篇

自置缓存的指标速度比较·进阶篇

(2/3)· 句柄编号不连续、缓存是否加速,三个实验把 MQL5 指标访问的暗坑摆上台面

新手友好 第 2/3 篇
很多人默认指标句柄从 0 连续编号,写循环时直接按整数序列访问,结果在多线程图表里取到空句柄。句柄其实是终端按唯一实例维护的计数器,和图表顺序无关。用错编号逻辑,EA 在加载多个品种时就会静默失效。

「指标句柄重复创建拖慢AUDJPY回测」

下面这段 MT5 日志来自 AUDJPY 的 M15 图表,时间集中在 2018.02.18 07:53:13 到 07:53:26 的 13 秒内。同一个 iMACD(10) 句柄在 ChartID 131571247244850509 和 131571247244850510 上被连续创建了 11 次,其中 07:53:20.218 这一秒就新建了 7 次。 重复建句柄不会直接报错,但每次 create handle 都占用内核资源;日志末尾显示 131571247244850509 上的 remove handle 返回了 true 后又返回 false,说明释放动作并非一次性干净退出。外汇与贵金属品种点差和跳动频繁,这类冗余调用在实盘可能放大延迟。 开 MT5 按 F11 调出专家日志,搜 'create handle iMACD' 看自己 EA 是否也出现同 ChartID 短时间多次建柄。若命中,优先把句柄放到全局或 OnInit 里只建一次。

◍ iMACD句柄释放的日志陷阱

在 AUDJPY 的 M15 图表上跑指标句柄管理时,日志会暴露一个容易被忽略的现象:2018.02.18 07:53:26.716 这一毫秒内,ChartID 131571247244850509 连续打印了 4 条 remove handle iMACD(10): false,说明 IndicatorRelease 对该句柄的释放调用返回了失败,且同一时刻被重复触发了 4 次。

  • 毫秒后的 07:53:36.116,新 ChartID 131571247244850510 的首条记录显示 remove handle 返回了 true,但紧接着的 5 条同 ChartID 日志又全是 false。也就是说,同一个图表上下文里,释放成功只发生了一次,后续重复调用全部失败。

这种重复 false 不是行情问题,而是 EA 在 OnDeinit 或定时器里没有做句柄有效性判断,导致对已经无效或正在释放的 iMACD 句柄反复调用 IndicatorRelease。外汇与贵金属交易本身杠杆高、风险大,这类资源泄漏在长周期挂机时可能拖慢终端,建议直接开 MT5 把这段日志对照自己的指标释放逻辑查一遍。

MQL5 / C++
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">26.716 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850509: AUDJPY,M15, remove handle iMACD(class="num">10): false
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">26.716 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850509: AUDJPY,M15, remove handle iMACD(class="num">10): false
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">26.716 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850509: AUDJPY,M15, remove handle iMACD(class="num">10): false
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">26.716 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850509: AUDJPY,M15, remove handle iMACD(class="num">10): false
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">36.116 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850510: AUDJPY,M15, remove handle iMACD(class="num">10): true
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">36.117 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850510: AUDJPY,M15, remove handle iMACD(class="num">10): false
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">36.117 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850510: AUDJPY,M15, remove handle iMACD(class="num">10): false
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">36.117 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850510: AUDJPY,M15, remove handle iMACD(class="num">10): false
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">36.117 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850510: AUDJPY,M15, remove handle iMACD(class="num">10): false
class="num">2018.02.class="num">18 class="num">07:class="num">53:class="num">36.117 iMACD and IndicatorRelease(AUDJPY,M15) ChartID: class="num">131571247244850510: AUDJPY,M15, remove handle iMACD(class="num">10): false

句柄缓存的甜头与暗坑

在 M1 周期跑一轮压力测试:生成 26 169 180 个逐笔报价、370 355 根柱线,同时访问 9 个指标并在每次 tick 取数。结果显示,套了句柄缓存的 MQL4 风格比裸写 MQL4 风格快出一截,但跟原生 MQL5 句柄访问比,依然彻底落败。 单指标场景也验证了同一结论:把 Use indicator 设 true、计时器关掉,MQL5 test 的耗时明显低于 Cache test 的两种变体。如果你在 EA 里高频读指标,直接走 MQL5 原生 iMACD 句柄是最省心的路。 别把句柄缓存当保险箱。它最大的漏洞是不校验句柄是否还活着——一旦你在 OnTimer() 里手动删了句柄(比如假设它等于 10),缓存层根本不知道,下次还把旧句柄丢出去。实测会直接抛 4807(ERR_INDICATOR_WRONG_HANDLE),说明缓存侧没有任何有效性控制。 外汇与贵金属市场高杠杆、高波动,这类底层取数 bug 可能在极端行情放大滑点与逻辑失效,上实盘前务必在策略测试器复现。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                MQL5 test.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.000"
class=class="str">"cmt">//--- 输入参数
input class="type">bool      UseOneIndicator=false;   class=class="str">"cmt">// 使用指标: "false" -> class="num">9 指标, "true" - class="num">1 指标
class=class="str">"cmt">//---
class="type">int             arr_handle_iMACD[];      class=class="str">"cmt">// 保存 iMACD 指标句柄的数值
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统初始化函数                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   if(UseOneIndicator)
      ArrayResize(arr_handle_iMACD,class="num">1);
   else
      ArrayResize(arr_handle_iMACD,class="num">9);
   if(!CreateHandle(arr_handle_iMACD))
      class="kw">return(INIT_FAILED);
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">//---
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逐笔报价函数                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+

「批量建 MACD 句柄与取值的实战写法」

做多周期或多参数扫描时,手动一个个建 iMACD 句柄太低效。下面这段把句柄塞进数组统一创建,快线周期从 30 起步、每档加 10,慢线固定 26、信号 9,用收盘价计算。 CreateHandle 里若任意句柄返回 INVALID_HANDLE,会打印品种、周期和错误码并直接 return false,避免半吊子句柄进后续逻辑。实盘跑下来,若 Symbol() 在 EURUSD、Period() 是 PERIOD_M1,失败提示会精确落到具体错误码,方便你定位是参数越界还是内存吃紧。 取数走 iMACDGet,用 CopyBuffer 按 buffer 编号(0 为主线、1 为信号线)和 index 抽单根柱的值;复制失败返 0.0 并被上层视为未计算。注意原文 fast_ema_repiod 是拼写错误,编译能过但变量名丑,建议改名 fast_ema_period。 末尾 include 的 SimpleCallMQL4.mqh 仅用于无缓存句柄的测试路径,正式策略里若不用可删,少一个依赖少一处崩点。外汇与贵金属波动剧烈,这类批量建指标在高波动段可能因报价中断导致句柄失效,务必在 OnTick 外层做有效性轮询。

MQL5 / C++
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   class="type">int arr_size=ArraySize(arr_handle_iMACD);
   for(class="type">int i=class="num">0;i<arr_size;i++)
     {
       class="type">class="kw">double macd_main_30=iMACDGet(arr_handle_iMACD[i],MAIN_LINE,class="num">0);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CreateHandle                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateHandle(class="type">int &arr_handles[])
  {
   class="type">int arr_size=ArraySize(arr_handles);
   for(class="type">int i=class="num">0;i<arr_size;i++)
     {
       class="type">int fast_ema_repiod=class="num">30+class="num">10*i;
       class=class="str">"cmt">//--- 创建 iMACD 指标的句柄
       arr_handles[i]=iMACD(NULL,class="num">0,fast_ema_repiod,class="num">26,class="num">9,PRICE_CLOSE);
       class=class="str">"cmt">//--- 如果句柄未能创建 
       if(arr_handles[i]==INVALID_HANDLE)
         {
          class=class="str">"cmt">//--- 告之失败并输出错误代码 
          PrintFormat("无法为品种 %s/%s 创建 iMACD 指标的句柄, 错误代码 %d",
                      Symbol(),
                      EnumToString(Period()),
                      GetLastError());
          class=class="str">"cmt">//--- 指标提前停止 
          class="kw">return(false);
         }
     }
   class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取 iMACD 缓存区数值                                                            |
class=class="str">"cmt">//|  缓存区编号如下:                                                            |
class=class="str">"cmt">//|   class="num">0 - MAIN_LINE, class="num">1 - SIGNAL_LINE                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
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];
class=class="str">"cmt">//--- 重置错误代码 
   ResetLastError();
class=class="str">"cmt">//--- 用索引为 class="num">0 的指标缓冲区中的数值填充 iMACDBuffer 数组的一部分 
   if(CopyBuffer(handle_iMACD,buffer,index,class="num">1,MACD)<class="num">0)
     {
      class=class="str">"cmt">//--- 如果复制失败,告之错误代码 
      PrintFormat("无法从 iMACD 指标复制数据,错误代码 %d",GetLastError());
      class=class="str">"cmt">//--- 以零结果退出 - 这意味着该指标被视为未计算 
      class="kw">return(class="num">0.0);
     }
   class="kw">return(MACD[class="num">0]);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <SimpleCall\SimpleCallMQL4.mqh> class=class="str">"cmt">// 用于测试无缓存句柄

◍ 句柄缓存与定时器释放的坑

在 MT5 里用 iMACD 取 30 周期 MACD 主线,若走带缓存句柄的封装(SimpleCallMQL4Caching),重复声明同变量名 macd_main_30 两次并不会报错,但第二次赋值纯属冗余,徒增阅读干扰。 测试时在 OnTimer 中调用 IndicatorRelease(10) 返回 true,本意是主动释放句柄,结果紧接着 iMACD 的 CopyBuffer 连续刷出 4 次 error=4807(句柄无效)。这说明定时器里提前释放后,后续同周期指标调用会直接失效。 外汇与贵金属行情下这类隐性失效很致命:你以为缓存省了开销,实则把信号链打断。开 MT5 把 IndicatorRelease 放到 OnDeinit 而不是 OnTimer,再观察 CopyBuffer 是否还报 4807,就能复现并定位问题。

MQL5 / C++
class=class="str">"cmt">//class="macro">#include <SimpleCall\SimpleCallMQL4Caching.mqh> // 用于测试缓存句柄
class=class="str">"cmt">//class="macro">#include <SimpleCall\SimpleCallString.mqh> // 用于测试字符串
class=class="str">"cmt">//class="macro">#include <SimpleCall\SimpleCallMQL4.mqh> // 用于测试无缓存句柄
class="macro">#include <SimpleCall\SimpleCallMQL4Caching.mqh> class=class="str">"cmt">// 用于测试缓存句柄
class=class="str">"cmt">//class="macro">#include <SimpleCall\SimpleCallString.mqh> // 用于测试字符串
  class="type">class="kw">double macd_main_30=iMACD(NULL,class="num">0,class="num">30,class="num">26,class="num">9,PRICE_CLOSE,MODE_MAIN,class="num">0);
  class="type">class="kw">double macd_main_30=iMACD(NULL,class="num">0,class="num">30,class="num">26,class="num">9,PRICE_CLOSE,MODE_MAIN,class="num">0);
class=class="str">"cmt">//class="macro">#include <SimpleCall\SimpleCallMQL4.mqh> // 用于测试无缓存句柄
class="macro">#include <SimpleCall\SimpleCallMQL4Caching.mqh> class=class="str">"cmt">// 用于测试缓存句柄
class=class="str">"cmt">//class="macro">#include <SimpleCall\SimpleCallString.mqh> // 用于测试字符串
OnTimer, IndicatorRelease(class="num">10)=true
iMACD: CopyBuffer error=class="num">4807
iMACD: CopyBuffer error=class="num">4807
iMACD: CopyBuffer error=class="num">4807
iMACD: CopyBuffer error=class="num">4807

三种句柄缓存实现的实测定价

把指标句柄存起来复用,核心就一条:按品种、周期、参数先查缓存,命中就直接吐数据,没命中才走 IndicatorCreate 并写回缓存。原文里给了三条路——结构数组、字符串数组、类封装,速度差异很实在。 字符串方案最劝退。基准测试里,MQL4 风格无缓存访问耗时 0:01:40.953;用『+』拼参数字符串跑到 0:05:20.953;换 StringConcatenate 也要 0:04:12.672。后者比『+』慢 21%,但整体比基准慢 2.5 倍,外汇贵金属高频调用下这种开销会直接吃掉滑点空间,字符串缓存可以扔了。 结构数组靠静态 Handles[] 配合 SMacd 类型的 Inputs 对象,首次进入建好就不再重建,GetHandle 里查不到才进 SHandle::GetHandle 创建。类缓存则给每个指标建 CHandle 静态对象,里头用 m_handle 和 m_indicators 两个数组分别存句柄和 CiIndicators 参数类,用 MQL4 风格封装跑完全程约 2 分 30 秒,工程上更干净。 下面这段 SHandle 模板结构就是结构数组方案的骨架,私有成员 Handle 与 Inputs 分离,重载『==』和『=』让缓存比对和写入变成一行事。开 MT5 把 SimpleCallMQL4Caching.mqh 挂上,改 iMACD 周期参数跑一遍,能直接看到句柄复用后重复调用的耗时塌下去。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//class="macro">#include &lt;SimpleCall\SimpleCallMQL4.mqh&gt; // 用于测试无缓存句柄</span>
<span class="preprocessor">class="macro">#include </span>&lt;SimpleCall\SimpleCallMQL4Caching.mqh&gt; <span class="comment">class=class="str">"cmt">// 用于测试缓存句柄</span>
<span class="comment">class=class="str">"cmt">//class="macro">#include &lt;SimpleCall\SimpleCallString.mqh&gt; // 用于测试字符串</span>
<span class="comment">...</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 结构 CHandle&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp; |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="kw">template</span>&lt;<span class="keyword">class="kw">typename</span> T&gt;
<span class="keyword">class="kw">struct</span> SHandle
&nbsp;&nbsp;{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Handle;
&nbsp;&nbsp; T&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Inputs;
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//| 含有初始化列表的构造函数&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; |</span>
&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;&nbsp;&nbsp;&nbsp;&nbsp; SHandle() : Handle(<span class="macro">INVALID_HANDLE</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//| 操作符 "==" 重载&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; |</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
&nbsp;&nbsp; <span class="keyword">class="type">bool</span> <span class="keyword">class="kw">operator</span>==(<span class="keyword">const</span> T &amp;Inputs2) <span class="keyword">const</span>
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="keyword">this</span>.Inputs == Inputs2);
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//| 操作符 "=" 重载&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; |</span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span> <span class="keyword">class="kw">operator</span>=(<span class="keyword">const</span> T &amp;Inputs2)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">this</span>.Inputs=Inputs2;
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到句柄占用与缓存命中状态,不用自己写打印脚本逐个图表排查。

常见问题

终端为每个唯一指标实例维护独立计数器,重启后基础编号可能从 10 起跳,且不同图表相同指标也各自计数,并非全局连续序列。
可以,小布盯盘对应品种页已集成句柄占用与缓存命中观测,省去手动加载测试 EA 的步骤。
通过保存 CreateIndicator 返回的句柄并复用,避免每次 OnTick 重新创建;文末实验展示其在重复访问时的响应差异。
不会冲突,终端按图表 ID 与实例分别计数,但编号空间不共享,不能跨图表用同一整数索引访问。