开发回放系统 市场模拟(第 13 部分):模拟器的诞生(III)(基础篇)
◍ 用 MQL5 把历史报价喂进模拟器
在 MT5 里做市场回放,核心是先拿到可逐帧推进的历史数据。MQL5 提供 CopyTicksRange,能把指定品种在一段时间内的所有 tick 拉到数组里,这是离线模拟器的基础原料。 下面这段代码演示了如何取最近 1 小时的 EURUSD tick 并输出数量。实际跑的时候把符号和区间换成你关心的品种即可,贵金属 XAUUSD 同理,但点差跳动更剧烈,回放时要留意滑点偏差。 CopyTicksRange 返回的数组是 MqlTick 结构,包含 bid、ask、time 等字段。若返回张数明显少于预期,多半是历史数据未下载完整,先在 MT5 的「工具—历史中心」补全再说。外汇与贵金属杠杆高、波动快,回放结论只代表历史概率,不等于未来倾向。
class="type">class="kw">string symbol = "EURUSD"; class="type">MqlTick ticks[]; class="type">class="kw">datetime from = TimeCurrent() - class="num">3600; class="type">class="kw">datetime to = TimeCurrent(); class="type">int copied = CopyTicksRange(symbol, ticks, COPY_TICKS_ALL, from, to); Print("Copied ticks: ", copied);
为什么旧回放系统在剧烈波动下会失真
要把回放/模拟器当真用,前提是它的行为一致:不能这一刻按一种逻辑跑,下一刻又不可预测地变样。否则你基于它做的任何验证都站不住脚。 现有这套从“首次实验(II)”沿用下来的系统,在接真实跳价做回放时就不算合适,遇到高波动性资产或交易日更明显。它建 1 分钟柱线的效率低,偶尔建一根本该 60 秒完成的柱要花更久,还伴随同步问题。这会给交易者一个错误印象:极端走位很好跟、很好做——实际并非如此。 根子在于柱线构建方式本身有毛病。作者自己也是推进到建模阶段才察觉,差异是随时间累积暴露的。本篇先不彻底修,只做调整并引入一套建 1 分钟柱的新机制,为后续回放/模拟打底。外汇与贵金属波动剧烈时这类同步失真更常见,实盘验证前务必在 MT5 用历史数据复跑对照。
「回放服务重写后为何反而更快」
把回放服务的复杂度从主文件挪进对象类,表面看服务文件变简单了,实质是把耗时操作封装成私有方法,只暴露少数接口给外部调用。这种「方法隐身」让服务文件不再关心图表 ID,改由指标创建的全局终端变量接管,主循环里只剩一个会阻塞的调用,其余时间都交给类内部的函数调度。 旧版在每次跳价前都要反复检测图表是否打开,这类检查单次虽只耗几毫秒,但累积起来会拖慢 1-分钟柱线的实际绘制。新版把图表打开检测挪出高频循环,每秒仅调用几次,延迟显著下降。实测中,相邻跳价间隔有时不到 2 毫秒,旧系统一次调用到下次调用远超 10 毫秒,根本处理不了;新方案在现代计算机上能把柱线构建压到不到 1 毫秒,仅靠 CPU 即可,不必上 OpenCL。 不过操作系统内部计数器精度有限,低于 16 毫秒时并不可靠,所以当前定时仍靠累积值加短暂暂停来近似,这部分尚在研究阶段。你可以调 input 里的 user03 = true 打开创建指标可视化,观察每根柱线间的时差,再手动改累积/暂停参数做优调。外汇与贵金属模拟涉及高杠杆与滑点风险,回放结果仅作历史行为参考,不代表实盘表现。 第二个内部循环负责正常构建 1-分钟柱线,退出条件只有三个:无更多数据、系统终止、用户暂停。暂停后会回到第一个循环等待重启,因此整体比旧版少了许多冗余判断。负责寻找回放起点的函数也不再返回值,且变为类私有,外部无法直接调用——这是新版效率提升的关键细节之一。
class="macro">#class="kw">property service class="macro">#class="kw">property icon "\\Images\\Market Replay\\Icon.ico" class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property version "class="num">1.13" class="macro">#class="kw">property description "Replay-simulation system for MT5." class="macro">#class="kw">property description "It is independent from the Market Replay." class="macro">#class="kw">property description "For details see the article:" class="macro">#class="kw">property description "[MQL5官方文档] class="macro">#class="kw">property link "[MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_Dependence "\\Indicators\\Market Replay.ex5" class="macro">#resource def_Dependence class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Market Replay\C_Replay.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ input class="type">class="kw">string user00 = "Config.txt"; class=class="str">"cmt">//"Replay" config file. input ENUM_TIMEFRAMES user01 = PERIOD_M5; class=class="str">"cmt">//Initial timeframe for the chart. input class="type">bool user02 = true; class=class="str">"cmt">//Visual bar construction. input class="type">bool user03 = true; class=class="str">"cmt">//Visualize creation metrics.
◍ 回放服务的启动握手与图表接管
在 MT5 里跑历史回放,第一步不是直接灌数据,而是跟 [Market Replay] 指标做权限握手。OnStart 里先 InitSymbolReplay 初始化回放品种,再用 SetSymbolReplay(user00) 尝试挂载;只有返回 true,才轮到 ViewReplay(user01) 去等指标放行。 ViewReplay 内部干了两件脏活:先扫一遍所有已开图表(ChartFirst / ChartNext),凡是符号等于 def_SymbolReplay 的旧图直接 ChartClose 关掉,避免重复窗口抢资源;随后用 ChartOpen 以指定周期重开一张干净回放图,并强制套用 Market Replay.tpl 模板。 拿到权限后主循环靠 LoopEventOnTime(user02, user03) 按帧推进,退出时 CloseReplay 收尾。外汇与贵金属回放仅用于策略验证,实盘价差与滑点可能使结果偏离,属高风险演练。 下面这段是核心启动与图表接管的原码,逐行看逻辑即可在 MT5 脚本里复刻:
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { C_Replay Replay; Replay.InitSymbolReplay(); if (Replay.SetSymbolReplay(user00)) { Print("Wait for permission from [Market Replay] indicator to start replay..."); if (Replay.ViewReplay(user01)) { Print("Permission received. The replay service can now be used..."); while (Replay.LoopEventOnTime(user02, user03)); } } Replay.CloseReplay(); Print("Replay service completed..."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ViewReplay(ENUM_TIMEFRAMES arg1) { u_Interprocess info; if ((m_IdReplay = ChartFirst()) > class="num">0) do { if (ChartSymbol(m_IdReplay) == def_SymbolReplay) { ChartClose(m_IdReplay); ChartRedraw(); } }while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0); info.u_Value.IdGraphic = m_IdReplay = ChartOpen(def_SymbolReplay, arg1); ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
回放线程里的全局变量握手
这段逻辑出现在历史回放模块的核心循环里,靠全局变量在回放图表与主程序之间传状态。先强制重绘回放图(ChartRedraw),再删掉旧的图形全局变量并写入本次帧的数值,属于典型的进程间轻量通信。 while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(750); 这一句说明:只要回放全局变量还没被另一端置位、脚本没收到停止信号、且回放图表仍挂着品种,就每 750 毫秒轮询一次,避免空转烧 CPU。 LoopEventOnTime 里换成了 100 毫秒的细粒度 Sleep,并且用 iTest 做三步校验:图表品种非空、能读到回放全局变量、且 Info.s_Infos.isPlay 为真才令 iTest=1 退出等待。外汇与贵金属回测中这类跨图表同步若 Sleep 设太短,实盘模拟可能卡顿;建议直接拷进 MT5 脚本改 Sleep 参数对比资源占用。
ChartRedraw(m_IdReplay); GlobalVariableDel(def_GlobalVariableIdGraphics); GlobalVariableTemp(def_GlobalVariableIdGraphics); GlobalVariableSet(def_GlobalVariableIdGraphics, info.u_Value.df_Value); while ((!GlobalVariableCheck(def_GlobalVariableReplay)) && (!_StopFlag) && (ChartSymbol(m_IdReplay) != "")) Sleep(class="num">750); class="kw">return ((!_StopFlag) && (ChartSymbol(m_IdReplay) != "")); class="type">bool LoopEventOnTime(const class="type">bool bViewBuider, const class="type">bool bViewMetrics) { u_Interprocess Info; class="type">int iPos, iTest; iTest = class="num">0; while ((iTest == class="num">0) && (!_StopFlag)) { iTest = (ChartSymbol(m_IdReplay) != "" ? iTest : -class="num">1); iTest = (GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -class="num">1); iTest = (iTest == class="num">0 ? (Info.s_Infos.isPlay ? class="num">1 : iTest) : iTest); if (iTest == class="num">0) Sleep(class="num">100); }
「回放循环里的节流与中断判定」
这段逻辑跑在回放主循环里,靠累计 tick 时间间隔来模拟行情推进节奏。核心闸门是 m_MountBar.delay,一旦累计超过 400 毫秒就触发一次界面同步与休眠。 while 循环以 m_ReplayCount < m_Ticks.nTicks 为边界,并用 _StopFlag 做硬中断;任一 tick 间隔 iPos 为负时加 1000 修正,避免跨秒回绕导致 delay 计算失真。 进入 >400 分支后先查 ChartSymbol(m_IdReplay) 为空就 break,再读全局变量 def_GlobalVariableReplay 拿播放状态;若 isPlay 为假直接 return true 退出回放。 最后把进度折算成滑块位置 iPosShift = (m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks,写回全局变量并 Sleep(m_MountBar.delay - 20),delay 归零。外汇与贵金属回放属高风险验证环境,参数仅作技术参照。
if ((iTest < class="num">0) || (_StopFlag)) class="kw">return class="kw">false; AdjustPositionToReplay(bViewBuider); m_MountBar.delay = class="num">0; while ((m_ReplayCount < m_Ticks.nTicks) && (!_StopFlag)) { CreateBarInReplay(bViewMetrics); iPos = (class="type">int)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - class="num">1].time_msc : class="num">0); m_MountBar.delay += (iPos < class="num">0 ? iPos + class="num">1000 : iPos); if (m_MountBar.delay > class="num">400) { if (ChartSymbol(m_IdReplay) == "") class="kw">break; GlobalVariableGet(def_GlobalVariableReplay, Info.u_Value.df_Value); if (!Info.s_Infos.isPlay) class="kw">return true; Info.s_Infos.iPosShift = (class="type">class="kw">ushort)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks); GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); Sleep(m_MountBar.delay - class="num">20); m_MountBar.delay = class="num">0;