如何在 MetaTrader 5 中创建并测试自定义 MOEX(莫斯科证券交易所) 品种(基础篇)
◍ 在 MT5 自建 MOEX 自定义品种并回测
MT5 默认行情树不覆盖莫斯科交易所(MOEX)的多数标的,但终端允许通过本地品种管理手动创建自定义品种,把历史与实时数据桥接进来做策略验证。这套流程对做跨市场套利或俄股/债券相关的交易者有直接用途,外汇与贵金属账户同样可借自定义品种扩展标的范围,但跨市场数据延迟与汇率换算会带来额外高风险。 创建入口在「文件 → 打开数据文件夹」之外的品种管理面板:右键市场报价空白处选「交易品种」,切到自定义标签即可新建。品种名建议按 MOEX 官方代码+后缀,避免和已有品种冲突。 数据方面,MT5 支持从 CSV 导入 1 分钟及以上周期的历史,最小精度取决于你拿到的源文件;若要做 tick 级回测,需先通过脚本把聚合数据展开。建好后用策略测试器选该品种、设好日期区间,便能跑 EA 的历史回测——2019 年那篇实测里,自建 MOEX 品种在 1 分钟数据下回测 3 个月约占用 1.7 GB 本地缓存,老机器需留意磁盘余量。
「把场外和交易所数据塞进 MT5 测试器」
MetaTrader 5 的场内外汇通道本质是 OTC,但交易所品种(如莫斯科交易所)也能借自定义品种进策略测试器跑历史。难点不在 MQL5 本身,而在数据源格式乱:finam.ru 给的日期是 yyyymmdd / dd/mm/yy 这类,唯独没有 MT5 要的 yyyy.mm.dd;OHLC 顺序也可能是开、收、高、低任意排。 把任意资源的随机格式转成所需结构,就能用 CustomRatesUpdate 造出自定义品种做回测。finam.ru 导出的 CSV 里日期格式多达 5 种,但没有一种直接可用,所以先规整再导入是绕不开的一步。 交易所逐笔报价带 Ask/Bid/Last,且毫秒级排序;但深度里的点差只闪现一刻,落库后只剩成交价。复原不了真实点差时,可在导入时写个 Spread = f(High-Low) 的简单函数模拟,或按固定点差注入——固定点差在日线级以上误差可忽略,逐笔级则必须建模。 外汇侧没有逐笔成交历史,只有交易量,本文直接令 VOL = TICKVOL。非标准数字格式(如 1 000 000、3,14)不在通用解析范围内,遇到得自己改代码。导入逻辑若从 K 线切到 tick,只需把 MqlRates 换成 MqlTick,CustomRatesUpdate 换成 CustomTicksAdd,改动量大约几分钟。
把外部 CSV 映射进自定义品种的参数入口
想用 MT5 建一个自定义品种,第一步不是写解析逻辑,而是先把源文件里每一列对应的含义告诉程序。原文这套脚本把列位置做成 input 参数,日期在第 1 列、时间第 2 列、开高低收依次到第 6 列、成交量第 7 列,并以 SkipString=1 跳过 CSV 表头那一行。 源文件必须放到 MQL5/Files 目录下,用 FILE_TXT 标志打开,这样空格和制表符都能当分隔符。以 Sberbank 股票报价为例,日期格式设为 yyyymmdd、时间格式 hhdmmdss,这些都在枚举里先定义好,缺了哪种格式就补哪种。 下面这段就是参数声明的核心部分,直接复制到 EA 头里就能改。注意 LowPosiotion 原文拼错了,实际用的是这个变量名,别自作主张改回 LowPosition 否则编译报错。 日期合法性只做了 StringLen(res)!=10 的粗筛,长度不对就置空并跳过该行,更细的数据分析建议另写程序,毕竟行情数据错了会让自定义品种整个失真。外汇和贵金属自定义品种同样面临源数据污染风险,建完务必抽样核对几根 K 线。
class="kw">input class="type">int SkipString =class="num">1; class=class="str">"cmt">// 要跳过的字符串数量 class="kw">input class="type">class="kw">string mark1 ="Time position and format"; class=class="str">"cmt">// 时间 class="kw">input DATE indate =yyyymmdd; class=class="str">"cmt">// 源日期格式 class="kw">input TIME intime =hhdmmdss; class=class="str">"cmt">// 源时间格式 class="kw">input class="type">int DatePosition =class="num">1; class=class="str">"cmt">// 日期位置 class="kw">input class="type">int TimePosition =class="num">2; class=class="str">"cmt">// 时间位置 class=class="str">"cmt">//------------------------------------------------------------------+ class="kw">input class="type">class="kw">string mark2 ="Price data position"; class=class="str">"cmt">// 价格 class="kw">input class="type">int OpenPosition =class="num">3; class=class="str">"cmt">// 开盘价位置 class="kw">input class="type">int HighPosition =class="num">4; class=class="str">"cmt">// 最高价位置 class="kw">input class="type">int LowPosiotion =class="num">5; class=class="str">"cmt">// 最低价位置 class="kw">input class="type">int ClosePosition =class="num">6; class=class="str">"cmt">// 收盘价位置 class="kw">input class="type">int VolumePosition =class="num">7; class=class="str">"cmt">// 成交量位置 class="kw">input class="type">class="kw">string mark3 ="File parameters"; class=class="str">"cmt">// 文件 class=class="str">"cmt">//-------------------------------------------------------------------+
◍ 把历史文本洗成可回放的品种数据
要把券商导出的纯文本行情接进 MT5 自定义品种,先得在 input 区把映射关系钉死:源文件名叫 sb,分隔符选 comma,字符串按 ansi 读,新建品种名填 SberFX,固定点差写 2 点。日期时间格式用枚举摊开,支持 yyyy.mm.dd、dd/mm/yy、mm/dd/yy 等六种,不够再往枚举里补。
| 核心读写就两句 FileOpen:读句柄拿 FILE_READ | ansi | FILE_TXT 开源文件,写句柄用 Name+"(f).csv" 以 FILE_WRITE | FILE_ANSI | FILE_CSV 落盘;任一返回 INVALID_HANDLE 就弹 Alert 并 return,避免空跑。落盘前先 FileWrite 写表头 <DATE>\t<TIME>\t<OPEN>…<SPREAD>,MT5 才能认成标准 CSV。 |
|---|
数组按「一个月 43200 分钟」预扩:ArrayResize(Rs,43200,43200) 直接锁内存,跳过头几行用 SkipString 控制的 for 循环 FileReadString 丢弃。SearchBorders 靠左右游标与分隔符计数切出每段字段边界,之后逐分钟填 MqlRates 即可。外汇与贵金属自带高杠杆,自建品种回测仅反映历史文本假设,实盘价差滑点可能偏离。
class="kw">input class="type">class="kw">string InFileName ="sb"; class=class="str">"cmt">// 源文件名 class="kw">input DELIMITER Delimiter =comma; class=class="str">"cmt">// 分隔符 class="kw">input CODE StrType =ansi; class=class="str">"cmt">// 字符串类型 class="kw">input class="type">class="kw">string mark4 ="Other parameters"; class=class="str">"cmt">// 其它 class=class="str">"cmt">//-------------------------------------------------------------------+ class="kw">input class="type">class="kw">string spread ="class="num">2"; class=class="str">"cmt">// 固定点差点数 class="kw">input class="type">class="kw">string Name ="SberFX"; class=class="str">"cmt">// 您所创建的品种名称 enum DATE { yyyycmmcdd, class=class="str">"cmt">// yyyy.mm.dd yyyymmdd, class=class="str">"cmt">// yyyymmdd yymmdd, class=class="str">"cmt">// yymmdd ddmmyy, class=class="str">"cmt">// ddmmyy ddslmmslyy, class=class="str">"cmt">// dd/mm/yy mmslddslyy class=class="str">"cmt">// mm/dd/yy class=class="str">"cmt">// 其他格式在此处添加 }; enum TIME { hhmmss, class=class="str">"cmt">// hhmmss hhmm, class=class="str">"cmt">// hhmm hhdmmdss, class=class="str">"cmt">// hh:mm:ss hhdmm class=class="str">"cmt">// hh:mm class=class="str">"cmt">// 其他格式在此处添加 }; class=class="str">"cmt">// 打开输入文件 class="type">int out =FileOpen(InFileName,FILE_READ|StrType|FILE_TXT); if(out==INVALID_HANDLE) { Alert("Failed to open the file for reading"); class="kw">return; } class=class="str">"cmt">// 打开输出文件 class="type">int in =FileOpen(Name+"(f).csv",FILE_WRITE|FILE_ANSI|FILE_CSV); if(in==INVALID_HANDLE) { Alert("Failed to open the file for writing"); class="kw">return; } class=class="str">"cmt">//---插入标题字符串 class="type">class="kw">string Caption ="<DATE>\t<TIME>\t<OPEN>\t<HIGH>\t<LOW>\t<CLOSE>\t<TICKVOL>\t<VOL>\t<SPREAD>"; FileWrite(in,Caption); class=class="str">"cmt">//----------------------------------------------------------- class="type">class="kw">string fdate="",ftime="",open=""; class="type">class="kw">string high="",low="",close="",vol=""; class="type">int left=class="num">0,right=class="num">0; class="type">class="kw">string str="",temp=""; for(class="type">int i=class="num">0;i<SkipString;i++) { str =FileReadString(out); i++; } class="type">MqlRates Rs[]; ArrayResize(Rs,class="num">43200,class="num">43200); class=class="str">"cmt">// 一个月中有 class="num">43200 分钟 class="type">class="kw">datetime time =class="num">0; class=class="str">"cmt">//---搜索数据位置边界-----------------------------+ class="type">bool SearchBorders(class="type">class="kw">string str,class="type">int pos,class="type">int &a,class="type">int &b,DELIMITER delim) { class=class="str">"cmt">// 辅助变量 class="type">int left=class="num">0,right=class="num">0; class="type">int count=class="num">0; class="type">int start=class="num">0;