开发回放系统 市场模拟(第 05 部分):加入预览·综合运用
回放引擎的 Tick 聚合与图表生命周期
做 MT5 市场回放,核心是把逐笔 tick 重组成 1 分钟 K 线并推到独立图表。下面这段逻辑直接操作自定义品种图表:开图、套模板、退场清场,都在几个函数里闭环。 ViewReplay 用 ChartOpen 以 PERIOD_M1 打开名为 def_SymbolReplay 的自定义品种,再 ApplyTemplate 载入 "Market Replay.tpl" 并 Redraw,返回图表 ID 供后续写数据。CloseReplay 则反向操作:关图、SymbolSelect(false) 移除市场报价可见性、CustomSymbolDelete 删品种、GlobalVariableDel 清全局变量,避免残留。 Event_OnTime 是回放节拍器。静态 MqlRates Rate[1] 只存一根 Bar;用 _dt 记录上一根时间,m_ReplayCount 遍历 m_Ticks.Info[]。当检测到新分钟(_dt != time),重置 vol 为 0;否则在同日毫秒内 while 循环把同毫秒 tick 全部累进 close/open/high/low 与 real_volume。 外汇与贵金属回放涉及高杠杆与滑点风险,实盘前务必用历史数据验证聚合是否丢失 tick。开 MT5 把这段接进 EA,先跑 2023 年 EURUSD 的 1 分钟样本,看 Rate[0].real_volume 累加是否与源 tick 数一致。
class="kw">return false; } class="kw">return true; } class="type">long ViewReplay(class="type">void) { m_IdReplay = ChartOpen(def_SymbolReplay, PERIOD_M1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl"); ChartRedraw(m_IdReplay); class="kw">return m_IdReplay; } class="type">void CloseReplay(class="type">void) { ChartClose(m_IdReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); } class="kw">inline class="type">int Event_OnTime(class="type">void) { class="type">bool bNew; class="type">int mili, iPos; u_Interprocess Info; class="kw">static class="type">MqlRates Rate[class="num">1]; class="kw">static class="type">class="kw">datetime _dt = class="num">0; if (m_ReplayCount >= m_Ticks.nTicks) class="kw">return -class="num">1; if (bNew = (_dt != m_Ticks.Info[m_ReplayCount].time)) { _dt = m_Ticks.Info[m_ReplayCount].time; Rate[class="num">0].real_volume = class="num">0; Rate[class="num">0].tick_volume = class="num">0; } mili = (class="type">int) m_Ticks.Info[m_ReplayCount].time_msc; do { while (mili == m_Ticks.Info[m_ReplayCount].time_msc) { Rate[class="num">0].close = m_Ticks.Info[m_ReplayCount].last; Rate[class="num">0].open = (bNew ? Rate[class="num">0].close : Rate[class="num">0].open); Rate[class="num">0].high = (bNew || (Rate[class="num">0].close > Rate[class="num">0].high) ? Rate[class="num">0].close : Rate[class="num">0].high); Rate[class="num">0].low = (bNew || (Rate[class="num">0].close < Rate[class="num">0].low) ? Rate[class="num">0].close : Rate[class="num">0].low); Rate[class="num">0].real_volume += (class="type">long) m_Ticks.Info[m_ReplayCount].volume_real; bNew = false; m_ReplayCount++; }
「回放进度与仓位重算的底层循环」
这段逻辑干了两件事:一边按毫秒级 tick 推进虚拟品种行情,一边根据滑块位置重算回放游标。mili 自增后和当前 tick 的 time_msc 做 do-while 比对,保证同一毫秒内的多笔 tick 被全部吃进,再借 CustomRatesUpdate 把合成 K 线推给 def_SymbolReplay。 进度同步靠全局变量传递。iPos 用 (m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks 算百分比位置,只有发生偏移才写 GlobalVariableSet,避免每 tick 无谓落盘。 AdjustPositionReplay 里若检测到滑块对应的 iPos 小于已回放计数,就 CustomRatesDelete 清掉从 m_dtPrevLoading 到 LONG_MAX 的历史,并把 m_ReplayCount 归零重跑——这意味着拖滑块往回拉会触发整段重放,而不是局部修补。 return 里的延时算法值得注意:time_msc 小于 mili 时补 (1000 - mili),否则减 mili,实质是把当前 tick 相对整秒起点的偏移换算成下次定时器间隔。外汇与贵金属回测用这类机制时波动放大,属高风险操作,参数不对可能跑出和实盘偏差极大的曲线。
mili++;
}while (mili == m_Ticks.Info[m_ReplayCount].time_msc);
Rate[class="num">0].time = m_Ticks.Info[m_ReplayCount].time;
CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
iPos = (class="type">int)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
GlobalVariableGet(def_GlobalVariableReplay, Info.Value);
if (Info.s_Infos.iPosShift != iPos)
{
Info.s_Infos.iPosShift = iPos;
GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
}
class="kw">return (class="type">int)(m_Ticks.Info[m_ReplayCount].time_msc < mili ? m_Ticks.Info[m_ReplayCount].time_msc + (class="num">1000 - mili) : m_Ticks.Info[m_ReplayCount].time_msc - mili);
}
class="type">int AdjustPositionReplay()
{
u_Interprocess Info;
class="type">MqlRates Rate[class="num">1];
class="type">int iPos = (class="type">int)((m_ReplayCount * def_MaxPosSlider * class="num">1.0) / m_Ticks.nTicks);
Info.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));
if (iPos < m_ReplayCount)
{
CustomRatesDelete(def_SymbolReplay, m_dtPrevLoading, LONG_MAX);
m_ReplayCount = class="num">0;
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;
Rate[class="num">0].time = m_Ticks.Info[m_ReplayCount].time - class="num">60;
CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1);
}
};
for (iPos = (iPos > class="num">0 ? iPos - class="num">1 : class="num">0); m_ReplayCount < iPos; m_ReplayCount++) Event_OnTime();
class="kw">return Event_OnTime();
}◍ 最后说句实在话
回放系统的核心配置与指标挂载逻辑在前文已拆透,这一节只补一个社区里真实踩过的坑。有用户在自建 EA 时照搬指标写法,用 IndicatorSetDouble 给 INDICATOR_CALCULATIONS 塞 OHLC 的 double 值,编译器报「必须是 double」,即便变量已显式转型、屏幕打印也显示 33573.43 / 33573.44 / 33569.98 / 33569.99 正常,依然编不过。 根因不在类型转换,而在运行时上下文:INDICATOR_CALCULATIONS 这类宏只服务于指标缓冲区,EA 里没有对应的指标环境,硬调必然失败。作者让他去翻 MQL5 的 running 文档才点醒——环境不对,代码再对也没用。 外汇与贵金属市场高杠杆、滑点无常,这类回放工具仅用于历史逻辑验证,实盘前务必在 MT5 策略测试器跑通再上真金。下回你抄片段时,先确认自己是写指标还是 EA,能省掉大半编译夜。
IndicatorSetDouble(INDICATOR_LEVELVALUE, class="num">0, class="num">100); IndicatorSetDouble(INDICATOR_LEVELVALUE, class="num">0, class="num">100); mas nessa parte da erro dizendo que tem que ser class="type">class="kw">double IndicatorSetDouble(INDICATOR_CALCULATIONS, class="num">1, (class="type">class="kw">double) open_price); IndicatorSetDouble(INDICATOR_CALCULATIONS, class="num">2, (class="type">class="kw">double) high_price); IndicatorSetDouble(INDICATOR_CALCULATIONS, class="num">3, (class="type">class="kw">double) low_price); IndicatorSetDouble(INDICATOR_CALCULATIONS, class="num">4, (class="type">class="kw">double) close_price);