开发回放系统 市场模拟(第 03 部分):调整设置(I)·进阶篇
◍ 在图表上挂一个播放/暂停位图按钮
想给 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 强制重绘。外汇与贵金属图表叠加此类控件时波动刷新快,建议先在模拟盘验证交互延迟。
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 强制重绘让按钮立刻可见。 实盘或回测接这套机制时,外汇和贵金属波动大、滑点不可控,全局变量传递有微小延迟,按钮状态只代表『用户意图』,不代表下单已成交,信号触发概率不等于盈利概率。
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 数值跳动。外汇与贵金属品种波动剧烈,这类交互逻辑仅用于辅助复盘,实盘信号仍需独立验证。
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 通信机制存在平台稳定性与断连风险,实盘前务必在策略测试器验证位宽。
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 决定发不发下一跳;delay 由 Replay.Event_OnTime() 回传,若返回负值(末跳已发)就 break。整个服务在末跳发完或图表关掉时永久结束。外汇与贵金属回放纯属本地仿真,实盘高风险,参数误用只会影响测试不构成保证。
别把正态当圣经
上面 delay = 3 只是示例初值,真实tick间隔可能远小于此;若你拿历史样本发现回放快进或卡顿,先调这个 int 再怀疑文件。
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(); } }