开发回放系统(第 39 部分):铺平道路(三)·综合运用
📘

开发回放系统(第 39 部分):铺平道路(三)·综合运用

第 3/3 篇

◍ 缓冲区零位和 CopyBuffer 的索引错位

MT5 里往指标缓冲区写数据,新手常踩的坑是固定写零位。OnCalculate 里写 m_Buff[0] 在指标内部没问题,但 EA 用 CopyBuffer 去读时,零位含义完全变了——CopyBuffer 的零索引对应的是当前柱,而不是指标代码里的数组下标零。 实际限制在于:CopyBuffer 最多返回 2000 个位置。如果指标在零位写入、且历史数据超过 2000 根柱,EA 侧通过 CopyBuffer 读到的「零位」其实可能是缓冲区里的第 2001 个位置,原始零位值被锁死在指标内读不出来。外汇与贵金属交易用这类跨周期指标通信时,这种错位会直接导致信号丢失,属于高风险隐蔽 bug。 正确做法是把指标缓冲区的零位始终当作 rate_total - 1 来对待,也就是从末端往回写。这样 EA 侧用零号索引读,拿到的就是最新值。下面这段模型代码就是典型的错误写法——直接写 m_Buff[0]: 读取时若用反向顺序,可调用 ArraySetAsSeries 指定 false 来正向访问,索引体系就和指标内一致了。先把这个索引错位搞明白,后面在多缓冲区传字符串才不会乱。

MQL5 / C++
class="num">35. class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">36. class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
class="num">37. {
class="num">38.    m_Buff[class="num">0] = user00 * class="num">3.0;
class="num">39.    
class="num">40.    class="kw">return rates_total;
class="num">41. }
class="num">42. class=class="str">"cmt">//+------------------------------------------------------------------+

用宏与缓冲区给指标上锁

这段 MQL5 片段演示了如何通过一个临时短名宏 def_ShortNameTmp 防止同一图表重复加载指标实例。OnInit 里先拿到 ChartID,若 ChartWindowFind 发现原短名已存在,就删掉临时实例并返回 INIT_FAILED,终端日志会打出 Only one instance is allowed。 代码里 user00 作为输入参数默认 0.0,OnInit 末尾用 Print 输出配置值,方便在 MT5 专家日志里确认加载状态。SetIndexBuffer 把 m_Buff 绑定为 INDICATOR_CALCULATIONS 类型,ArrayInitialize 填 EMPTY_VALUE,避免历史残留数据干扰。 OnCalculate 取 rates_total-3 作为写入起点,把字符串 Config 转字符数组后写入 info.dValue,再将 user00 分别乘以 2.0 和 2.5 落进后两个缓冲区位。若你把 user00 设为 1.0,对应缓冲位会写出 2.0 与 2.5,开 MT5 挂上指标即可在数据窗口核验。 外汇与贵金属杠杆品种波动剧烈,这类自定义指标仅作结构验证,实盘信号须结合自身风控,亏损概率始终存在。

MQL5 / C++
class="macro">#define def_ShortNameTmp      def_ShortName + "_Tmp"
class="kw">input class="type">class="kw">double user00 = class="num">0.0;
class="type">long m_id;
class="type">class="kw">double m_Buff[];
class="type">int OnInit()
{
   m_id = ChartID();
   IndicatorSetString(INDICATOR_SHORTNAME, def_ShortNameTmp);
   if (ChartWindowFind(m_id, def_ShortName) != -class="num">1)
   {
      ChartIndicatorDelete(m_id, class="num">0, def_ShortNameTmp);
      Print("Only one instance is allowed...");
      class="kw">return INIT_FAILED;
   }
   IndicatorSetString(INDICATOR_SHORTNAME, def_ShortName);
   Print("Indicator configured with the following value:", user00);
   SetIndexBuffer(class="num">0, m_Buff, INDICATOR_CALCULATIONS);
   ArrayInitialize(m_Buff, EMPTY_VALUE);
   class="kw">return INIT_SUCCEEDED;
}
class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[])
{
   uCharDouble info;
   class="type">int pos = rates_total - class="num">3;
   StringToCharArray("Config", info.cInfo);
   m_Buff[pos + class="num">0] = info.dValue;
   m_Buff[pos + class="num">1] = user00 * class="num">2.0;
   m_Buff[pos + class="num">2] = user00 * class="num">2.5;
   class="kw">return rates_total;
}
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Mode Swap\Defines.mqh>
class="macro">#define def_SWAP "Indicators\\Mode Swap\\Swap MSG.ex5"
class="macro">#resource "\" + def_SWAP

