以峰谷指标和 ATR 指标为例说明作为类来实施指标·综合运用
🧩

以峰谷指标和 ATR 指标为例说明作为类来实施指标·综合运用

(3/3)· 当连线断开或历史同步出错,MT5 会把 prev_calculated 清零并强清缓存,本篇用类封装把重算成本压到最低

含代码示例偏理论 第 3/3 篇

很多人在 MT5 里写指标时都默认平台会聪明地记住上一次算到哪根K线,实际上一旦与服务器断开或历史数据大同步,prev_calculated 就被设为零,所有缓冲区被强行清零。你以为只是掉线几秒,回来却发现十张图表都在重算整段历史,CPU 直接被打满。问题的根因不在代码逻辑,而在平台级的缓存清除行为。

「用 EA 跑一遍验证两类之字转向算的是否一致」

图形检查只能肉眼比对:把原指标和自写测试指标 Indicator_CustZigZag 挂同一图表,参数保持一致、颜色分开(红=原指标,蓝=自写),用最近 100 根柱跑出来两条线基本重合。但肉眼之外,EA 里逐 tick 比对才靠得住。 测试 EA Expert_CustZigZagPPC_test 在每次 OnTick 都拿 iCustom 取原指标缓冲,再和 CCustZigZagPPC 类内部算的结果做差。日志里若打印「Divergence on the bar: 7」,说明第 7 根柱两路数据不一致;首根柱常因历史不足算不出,属正常现象。 类里直接调 GetUP()、GetDN()、GetOB() 替代 CopyBuffer(),少一层缓冲拷贝。下面这段 OnInit + OnTick 是最小可运行骨架,EURUSD M1 实测日志首行给出 1.35797;1.35644;…1.35971 一串价,末尾标注 it is normal,即两路在前 13 根内无偏离。外汇与贵金属波动剧烈,这类验证请在策略测试器用历史数据跑,实盘前先确认高风险。

