开发回放系统  市场模拟(第 03 部分):调整设置(I)·进阶篇
📘

开发回放系统 市场模拟(第 03 部分):调整设置(I)·进阶篇

第 2/3 篇

◍ 在图表上挂一个播放/暂停位图按钮

想给 MT5 图表加一个可点击的播放/暂停控件,核心是用 OBJ_BITMAP_LABEL 创建位图标签,并绑定两张状态图。下面这段类方法展示了按钮从创建到析构的完整生命周期。 创建时先 ObjectCreate 生成标签,坐标用 XDISTANCE=5、YDISTANCE=25 把按钮推到图表左上角偏下位置,STATE 设 false 表示初始为暂停态。接着用 OBJPROP_BMPFILE 的索引 0 和 1 分别载入暂停图与播放图,切换状态时 MT5 会自动换图。 析构函数里直接 ObjectDelete(ChartID(), m_szBtnPlay) 清掉对象,避免重复加载报错。Init 方法有防重入判断:m_szBtnPlay 非空就 return,否则才建按钮并 ChartRedraw 强制重绘。外汇与贵金属图表叠加此类控件时波动刷新快,建议先在模拟盘验证交互延迟。

MQL5 / C++
ObjectCreate(id, m_szBtnPlay, OBJ_BITMAP_LABEL, class="num">0, class="num">0, class="num">0);
ObjectSetInteger(id, m_szBtnPlay, OBJPROP_XDISTANCE, class="num">5);
ObjectSetInteger(id, m_szBtnPlay, OBJPROP_YDISTANCE, class="num">25);
ObjectSetInteger(id, m_szBtnPlay, OBJPROP_STATE, false);
ObjectSetString(id, m_szBtnPlay, OBJPROP_BMPFILE, class="num">0, "::" + def_ButtonPause);
ObjectSetString(id, m_szBtnPlay, OBJPROP_BMPFILE, class="num">1, "::" + def_ButtonPlay);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">public:
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Controls()
  {
    m_szBtnPlay = NULL;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
~C_Controls()
  {
    ObjectDelete(ChartID(), m_szBtnPlay);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Init(class="type">void)
  {
    if (m_szBtnPlay != NULL) class="kw">return;
    CreateBtnPlayPause(m_id = ChartID());
    GlobalVariableTemp(def_GlobalVariableReplay);
    ChartRedraw();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「用全局变量把回放按钮状态甩给 EA」

在 MT5 里做历史回放面板,最麻烦的是把图表上的按钮点击传到 EA 逻辑里。上面这段把播放/暂停按钮的 OBJPROP_STATE 读出来,塞进一个 union 结构再写成全局变量,EA 那边轮询全局变量就能知道用户是不是点了播放。 DispatchMessage 在收到 CHARTEVENT_OBJECT_CLICK 且 sparam 等于 m_szBtnPlay 时,才执行读取和写入。也就是说,只有点中这个特定按钮才会触发状态同步,其他图表对象点击会被忽略,避免无谓的全局变量写操作。 Init 里先判断 m_szBtnPlay 非空就直接 return,保证按钮只创建一次;随后用 ChartID() 拿到图表句柄建按钮,并调用 GlobalVariableTemp 把回放用的全局变量注册成临时变量(终端退出即清掉),最后 ChartRedraw 强制重绘让按钮立刻可见。 实盘或回测接这套机制时,外汇和贵金属波动大、滑点不可控,全局变量传递有微小延迟,按钮状态只代表『用户意图』,不代表下单已成交,信号触发概率不等于盈利概率。

MQL5 / C++
class="type">void DispatchMessage(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
              {
              u_Interprocess Info;
              
              class="kw">switch (id)
              {
              case CHARTEVENT_OBJECT_CLICK:
                  if (sparam == m_szBtnPlay)
                  {
                  Info.s_Infos.isPlay = (class="type">bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                  GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
                  }
                  class="kw">break;
              }
              };
class="type">void Init(class="type">void)
{
    if (m_szBtnPlay != NULL) class="kw">return;
    CreateBtnPlayPause(m_id = ChartID());
    GlobalVariableTemp(def_GlobalVariableReplay);
    ChartRedraw();
}
class="type">void DispatchMessage(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
    u_Interprocess Info;
    
    class="kw">switch (id)
    {

图表按钮点击如何驱动回放开关

在 MT5 的图表事件回调里,CHARTEVENT_OBJECT_CLICK 是捕捉用户点击界面对象的主入口。当 sparam 等于预先定义的播放按钮名 m_szBtnPlay,就说明交易者按下了回放控制键。 下面这段分支直接读取按钮的 OBJPROP_STATE 属性,把布尔值写进 Info.s_Infos.isPlay,同时用 GlobalVariableSet 把当前结构值推到全局变量 def_GlobalVariableReplay。这样 EA 与指标之间就能跨周期共享“是否处于回放态”的状态。 [CODE] case CHARTEVENT_OBJECT_CLICK: if (sparam == m_szBtnPlay) { Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE); GlobalVariableSet(def_GlobalVariableReplay, Info.Value); } break; } [/CODE] 逐行拆解:第1行等待对象点击事件;第2行筛选是否是播放按钮被点;第4行用 ObjectGetInteger 取按钮的勾选状态并强转 bool;第5行把 Info.Value 写入名为 def_GlobalVariableReplay 的全局变量;第6行跳出分支避免继续穿透。 开 MT5 新建一个带按钮的面板,把 m_szBtnPlay 换成你的按钮 ID,挂上这段后点一下按钮,在全局变量窗口应能立刻看到 def_GlobalVariableReplay 数值跳动。外汇与贵金属品种波动剧烈,这类交互逻辑仅用于辅助复盘,实盘信号仍需独立验证。

MQL5 / C++
case CHARTEVENT_OBJECT_CLICK:
  if (sparam == m_szBtnPlay)
  {
    Info.s_Infos.isPlay = (class="type">bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
    GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
  }
  class="kw">break;
}

◍ 用联合体在 EA 与服务间塞进 64 位指令

把回放系统从脚本改成 EA 之后,进程间通信不能再靠脚本的一次性执行,得靠终端全局变量常驻传递。Interprocess.mqh 里只定义了一个全局变量名 "Replay Infos",但它不是普通字符串,而是 EA 和后台服务共用的通信句柄。 难点在 union(联合体)。MT5 终端全局变量只认 double 类型,固定 64 位;而我们要传的是播放状态、小时、分钟这类离散字段。double 和 long 都是 64 位,把二者做成联合,就能在同一块内存上既写控制位、又通过 double 交给全局变量。 位长是这里的核心数据:bool 占 1 位、char/uchar 占 8 位、short/ushort 16 位、int/uint 32 位、long/ulong 64 位;float 32 位、double 64 位。一个 uchar 变量等价于 8 个 bool 的位集合,联合长度就是 1 字节。代码里 Data.info=0x12 和分别置位 Data.bits[4]、bits[1] 为真,在初始位全 0 时结果一致。 原始联合 u_Interprocess 把 double Value 和结构体 s_Infos 叠在一起:bool isPlay 占 1 位,Time[3] 里每个元素含 Hours、Minutes 两个 char(各 8 位)。三层叠加后仍必须 ≤64 位,否则写进全局变量会丢信息。 别直接手写 double 数值去表达指令,那既难读又易错。正确做法是用命名片段置 0/1,让服务侧解出该干嘛。外汇与贵金属交易使用这类 EA 通信机制存在平台稳定性与断连风险,实盘前务必在策略测试器验证位宽。

MQL5 / C++
class="macro">#define def_GlobalVariableReplay "Replay Infos"
class=class="str">"cmt">//+------------------------------------------------------------------+
union u_Interprocess
{
    class="type">class="kw">double Value;
    class="kw">struct st_0
    {
        class="type">bool isPlay;
        class="kw">struct st_1
        {
            class="type">char Hours;
            class="type">char Minutes;
        }Time[class="num">3];
    }s_Infos;
};
union u_00
{
    class="type">char info;
    class="type">bool bits[class="num">8];
}Data;
Data.info = 0x12;
或 
Data.bits[class="num">4] = true;
Data.bits[class="num">1] = true;

「回放服务为什么跑起来却没反应」

把那段短代码直接挂上 'WINZ21_202110220900_202110221759' 文件去跑,多数人第一反应是「什么都没发生」。即便换成附件里的样本文件,现象也一样。根子在 id = Replay.ViewReplay(); 这一行——它内部悄悄做了窗口探测和全局变量握手,没搞懂就盲目启动,系统当然沉默。 数据流先拆开看:Replay.CreateSymbolReplay(user01) 从指定文件载跳价,失败就 return 终止;随后 ViewReplay() 载入 EA 并进入循环,靠 GlobalVariableCheck(def_GlobalVariableReplay) 轮询(每次 Sleep(750))等外部进程发许可。这里用全局终端变量当跨进程通信,是服务与外挂控制器之间的唯一桥梁。 暂停态的判断很刁钻:循环里同时测「回放窗口是否还在」和「全局变量是否存续」,任一不满足就退出。暂停时用联合 u_Interprocess 抓双精度里的位标记 isPlay,没有这个 union 位操作根本读不准状态。首次进暂停只发一次跳价,然后才 t1 = GetTickCount64() 记时——所谓暂停只是不再往回放品种塞 tick,服务本身没真挂起。 真要播放时,靠 (GetTickCount64() - t1) >= delay 决定发不发下一跳;delayReplay.Event_OnTime() 回传,若返回负值(末跳已发)就 break。整个服务在末跳发完或图表关掉时永久结束。外汇与贵金属回放纯属本地仿真,实盘高风险,参数误用只会影响测试不构成保证。 别把正态当圣经 上面 delay = 3 只是示例初值,真实tick间隔可能远小于此;若你拿历史样本发现回放快进或卡顿,先调这个 int 再怀疑文件。

MQL5 / C++
class="macro">#class="kw">property service
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Market Replay\C_Replay.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">string     user01 = "WINZ21_202110220900_202110221759"; class=class="str">"cmt">//File with ticks
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Replay        Replay;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
{
        class="type">class="kw">ulong t1;
        class="type">int delay = class="num">3;
        class="type">long id;
        u_Interprocess Info;
        class="type">bool bTest = false;
        
        if (!Replay.CreateSymbolReplay(user01)) class="kw">return;
        id = Replay.ViewReplay();
        Print("Waiting for permission to start replay ...");
        class="kw">while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(class="num">750);
        Print("Replay service started ...");
        t1 = GetTickCount64();
        class="kw">while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest)  bTest = (Replay.Event_OnTime() > class="num">0); else        t1 = GetTickCount64();
                }else if ((GetTickCount64() - t1) >= (class="type">uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < class="num">0) class="kw">break;
                        t1 = GetTickCount64();
                }
        }

常见问题

多半是位图按钮没绑定点击事件或窗口重绘被阻塞,检查按钮对象的 OBJPROP_ONCLICK 回调是否生效,并确认图表没有被其他脚本独占刷新。
全局变量有写入延迟,建议在点击时立即写值并让 EA 每帧轮询,同时加一个版本号字段避免读到旧状态。
可以,小布能扫描当前图表上的按钮对象与全局变量映射,提示回调缺失或状态字冲突,省去手动翻代码。
注意字节对齐和大小端,联合体里浮点与整型共用内存时别跨平台直接拷贝,先在本地打印十六进制核对布局。
先确认服务与图表 EA 的全局变量名一致,再看联合体指令高位是否被截断,最后查图表事件队列是否堆积未处理。