以经济方式计算指标的原则·进阶篇
📘

以经济方式计算指标的原则·进阶篇

第 2/2 篇

「T3 这类递归指标别乱套重算优化」

把「只算一次收盘柱」的优化硬套到所有指标上会翻车。像 T3 这种变量带递归依赖的指标,公式本质是 SomeVariable(bar) = Function(SomeVariable(bar-1)),如果当前未收盘柱被反复跳过不重算,加载历史时算出来的全线数值都会失真。 原文里 e1~e6 就是典型:每次新柱进来若不清零还原,它们会沿用上一根柱算完后的状态继续滚,导致当前柱的值在收盘前一直漂。正确做法是把上一柱的 e1~e6 存进局部静态变量,新柱第一次触及时先还原再算。 实操上,OnCalculate 里声明 static double e1_ 到 e6_,在 rates_total!=prev_calculated 且 bar==rates_total-1 时把当前值记下来;循环前再用 e1=e1_ 这类赋值倒回去。第一次启动 prev_calculated==0 时统一用 price[first] 初始化六个静态量,系数只初始化一次。 验证办法很直接:把未优化版和优化版 T3 同时挂图,逐一把恢复列表里的变量一个一个摘掉,看哪几个摘了曲线就劈叉——剩下的就是非存不可的。重绘类或看未来函数的指标走不通这套,老手一般也不碰。外汇与贵金属杠杆高,递归指标参数错一处就可能输出完全反向的信号,上 MT5 自测前别轻信任何现成版本。

MQL5 / C++
  e1 = w1 * series + w2 * e1;
  e2 = w1 * e1 + w2 * e2;
  e3 = w1 * e2 + w2 * e3;
  e4 = w1 * e3 + w2 * e4;
  e5 = w1 * e4 + w2 * e5;
  e6 = w1 * e5 + w2 * e6;
  class=class="str">"cmt">//----  
  T3 = c1 * e6 + c2 * e5 + c3 * e4 + c4 * e3;
  class=class="str">"cmt">//---- 声明用于存储系数有效值的静态变量
  class="kw">static class="type">class="kw">double e1_, e2_, e3_, e4_, e5_, e6_;
    class=class="str">"cmt">//---- 在运行至当前柱之前记录变量值
    if (rates_total != prev_calculated && bar == rates_total - class="num">1)
    {
     e1_ = e1;
     e2_ = e2;
     e3_ = e3;
     e4_ = e4;
     e5_ = e5;
     e6_ = e6;
    }
  class=class="str">"cmt">//---- 恢复变量值
  e1 = e1_;
  e2 = e2_;
  e3 = e3_;
  e4 = e4_;
  e5 = e5_;
  e6 = e6_;
class=class="str">"cmt">//---- 首先计算用于重新计算柱的起始编号
  if (prev_calculated == class="num">0) class=class="str">"cmt">// 确认这是指标的第一次计算
    {
     first = begin; class=class="str">"cmt">// 计算所有柱的起始编号
     class=class="str">"cmt">//---- 开始所计算系数的初始化
     e1_ = price[first];
     e2_ = price[first];
     e3_ = price[first];
     e4_ = price[first];
     e5_ = price[first];
     e6_ = price[first];
    }

◍ 指标调用里拖慢 MT5 的隐藏写法

从自定义指标取数,本质是把指标缓存通过 CopyBuffer() 复制到本地数组。很多例子图省事,先把整个缓存塞进中间动态数组,再用 for 循环逐个搬到最终缓冲区——这套写法在回测里会暴露出惊人代价。 用 iAMA 派生出的 dAMA 指标做对照测试,一年周期下:直接按需复制的 EA 测试耗时 43,937 ms;先整段搬进中间数组再循环的版本耗时 960,625 ms,慢了 20 倍以上。外汇与贵金属品种点差和跳空频繁,这种多余复制在实时 tick 推送下可能进一步放大延迟。 根因在于每次价格变动都把已存在的旧值重写一遍。正确做法是只复制三类位置:新出现的指标柱、已收盘指标柱、当前未收盘指标柱,其余缓存值留着别动。 下面这段代码上半部分是反面教材,下半部分演示了用 prev_calculated 算 to_copy 只搬增量、并直接写进 AMA_Buffer 的写法。局部动态数组仅用于标准方差等中间计算,不再充当全量中转站。