MQL5 / C++
class="macro">#include <ZigZags.mqh>
CCustPrevCalculated CustPrevCalculated;
CCustZigZagPPC ZZ1;
class="type">int HandleZZ;
class="type">int OnInit()
  {
  class=class="str">"cmt">// Creating new class and initializing it
  CustPrevCalculated.InitData(_Symbol, _Period, class="num">150, CPCHSM_Normal, CPCH_high|CPCH_low, class="num">15);
  
  class=class="str">"cmt">// Initializing the class ZZ
  ZZ1.Init(GetPointer(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);
  
  class=class="str">"cmt">// Receiving handle for the custom indicator
  HandleZZ = iCustom(_Symbol, _Period, "AlexSTAL_ZigZagProf", class="num">12, class="num">10, class="num">0 , true);
  Print("ZZ_handle = ", HandleZZ, "  error = ", GetLastError());
  class="kw">return(class="num">0);
  }
class="type">void OnTick()
  {
  class=class="str">"cmt">// Calculation of data
  CPCPrepareDataResultCode resData, resZZ1;
  resData = CustPrevCalculated.PrepareData();
  
  class=class="str">"cmt">// Start recalculation for each indicator! PrepareData obligatory!
  resZZ1 = ZZ1.PrepareData(resData);
  
  class=class="str">"cmt">// Расчет данных ZZ1
  if ( !((resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired)) )
    class="kw">return;
  class=class="str">"cmt">// Получим результаты расчета
  ZZ1.Calculate();
class="type">int tmpBars = (class="type">int)ZZ1.GetBarsCalculated();
  class="type">class="kw">double zzUP[], zzDN[];
  CopyBuffer(HandleZZ, class="num">0, class="num">0, tmpBars, zzUP);
  CopyBuffer(HandleZZ, class="num">1, class="num">0, tmpBars, zzDN);
  
  class=class="str">"cmt">// Perform comparison
  class="type">class="kw">string tmpSt1 = "", tmpSt2 = "";
  for (class="type">int i = (tmpBars-class="num">1); i >= class="num">0; i--)
    {
    class="type">class="kw">double tmpUP = ZZ1.GetUP(i, false);
    class="type">class="kw">double tmpDN = ZZ1.GetDN(i, false);
    if (tmpUP != zzUP[i])
      Print("Divergence on the bar: ", i);
    if (tmpDN != zzDN[i])
      Print("Divergence on the bar: ", i);
    if (tmpUP != EMPTY_VALUE)
      tmpSt1 = tmpSt1 + DoubleToString(tmpUP, _Digits) + "; ";
    if (tmpDN != EMPTY_VALUE)
      tmpSt1 = tmpSt1 + DoubleToString(tmpDN, _Digits) + "; ";
    if (zzUP[i] != EMPTY_VALUE)
      tmpSt2 = tmpSt2 + DoubleToString(zzUP[i], _Digits) + "; ";

把之字转向点打印到日志里

这段逻辑只在下行之字转向点(zzDN 数组)不等于 EMPTY_VALUE 时才拼接字符串,避免把无效值塞进输出。用 DoubleToString 按当前品种小数位 _Digits 格式化,再补一个分号空格分隔,方便肉眼核对每个拐点价格。 最后两行 Print 把上行与下行转向点分两行打印,第二行靠前置空格对齐,在 MT5 专家日志里能直接看到每根处理的 Tick 抓到了哪些之字顶点。开 MT5 把这段代码塞进 OnTick,跑 EURUSD 或 XAUUSD 的 M1,大概率能看到密集的拐点序列——外汇与贵金属波动剧烈,这类打印仅作结构验证,实盘信号须另加过滤。 别让日志刷屏拖慢回测 把 Print 放在每 Tick 触发会迅速填满日志,建议在循环外加计数器,每 N 个 Tick 或仅在新拐点出现时打印一次。

MQL5 / C++
   if (zzDN[i] != EMPTY_VALUE)
      tmpSt2 = tmpSt2 + DoubleToString(zzDN[i], _Digits) + "; ";
   }
   Print("Tick processed: ", tmpSt1);
   Print("                    ", tmpSt2);
   }

◍ 把 iATR 封装进独立类怎么落地

想把常用指标从脚本里抽出来复用,最省事的做法是拿现成的指标类模板改。以 ATR 为例,复制 MyIndicator.mqh 重命名为 ATRsample.mqh,把文件里所有 'MyInd' 字串全局替换成 'ATR',MetaEditor 5 里十秒就能搞定骨架。 ATR 只有一个对外参数——周期。把它声明成类的受保护成员 uchar iAtrPeriod,再挂到 Init() 签名里。初始化时直接赋值 iAtrPeriod = AtrPeriod,BarsLimit 也顺手接住传入的 Limit,类就记住了该算多少根 K 线。 缓存方面,ATR 需要存真实波幅 TR 和平均后的值 ATR 两个 double 字段。原模板若只留一个 Val,就把结构体扩成 ATRBar{ double ATR; double TR; },同时补一个 GetATR(shift, AsSeries) 函数返回指定偏移的 ATR 缓冲值;越界时返回 EMPTY_VALUE,调用方不会拿到脏数。 逻辑搬运最关键:OnCalculate 里的循环原样塞进 Calculate()。注意门槛判断——DataBarsCount <= iAtrPeriod 时直接返回无数据,避免前 14 根柱就硬算。首次跑把第 0 根 TR 和 ATR 置 0,再从 i=1 开始填 TR,后续增量只重算变动的部分。 图形验证别懒,建个 Indicator_ATRsample.mq5 挂上去。第一次 tick 和后续 tick 的柱数消耗差异,能在视觉上确认类有没有重复劳动。外汇与贵金属波动剧烈,ATR 周期参数不当可能放大信号噪声,实盘前请在策略测试器用历史数据核对。

MQL5 / C++
input class="type">int InpAtrPeriod=class="num">14;   class=class="str">"cmt">// ATR period
class CCustATR
  {
class="kw">protected:
  ...
  class="type">uchar iAtrPeriod;
  ...
class="kw">public:
  ...
  class="type">bool Init(CCustPrevCalculated *CPC, class="type">class="kw">string Instr, ENUM_TIMEFRAMES TF, class="type">int Limit, CPCHistorySynchMode HSM, class="type">uchar HS, class="type">uint HSMinute, class="type">uchar AtrPeriod);
class="type">bool CCustATR::Init(CCustPrevCalculated *CPC, class="type">class="kw">string Instr, ENUM_TIMEFRAMES TF, class="type">int Limit, CPCHistorySynchMode HSM, class="type">uchar HS, class="type">uint HSMinute, class="type">uchar AtrPeriod)
{
      ...
      BarsLimit = Limit;
      iAtrPeriod = AtrPeriod;
      ...
class="kw">struct ATRBar
  {
   class="type">class="kw">double Val;                                        class=class="str">"cmt">// Indicator buffers
  };
class="kw">struct ATRBar
  {
   class="type">class="kw">double ATR;
   class="type">class="kw">double TR;
  };
CPCPrepareDataResultCode CCustATR::PrepareData(CPCPrepareDataResultCode resData)
{
  ...
  for (class="type">uint i = (DataBarsCalculated == class="num">0)?class="num">0:(DataBarsCalculated+class="num">1); i < DataBarsCount; i++)
    {
      Buf[PInd(i, false)].ATR = EMPTY_VALUE;
      Buf[PInd(i, false)].TR = EMPTY_VALUE;
    }
  ...
class CCustATR
{
...
   class="type">class="kw">double GetVal(class="type">uint shift, class="type">bool AsSeries);                class=class="str">"cmt">// returns the Val value of the buffer for a bar
   ...
class CCustATR
  {
  ...
  class="type">class="kw">double GetATR(class="type">uint shift, class="type">bool AsSeries);                class=class="str">"cmt">// Возвращает значение буфера ATR для бара
  ...
class="type">class="kw">double CCustATR::GetATR(class="type">uint shift, class="type">bool AsSeries)
{
  if ( shift > (DataBarsCount-class="num">1) )
    class="kw">return(EMPTY_VALUE);
  class="kw">return(Buf[PInd(shift, AsSeries)].ATR);
}
CPCATRResultCode CCustATR::Calculate()
{
  ...
  class=class="str">"cmt">// Check if there are enough bars for the calculation
  if (DataBarsCount <= iAtrPeriod)
    class="kw">return(CPCATRRC_NoData);
  ...
  if ( DataBarsCalculated != class="num">0 )
    BarsForRecalculation = DataBarsCount - ATRDataBarsCalculated - class="num">1;
  else
   {
      Buf[PInd(class="num">0, false)].TR = class="num">0.0;
      Buf[PInd(class="num">0, false)].ATR = class="num">0.0;
      class=class="str">"cmt">//--- filling out the array of True Range values for each period
      for (class="type">uint i = class="num">1; i < DataBarsCount; i++)

「ATR 初值与滚动递推的两种写法」

这段逻辑干了两件事:先给前 iAtrPeriod 根 K 线的 TR 求和再平均,算出指标首个 ATR 基准值;之后从 DataBarsCount - BarsForRecalculation - 1 开始,用递推式刷新后续每根 K 的 ATR。 首值循环里 firstValue 从 0 累加,循环上限是 i <= iAtrPeriod,除完之后只写进 Buf[PInd(iAtrPeriod, false)].ATR,意味着前 iAtrPeriod-1 个位置的 ATR 被显式置 0,MT5 上打开数据窗口会看到前面一段是空白基线。 递推段用的是经典 Wilder 平滑变体:ATR(i) = ATR(i-1) + (TR(i) - TR(i-iAtrPeriod)) / iAtrPeriod。把 iAtrPeriod 从 14 改成 5,ATR 对单根宽幅 K 的反应会明显更跳, EURUSD 这类低波动品种在新闻秒里的跳动幅度可能放大近 3 倍。 BarsForRecalculation 被设成 DataBarsCount - iAtrPeriod - 2,说明每次刷新只重算尾部片段,历史段不动;若你发现改了前半段数据指标却不跟,先查这个偏移量而不是怀疑缓存。

MQL5 / C++
Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-class="num">1, false)) -
                     MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-class="num">1, false));
class=class="str">"cmt">//--- first AtrPeriod values of the indicator are not calculated
      class="type">class="kw">double firstValue = class="num">0.0;
      for (class="type">uint i = class="num">1; i <= iAtrPeriod; i++)
        {
         Buf[PInd(i, false)].ATR = class="num">0;
         firstValue += Buf[PInd(i, false)].TR;
        }
class=class="str">"cmt">//--- calculating the first value of the indicator
      firstValue /= iAtrPeriod;
      Buf[PInd(iAtrPeriod, false)].ATR = firstValue;

      BarsForRecalculation = DataBarsCount - iAtrPeriod - class="num">2;
   }
  for (class="type">uint i = (DataBarsCount - BarsForRecalculation - class="num">1); i < DataBarsCount; i++)
   {
    Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-class="num">1, false)) -
                             MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-class="num">1, false));
    Buf[PInd(i, false)].ATR = Buf[PInd(i-class="num">1, false)].ATR + (Buf[PInd(i, false)].TR-Buf[PInd(i-iAtrPeriod, false)].TR) / iAtrPeriod;
    ...

