自置缓存的指标速度比较·进阶篇
(2/3)· 句柄编号不连续、缓存是否加速,三个实验把 MQL5 指标访问的暗坑摆上台面
「指标句柄重复创建拖慢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 把这段日志对照自己的指标释放逻辑查一遍。
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 可能在极端行情放大滑点与逻辑失效,上实盘前务必在策略测试器复现。
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 外层做有效性轮询。
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,就能复现并定位问题。
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 周期参数跑一遍,能直接看到句柄复用后重复调用的耗时塌下去。
<span class="comment">class=class="str">"cmt">//class="macro">#include <SimpleCall\SimpleCallMQL4.mqh> // 用于测试无缓存句柄</span> <span class="preprocessor">class="macro">#include </span><SimpleCall\SimpleCallMQL4Caching.mqh> <span class="comment">class=class="str">"cmt">// 用于测试缓存句柄</span> <span class="comment">class=class="str">"cmt">//class="macro">#include <SimpleCall\SimpleCallString.mqh> // 用于测试字符串</span> <span class="comment">...</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 结构 CHandle |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="kw">template</span><<span class="keyword">class="kw">typename</span> T> <span class="keyword">class="kw">struct</span> SHandle { <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">int</span> Handle; T Inputs; <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 含有初始化列表的构造函数 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> SHandle() : Handle(<span class="macro">INVALID_HANDLE</span>) { } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 操作符 "==" 重载 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> <span class="keyword">class="kw">operator</span>==(<span class="keyword">const</span> T &Inputs2) <span class="keyword">const</span> { <span class="keyword">class="kw">return</span>(<span class="keyword">this</span>.Inputs == Inputs2); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 操作符 "=" 重载 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> <span class="keyword">class="kw">operator</span>=(<span class="keyword">const</span> T &Inputs2) { <span class="keyword">this</span>.Inputs=Inputs2; } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>