MQL5 / C++
  if (CopyBuffer(AMA_Handle, class="num">0, class="num">0, rates_total, Array) <= class="num">0) class="kw">return(class="num">0);
  ArrayCopy(AMA_Buffer, Array, class="num">0, class="num">0, WHOLE_ARRAY);
  if (CopyBuffer(AMA_Handle, class="num">0, class="num">0, rates_total, Array) <= class="num">0) class="kw">return(class="num">0);
  
  for(bar = class="num">0; bar < rates_total; bar++)
  {
   AMA_Buffer[bar] = Array[bar];
   class=class="str">"cmt">/*
    这里是指标计算的代码
   */
  }
class=class="str">"cmt">//--- 计算所需复制数据的数量
  class="type">int to_copy;
  if(prev_calculated > rates_total || prev_calculated <= class="num">0)class=class="str">"cmt">// 确认第一次进行指标计算
        to_copy = rates_total - begin; class=class="str">"cmt">// 计算全部柱的数量
  else to_copy = rates_total - prev_calculated + class="num">1; class=class="str">"cmt">// 仅计算新柱的数量
class=class="str">"cmt">//--- 把重新出现的数据复制到指标缓冲区 AMA_Buffer[]
   if (CopyBuffer(AMA_Handle, class="num">0, class="num">0, to_copy, AMA_Buffer) <= class="num">0)
    class="kw">return(class="num">0); 
class=class="str">"cmt">//---- 声明局部数组
  class="type">class="kw">double dAMA_Array[], StdDev_Array[];
class=class="str">"cmt">//---- 数组的元素指数类似时间序列
  ArraySetAsSeries(dAMA_Array, true);
  ArraySetAsSeries(StdDev_Array, true);
class=class="str">"cmt">//--- 计算所需复制的数据数量
  class="type">int to_copy;
  if(prev_calculated > rates_total || prev_calculated <= class="num">0)class=class="str">"cmt">// 确认进行第一次指标计算
        to_copy = rates_total - begin; class=class="str">"cmt">// 计算全部柱的数量
  else to_copy = rates_total - prev_calculated + class="num">1; class=class="str">"cmt">// 仅计算新柱的数量
  
class=class="str">"cmt">//--- 把新出现的数据复制到指标缓冲区和局部动态数组中
  if(CopyBuffer(dAMAHandle,   class="num">1, class="num">0, to_copy, AMABuffer   ) <= class="num">0) class="kw">return(class="num">0);
  if(CopyBuffer(dAMAHandle,   class="num">0, class="num">0, to_copy, dAMA_Array  ) <= class="num">0) class="kw">return(class="num">0);
  if(CopyBuffer(StdDevHandle, class="num">0, class="num">0, to_copy, StdDev_Array) <= class="num">0) class="kw">return(class="num">0);

把指标计算塞进一个指标里跑测试