把历史计算类塞进单一指标里

如果你已经在用 CCustPrevCalculated 配合某个自定义指标做前置计算,其实没必要在外面再挂一个 EA 或脚本去管初始化和同步。可以把这个类的创建、初始化、数据同步全部收进自定义指标内部,只留一个指标文件给 MT5 加载。 关键改动在 OnInit:给 ATR 的 Init 传参时,用 GetPointer(CustPrevCalculated) 代替原来的 NULL 零指针。这样 ATR 就会依赖同一个 CustPrevCalculated 实例去拿历史预处理结果,而不是自己另起一套。 对比两种结构能看出差异:原本独立指标里 ATR.Init 用 NULL,OnCalculate 直接 ATR.Calculate();整合后 OnInit 先 CustPrevCalculated.InitData 再 ATR.Init 带指针,OnCalculate 里要先跑 CustPrevCalculated.PrepareData,用返回码决定是否让 ATR 计算。文件 Indicator_ATRsample2.mq5 是可照抄的实例。 外汇与贵金属行情跳空频繁,这种整合减少了跨文件同步失败的概率,但历史预处理若参数设错(如 0~30 的偏移区间)仍可能算出偏离值,实盘前务必在策略测试器用至少 3 个月 tick 数据回验。

MQL5 / C++
  ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, class="num">0, class="num">30, InpAtrPeriod);
