如何在 MetaTrader 5 中创建并测试自定义 MOEX(莫斯科证券交易所) 品种(基础篇)
📘

如何在 MetaTrader 5 中创建并测试自定义 MOEX(莫斯科证券交易所) 品种(基础篇)

第 1/3 篇

◍ 在 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 线。

MQL5 / C++
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_READansiFILE_TXT 开源文件,写句柄用 Name+"(f).csv" 以 FILE_WRITEFILE_ANSIFILE_CSV 落盘;任一返回 INVALID_HANDLE 就弹 Alert 并 return,避免空跑。落盘前先 FileWrite 写表头 <DATE>\t<TIME>\t<OPEN>…<SPREAD>,MT5 才能认成标准 CSV。

数组按「一个月 43200 分钟」预扩:ArrayResize(Rs,43200,43200) 直接锁内存,跳过头几行用 SkipString 控制的 for 循环 FileReadString 丢弃。SearchBorders 靠左右游标与分隔符计数切出每段字段边界,之后逐分钟填 MqlRates 即可。外汇与贵金属自带高杠杆,自建品种回测仅反映历史文本假设,实盘价差滑点可能偏离。

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

常见问题

在品种列表右键选「自定义品种」,新建后填交易所代码与基础属性即可生成空白品种,再导入数据。
先把记录整理成标准 CSV,通过自定义品种的「从文件导入」映射时间、价、量字段,就能进测试器。
小布可读取你导入的自定义品种,自动标出缺失时段与异常 Tick,省去肉眼核对。
按时间升序重排,统一为「时间,价,量」三列存 CSV,再用导入映射即可回放。
在品种属性改时区偏移量,重导数据前确认与数据源本地时间一致,避免错位。