开发回放系统  市场模拟(第 13 部分):模拟器的诞生(III)(基础篇)
📘

开发回放系统 市场模拟(第 13 部分):模拟器的诞生(III)(基础篇)

第 1/3 篇

◍ 用 MQL5 把历史报价喂进模拟器

在 MT5 里做市场回放,核心是先拿到可逐帧推进的历史数据。MQL5 提供 CopyTicksRange,能把指定品种在一段时间内的所有 tick 拉到数组里,这是离线模拟器的基础原料。 下面这段代码演示了如何取最近 1 小时的 EURUSD tick 并输出数量。实际跑的时候把符号和区间换成你关心的品种即可,贵金属 XAUUSD 同理,但点差跳动更剧烈,回放时要留意滑点偏差。 CopyTicksRange 返回的数组是 MqlTick 结构,包含 bid、ask、time 等字段。若返回张数明显少于预期,多半是历史数据未下载完整,先在 MT5 的「工具—历史中心」补全再说。外汇与贵金属杠杆高、波动快,回放结论只代表历史概率,不等于未来倾向。

MQL5 / C++
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-分钟柱线,退出条件只有三个:无更多数据、系统终止、用户暂停。暂停后会回到第一个循环等待重启,因此整体比旧版少了许多冗余判断。负责寻找回放起点的函数也不再返回值,且变为类私有,外部无法直接调用——这是新版效率提升的关键细节之一。

MQL5 / C++
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 脚本里复刻:

MQL5 / C++
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 参数对比资源占用。

MQL5 / C++
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 归零。外汇与贵金属回放属高风险验证环境,参数仅作技术参照。

MQL5 / C++
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;

常见问题

把历史报价按时间顺序逐根喂给图表即可,核心是用循环控制每根数据的推送节奏,避免一次性灌入导致图表卡死。
旧回放系统在高波动时会因推送频率跟不上实际tick密度而丢数据失真,重写后的模拟器改用独立线程接管图表才解决。
可以,小布能按你选的时段和品种生成回放任务并接管图表,你只需专注看价格行为,不用自己写代码。
需要一次启动握手,模拟器先声明接管再读全局变量确认,跳过这步图表不会切换至回放模式。
回放循环里有节流和中断检查,当外部强制停止信号触发或推送间隔低于阈值时会退出,属正常保护机制。