「把自定义指标挂上图表并读缓冲」

这段初始化逻辑解决一个实操问题:EA 启动时如何不重复加载 SWAP 类自定义指标,又能顺手把它的前三个缓冲值抓出来打印。外汇与贵金属品种点差和掉期波动大,用脚本先验证指标句柄有效性,比盲目 iCustom 更稳。 先看输入与全局句柄。user00 默认 2.2 作为指标参数传入;m_handle 存指标句柄,m_id 存当前图表 ID,后续增删指标都靠这两个变量定位。 OnInit 里先用 ChartIndicatorGet 按短名查是否已挂载,查不到才用 iCustom(NULL, PERIOD_CURRENT, "::"+def_SWAP, user00) 现建一个并 ChartIndicatorAdd 到主窗口。这样重复加载 EA 不会叠出多个同名指标。 读数据时用 CopyBuffer(m_handle, 0, 0, 3, Buff),只拉 0~2 共 3 根缓冲;若返回值小于 3 说明读取失败直接报错。Buff[0] 塞进 uCharDouble 结构做字符转换,Buff[1]、Buff[2] 转字符串拼成日志,Print 出来就能在终端看到指标回传的内容。 OnDeinit 负责清理:ChartIndicatorDelete 删图表上的指标,IndicatorRelease 释放句柄,避免 MT5 退出时残留对象。OnTick 留空,说明这版只做初始化验证,不介入实时报价。

MQL5 / C++
class="kw">input class="type">class="kw">double user00 = class="num">2.2;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int m_handle;
class="type">long m_id;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{  
   class="type">class="kw">double Buff[];
   uCharDouble Info;
   class="type">int iRet;
   class="type">class="kw">string szInfo;
   
   m_id = ChartID();
   if ((m_handle = ChartIndicatorGet(m_id, class="num">0, def_ShortName)) == INVALID_HANDLE)
   {
      m_handle = iCustom(NULL, PERIOD_CURRENT, "::" + def_SWAP, user00);
      ChartIndicatorAdd(m_id, class="num">0, m_handle);
   }
   ArraySetAsSeries(Buff, false);
   if (m_handle == INVALID_HANDLE) szInfo = "Invalid handler to read the buffer.";
   else
   {
      if ((iRet = CopyBuffer(m_handle, class="num">0, class="num">0, class="num">3, Buff)) < class="num">3) szInfo = "Buffer reading failed.";
      else
      {
         Info.dValue = Buff[class="num">0];
         szInfo = CharArrayToString(Info.cInfo) + " [ " + (class="type">class="kw">string)Buff[class="num">1] + " ] [ " + (class="type">class="kw">string)Buff[class="num">2] + " ]";
      }
   }
   Print("Return => ", szInfo);
           
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
   ChartIndicatorDelete(m_id, class="num">0, def_ShortName);
   IndicatorRelease(m_handle);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 下一篇接什么

前面三篇把指标与回放系统之间的数据传递骨架搭好了,这一篇收在「不附文件」的硬要求上:作者刻意只给思路不给工程包,逼你手写那几段短代码形成肌肉记忆。外汇与贵金属模拟盘本身高风险,纯代码练手不等于实盘可盈利,别把回放跑顺就当成了胜率保证。 下一篇要做的就是把 Chart Trader 接进回放/模拟系统,届时过程会比现在复杂一个量级。建议你现在就开 MT5 新建一个空 EA,把前几节说的指标窗口隐藏和跨进程传数先默写一遍——代码量很小,但漏掉任一环节,后面集成只会更晕。

常见问题

多半是索引错位:CopyBuffer 的 start_pos 与缓冲区实际写入位置没对齐。先打印缓冲长度和索引,确认零位落在哪一行再读。
能降低并发覆盖概率,但要在 OnInit 里初始化锁状态。配合缓冲区序号校验,可以避免多周期切换时的错写。
可以。小布能扫描当前图表指标,提示缓冲区索引错位或零位异常,省去你手动逐行打印排查。
检查指标是否用正确句柄调用、缓冲编号是否匹配。常见问题是挂图用默认缓冲,EA 却读了扩展缓冲。
倾向接着讲多指标协同与缓冲同步调度,把前面零位、上锁、挂图读缓冲串成一套可用框架。