开发回放系统 市场模拟(第 09 部分):自定义事件·综合运用
◍ 回放定位里的滑块换算与越界保护
在 MT5 回放引擎里,用户拖动的滑块值 iPosShift 取值范围是 0 到 400(def_MaxPosSlider),它并不直接等于 tick 序号,而是要先换算成实际回放位置。代码用 iPos = (int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * 1.0) / (def_MaxPosSlider + 1))) 做归一化,避免除零同时把 400 档映射到整段 tick 流。 换算完若 iPos < m_ReplayCount,说明目标位置还在已加载历史区内,这时会调用宏 macroSearchPosition 重算 dt_Local 与 m_ReplayCount,再用 CustomRatesDelete 清掉该时间之后的自定义 K 线。注意 m_ReplayCount 来自之前已回放的 tick 数,若滑块回拖就可能触发删除重算,外汇与贵金属 tick 重播中这种越界保护缺失会直接让图表错乱。 首次加载时 m_dtPrevLoading == 0,代码把 Rate[0] 的四价全部设为 m_Ticks.Info[m_ReplayCount].last,tick_volume 置 0。这一步等于用当前回放点的 last 价造出一根零成交 K 线当作锚点,后续 CustomRates 追加才有基准。
class="type">bool isWait; class=class="str">"cmt">// Asks the user to wait... class="type">class="kw">ushort iPosShift; class=class="str">"cmt">// A value between class="num">0 and class="num">400 ... }; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int AdjustPositionReplay(class="kw">const class="type">bool bViewBuider) { class="macro">#define macroSearchPosition { \ dt_Local = m_dtPrevLoading; m_ReplayCount = count = class="num">0; \ if (!bViewBuider) for (count = class="num">1; (count < m_Ticks.nRate) && (m_Ticks.Rate[count - class="num">1].spread < iPos); count++) \ { dt_Local = m_Ticks.Rate[count].time; m_ReplayCount = m_Ticks.Rate[count - class="num">1].spread; } \ } u_Interprocess Info; class="type">MqlRates Rate[def_BarsDiary]; class="type">int iPos = (class="type">int)((m_ReplayCount * def_MaxPosSlider * class="num">1.0) / m_Ticks.nTicks), count; class="type">class="kw">datetime dt_Local; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.iPosShift == iPos) class="kw">return class="num">0; iPos = (class="type">int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * class="num">1.0) / (def_MaxPosSlider + class="num">1))); if (iPos < m_ReplayCount) { macroSearchPosition; CustomRatesDelete(def_SymbolReplay, dt_Local, LONG_MAX); if (m_dtPrevLoading == class="num">0) { Rate[class="num">0].close = Rate[class="num">0].open = Rate[class="num">0].high = Rate[class="num">0].low = m_Ticks.Info[m_ReplayCount].last; Rate[class="num">0].tick_volume = class="num">0;
「回放引擎的自定义事件与按钮资源绑定」
这段逻辑处在行情回放类的收尾段:当重放计数落后于界面游标且未处于构建视图状态时,先执行 macroSearchPosition 定位,再一次性把 m_Ticks.Rate 中 count 根自定义 K 线推给 def_SymbolReplay 品种。若 bViewBuider 为真,则把 Info.s_Infos.isWait 置 true 并通过 GlobalVariableSet 把状态写进全局变量 def_GlobalVariableReplay,让 UI 线程感知阻塞。 循环段用 for(iPos = iPos>0?iPos-1:0; m_ReplayCount<iPos && !_StopFlag; m_ReplayCount++) Event_OnTime() 逐根回放,每步都检查终端停止标志 _StopFlag,避免 MT5 退出时死循环。回放结束后从全局变量读回 Info.u_Value.df_Value,清掉 isWait 标志,再 return Event_OnTime() 交还控制权。 下方枚举 EventCustom 只定义了 Ev_WAIT_ON / Ev_WAIT_OFF 两个自定义事件,用于播放器在「等待数据」与「继续」之间切换。六个 #define 把按钮位图路径收敛成 def_ButtonPlay、def_ButtonPause、def_ButtonLeft、def_ButtonRight、def_ButtonPin、def_ButtonWait,并逐一用 #resource 编译进 ex5;路径都挂在 Images\Market Replay\ 下,少一个文件编译就会报 cannot open resource。 C_Controls 类 private 区先存 m_szBtnPlay、m_id、m_bWait,再用 st_00 结构打包左移、右移、钉住、滑条四个子控件名与 posPinSlider 整数。开盘前你可直接在 MT5 的 MQL5\Images\Market Replay 目录塞入这六张 bmp,否则回放面板按钮会加载成空白。
Rate[class="num">0].time = m_Ticks.Info[m_ReplayCount].time - class="num">60; CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1); } if ((iPos > m_ReplayCount) && (!bViewBuider)) { macroSearchPosition; CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, count); } if (bViewBuider) { Info.s_Infos.isWait = true; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); } for (iPos = (iPos > class="num">0 ? iPos - class="num">1 : class="num">0); (m_ReplayCount < iPos) && (!_StopFlag); m_ReplayCount++) Event_OnTime(); Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait = class="kw">false; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); class="kw">return Event_OnTime(); } enum EventCustom {Ev_WAIT_ON, Ev_WAIT_OFF}; class="macro">#define def_ButtonPlay "Images\\Market Replay\\Play.bmp" class="macro">#define def_ButtonPause "Images\\Market Replay\\Pause.bmp" class="macro">#define def_ButtonLeft "Images\\Market Replay\\Left.bmp" class="macro">#define def_ButtonRight "Images\\Market Replay\\Right.bmp" class="macro">#define def_ButtonPin "Images\\Market Replay\\Pin.bmp" class="macro">#define def_ButtonWait "Images\\Market Replay\\Wait.bmp" class="macro">#resource "\\" + def_ButtonPlay class="macro">#resource "\\" + def_ButtonPause class="macro">#resource "\\" + def_ButtonLeft class="macro">#resource "\\" + def_ButtonRight class="macro">#resource "\\" + def_ButtonPin class="macro">#resource "\\" + def_ButtonWait class C_Controls { class="kw">private : class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string m_szBtnPlay; class="type">long m_id; class="type">bool m_bWait; class="kw">struct st_00 { class="type">class="kw">string szBtnLeft, szBtnRight, szBtnPin, szBarSlider; class="type">int posPinSlider;
播放键触发滑条控件的开关逻辑
在自定义指标或 EA 的 C_Controls 类里,构造函数把播放按钮和各滑条子对象指针先置为 NULL,避免未创建时误访问。m_id 初始化为 0、m_bWait 为 false,意味着启动阶段不拦截任何图表事件。 DispatchMessage 接管图表事件分发,用静态变量 six 与 sps 记录滑条状态,局部变量 x、y、px1、px2 留给坐标换算。CHARTEVENT_OBJECT_CLICK 分支先判断 m_bWait,若正处于等待态直接 break,防止重复触发。 当点击对象名等于 m_szBtnPlay 时,通过 ObjectGetInteger 读 OBJPROP_STATE 得到按钮是否按下,赋给进程间结构体的 isPlay 字段。若 isPlay 为 false 就 CreteCtrlSlider 创建滑条;为 true 则 RemoveCtrlSlider 销毁并把 szBtnPin 清空——这一正一反就是播放/暂停切换滑条面板的硬开关。 实盘接这套代码时注意:外汇与贵金属杠杆高、滑点随机,图表对象点击响应依赖主图重绘节奏,回测里点按钮不会触发真实事件,只能在 MT5 可视化回测或实盘手动验证。
C_Controls() : m_id(class="num">0), m_bWait(class="kw">false) { m_szBtnPlay = NULL; m_Slider.szBarSlider = NULL; m_Slider.szBtnPin = NULL; m_Slider.szBtnLeft = NULL; m_Slider.szBtnRight = NULL; } 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">static class="type">int six = -class="num">1, sps; class="type">int x, y, px1, px2; class="kw">switch (id) { case CHARTEVENT_OBJECT_CLICK: if (m_bWait) class="kw">break; if (sparam == m_szBtnPlay) { Info.s_Infos.isPlay = (class="type">bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE); if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else { RemoveCtrlSlider(); m_Slider.szBtnPin = NULL; } } } }
◍ 回放滑块与播放暂停的消息派发
回放控制面板的滑块拖动依赖全局变量传递位移量。当滑块位置变更时,先把物理像素位移强转成 ushort 写进 Info.s_Infos.iPosShift,再用 GlobalVariableSet 把打包后的 8 字节值刷进 def_GlobalVariableReplay,最后 ChartRedraw 强制重绘,界面才会跟着动。 左右微调按钮走的是另一分支:收到 szBtnLeft 就给 posPinSlider 减 1,收到 szBtnRight 则加 1,步长固定为 1 个像素单位,靠 PositionPinSlider 内部做边界 clamp。 播放/等待状态的切换由自定义图表事件驱动。Ev_WAIT_ON 把 m_bWait 置 true 并画成等待位图;Ev_WAIT_OFF 则从全局变量读回当前回放结构体,按 Info.s_Infos.isPlay 还原按钮状态,避免子窗口重启后状态丢失。 所有鼠标与自定义事件最终在 OnChartEvent 里转交 Control.DispatchMessage 处理,EA 主体不直接碰控件逻辑,分层清晰便于在 MT5 里单独改控制类而不动框架。
Info.s_Infos.iPosShift = (class="type">class="kw">ushort) m_Slider.posPinSlider; GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); ChartRedraw(); }else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - class="num">1); else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + class="num">1); class="kw">break; class=class="str">"cmt">// ... The rest of the code .... class="type">void CreateBtnPlayPause(class="type">bool state) { m_szBtnPlay = def_PrefixObjectName + "Play"; CreateObjectBitMap(class="num">5, class="num">25, m_szBtnPlay, (m_bWait ? def_ButtonWait : def_ButtonPause), (m_bWait ? def_ButtonWait : def_ButtonPlay)); ObjectSetInteger(m_id, m_szBtnPlay, OBJPROP_STATE, state); } 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">static class="type">int six = -class="num">1, sps; class="type">int x, y, px1, px2; class="kw">switch (id) { case (CHARTEVENT_CUSTOM + Ev_WAIT_ON): m_bWait = true; CreateBtnPlayPause(true); class="kw">break; case (CHARTEVENT_CUSTOM + Ev_WAIT_OFF): m_bWait = class="kw">false; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); CreateBtnPlayPause(Info.s_Infos.isPlay); class="kw">break; class=class="str">"cmt">// ... The rest of the code ... class="type">void OnChartEvent(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) { Control.DispatchMessage(id, lparam, dparam, sparam); }
「用全局变量在回放里切等待态」
在 MT5 自定义指标里,想让图表知道「回放引擎正在等信号」还是「已经放行」,靠的是进程间全局变量而不是本地变量。上面这段 OnCalculate 就是典型接法:每次报价进来都去读一个名叫 def_GlobalVariableReplay 的全局变量,把它塞进联合体 u_Interprocess,再取里面的 s_Infos.isWait 标志位。 静态布尔 bWait 只在本指标实例内记忆「上一次是不是已经发过等待事件」,避免每根 K 线都重复推 EventChartCustom。第一次发现 isWait 为真,就向图表推 Ev_WAIT_ON 并把 bWait 置真;之后若标志翻回假,推 Ev_WAIT_OFF 并回写当时的 df_Value 作为自定义事件参数,同时 bWait 归位。 实盘或复盘里你可以直接打开 MT5 全局变量窗口看 def_GlobalVariableReplay 的实时数值,若它长时间停在 isWait=1 对应的位模式,说明回放脚本卡在等待条件,手动排查比猜指标为何不动要快得多。外汇与贵金属品种波动受杠杆与流动性影响,这类等待态切换频繁,属正常高风险现象。
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="kw">static class="type">bool bWait = class="kw">false; u_Interprocess Info; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (!bWait) { if (Info.s_Infos.isWait) { EventChartCustom(m_id, Ev_WAIT_ON, class="num">0, class="num">0, ""); bWait = true; } }else if (!Info.s_Infos.isWait) { EventChartCustom(m_id, Ev_WAIT_OFF, class="num">0, Info.u_Value.df_Value, ""); bWait = class="kw">false; } class="kw">return rates_total; }
记住这一条就够了
控制指标在 OnCalculate 里每次都把当前状态写进终端全局变量,同时用局部静态变量标记回放服务是否繁忙;一旦发现服务忙就发自定义事件告警,状态翻转后再发就绪事件,这套握手逻辑靠 EventChartCustom 就能闭环。 EventChartCustom 并不只限当前图表和指标,把参数填对,就能向任意图表、指标或 EA 抛事件,剩下交由 MT5 内核分发,这点很多老手都误以为平台做不到。 外汇与贵金属仿真回放属于高风险验证环境,实盘前务必在策略测试器跑通。把静态变量和自定义事件的联动吃透,你就能自己扩出一套可控的行情重演框架。