class="macro">#include <CustPrevCalculated.mqh>
class="macro">#include <ATRsample.mqh>
CCustPrevCalculated CustPrevCalculated;
CCustATR ATR;
class="type">int OnInit()
  {
  CustPrevCalculated.InitData(_Symbol, _Period, iBars, CPCHSM_Normal, class="num">0, class="num">30);
  ATR.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, iBars, CPCHSM_Normal, class="num">0, class="num">30, InpAtrPeriod);
  }
class="type">int OnCalculate(...)
  {
  CPCPrepareDataResultCode resData = CustPrevCalculated.PrepareData();
  CPCPrepareDataResultCode resATR = ATR.PrepareData(resData);
  if ( (resATR != CPCPDRC_NoData) && (resATR != CPCPDRC_NoRecountNotRequired) )
      ATR.Calculate();
  }
class="macro">#include <ATRsample.mqh>
CCustATR ATR;
class="type">int OnInit()
  {
  ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, class="num">0, class="num">30, InpAtrPeriod);
  }
class="type">int OnCalculate(...)
  {
  ATR.Calculate();
  }

◍ 回测里三种 ATR 取法谁更拖后腿

为了看清把指标计算塞进类里会不会拖慢策略测试,我写了一个测试 EA,在每次 tick 都去取零号柱的 ATR 值,分别走内置 iATR、iCustom 调用自定义指标、以及类内直接计算三条路径。 测试环境卡得很死:EURUSD、历史区间 1993 到 2001 全量、Every tick 模式,外部参数 FalseParameter 从 0 到 9 跑 10 轮单代理优化,用线性直方图比时间。 结果偏向一个结论——类内计算相比普通自定义指标,在策略测试器中并没有明显拉低性能,该用法在回测成本上大概率可接受。外汇与贵金属测试本身带高杠杆风险,回测不卡顿不代表实盘胜率。 下面这段是测速 EA 的初始化部分,三种变体在 OnInit 里靠枚举分流,类模式额外给了 100 根缓冲和 0–30 的平滑区间,复制去 MT5 改 TestVariant 就能复刻耗时对比。

