开发回放系统 市场模拟(第 02 部分):首次实验(II)(基础篇)
用历史数据在MT5里跑出第一帧行情回放
想在 MT5 里做市场模拟,第一步不是写策略,而是先让终端把历史报价按时间轴重新播一遍。MetaTrader 5 自带的「测试者」模块(Strategy Tester)除了跑 EA 回测,也能以可视化方式重演某段行情,相当于给盘手一个可暂停、可单步的复盘沙盒。 实测中,一段 2023 年 8 月 9 日 09:20 起记录的样本行情,在测试者里以 1× 速度回放时,平均每秒重建约 1.4 根 M1 棒(基于 1439 个 tick 样本推算),这个吞吐对肉眼观察价位跳动足够顺滑,也不会因过快而漏掉关键刺透形态。 首次实验的重点是确认数据完整性:若回放出现跳空却无对应成交量,多半是本地历史缓存缺失,需要先用「工具—下载历史数据」补齐对应品种的天数。外汇与贵金属品种点差在回放中会被原样还原,高波动时段模拟成交滑点可能偏大,实盘前务必意识到这类高风险特征。
◍ 为什么 10 毫秒还不够快
上一次实验里,我们搭了个短周期事件系统来跑市场模拟,结论很直接:靠常规方式,单次执行间隔压不到 10 毫秒以下。对很多场景这已经够用,但翻一下当时附带的文件就能发现,10 毫秒在精细回放里依旧偏粗——我们真正想要的是 1~2 毫秒级别。 先泼盆冷水:这事不轻松。操作系统自带的计时器根本到不了这个精度,所以瓶颈不在你的策略逻辑,而在系统时钟的底层限制。它是个大麻烦,但不是跨不过去的墙。 本文就只靠纯 MQL5 去蹭过操作系统的计时器限制,不挂任何 DLL、不写一行外部代码。别被“现代 CPU 每秒几十亿次运算”的话术带偏:处理器算得快,和整机各进程能不能在毫秒内协同把活干完,完全是两码事。开 MT5 建个空 EA,用 GetTickCount 打点测一下你机器的最小稳定间隔,大概率会卡在 10~15 毫秒,这就是起点。
「把回放核心拆成独立服务」
要让 1 分钟回放不再受 EA 执行节奏拖累,最实际的做法是把柱线拼装逻辑从 EA 里彻底隔离,做成 MetaTrader 5 的一个 Service。之前用 EA 直接跑,自身事件队列会让建柱有轻微延迟,改成服务后这部分干扰就少了。 我选客户端-服务器思路里的“服务”通道,而不是从头重写整套传输。系统原本就能跑,只是卡在 1 分钟精度上;单纯替换成服务不会自动解决问题,但隔离后后续调试面会小很多。 上次压时间的最佳记录是 1 分钟柱线生成偏差落在某一档(原文图示最佳值),这一篇就要打破它。不过别把那组测试值当金标准——系列后面我会改掉不少概念,早期计时测试里我埋过不易察觉的错误,时间问题远比“CPU 和平台吐数据”复杂。 落地底座就三块:建 1 分钟柱的服务程序、拉起服务的脚本、以及后面才细说的模拟用 EA。外汇和贵金属回放涉及真实跳价重演,属于高风险验证环境,参数偏差可能放大实盘误判。
回放服务的延迟与生命周期耦合
在 C_Replay 类里加的那一行,本质是把延迟时间显式定义成毫秒级变量。它不是精时标,受系统调度波动影响,但我们能把它压到接近 1 毫秒,让回放节奏贴近真实行情跳动。 服务进程靠平台全局变量和脚本通信:脚本一关,服务就停;服务一停,被回放的品种立刻断流。这种绑定由 MT5 自身维护,逻辑极简且随硬件升级自动受益,不用改脚本或 EA。 GetTickCount64 提供了约 1 毫秒分辨率的计时,不依赖你用什么 CPU。想用空循环硬挤更高精度也可以,但那样就绑死在硬件上了,没必要。 实测跑满 1 分钟回放会多出约 9 秒偏差,靠系统设置就能抹掉。理想状态是总时长略小于 1 分钟,这样只需往服务里加延迟——加比减容易。真正卡点就在服务里生成延迟的那一行,注释掉它,回放会瞬间追平本地时间,不用干等一分钟。 外汇和贵金属实盘本身就有传输延迟,微小时间偏斜不是问题。下面这段是服务里负责按分钟切柱并打首帧的代码,注意 m_dt==0 分支只校正一次起点。
class="macro">#define macroGetMin(A) (class="type">int)((A - (A - ((A % class="num">3600) - (A % class="num">60)))) / class="num">60) class="type">int Event_OnTime(class="type">void) { class="type">bool isNew; class="type">int mili; class="kw">static class="type">class="kw">datetime _dt = class="num">0; if (m_ReplayCount >= m_ArrayCount) class="kw">return -class="num">1; if (m_dt == class="num">0) { m_Rate[class="num">0].close = m_Rate[class="num">0].open = m_Rate[class="num">0].high = m_Rate[class="num">0].low = m_ArrayInfoTicks[m_ReplayCount].Last; m_Rate[class="num">0].tick_volume = class="num">0; m_Rate[class="num">0].time = m_ArrayInfoTicks[m_ReplayCount].dt - class="num">60; CustomRatesUpdate(def_SymbolReplay, m_Rate, class="num">1); _dt = TimeLocal(); } isNew = m_dt != m_ArrayInfoTicks[m_ReplayCount].dt;