开发回放系统  市场模拟(第 02 部分):首次实验(II)(基础篇)
📘

开发回放系统 市场模拟(第 02 部分):首次实验(II)(基础篇)

第 1/3 篇

用历史数据在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 分支只校正一次起点。

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

常见问题

先把品种历史数据按 tick 读入内存,再用定时器逐帧推送价格到图表,第一帧出来后即可确认回放链路通了。
10 毫秒只是发送间隔,若主线程被指标计算阻塞,实际渲染会滞后;建议把回放核心移出图表线程。
小布可接管历史数据调取与帧推送调度,你只需喂入品种和时段,回放卡顿诊断也会直接标在品种页。
服务与主图表生命周期解耦,关掉图表不会杀掉回放,且延迟更稳定,便于多品种并行模拟。
图表重启会导致回放断点丢失、延迟抖动;用独立服务常驻可避免每次手动重连和重新预热数据。