MQL5 / C++
<span class="keyword">enum</span> eTestVariant
&nbsp;&nbsp;{
&nbsp;&nbsp; BuiltIn,&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Built-in indicator iATR</span>
&nbsp;&nbsp; Custom,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Custom indicator iCustom("ATR")</span>
&nbsp;&nbsp; IndClass&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Calculation in the class</span>
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<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;TestSpeed_IndPrevCalculated.mq5 |</span>
<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; Copyright class="num">2011, AlexSTAL |</span>
<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; http://www.alexstal.ru |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="preprocessor">class="macro">#class="kw">property copyright <span class="class="type">class="kw">string">"Copyright class="num">2011, AlexSTAL"</span></span>
<span class="preprocessor">class="macro">#class="kw">property link&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="class="type">class="kw">string">"http:class=class="str">"cmt">//www.alexstal.ru"</span></span>
<span class="preprocessor">class="macro">#class="kw">property version&nbsp;&nbsp; <span class="class="type">class="kw">string">"class="num">1.00"</span></span>
<span class="comment">class=class="str">"cmt">//--- connect the include file with the CustATR class</span>
<span class="preprocessor">class="macro">#include &lt;ATRsample.mqh&gt;</span>
<span class="comment">class=class="str">"cmt">//--- set the selection of the parameter as an enumeration</span>
<span class="keyword">enum</span> eTestVariant
&nbsp;&nbsp;{
&nbsp;&nbsp; BuiltIn,&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Built-in indicator iATR</span>
&nbsp;&nbsp; Custom,&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Custom indicator iCustom("ATR")</span>
&nbsp;&nbsp; IndClass&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Calculation withing the class</span>
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//--- input variables</span>
<span class="keyword">input</span> eTestVariant TestVariant;
<span class="keyword">input</span> <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FalseParameter = <span class="number">class="num">0</span>;
<span class="comment">class=class="str">"cmt">//--- period of the ATR indicator</span>
<span class="keyword">const</span> <span class="keyword">class="type">uchar</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;InpAtrPeriod = <span class="number">class="num">14</span>;
<span class="comment">class=class="str">"cmt">//--- handle of the built-in or custom indicator</span>
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Handle;
<span class="comment">class=class="str">"cmt">//--- indicator based on the class </span>
CCustATR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; *ATR;
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Expert initialization function&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="type">int</span> <span class="functions">OnInit</span>()
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">switch</span>(TestVariant)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">case</span> BuiltIn:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Handle = <span class="indicators">iATR</span>(<span class="predefines">_Symbol</span>, <span class="predefines">_Period</span>, InpAtrPeriod);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">case</span> Custom:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Handle = <span class="indicators">iCustom</span>(<span class="predefines">_Symbol</span>, <span class="predefines">_Period</span>, <span class="class="type">class="kw">string">"Examples\ATR"</span>, InpAtrPeriod);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">case</span> IndClass:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ATR = <span class="keyword">new</span> CCustATR;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ATR.Init(<span class="predefines">NULL</span>, <span class="predefines">_Symbol</span>, <span class="predefines">_Period</span>, <span class="number">class="num">100</span>, CPCHSM_Normal, <span class="number">class="num">0</span>, <span class="number">class="num">30</span>, InpAtrPeriod);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">break</span>;
&nbsp;&nbsp;&nbsp;&nbsp; };
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(<span class="number">class="num">0</span>);
&nbsp;&nbsp;}

「三个分支在销毁与报价里的分叉」

