开发回放系统 市场模拟(第 20 部分):外汇(I)(基础篇)
◍ 用 MT5 复刻外汇历史行情做本地回放
MetaTrader 5 的回放系统允许把已下载的历史报价按时间轴重新走一遍,相当于在本地搭一个离线外汇行情模拟器。对价格行为交易者来说,这比直接看静态图表更有用:你能用肉眼重新经历一段行情的微观推进,而不是靠记忆脑补。 具体落地时,先在 MT5 的「报价管理」里确保对应品种的历史数据完整,再打开回放面板加载时间段。外汇与贵金属品种点差会在回放中按当时市场状态重现,重仓或剥头皮策略在回放里验证时务必意识到真实滑点和隔夜利息并未完全计入,这类品种本身高风险,回放结论只代表历史概率倾向。 一个可验证的细节:官方示例文章发布于 2024 年 3 月 19 日,基础回放框架在 MT5 测试者模块中即可调出,不需要额外插件。你今天开 MT5 按 F12 进策略测试器切到「可视化」模式,就能立刻确认这套机制是否还按原文描述工作。
为什么外汇也值得进回放系统
很多人一开始做市场模拟都盯着股票,毕竟那里有停盘、有跳空,回放系统能补上复盘训练的缺口。外汇是 24 小时连续撮合,表面看开个 MT5 模拟账户就能练,似乎不需要回放。 但外汇有自己的结构问题:点差随流动性跳动、隔夜利息切换、不同券商报价微偏移,这些在股票环境里根本复现不出来。把回放引擎从股票挪到外汇,本质是要改数据接入和事件触发逻辑。 MetaTrader 5 的架构其实能扛住这类改造,它本不只为外汇设计,限制你的只是对目标市场的理解深度。后面会拆具体要动哪几个模块。
「外汇跳价回放为何要先改系统」
本文最初目的不是铺开外汇交易全貌,而是让系统先能跑一次市场回放,模拟部分留到其他时候。若手里只有柱线没有跳价,稍加处理也能近似模拟外汇的可能成交,但在接入模拟器前,直接把外汇数据塞进现有系统会报一连串错。 这些错误避不开,但能克服:为了给外汇建回放,得调整此前沿用的一些概念,换来系统对奇异数据的弹性。附件里带了一个真实货币对的跳价数据,不是合成序列,可直接看盘口形势。 外汇在模拟和回放上并不轻松,信息类型虽眼熟,却有自身特征。后面要拆的正是这些特性——懂了它们,才明白别的品种为何也一样有琢磨头。
◍ 出价与要价为何能重合
外汇图表上的 BID 与 ASK 经常显示成同一个值,这跟股市永远带点差完全不同。习惯了股票策略的人刚切到外汇容易懵,因为原先靠价差触发的逻辑得推倒重来。 外汇里真正能掀桌的是央行。B3(巴西证券交易所)的老交易员都见过央行对美元动手——没预告、不商量,原本盈利的仓位可能几个跳价就翻成巨亏。新手在这类干预里爆仓不算稀奇,极端时还会被经纪商追债。 我们做程序的不管宏观,只看行情怎么给。外汇图表以 BID 值为基准绘制,而 B3 的迷你美元期货用的是「最后成交价」。两种数据源在 tick 文件里的字段布局天差地别:外汇没有最后价也没有成交量,B3 两者都有,很多模型直接吃这两列。 回放系统最早就是照着 B3 结构写的。要把它挪到外汇用,要么大改字段解析,要么牺牲一部分通用性。我倾向找折中,不重写整套,但该适配的坑一个也绕不掉。
把浮点精度拉到 8 位再建回放品种
做外汇回放前,先得修浮点系统。之前那版只支持小数点后 4 位,对外汇不够用:B3 框架处理 4 位小数没问题,但真实外汇报价大多要 5 位,直接上 8 位能让系统宽松覆盖各种品种,避免后面踩精度坑。 有个细节先记着:现在设 8 位只是暂时够用,后面因为一些还没法展开的原因,可能还得改回更小位数,但眼下先这么跑。 回放起点很朴素——图表预览用 1 分钟柱线,跳价从外部文件读真实 tick。相当于先退到最基础系统,后面再慢慢加料。 下面这段 C_Replay 构造函数就是干这事:建一个自定义品种,清掉旧数据,把小数位钉死在 8 位。外汇和贵金属杠杆高、滑点诡异,开 MT5 跑之前先确认你的 def_SymbolReplay 没跟实盘品种重名。
C_Replay(class="kw">const class="type">class="kw">string szFileConfig) { m_ReplayCount = class="num">0; class=class="str">"cmt">// 回放计数器归零 m_dtPrevLoading = class="num">0; class=class="str">"cmt">// 上次加载时间归零 m_Ticks.nTicks = class="num">0; class=class="str">"cmt">// tick 缓冲区数量归零 Print("************** Serviço Market Replay **************"); class=class="str">"cmt">// 打印回放服务横幅 srand(GetTickCount()); class=class="str">"cmt">// 用系统计时做随机种子 GlobalVariableDel(def_GlobalVariableReplay); class=class="str">"cmt">// 删掉全局变量防残留 SymbolSelect(def_SymbolReplay, class="kw">false); class=class="str">"cmt">// 先从市场窗取消选中 CustomSymbolDelete(def_SymbolReplay); class=class="str">"cmt">// 删掉同名自定义品种 CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\%s", def_SymbolReplay), _Symbol); class=class="str">"cmt">// 基于当前品种建自定义符号 CustomRatesDelete(def_SymbolReplay, class="num">0, LONG_MAX); class=class="str">"cmt">// 清掉所有历史柱线 CustomTicksDelete(def_SymbolReplay, class="num">0, LONG_MAX); class=class="str">"cmt">// 清掉所有历史 tick SymbolSelect(def_SymbolReplay, true); class=class="str">"cmt">// 重新选中自定义品种 CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0); class=class="str">"cmt">// 设 tick 大小为 class="num">0 CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0); class=class="str">"cmt">// 设 tick 价值为 class="num">0 CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0); class=class="str">"cmt">// 设手步长为 class="num">0 CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); class=class="str">"cmt">// 写描述 CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, class="num">8); class=class="str">"cmt">// 关键:小数位设 class="num">8 位 m_IdReplay = (SetSymbolReplay(szFileConfig) ? class="num">0 : -class="num">1); class=class="str">"cmt">// 读配置,成功置 class="num">0 否则 -class="num">1 }
「回放数据怎么自动认出外汇还是证券」
做回放引擎时,最省事的做法是不让用户手动选市场类型。思路是利用一个现象:某些跳价文件里读不到最后价或成交量,另一些能读到。系统在加载跳价文件时检测这一点,就能无声切换显示模型,用户完全无感。 代码里先塞一个枚举 ePlotType,把市场类型收窄成两种:PRICE_EXCHANGE(证券)和 PRICE_FOREX(外汇)。默认先按外汇模型走,一旦读跳价文件发现带成交量,ModePlot 就改成证券。注意这套自动识别只在回放启动、读取跳价文件时生效;模拟环境先不碰。 当前阶段有个坑:必须先读跳价文件再读柱线文件。若反着来,柱线加载后执行的那行显示模式设置会直接清空图表上已有的东西,因为数据没经缓冲直冲图表。在修好之前,务必只用跳价文件(真实或模拟的)做回放,别单独喂柱线文件。 初始化状态要用一个 bool 变量盯住。系统自己知道初始化完没,但加变量是为了保住用户偏好:证券模式下有人偏要看出价/要价线,若重启系统不标已初始化,就会回退到内部默认把线藏了。标了已初始化后,用户配置得以保留。 打开 MT5「品种」窗口能看到当前计算模式和显示模型,对应代码里 SYMBOL_TRADE_CALC_MODE 的简化输出。想深究其他计算类型自己去查文档,这里只留最朴素的两种。外汇高风险,回测结论仅代表历史数据倾向,不预示实盘。
class C_FileTicks { class="kw">protected: enum ePlotType {PRICE_EXCHANGE, PRICE_FOREX}; class="kw">struct st00 { class="type">MqlTick Info[]; class="type">MqlRates Rate[]; class="type">int nTicks, nRate; class="type">bool bTickReal; ePlotType ModePlot; }m_Ticks; class=class="str">"cmt">//... The rest of the class code.... class="kw">inline class="type">bool ReadAllsTicks(class="kw">const class="type">bool ToReplay) { class="macro">#define def_LIMIT(INT_MAX - class="num">2) class="macro">#define def_Ticks m_Ticks.Info[m_Ticks.nTicks] class="type">class="kw">string szInfo; class="type">MqlRates rate; Print("Loading ticks for replay. Please wait..."); ArrayResize(m_Ticks.Info, def_MaxSizeArray, def_MaxSizeArray); m_Ticks.ModePlot = PRICE_FOREX; class="kw">while ((!FileIsEnding(m_File)) && (m_Ticks.nTicks < def_LIMIT) && (!_StopFlag)) { ArrayResize(m_Ticks.Info, m_Ticks.nTicks + class="num">1, def_MaxSizeArray); szInfo = FileReadString(m_File) + " " + FileReadString(m_File); def_Ticks.time = StringToTime(StringSubstr(szInfo, class="num">0, class="num">19));