如何在 MetaTrader 5 里快速开发并调试交易策略·进阶篇
📘

如何在 MetaTrader 5 里快速开发并调试交易策略·进阶篇

第 2/3 篇

逐笔报价怎么塞进指标缓冲区

在自定义指标里想做逐笔 Tick 统计,第一步是把三个数组清零并改成时间序列方向。ZeroMemory 把 TickPriceBuffer、SignalBuffer、SignalColors 清掉旧值,随后 ArraySetAsSeries 带 true 参数让下标 0 永远指向最新一根,写缓存时不用每次算尾索引。 核心移位逻辑在 ApplyTick 里:用 ArrayCopy 把自身从 0 拷到 1、长度减一,等于全体后移一格,再把 price 写进 [0]。这段没有循环,ArraySize 取长度后三次拷贝完成价格、信号、颜色的同步滚动。 主计算段用 lastprice=close[rates_total-1] 取当前收盘价当报价源,ticks_counter 每来一次新价自增,再调 ApplyTick 完成移位。最后 return(rates_total) 让 MT5 下次用相同总量调用,避免重算历史。 回测时若勾了「每笔 tick」,ticks_counter 在 EURUSD M1 上单根可能跳几十到上百次,缓冲区 size 设太小会丢早期样本,建议开 MT5 把 size 打印出来确认。外汇与贵金属杠杆高,tick 级信号仅作概率参考,实盘前务必在策略测试器跑够样本。

MQL5 / C++
   ZeroMemory(TickPriceBuffer);
   ZeroMemory(SignalBuffer);
   ZeroMemory(SignalColors);
   class=class="str">"cmt">//--- 序列数组向后定位, 在这种情况下最方便
   ArraySetAsSeries(SignalBuffer,true);
   ArraySetAsSeries(TickPriceBuffer,true);
   ArraySetAsSeries(SignalColors,true);
   first=class="kw">false;
 }
class=class="str">"cmt">//--- 使用当前的收盘价作为价格
  class="type">class="kw">double lastprice=close[rates_total-class="num">1];
class=class="str">"cmt">//--- 瞬时报价计数
  ticks_counter++;
  ApplyTick(lastprice); class=class="str">"cmt">// 执行计算和缓存区移位
class=class="str">"cmt">//--- 返回 prev_calculated 的数值用于下次调用
  class="kw">return(rates_total);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 应用瞬时报价进行计算                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ApplyTick(class="type">class="kw">double price)
  {
  class="type">int size=ArraySize(TickPriceBuffer);
  ArrayCopy(TickPriceBuffer,TickPriceBuffer,class="num">1,class="num">0,size-class="num">1);
  ArrayCopy(SignalBuffer,SignalBuffer,class="num">1,class="num">0,size-class="num">1);
  ArrayCopy(SignalColors,SignalColors,class="num">1,class="num">0,size-class="num">1);
class=class="str">"cmt">//--- 保存最后的价格值
  TickPriceBuffer[class="num">0]=price;
class=class="str">"cmt">//---
  }
class=class="str">"cmt">//--- 只在价格变化时计算
  if(lastprice!=TickPriceBuffer[class="num">0])
    {
      ticks_counter++;      class=class="str">"cmt">// 瞬时报价计数
      ApplyTick(lastprice); class=class="str">"cmt">// 执行计算和缓存区移位
    }

「用独立缓冲存报价差值再算标准差」

给瞬时报价指标加一个辅助缓冲区是最直接的做法:单独开一个 DeltaTickBuffer 数组,只用来装每笔 tick 的价格增量,主价格序列和信号数组不会被这类中间计算污染。上面代码片段里缓冲区总数从 3 扩到 4,第 3 号索引绑定给 DeltaTickBuffer 且类型设为 INDICATOR_CALCULATIONS,说明它不参与绘图,只供后台迭代。 初始化时把这个数组也 ZeroMemory 清掉,并用 ArraySetAsSeries(...,true) 设成时间倒序,和另外三个缓冲区的处理方式保持一致,这样在 OnCalculate 首次触发时所有序列索引方向统一,写循环不会错位。 ApplyTick 函数里用 ArrayCopy 把 DeltaTickBuffer 整体向后挪一位,再把最新差值写进 [0],相当于维护一个滚动的差值窗口。之后另写 getStdDev() 去暴力遍历这个窗口就能出标准差,逻辑层和绘图层彻底分开。 代码写完后按 F5 进 MT5 调试,终端里能直接看到红蓝圆圈的 signal 绘制和差值缓冲的联动;外汇和贵金属 tick 频率高,这类高频计算在实盘可能拖慢执行,调试阶段就该顺手查掉明显的循环冗余。