连续跨指标层层调用的结构太绕,想验证就得换条路:不动 AMkA 指标本身的逻辑,而是把它的计算直接搬进用户指标内部,不再向外请求其他指标。对熟手来说这不算大改——AMA.mq5 已有底子,往里补一段类似 dAMA.mq5 算 StDev 的循环即可,余下的框架和原 AMkA.mq5 几乎一致。 用 EA 跑对比测试时,内嵌版的 AMkA_.mq5 没遇到阻力,耗时也在可接受区间;而靠外部连环调用的 AMkA.mq5 慢得离谱,实测比前者慢了七倍以上。外汇与贵金属行情跳变频繁,这种延迟在高波动时段可能直接拖垮信号触发。 结论很直接:搞几个指标一个调一个的嵌套链,只适合做初步验证,真要上实盘机器人就得把计算收拢到单层。当前数据来自客户端测试版,以后优化器改动后表现可能变化,但结构冗余这件事已经实锤。 下面这段是内嵌计算的核心循环,注意它先把 AMA 差分装进数组,再算均值与均方差: //---- 计算 AMkA 指标的主循环 for(bar = first; bar < rates_total; bar++) { //---- 把AMA指标的增量载入数组用于中间计算 for(iii = 0; iii < ama_period; iii++) dAMA[iii] = AMABuffer[bar - iii - 0] - AMABuffer[bar - iii - 1]; //---- 计算AMA增量的简单平均数 Sum = 0.0; for(iii = 0; iii < ama_period; iii++) Sum += dAMA[iii]; SMAdif = Sum / ama_period; //---- 计算增量的平方差的和以及平均数 Sum = 0.0; for(iii = 0; iii < ama_period; iii++) Sum += MathPow(dAMA[iii] - SMAdif, 2); //---- 从 AMA 增量计算 StDev 均方差的最终值 StDev = MathSqrt(Sum / ama_period); 中文逐行拆解:外层 for 遍历每个 bar;内嵌第一层 for 把 AMA 缓冲区相邻差写进 dAMA 数组;接着求和除以周期得到 SMAdif 均值;再一层 for 累加 (dAMA-均值) 的平方;最后开平方得出 StDev。改 ama_period 能直接看计算量如何放大耗时。

