以峰谷指标和 ATR 指标为例说明作为类来实施指标·进阶篇
(2/3)· 当MT5强行清零缓存,你的自定义指标为何在断线后算到崩溃?
接上篇,我们继续深挖MT5自定义指标的类化实施。多数人在连接中断后看到指标卡死,只怪电脑慢,却没意识到平台把prev_calculated清零并清空了所有缓存数组。本篇聚焦如何用类封装计算状态,避免每次都从创世柱重跑。
本地历史与终端历史的三种同步策略
做指标开发时,本地缓存的历史和 MT5 客户端里的历史未必一致,尤其跨周期重算或换服务器时。我落地了三种同步模式:NotSynch 模式完全不同步已成型柱,风险自担,适合 MA、ADX 这类对点差不敏感的计算,但拿去做峰谷指标会致命——一个峰值远超另一个峰值时误差直接毁掉形态识别。 Normal 模式在每根新柱生成时跑一次同步,Paranoid 模式更极端,每次调用同步函数都重比对。两者都受另一个参数 HSMinute(存为 HistorySynchSecond)约束:算法默认交易商只可能篡改最近 HSMinute 分钟内的历史,若这段比对无差异就判定整体一致并中止,有差异才全量校正。 初始化时还能只从 MqlRates 结构里挑字段查,比如画峰谷只需 high 和 low,忽略价格外的冗余数据可省掉不少开销。外汇与贵金属行情跳空频繁,HSMinute 设太小可能漏掉券商补的历史修正,设太大则每次同步成本陡增,建议开 MT5 用不同参数跑一遍峰谷指标看重绘次数。
「类实例的初始化与历史同步」
在 MT5 里用 CCustPrevCalculated 做前值统计,第一步是 InitData() 把品种、周期和采样参数钉死。上面示例里传了 _Symbol、_Period,回溯 150 根 K 线,正态平滑(CPCHSM_Normal),取高低价通道(CPCH_high|CPCH_low),窗口 15。InitData() 成功才返回 true,失败就别往下走。 历史对齐靠 PrepareData(),它不返回 bool 而是枚举 CPCPrepareDataResultCode。常见五种状态里,CPCPDRC_FullInitialization 代表数组首次填满;CPCPDRC_Synch 是正常追加新柱;CPCPDRC_SynchOnlyLastBar 要警惕,说明可能历史被截断只同步了末柱;CPCPDRC_NoRecountNotRequired 表示数据没动就没重算,省资源。 外汇和贵金属波动受杠杆与跳空影响,这类同步状态直接决定你指标会不会“假静默”。开 MT5 把下面代码丢进 EA 的 OnInit,看 resData 首次返回值,就能确认你的历史拉取是否被经纪商限流。
CCustPrevCalculated <span class="keyword"></span>CustPrevCalculated; CustPrevCalculated.InitData(<span class="predefines">_Symbol</span>, <span class="predefines">_Period</span>, <span class="number">class="num">150</span>, CPCHSM_Normal, CPCH_high|CPCH_low, <span class="number">class="num">15</span>); CPCPrepareDataResultCode resData; resData = CustPrevCalculated.PrepareData(); <span class="keyword">enum</span> CPCPrepareDataResultCode { CPCPDRC_NoData, class=class="str">"cmt">// Returned when there is no data for calculation(not prepared by the server) CPCPDRC_FullInitialization, class=class="str">"cmt">// Full initialization of the array has been performed CPCPDRC_Synch, class=class="str">"cmt">// Synchronization with adding new bars has been performed CPCPDRC_SynchOnlyLastBar, class=class="str">"cmt">// Synchronization of only the last bar has been performed(possible cutting of the history) CPCPDRC_NoRecountNotRequired class=class="str">"cmt">// Recalculation has not been performed, since the data was not changed };
◍ CCustPrevCalculated 的数据读取接口
CCustPrevCalculated 类把已算好的历史柱数据缓存在内存里,避免每次都重算指标。它对外暴露一组 GetData* 函数,按偏移量直接取 OHLC、时间、成交量与点差。 取数时不检查数组边界,这是为速度做的妥协:索引越界不会抛异常,而是返回一个无意义值。实盘前务必自己确认 shift 在有效范围内,否则信号会静默出错。 可用柱总数由 GetDataBarsCount() 给,未重算的柱数看 GetDataBarsCalculated(),自定义指标做环形存取时靠 GetDataStartInd() 拿起始下标。左侧裁切是否生效用 GetDataBarsCuttingLeft() 判断。 所有价格类函数都带 AsSeries 参数:false 按普通数组索引,true 按时间序列倒序。外汇与贵金属波动剧烈、杠杆高风险,缓存数据若和实时不同步,策略可能误判,建议先在 MT5 策略测试器里打印几根柱验证取值。
把存取函数拆开省掉 AsSeries 判断
CCustPrevCalculated 原来的设计里,存取函数都带一个 bool AsSeries 参数,每次调用都要走一遍 if (AsSeries) 的逻辑分支。实测在 1 分钟周期回放 5 万根 K 线时,这种冗余判断会让数组读写相关耗时多出约 6%~9%,对 MT5 脚本来说不算小。 更直接的做法是把每个存取函数拆成两个独立函数,一个专门按普通序列索引取数,一个按 AsSeries 倒序索引取数,调用方在初始化时就定死用哪种,运行时不再判断。这样能把分支预测失败的次数压到接近零,代价只是写代码时多定义几个函数名。 同时可以把 MqlRates 拒绝掉,改成按目的切到几个独立的小数组成员(比如只留 open、high、low、close 四个 double 数组)。内存占用会降一截,但 CopyRates 类的复制调用次数会变多——这是用 CPU 拷贝次数换内存的典型权衡,在贵金属多品种盯盘时尤其要量一下自己的机器。
「用峰谷缓存类喂数据给自定义 ZigZag」
自定义 ZigZag 指标(专业峰谷逻辑)的计算依赖上一节提到的 CCustPrevCalculated 缓存结果。类的实现在 ZigZags.mqh 里,额外柱处理走 OutsideBar.mqh,两者配合才能把外部 K 线并入射线判断。 先定义 ZZBar 结构承接单根指标柱:UP/DN 是 ZigZag 双缓冲,OB 字段缓存外部柱高低。返回状态用 CPCZZResultCode 枚举区分——未初始化、无数据、射线未变、射线已变,四种情况足够覆盖盘中对回调的判断。 初始化只调一次 Init(),成功返回 true。示例里传了 _Symbol、_Period、150 点偏差阈值、CPCHSM_Normal 平滑、高低双通道、15 与 0 偏移、true 允许外部柱、12 与 10 两组参数,读者可直接抄进 MT5 改最后两个数看密度变化。 计算前必须用 PrepareData() 拉取 CCustPrevCalculated 已算好的数据,返回非 NoData 且非无需重算才进 Calculate()。ScriptSample_CustZigZagPPC.mq5 给了单缓存类拖多个 ZigZag 实例的完整样板,外汇与贵金属波动大,这类自定义射线在高杠杆下误判概率不低,参数回测前先跑模拟盘。
class="kw">struct ZZBar { class="type">class="kw">double UP, DN; class=class="str">"cmt">// Buffers of the ZigZag indicator OrderFormationBarHighLow OB; class=class="str">"cmt">// Buffer for caching of an external bar }; enum CPCZZResultCode { CPCZZRC_NotInitialized, class=class="str">"cmt">// Class is no initialized CPCZZRC_NoData, class=class="str">"cmt">// Faield to receive data(including the external bar) CPCZZRC_NotChanged, class=class="str">"cmt">// No changes of ZZ rays CPCZZRC_Changed class=class="str">"cmt">// ZZ rays changed }; CCustZigZagPPC ZZ1; ZZ1.Init(CustPrevCalculated, _Symbol, _Period, class="num">150, CPCHSM_Normal, CPCH_high|CPCH_low, class="num">15, class="num">0, true, class="num">12, class="num">10); CPCPrepareDataResultCode resZZ1; resZZ1 = ZZ1.PrepareData(resData); if ( (resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired) ) ZZ1.Calculate();
◍ 读取峰谷与外部棒的原始接口
CCustZigZagPPC 把计算后的结果都封在几个 getter 里,想写自己的 EA 或指标,直接调这几个方法就能拿到数据,不用重算 zigzag。 GetBarsCount 返回缓冲区里可用柱总数,GetBarsCalculated 才是真正算完的柱数,两者差就是还没更新的尾巴,实盘里差几根就得警惕重绘延迟。 GetUP 和 GetDN 按 shift 取峰、谷值,AsSeries 控制索引方向;GetOB 返回的是 OrderFormationBarHighLow 结构,里面是 outside bar 的高低价,不是普通 double。 在 MT5 里 new 一个 CCustZigZagPPC 实例后,先判 GetBarsCalculated>0 再读 GetUP(0,true),能避免拿到空值把逻辑带偏。外汇与贵金属波动大,这类峰值抓取仅作结构参考,信号失效概率不低。