开发回放系统(第 73 部分):不寻常的通信(二)·进阶篇
(2/3)· 当自定义指标在初始化时莫名返回无关图表的错误,回放系统的通信层远比想象中古怪
◍ 从图表抓取指标缓冲区的真实写法
想在 EA 里直接读图表上某个指标的缓冲区,而不是重新计算一遍,核心就是 ChartIndicatorGet 配合 CopyBuffer。上面这段循环每 250 毫秒尝试一次,把名为 TEST 的指标第 0 条缓冲的最新 1 根数据读进 Buff 数组。 第 20 行先用 ChartIndicatorGet 拿图表 id 上副图指标 TEST 的句柄,再交给 CopyBuffer 取 0 号缓冲、从 0 偏移取 1 个值。第 21 行用 PrintFormat 把返回值 ret 和 Buff[0] 打进日志,ret 大于 0 才代表真正取到了数据。 第 22 行 Sleep(250) 是故意让出 CPU,避免死循环把终端卡死;第 23 行靠 _StopFlag 和用户停止信号退出,ret 小于等于 0 也会 break。外汇与贵金属行情跳动快、重连频繁,这种轮询方式在真实账户上可能因句柄失效返回 -1,开 MT5 挂个自定义指标名叫 TEST 就能当场验证返回值和日志输出。
class="num">20. ret = CopyBuffer(ChartIndicatorGet(id, class="num">0, "TEST"), class="num">0, class="num">0, class="num">1, Buff); class="num">21. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[class="num">0]); class="num">22. Sleep(class="num">250); class="num">23. } class="kw">while ((!_StopFlag) && (ret > class="num">0)); class="num">24. } class="num">25. class=class="str">"cmt">//+------------------------------------------------------------------+
拆解 C_Replay 头文件的重构点
重构 C_Replay.mqh 时,第一刀砍在第 23 行:原先的静态句柄变量 Handle 被整个删除,因为它依赖静态句柄机制,而上一节已论证这在回放引擎里不可靠。连带第 75–93 行的 UpdateIndicatorControl 过程及其所有引用一并清除,功能必须找新路子补上。
InitBaseControl 从第 243 行起有细微但关键的改动。第 253–255 行尝试加载控制指标,但指标加载有轻微延迟,不会瞬间完成;第 253 行先做存在性检查,第 256、258 行写死期望加载的指标名,真正触发在第 185 行。第 188 行循环死等指标挂上图表,而第 190 行在等鼠标指标时却去查控制指标缓冲区——原因是用户随时可能切图表周期。
周期切换的致命弱点暴露在第 52 行起的读取过程。第 54 行静态变量初始化为 0;若第 58 行缓冲区读取返回负值,说明控制指标已被删、图表基本废了,应用直接终止。第 64 行的时间框架锁定检查若返回 true(用户没切周期)就什么都不做,返回 false 则第 69–70 行存新周期和最后状态,第 72 行调另一过程。第 37 行才是真正向 MT5 发图表事件的地方,和缓冲区校验拆开,因为有时只发事件、有时要确认值变。
LoopEventOnTime 的修改虽不结构性,但解释了拆分原因:第 275 行首播前发自定义事件重验鼠标与控制指标,暂停后重跑;第 279 行只观察不触发;第 285 行再发事件让鼠标指标显剩余时间;第 293–294 行更新鼠标状态以侦测竞价模式进出;第 306 行查周期变化并重载。外汇与贵金属回放涉及高杠杆高风险,切周期逻辑跑错可能让模拟完全失真。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| C_Replay.mqh | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_ConfigService.mqh" class="macro">#include "C_Controls.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_IndicatorControl "Indicators\\Market Replay.ex5" class="macro">#resource "\\" + def_IndicatorControl class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_CheckLoopService((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != "")) class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_ShortNameIndControl "Market Replay Control" class="macro">#define def_MaxSlider(def_MaxPosSlider + class="num">1) class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Replay : class="kw">public C_ConfigService { class="kw">private : class="kw">struct st00 { C_Controls::eObjectControl Mode; uCast_Double Memory; class="type">class="kw">ushort Position; class="type">int Handle; }m_IndControl; class="kw">struct st01 { class="type">long IdReplay;
「回放控制结构体的字段与事件派发」
这段声明把回放引擎需要的运行时状态压进一个结构体:CountReplay 记录已重播的 tick 数,PointsPerTick 存每个 tick 对应的点数步长,tick 和 Rate 分别用长度为 1 的数组承接实时 tick 与 K 线数据。注意 MqlTick 与 MqlRates 都只开 1 格,说明回放是逐笔推,不是批量灌。 SendEventCustom 负责把控制面板的狀态塞进 16 位/8 位分段内存再抛出自定义图表事件。第 43–44 行写死 '_8b[7]='D'、_8b[6]='M'',相当于打了一个 'DM' 指纹,方便接收端判别是不是本回放器发的包。 派发完事件后用 CustomBookAdd 往虚拟品种 def_SymbolReplay 挂一笔 volume=1、price=1.0 的市价单快照。这一步不是真成交,只是让订单簿有内容可刷,MT5 里开对应的回放副图就能看到簿面被激活。 CheckIndicatorControl 用 static 变量记忆上次的 TimeFrame 与模式,避免每 tick 重读。第 58 行若 CopyBuffer 取 'Market Replay Control' 指标缓冲区失败就直接 ChartClose 关掉回放窗口——实盘外接该指标缺失时,这个保护会立刻杀窗,调试时别漏挂控件。
class="type">int CountReplay; class="type">class="kw">double PointsPerTick; class="type">MqlTick tick[class="num">1]; class="type">MqlRates Rate[class="num">1]; } m_Infos; stInfoTicks m_MemoryData; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">bool MsgError(class="type">class="kw">string sz0) { Print(sz0); class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void SendEventCustom(class="kw">const ENUM_BOOK_TYPE Arg1 = BOOK_TYPE_BUY_MARKET) { MqlBookInfo book[class="num">1]; m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode; m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;; m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;; EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, ""); book[class="num">0].price = class="num">1.0; book[class="num">0].volume = class="num">1; book[class="num">0].type = Arg1; CustomBookAdd(def_SymbolReplay, book, class="num">1); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void CheckIndicatorControl(class="type">void) { class="kw">static class="type">uchar memTimeFrame = class="num">0; class="kw">static C_Controls::eObjectControl memMode = m_IndControl.Mode; class="type">class="kw">double Buff[]; if (CopyBuffer(ChartIndicatorGet(m_Infos.IdReplay, class="num">0, "Market Replay Control"), class="num">0, class="num">0, class="num">1, Buff) < class="num">0) ChartClose(m_Infos.IdReplay); m_IndControl.Memory.dValue = Buff[class="num">0]; if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] >= m_IndControl.Position) {
◍ 指标控制块的缓冲同步与回放事件
这段逻辑负责把外部指标控制对象的状态搬进内存结构,并在回放模式下触发图表自定义事件。核心判断落在 m_IndControl.Memory._16b[eCtrlPosition] 与当前 Position 是否相等:相等才允许 CopyBuffer 取最新值,避免每帧无谓读取。
当状态字节被置为 ePlay 时,代码会把 Position 同步为内存里的目标位,并反向把当前模式和位置写回 _16b 数组,同时塞入 'D'、'M' 两个标记字节。这一手是给回放引擎发初始化信号用的。
EventChartCustom 用 evCtrlReplayInit 把 dValue 推到图表端,参数 0 和空串说明它只传一个双精度数值。外汇与贵金属行情受杠杆与跳空影响,这类自定义事件若频率过高,可能拖慢 MT5 主线程,实盘前建议在策略测试器里跑一遍帧率。
直接把下面这段贴进 EA 的 UpdateIndicatorControl 里就能对照看句柄无效分支是否提前返回,少踩 CopyBuffer 报 4011 的坑。
class="kw">inline class="type">void UpdateIndicatorControl(class="type">void) { class="type">class="kw">double Buff[]; if (m_IndControl.Handle == INVALID_HANDLE) class="kw">return; if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) { if (CopyBuffer(m_IndControl.Handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1) m_IndControl.Memory.dValue = Buff[class="num">0]; if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; { m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode; m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;; m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;; EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, ""); } }
回放图表回收与模拟报价刷新
做历史逐笔回放时,先要把上一轮挂在终端里的回放图表清掉,否则重复开图会拖慢 MT5 响应。下面这段 SweepAndCloseChart 从 ChartFirst 起遍历,遇到符号等于 def_SymbolReplay 的图表直接 ChartClose,直到 ChartNext 返回 0 才停。
class="type">void SweepAndCloseChart(class="type">void) { class="type">long id; if ((id = ChartFirst()) > class="num">0) do { if (ChartSymbol(id) == def_SymbolReplay) ChartClose(id); } class="kw">while ((id = ChartNext(id)) > class="num">0); }
class="kw">inline class="type">int RateUpdate(class="type">bool bCheck) { class="kw">static class="type">int st_Spread = class="num">0; st_Spread = (bCheck ? (class="type">int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + class="num">1); m_Infos.Rate[class="num">0].spread = (class="type">int)(def_MaskTimeService | st_Spread); CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); class="kw">return class="num">0; }
class="kw">inline class="type">void CreateBarInReplay(class="type">bool bViewTick) { class="type">bool bNew; class="type">class="kw">double dSpread; class="type">int iRand = rand(); if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[class="num">0], bNew)) { m_Infos.tick[class="num">0] = m_MemoryData.Info[m_Infos.CountReplay]; if (m_MemoryData.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)
class="type">void SweepAndCloseChart(class="type">void) { class="type">long id; if ((id = ChartFirst()) > class="num">0) do { if (ChartSymbol(id) == def_SymbolReplay) ChartClose(id); } class="kw">while ((id = ChartNext(id)) > class="num">0); } class="kw">inline class="type">int RateUpdate(class="type">bool bCheck) { class="kw">static class="type">int st_Spread = class="num">0; st_Spread = (bCheck ? (class="type">int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + class="num">1); m_Infos.Rate[class="num">0].spread = (class="type">int)(def_MaskTimeService | st_Spread); CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); class="kw">return class="num">0; } class="kw">inline class="type">void CreateBarInReplay(class="type">bool bViewTick) { class="type">bool bNew; class="type">class="kw">double dSpread; class="type">int iRand = rand(); if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[class="num">0], bNew)) { m_Infos.tick[class="num">0] = m_MemoryData.Info[m_Infos.CountReplay]; if (m_MemoryData.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)
「回放图表里的报价线怎么跟着品种切换」
上面那段代码干的事很具体:在回放引擎里,根据 last 价和当前 bid/ask 的相对位置,反向推算出另一侧的报价。若 last 高于 bid,就把 ask 设为 last、bid 设为 last 减点差;若 last 低于 bid,则 ask 设为 last 加点差、bid 设为 last。 落到图表显示,AdjustViewDetails 里用 ChartSetInteger 控制三条线:外汇模式(PRICE_FOREX)才显示 ask/bid 线,交易所模式(PRICE_EXCHANGE)显示 last 线。外汇和贵金属点差跳动快,这种显隐切换能避免看图时误把 last 当成交价。 还有个细节,CopyRates 取 PERIOD_M1 的 1 根 K 线,若 close 大于 0 且是交易所模式,就把 tick[0].last 同步成 rate[0].close。开 MT5 挂上回放脚本,改 ModePlot 参数从 PRICE_FOREX 切到 PRICE_EXCHANGE,能看到报价线从双线变单线,这是验证逻辑最直接的方式。
{
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)
{
m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last + dSpread;
m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last;
}
if (bViewTick)
CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
RateUpdate(true);
}
m_Infos.CountReplay++;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void AdjustViewDetails(class="type">void)
{
class="type">MqlRates rate[class="num">1];
ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
CopyRates(def_SymbolReplay, PERIOD_M1, class="num">0, class="num">1, rate);
if ((m_Infos.CountReplay == class="num">0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == class="num">0; m_Infos.CountReplay++);
if (rate[class="num">0].close > class="num">0)
{
if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
m_Infos.tick[class="num">0].last = rate[class="num">0].close;