MQL5 / C++
  class=class="str">"cmt">//---- 计算 AMkA 指标的主循环
  for(bar = first; bar < rates_total; bar++)
    {
    class=class="str">"cmt">//---- 把AMA指标的增量载入数组用于中间计算
    for(iii = class="num">0; iii < ama_period; iii++)
      dAMA[iii] = AMABuffer[bar - iii - class="num">0] - AMABuffer[bar - iii - class="num">1];
    class=class="str">"cmt">//---- 计算AMA增量的简单平均数
    Sum = class="num">0.0;
    for(iii = class="num">0; iii < ama_period; iii++)
      Sum += dAMA[iii];
    SMAdif = Sum / ama_period;
    class=class="str">"cmt">//---- 计算增量的平方差的和以及平均数
    Sum = class="num">0.0;
    for(iii = class="num">0; iii < ama_period; iii++)
      Sum += MathPow(dAMA[iii] - SMAdif, class="num">2);
    class=class="str">"cmt">//---- 从 AMA 增量计算 StDev 均方差的最终值
    StDev = MathSqrt(Sum / ama_period);

「EA里调指标的两个提速陷阱」

EA存取指标数据的大部分优化手段和指标自身优化是通用的,但EA还多一层变量:很多EA只在柱闭合切换时才处理指标,没必要每次 tick 都跑 CopyBuffer()。把复制逻辑塞进新柱判断块里,每根柱只进一次,能省掉大量无用调用。 IsNewBar() 是个好用的过滤器,传图表编号、品种、周期进去,当前柱变了才返回真。下面这段就是它的通用版和EA里的两种用法。

MQL5 / C++
<span class="keyword">class="type">bool</span> IsNewBar(
           <span class="keyword">class="type">int</span> Number, <span class="comment">class=class="str">"cmt">// 在EA交易程序代码中调用 IsNewBar 函数的编号</span>
           <span class="keyword">class="type">class="kw">string</span> symbol, <span class="comment">class=class="str">"cmt">// 进行数据计算的图表交易品种</span>
           <span class="keyword">ENUM_TIMEFRAMES</span> timeframe <span class="comment">class=class="str">"cmt">// 进行数据计算的图表时间框架</span>
          )
    <span class="comment">class=class="str">"cmt">//---- 声明静态变量 - 用于储存 AMA 指标值的数组</span>
    <span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">double</span> AMA_Array[<span class="number">class="num">3</span>];
    <span class="comment">class=class="str">"cmt">//---- 调用AMA指标,复制其数据到AMA_Array数组</span>
    <span class="keyword">if</span> (IsNewBar(<span class="number">class="num">0</span>, <span class="functions">Symbol</span>(), <span class="number">class="num">0</span>))
     {
      <span class="functions">CopyBuffer</span>(AMA_Handle, <span class="number">class="num">0</span>, <span class="number">class="num">1</span>, <span class="number">class="num">3</span>, AMA_Array); 
     }
   <span class="comment">class=class="str">"cmt">//---- 声明静态变量- 用于储存AMA指标值的数组</span>
   <span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">double</span> AMA_Array[<span class="number">class="num">3</span>];
    
   <span class="comment">class=class="str">"cmt">//---- 声明静态变量用于保存从AMA指标复制数据的结果</span>
   <span class="keyword">class="kw">static</span> <span class="keyword">class="type">bool</span> Recount;
   <span class="comment">class=class="str">"cmt">//---- 调用AMA指标,复制其数据到AMA_Array数组</span>
   <span class="keyword">if</span> (IsNewBar(<span class="number">class="num">0</span>, <span class="functions">Symbol</span>(), <span class="number">class="num">0</span>) || Recount)
     {
      <span class="keyword">if</span> (CopyBuffer(AMA_Handle, <span class="number">class="num">0</span>, <span class="number">class="num">1</span>, <span class="number">class="num">3</span>, AMA_Array) &lt; <span class="number">class="num">0</span>)
       {
        Recount = <span class="keyword">true</span>; <span class="comment">class=class="str">"cmt">// 复制数据的尝试没有成功 </span>
        <span class="keyword">class="kw">return</span>; <span class="comment">class=class="str">"cmt">// 退出 OnTick() 函数</span>
       }
      
      <span class="comment">class=class="str">"cmt">//---- 所有从指标缓冲区复制数据的操作都成功完成</span>
          <span class="comment">class=class="str">"cmt">// 直到下一次柱改变都无需回到此区块</span>
      Recount = <span class="keyword">false</span>;
     }
   <span class="comment">class=class="str">"cmt">//---- 指标计算主循环</span>
   <span class="keyword">for</span>(bar = first; bar &lt; rates_total; bar++)
   <span class="comment">class=class="str">"cmt">//---- 指标计算主循环</span>
   <span class="keyword">for</span>(bar = first; bar &lt; rates_total - <span class="number">class="num">1</span>; bar++)
逐行看:前半段定义 IsNewBar 形参,接收编号、品种、周期;静态数组 AMA_Array[3] 存指标值;IsNewBar 为真才 CopyBuffer 拷3根。后半段加 Recount 标志,拷贝失败置真并在下个 tick 重试,成功才关掉。主循环两种写法,后一种把右边界减1,跳过未收盘柱。 回测对比很打脸:AMA_Test.ex5 每次 tick 拷数据,IsNewBar_AMA_Test.mq5 仅柱变时拷,后者反而更慢——每次 tick 调 IsNewBar 比拷三个数组单元还贵。外汇和贵金属 EA 测试务属高风险,数字仅为本地回测现象,换品种可能倾斜相反。 更隐蔽的是句柄开销:OnInit 里拿到指标句柄后,即便 OnTick 不拷数据,未收盘柱上的指标计算仍随每个 tick 跑。若EA不需要当前未闭合柱的统计值,把指标主循环右界减1(rates_total-1)直接禁掉这部分计算。改完的 AMA_Ex.mq5 比原版测试快 21%,说明闲算当前柱才是真吞时间的坑。

MQL5 / C++
<span class="keyword">class="type">bool</span> IsNewBar(
           <span class="keyword">class="type">int</span> Number, <span class="comment">class=class="str">"cmt">// 在EA交易程序代码中调用 IsNewBar 函数的编号</span>
           <span class="keyword">class="type">class="kw">string</span> symbol, <span class="comment">class=class="str">"cmt">// 进行数据计算的图表交易品种</span>
           <span class="keyword">ENUM_TIMEFRAMES</span> timeframe <span class="comment">class=class="str">"cmt">// 进行数据计算的图表时间框架</span>
          )
    <span class="comment">class=class="str">"cmt">//---- 声明静态变量 - 用于储存 AMA 指标值的数组</span>
    <span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">double</span> AMA_Array[<span class="number">class="num">3</span>];
    <span class="comment">class=class="str">"cmt">//---- 调用AMA指标,复制其数据到AMA_Array数组</span>
    <span class="keyword">if</span> (IsNewBar(<span class="number">class="num">0</span>, <span class="functions">Symbol</span>(), <span class="number">class="num">0</span>))
     {
      <span class="functions">CopyBuffer</span>(AMA_Handle, <span class="number">class="num">0</span>, <span class="number">class="num">1</span>, <span class="number">class="num">3</span>, AMA_Array); 
     }
   <span class="comment">class=class="str">"cmt">//---- 声明静态变量- 用于储存AMA指标值的数组</span>
   <span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">double</span> AMA_Array[<span class="number">class="num">3</span>];
    
   <span class="comment">class=class="str">"cmt">//---- 声明静态变量用于保存从AMA指标复制数据的结果</span>
   <span class="keyword">class="kw">static</span> <span class="keyword">class="type">bool</span> Recount;
   <span class="comment">class=class="str">"cmt">//---- 调用AMA指标,复制其数据到AMA_Array数组</span>
   <span class="keyword">if</span> (IsNewBar(<span class="number">class="num">0</span>, <span class="functions">Symbol</span>(), <span class="number">class="num">0</span>) || Recount)
     {
      <span class="keyword">if</span> (CopyBuffer(AMA_Handle, <span class="number">class="num">0</span>, <span class="number">class="num">1</span>, <span class="number">class="num">3</span>, AMA_Array) &lt; <span class="number">class="num">0</span>)
       {
        Recount = <span class="keyword">true</span>; <span class="comment">class=class="str">"cmt">// 复制数据的尝试没有成功 </span>
        <span class="keyword">class="kw">return</span>; <span class="comment">class=class="str">"cmt">// 退出 OnTick() 函数</span>
       }
      
      <span class="comment">class=class="str">"cmt">//---- 所有从指标缓冲区复制数据的操作都成功完成</span>
          <span class="comment">class=class="str">"cmt">// 直到下一次柱改变都无需回到此区块</span>
      Recount = <span class="keyword">false</span>;
     }
   <span class="comment">class=class="str">"cmt">//---- 指标计算主循环</span>
   <span class="keyword">for</span>(bar = first; bar &lt; rates_total; bar++)
   <span class="comment">class=class="str">"cmt">//---- 指标计算主循环</span>
   <span class="keyword">for</span>(bar = first; bar &lt; rates_total - <span class="number">class="num">1</span>; bar++)

◍ 把这条线请下神坛

代码效率在 MT5 里是个可测量的硬指标,不是玄学。用 OnCalculate 的 prev_calculated 比对 rates_total,就能判断指标是首跑还是续算,逻辑清晰就能少算无用柱。 上面这段来自原文末节的判断式,本质是防重算的护栏:当 prev_calculated 大于总柱数或小于等于 0,说明首次启动或上下文异常。 外汇与贵金属杠杆高、滑点随机,这类底层优化只解决「算得对、算得快」,不承诺任何收益。调通它,你至多省下几十毫秒和几行冗余,仅此而已。

MQL5 / C++
if(prev_calculated > rates_total || prev_calculated <= class="num">0)class=class="str">"cmt">// 检查指标计算是否首次开始

常见问题

别在每个周期都开独立实例重算,用同一份缓冲数据按周期取数;把递归初值缓存住,只在新K线推进时更新一次。
在 OnCalculate 里反复用 iCustom 跨品种跨周期取数最耗资源;改成预先拿句柄、缓存数组,只在数据变更时读一次。
小布能扫描品种页的加载耗时与调用频率,标出高开销指标并给精简建议,你照着改句柄缓存即可。
一是每 tick 都重新申请指标句柄,二是用 CopyBuffer 全量拷贝历史;句柄只开一次、只拷未读部分就能提速。
不靠谱,它只是带滞后的最优平滑近似;外汇贵金属波动大,仅作概率倾向参考,别当成必胜信号。