开发回放系统(第 49 部分):事情变得复杂 (一)·进阶篇
(2/3)· 还在让回放指标裸露在 MT5 列表里?本篇用实战拆解演示如何逐步剥离冗余绑定
「回放等待状态与生命周期的事件桥接」
指标主循环里用 OnCalculate 轮询跨进程全局变量,把「回放等待」信号转成图表事件。第53行先读 def_GlobalVariableReplay 的 double 值塞进联合体 Info,第54–65行用静态 bool bWait 做边沿检测:只在 isWait 从 false 变 true 时发 ev_WaitOn,恢复时发 ev_WaitOff 并回传当时的 df_Value,避免每根 K 线重复触发事件。 OnChartEvent 只有一行,把 id/lparam/dparam/sparam 直接甩给 control 指针的 DispatchMessage,说明所有鼠标键盘交互都委托给一个外部控件对象,指标本身不解析图表事件。 OnDeinit 里能看到卸载时的位运算收尾:REASON_CHARTCHANGE 分支把 ul 左移 def_BitShift 位,再对 GlobalVariableGet(def_GlobalVariableIdGraphics) 读出的图形 ID 做异或翻转,相当于通知另一进程「这张图的图形资源位变了」。外汇与贵金属 EA 跑这套跨进程通信时,全局变量被其他终端改写可能引发状态错乱,实盘前建议在 MT5 策略测试器用打印看每次 reason 分支是否如预期触发。
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(def_InfoTerminal.ID, C_Controls::ev_WaitOn, class="num">1, class="num">0, ""); bWait = true; } }else if (!Info.s_Infos.isWait) { EventChartCustom(def_InfoTerminal.ID, C_Controls::ev_WaitOff, class="num">1, Info.u_Value.df_Value, ""); bWait = class="kw">false; } class="kw">return rates_total; } 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); } class="type">void OnDeinit(class="kw">const class="type">int reason) { u_Interprocess Info; class="type">class="kw">ulong ul = class="num">1; class="kw">switch (reason) { case REASON_CHARTCHANGE: ul <<= def_BitShift; Info.u_Value.df_Value = GlobalVariableGet(def_GlobalVariableIdGraphics); Info.u_Value.IdGraphic ^= ul;
用全局变量接管回放会话的生灭
EA 在 OnDeinit 里靠 reason 参数区分卸载场景,只对回放专用品种做清理,避免误关普通图表。下面这段把图形状态写进全局变量后直接 break,是保留可视化上下文的典型写法。
- GlobalVariableSet(def_GlobalVariableIdGraphics, Info.u_Value.df_Value);
- break;
- case REASON_REMOVE:
- case REASON_CHARTCLOSE:
- if (def_InfoTerminal.szSymbol != def_SymbolReplay) break;
- GlobalVariableDel(def_GlobalVariableReplay);
- ChartClose(def_InfoTerminal.ID);
- break;
- }
- delete control;
- delete terminal;
- }
- //+------------------------------------------------------------------+
逐行看:86 行把图形 ID 对应的数值塞进全局变量,终端重启后还能读回来;90 行先比对品种名,不是回放品种就直接跳出,说明普通图表卸载不会触发 91–92 的删除与关图;91 行删掉回放全局变量,92 行关掉对应图表 ID。 开 MT5 把 def_SymbolReplay 换成你自己的回放前缀,挂在 EURUSD 上手动关图,验证普通图表不会被这段逻辑误关。外汇与贵金属回测环境波动剧烈,这类自动化清理在高波动时段可能延迟执行,属于高风险操作环节。
class="num">86. GlobalVariableSet(def_GlobalVariableIdGraphics, Info.u_Value.df_Value); class="num">87. class="kw">break; class="num">88. case REASON_REMOVE: class="num">89. case REASON_CHARTCLOSE: class="num">90. if (def_InfoTerminal.szSymbol != def_SymbolReplay) class="kw">break; class="num">91. GlobalVariableDel(def_GlobalVariableReplay); class="num">92. ChartClose(def_InfoTerminal.ID); class="num">93. class="kw">break; class="num">94. } class="num">95. class="kw">delete control; class="num">96. class="kw">delete terminal; class="num">97. }
◍ 把指标锁进服务内部
回放模拟器早期靠一个全局终端变量指定指标该挂哪张图,用户碰不了别的图表。但这套思路在「服务统一调度」面前显得多余——指标作为服务资源存在时,用户根本访问不到文件,安全层级和访问控制逻辑整个变了。 上一节我没急着删 ID 变量,就是怕系统改残。优秀程序员的底气往往是「不着急」:一个问题一个问题拆,保留现有功能再慢慢扩。座右铭就一句——分而治之。 清理 Interprocess.mqh 后编译,报错数量会吓人(图06展示了一次十几个错误的输出),但别慌,从清单第一个错顺次修就行。C_Replay.mqh 第28行起要去掉 u_Value 引用;指标文件跟着改完仍报错(图07、图08),再修 C_Controls.mqh 里同名的冗余引用。全部改完编译通过时(图10),指标也一并编进服务 exe 了。 此时若手动删掉指标可执行文件,MT5 里跑系统会只开图、不加载控制指标——因为原模板仍把它当独立文件调,而我们的目标恰恰是绕开模板、让服务自己挂指标。这部分改动留到下篇:要删掉重复代码,否则直接走服务会令控制指标极不稳定。外汇与贵金属模拟环境同样存在平台限制带来的高风险,动手前建议在沙盒账户验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_SymbolReplay "RePlay" class="macro">#define def_GlobalVariableReplay def_SymbolReplay + "_Infos" class="macro">#define def_GlobalVariableServerTime def_SymbolReplay + "_Time" class="macro">#define def_MaxPosSlider class="num">400 class=class="str">"cmt">//+------------------------------------------------------------------+ union u_Interprocess { class="type">class="kw">double df_Value; class=class="str">"cmt">// Value of the terminal global variable... class="kw">struct st_0 { class="type">bool isPlay; class=class="str">"cmt">// Indicates whether we are in Play or Pause mode... class="type">bool isWait; class=class="str">"cmt">// Tells the user to wait... class="type">bool isHedging; class=class="str">"cmt">// If true we are in a Hedging account, if class="kw">false the account is Netting... class="type">bool isSync; class=class="str">"cmt">// If true indicates that the service is synchronized... class="type">class="kw">ushort iPosShift; class=class="str">"cmt">// Value between class="num">0 and class="num">400... }s_Infos; class="type">class="kw">datetime ServerTime; }; class=class="str">"cmt">//+------------------------------------------------------------------+ union uCast_Double { class="type">class="kw">double dValue; class="type">long _long; class=class="str">"cmt">// class="num">1 Information class="type">class="kw">datetime _datetime; class=class="str">"cmt">// class="num">1 Information }
「用联合体偷看 double 的字节布局」
在 MQL5 里想做进程间传参或回放控制,常要把一个 double 拆成原始字节。上面这段结构里用了联合体思路:int 数组按 sizeof(double) 取长度得到 2 个成员(在 64 位下实际是占位写法,注释写「2 Informations」),char 数组同样长度得到 8 字节(注释「8 Informations」),本质是把 8 字节双精度浮点映射到 8 个可寻址 char。 真正落地时,C_Replay 类私有继承 C_ConfigService,内部用两个匿名 struct 分别挂 K 线缓冲(st01 含 MqlRates Rate[1] 与 memDT)和回放状态(st02 含 bInit、PointsPerTick、MqlTick tick[1])。AdjustPositionToReplay 从全局变量读回放进度:Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay),再比对 iPosShift 与 (m_ReplayCount * def_MaxPosSlider * 1.0) / m_Ticks.nTicks,相等就直接 return,避免重复刷新。 开 MT5 把这段贴进 EA 工程,改 def_GlobalVariableReplay 名字后编译,能在全局变量窗口看到回放游标随 tick 推进。外汇与贵金属杠杆高,回放仅用于历史逻辑验证,实盘信号仍可能失效。
class="type">int _int[class="kw">sizeof(class="type">class="kw">double)]; class=class="str">"cmt">// class="num">2 Informations class="type">char _char[class="kw">sizeof(class="type">class="kw">double)]; class=class="str">"cmt">// class="num">8 Informations }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_ConfigService.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Replay : class="kw">private C_ConfigService { class="kw">private : class="type">long m_IdReplay; class="kw">struct st01 { class="type">MqlRates Rate[class="num">1]; class="type">class="kw">datetime memDT; }m_MountBar; class="kw">struct st02 { class="type">bool bInit; class="type">class="kw">double PointsPerTick; class="type">MqlTick tick[class="num">1]; }m_Infos; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void AdjustPositionToReplay(class="kw">const class="type">bool bViewBuider) { u_Interprocess Info; class="type">MqlRates Rate[def_BarsDiary]; class="type">int iPos, nCount; Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay); if (Info.s_Infos.iPosShift == (class="type">int)((m_ReplayCount * def_MaxPosSlider * class="num">1.0) / m_Ticks.nTicks)) class="kw">return;
回放引擎里的定位与推送逻辑
这段回放核心在做一件事:根据滑块偏移量 iPosShift 在已缓存的 tick 序列里算出锚点位置,再把对应时间的 K 线塞进回放品种。锚点计算用了 (int)(nTicks * (iPosShift*1.0)/(def_MaxPosSlider+1)),意味着滑块满偏时 iPos 最大逼近 nTicks-1,不会越界。 若 bViewBuider 为真,只置等待标志并写全局变量 def_GlobalVariableReplay,把界面层挡住等人工步进;否则走自动推送:两个 for 空循环把 m_ReplayCount 和 nCount 顶到锚点时间之后,再调 CustomRatesUpdate 把历史 Rate 批量刷进 def_SymbolReplay。 最后一行 for 从 iPos-1 往回补到 m_ReplayCount,用 CreateBarInReplay(false) 逐根补 bar,并用 CustomTicksAdd 把 Info 里的 tick 附加进去;结束置 isWait=false 并回写全局变量。外汇与贵金属回放属高风险沙盒,结果仅作概率参考,实盘须独立验证。
iPos = (class="type">int)(m_Ticks.nTicks * ((Info.s_Infos.iPosShift * class="num">1.0) / (def_MaxPosSlider + class="num">1))); Rate[class="num">0].time = macroRemoveSec(m_Ticks.Info[iPos].time); CreateBarInReplay(true); if (bViewBuider) { Info.s_Infos.isWait = true; GlobalVariableSet(def_GlobalVariableReplay, Info.df_Value); }else { for(; Rate[class="num">0].time > (m_Ticks.Info[m_ReplayCount].time); m_ReplayCount++); for (nCount = class="num">0; m_Ticks.Rate[nCount].time < macroRemoveSec(m_Ticks.Info[iPos].time); nCount++); nCount = CustomRatesUpdate(def_SymbolReplay, m_Ticks.Rate, nCount); } for (iPos = (iPos > class="num">0 ? iPos - class="num">1 : class="num">0); (m_ReplayCount < iPos) && (!_StopFlag);) CreateBarInReplay(class="kw">false); CustomTicksAdd(def_SymbolReplay, m_Ticks.Info, m_ReplayCount); Info.df_Value = GlobalVariableGet(def_GlobalVariableReplay); Info.s_Infos.isWait = class="kw">false; GlobalVariableSet(def_GlobalVariableReplay, Info.df_Value); class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void CreateBarInReplay(class="kw">const class="type">bool bViewTicks)
◍ 回放模式下买卖价的随机点差补丁
在回放(Replay)且非真实 tick 的交易所报价模式里,引擎不会自带点差,需要手动给 ask/bid 补一层。上面这段代码就是干这件事的:用 rand() 取一个伪随机整数,再按区间判断是否额外加一个最小变动价位。 关键点在于 iRand 的判定窗口——只有随机数落在 29080 到 32767 之间(约占总范围 3658 / 32767 ≈ 11.2%)才可能叠加一个 PointsPerTick,且奇偶各半决定加或不加。这意味着绝大多数回放 tick 点差恒为 PointsPerTick,仅约 5.6% 概率出现双倍点差。 若当前 last 价格高于原 ask,代码会把 ask 拉到 last、bid 设为 last 减点差;若 last 低于 bid 则走另一分支处理。外汇与贵金属回测若忽略这套补丁,滑点假设会偏乐观,实盘大概率跑出更大偏差。开 MT5 把这段塞进你的回放类,打印 dSpread 分布就能看到真实点差厚度。
{
class="macro">#define def_Rate m_MountBar.Rate[class="num">0]
class="type">bool bNew;
class="type">class="kw">double dSpread;
class="type">int iRand = rand();
if (BuildBar1Min(m_ReplayCount, def_Rate, bNew))
{
m_Infos.tick[class="num">0] = m_Ticks.Info[m_ReplayCount];
if ((!m_Ticks.bTickReal) && (m_Ticks.ModePlot == PRICE_EXCHANGE))
{
dSpread = m_Infos.PointsPerTick + ((iRand > class="num">29080) && (iRand < class="num">32767) ? ((iRand & class="num">1) == class="num">1 ? m_Infos.PointsPerTick : class="num">0 ) : class="num">0 );
if (m_Infos.tick[class="num">0].last > m_Infos.tick[class="num">0].ask)
{
m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last;
m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last - dSpread;
}else if (m_Infos.tick[class="num">0].last < m_Infos.tick[class="num">0].bid)