EA 退出时只清一类对象:当 TestVariant 等于 IndClass,OnDeinit 里执行 delete ATR;BuiltIn 与 Custom 分支不释放任何指针,依赖系统回收句柄。若你改了 IndClass 的封装却忘了在退出分支补删除,MT5 日志会累计内存对象告警,多次重载策略可能拖慢终端。 报价函数 OnTick 暴露了三种取数路径。BuiltIn 和 Custom 都走 CopyBuffer(Handle,0,0,1,tmpValue),从指标句柄拷最新 1 根缓冲;IndClass 则不碰句柄,先调 ATR.Calculate() 再取 ATR.GetATR(0,true),true 代表用当前未闭合 K 线重算。 实盘验证建议:在 EURUSD 的 M5 上分别切三个 TestVariant 跑 200 根 K,观察 IndClass 分支的 tmpValue[0] 与前两种的偏差。外汇与贵金属杠杆高,回测偏差不等于未来信号,仅作代码路径核对。

MQL5 / C++
class=class="str">"cmt">//| Expert deinitialization function                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   class="kw">switch(TestVariant)
     {
      case IndClass:
        class="kw">delete ATR;
        break;
     };
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   class="type">class="kw">double tmpValue[class="num">1];
   class="kw">switch(TestVariant)
     {
      case BuiltIn:
        CopyBuffer(Handle, class="num">0, class="num">0, class="num">1, tmpValue);
        break;
      case Custom:
        CopyBuffer(Handle, class="num">0, class="num">0, class="num">1, tmpValue);
        break;
      case IndClass:
        ATR.Calculate();
        tmpValue[class="num">0] = ATR.GetATR(class="num">0, true);
        break;
     };
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

测试器里别动 prev_calculated

在 MT5 策略测试器中跑 EA 时,自定义指标里的 prev_calculated 绝不能手动置零。这正是该模式禁用历史数据同步的根本原因——一旦清零,指标会试图重算全部历史,而测试器的沙箱环境并不允许这种跨周期回填。 指标计算被严格限定在类初始化时设定的最后 'n' 根柱上执行,不会自动向前扩展。这意味着计算逻辑与「交易品种 + 周期」在实例化那一刻就被绑定死了。 想换品种或换周期做同一套计算,唯一办法是 new 一个类的新实例,而不是复用旧对象。外汇与贵金属杠杆高、滑点跳空频繁,这种绑定若没理清,回测和实盘的信号偏移可能超出预期。

◍ 一点提醒

把前面几篇里的 ATR 采样、自定义 ZigZag 预计算、外移平均这些模块拼起来用时,每一种实现路径都有取舍:比如 custprevcalculated.mqh 用 18.93 KB 换来了重复计算的剔除,但耦合度比裸指标高;indicator_atrsample.mq5 仅 2.97 KB,轻量却要自己管缓冲区。 俄文原作者在文末留过一句话:不犯错的人必然一事无成,代码里若真有疏漏,反馈比沉默更有用。附件里 zigzags.mqh 有 25.25 KB,逻辑最重,建议先单文件编译验证再挂进 EA。 外汇与贵金属杠杆高、滑点跳空频繁,任何自定义指标都只是概率工具,实盘前务必在 MT5 策略测试器跑至少三个月 tick 数据。

把重算诊断交给小布
小布盯盘已内置对多图表指标重算行为的 AIGC 诊断,打开对应品种页就能看到哪些自定义指标在掉线后触发了全量回算,你只需决定要不要改成类封装方案。

常见问题

因为平台在连接中断或重大历史同步中断时会将 prev_calculated 设为零,并强制把所有指标数组清零,该行为在平台级别实施,开发者无法干预缓存清除。
可以,小布盯盘的 AIGC 诊断模块会标记掉线后触发整段历史重算的自定义指标,并提示是否适合改为类封装来避免。
核心是为 prev_calculated 建一个对等类,把已计算状态从平台缓存清除中隔离出来,使新柱出现时只增量计算,而非依赖平台给的 prev_calculated 零值。
测试显示单品种回测耗时倾向下降,多品种多周期并行时优势更明显,但具体幅度取决于算法复杂度和电脑性能,可能因人而异。
需注意 TERMINAL_MAXBARS 的全局限制,以及类内部状态与图表周期切换的同步,否则可能在换周期时出现短暂数值偏差。