MQL5 / C++
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_buffers class="num">4
class="macro">#class="kw">property indicator_plots   class="num">2
...
class=class="str">"cmt">//--- 指标缓存区
class="type">class="kw">double    TickPriceBuffer[];
class="type">class="kw">double    SignalBuffer[];
class="type">class="kw">double    DeltaTickBuffer[];
class="type">class="kw">double    ColorsBuffers[];
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓存区映射
   SetIndexBuffer(class="num">0,TickPriceBuffer,INDICATOR_DATA);
   SetIndexBuffer(class="num">1,SignalBuffer,INDICATOR_DATA);
   SetIndexBuffer(class="num">2,SignalColors,INDICATOR_COLOR_INDEX);
   SetIndexBuffer(class="num">3,DeltaTickBuffer,INDICATOR_CALCULATIONS);
...
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const ...)
class=class="str">"cmt">//--- 在首次调用期间将指标缓存区清零并标记为序列
   if(first)
     {
      ZeroMemory(TickPriceBuffer);
      ZeroMemory(SignalBuffer);
      ZeroMemory(SignalColors);
      ZeroMemory(DeltaTickBuffer);
      class=class="str">"cmt">//--- 序列数组向后定位, 在这种情况下最方便
      ArraySetAsSeries(TickPriceBuffer,true);
      ArraySetAsSeries(SignalBuffer,true);
      ArraySetAsSeries(SignalColors,true);
      ArraySetAsSeries(DeltaTickBuffer,true);
      first=class="kw">false;
     }
...
   class="kw">return(rates_total);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 应用瞬时报价进行计算                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ApplyTick(class="type">class="kw">double price)
  {
   class="type">int size=ArraySize(TickPriceBuffer);
   ArrayCopy(TickPriceBuffer,TickPriceBuffer,class="num">1,class="num">0,size-class="num">1);
   ArrayCopy(SignalBuffer,SignalBuffer,class="num">1,class="num">0,size-class="num">1);
   ArrayCopy(SignalColors,SignalColors,class="num">1,class="num">0,size-class="num">1);
   ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,class="num">1,class="num">0,size-class="num">1);
class=class="str">"cmt">//--- 保存最后的价格值
   TickPriceBuffer[class="num">0]=price;

◍ 用 tick 级标准差抓报价异常跳变

在 MT5 的自定义指标里,把每个即时报价(tick)的价格差存进 DeltaTickBuffer,再拿最近 ticks 个样本算标准差,就能给盘口噪声定一个动态基准。比起写死的点值阈值,这种方法在伦敦盘与亚盘波动率切换时不容易误报。 下面这段是标准差的暴力实现:先累加所有 Delta 求均值 average,再跑第二轮把每个样本与均值的差的平方求和,最后除以 (number-1) 开根号。注意分母是 number-1 而非 number,这是样本标准差,对小样本更保守。 ApplyTick 函数负责把旧数组整体后移一位(ArrayCopy 偏移 1),把新 price 塞进 [0],重算 Delta 与 stddev。当 MathAbs(DeltaTickBuffer[0]) 大于 gap*stddev 时,认为出现了异常跳变:涨用蓝色点、跌用红色点,并 PrintFormat 输出触发时的报价、变化点数、阈值点数和标准差点数,方便回看日志。 实盘里把 gap 从默认 3 调到 4,EURUSD 在常规时段触发频率可能下降约 40%,但重大数据行情仍会连发信号。外汇与贵金属杠杆高,这类信号只描述波动特征,不预示方向,验证时请在策略测试器用真实 tick 数据跑一遍。

MQL5 / C++
class=class="str">"cmt">//--- 计算与前一数值的差值
   DeltaTickBuffer[class="num">0]=TickPriceBuffer[class="num">0]-TickPriceBuffer[class="num">1];
class=class="str">"cmt">//--- 得到标准差
   class="type">class="kw">double stddev=getStdDev(ticks); 
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "暴力" 计算标准差                                     |//+------------------------------------------------------------------+
class="type">class="kw">double getStdDev(class="type">int number)
  {
  class="type">class="kw">double summ=class="num">0,sum2=class="num">0,average,stddev;
class=class="str">"cmt">//--- 统计变化之和, 并计算期望收益
  for(class="type">int i=class="num">0;i<ticks;i++)
      summ+=DeltaTickBuffer[i];
  average=summ/ticks;
class=class="str">"cmt">//--- 现在计算标准差
  sum2=class="num">0;
  for(class="type">int i=class="num">0;i<ticks;i++)
      sum2+=(DeltaTickBuffer[i]-average)*(DeltaTickBuffer[i]-average);
  stddev=MathSqrt(sum2/(number-class="num">1));
  class="kw">return (stddev);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 应用瞬时报价进行计算                                           |//+------------------------------------------------------------------+
class="type">void ApplyTick(class="type">class="kw">double price)
  {
  class="type">int size=ArraySize(TickPriceBuffer);
  ArrayCopy(TickPriceBuffer,TickPriceBuffer,class="num">1,class="num">0,size-class="num">1);
  ArrayCopy(SignalBuffer,SignalBuffer,class="num">1,class="num">0,size-class="num">1);
  ArrayCopy(SignalColors,SignalColors,class="num">1,class="num">0,size-class="num">1);
  ArrayCopy(DeltaTickBuffer,DeltaTickBuffer,class="num">1,class="num">0,size-class="num">1);   
class=class="str">"cmt">//--- 保存最后的价格值
  TickPriceBuffer[class="num">0]=price;
class=class="str">"cmt">//--- 计算与前一数值的差值
  DeltaTickBuffer[class="num">0]=TickPriceBuffer[class="num">0]-TickPriceBuffer[class="num">1];  
class=class="str">"cmt">//--- 得到标准差
  class="type">class="kw">double stddev=getStdDev(ticks);  
class=class="str">"cmt">//--- 如果价格变化超出指定的阀值
  if(MathAbs(DeltaTickBuffer[class="num">0])>gap*stddev) class=class="str">"cmt">// 将会在第一个瞬时报价上显示一个信号, 保留它作为一个 "特征"
    {
      SignalBuffer[class="num">0]=price;     class=class="str">"cmt">// 放置一个圆点
      class="type">class="kw">string col="Red";          class=class="str">"cmt">// 圆点省缺为红色
      if(DeltaTickBuffer[class="num">0]>class="num">0)   class=class="str">"cmt">// 价格暴涨
        {
         SignalColors[class="num">0]=class="num">1;      class=class="str">"cmt">// 则圆点为蓝色
         col="Blue";             class=class="str">"cmt">// 保存日志
        }
      else                      class=class="str">"cmt">// 价格暴跌
        SignalColors[class="num">0]=class="num">0;      class=class="str">"cmt">// 圆点为红色
      class=class="str">"cmt">//--- 输出消息至智能程序日志
      PrintFormat("瞬时报价=%G 变化=%.1f pts, 触发=%.3f pts, 标准差=%.3f pts %s",
                  TickPriceBuffer[class="num">0],DeltaTickBuffer[class="num">0]/_Point,gap*stddev/_Point,stddev/_Point,col);
    }

无信号时的缓冲归零处理

在自定义指标的信号计算收尾处,当价格行为不满足任何触发条件时,需要将当前柱的信号缓冲直接置零。 这一行 else SignalBuffer[0]=0; 紧接前面的 if 判断,作用是将第 0 号元素(即当前未闭合或刚闭合的柱)的信号值清为 0,避免上一根柱的残留信号污染实时显示。 从 MT5 数据窗口观察,若省略该赋值,指标线可能在信号消失后仍维持前值的视觉假信号,尤其在切换周期时更易暴露。 外汇与贵金属市场波动剧烈、杠杆风险高,这类缓冲清理虽小,却是回测与实盘一致性的一道防线,建议开 MT5 把这段贴进 OnInit 后的信号函数里跑一遍验证。

MQL5 / C++
else SignalBuffer[class="num">0]=class="num">0; class=class="str">"cmt">// 无信号
class=class="str">"cmt">//---
}

常见问题

给指标开一个独立缓冲区专门存相邻 tick 的报价差,用 SetIndexBuffer 绑定,别和主图线共用缓冲,避免重绘时覆盖。
没有统一值,建议先对品种跑一周 tick 算基准标准差,把阈值设为基准的 3 倍左右,再按品种波动率微调。
可以,小布盯盘的 AIGC 已内置 tick 异常诊断,打开对应品种页就能看到实时跳变提醒,不用自己写缓冲代码。
在 OnCalculate 里判断无信号分支,直接给缓冲位写 EMPTY_VALUE 或 0,防止历史值误导后续绘图与回看。
贵金属点值大、跳变更陡,同样阈值下误报更多,建议单独标定标准差基准并调高倍数,且牢记杠杆品种高风险。