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

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

第 2/3 篇

「解析外部数据里的分隔符与日期串」

把 CSV 或文本行情接进 MT5 脚本时,第一道坎是分隔符和日期格式不统一。下面这段逻辑先用 switch 把逗号、制表符、空格、分号映射成 delimiter 字符串,再用 StringFind 循环切位,定位第 pos 个字段的左右边界。 切字段时若 right==-1 且 count==0,说明首轮就找不到分隔符,直接 Print("Wrong date") 并返回 false,避免脏数据继续跑。若不是首字段而找不到分隔符,则把 right 设为 StringLen(str)-1,取末尾整段返回,保证最后一段不被丢弃。 日期归一化函数 DateFormat 只认 10 位长度:yyyymmdd 用 StringSubstr 拆成 4+2+2 再拼点号;yymmdd 则按前两位是否 ≥70 决定补 19 或 20(70 年分界是 Unix 时间惯例的变体),拼完仍校验长度,不符就置空串。 主循环里 FileIsEnding(out) 控制读取,每行先 SearchBorders 抓日期位置,再 StringSubstr 提出 temp 丢给 DateFormat。外汇与贵金属数据接口高风险,历史文件缺字段或编码错乱都可能让 res 变空,实盘前务必用几行样本在 MT5 策略测试器里跑通。

MQL5 / C++
class="type">class="kw">string delimiter="";
class=class="str">"cmt">//-------------------------------------------------------------------+
class="kw">switch(delim)
{
case comma : delimiter =",";
  class="kw">break;
case tab : delimiter ="/t";
  class="kw">break;
case space : delimiter =" ";
  class="kw">break;
case semicolon : delimiter =";";
  class="kw">break;
}
class="kw">while(count!=pos||right!=-class="num">1)
  {
  right =StringFind(str,delimiter,start);
   
  if(right==-class="num">1&&count==class="num">0){Print("Wrong date");class="kw">return false;} class=class="str">"cmt">//Incorrect data

  if(right==-class="num">1)
    {
    right =StringLen(str)-class="num">1;
    a =left;
    b =right;
    class="kw">break;
    }

  count++;
    if(count==pos)
    {
    a =left;
    b =right-class="num">1;
    class="kw">return true;
    }
  left =right+class="num">1;
  start =left;
  }
class="kw">return true;
}
class=class="str">"cmt">//---日期格式-------------------------------------------------+
class=class="str">"cmt">//class="num">2017.01.class="num">02
class="type">class="kw">string DateFormat(class="type">class="kw">string str,DATE date)
{
class="type">class="kw">string res="";
class="type">class="kw">string yy="";
class="kw">switch(date)
  {
  case yyyycmmcdd :  class=class="str">"cmt">//我们的格式
    res =str;
    if(StringLen(res)!=class="num">10)res=""; class=class="str">"cmt">// 检查日期格式
  case yyyymmdd :
    res =StringSubstr(str,class="num">0,class="num">4)+"."+StringSubstr(str,class="num">4,class="num">2)+"."+StringSubstr(str,class="num">6,class="num">2);
    if(StringLen(res)!=class="num">10)res=""; class=class="str">"cmt">// 检查日期格式
    class="kw">break;
  case yymmdd :
    yy =StringSubstr(str,class="num">0,class="num">2);
    if(StringToInteger(yy)>=class="num">70)
      yy ="class="num">19"+yy;
    else
      yy ="class="num">20"+yy;
    res =yy+"."+StringSubstr(str,class="num">2,class="num">2)+"."+StringSubstr(str,class="num">4,class="num">2);
    if(StringLen(res)!=class="num">10)res=""; class=class="str">"cmt">// 检查日期格式
    class="kw">break;
class=class="str">"cmt">//---其他格式 (完整代码在文件当中)-------------
class=class="str">"cmt">//如有必要,添加其他格式的解析      
  class="kw">default :
    class="kw">break;
  }
class="kw">return res;
}
class="kw">while(!FileIsEnding(out))
  {
  str =FileReadString(out);
  count++;
class=class="str">"cmt">//---fdate-----------------------------
  if(SearchBorders(str,DatePosition,left,right,Delimiter))
  {
  temp =StringSubstr(str,left,right-left+class="num">1);
  fdate =DateFormat(temp,indate);

◍ 把 CSV 字段塞进 MqlRates 的实操细节

解析 CSV 时若日期字符串为空,直接打印错误并跳过当前行,能避免后续 StringToDouble 拿到 0 值污染整段 K 线数组。 下面这段是把拆出来的字符串拼回一行写文件,同时填进 MqlRates 结构:日期时间、开高低收走 StringToDouble,成交量用 StringToInteger,点差强转 int。注意原逻辑里 vol 被写了两次,real_volume 与 tick_volume 都填了同一来源,实盘若拿不到真实成交量,tick_volume 会失真。 循环结束后用 ArrayResize(Rs,i) 收缩数组到实际条数,再关掉输入输出句柄。外汇与贵金属保证金交易杠杆高,回测用合成 CSV 前务必核对 spread 字段单位,否则夏普率可能虚高。

MQL5 / C++
  if(fdate==""){Print("Error in class="type">class="kw">string  ",count);class="kw">continue;}
  }
  else {Print("Error in class="type">class="kw">string  ",count);class="kw">continue;}
class=class="str">"cmt">//---其他数据的处理方式类似
class=class="str">"cmt">//-------------------------------------------------------------------+
  str =fdate+","+ftime+","+open+","+high+","+low+","+close+","+vol+","+vol+","+Spread;
  FileWrite(in,str);
class=class="str">"cmt">//---填充 class="type">MqlRates -----------------------------------------------+
  Rs[i].time           =time;
  Rs[i].open           =StringToDouble(open);
  Rs[i].high           =StringToDouble(high);
  Rs[i].low            =StringToDouble(low);
  Rs[i].close          =StringToDouble(close);
  Rs[i].real_volume    =StringToInteger(vol);
  Rs[i].tick_volume    =StringToInteger(vol);
  Rs[i].spread         =class="type">int(StringToInteger(Spread));
  i++;
class=class="str">"cmt">//-------------------------------------------------------------------+  
  }
  ArrayResize(Rs,i);
  FileClose(out);
  FileClose(in);

用 CustomSymbolCreate 把数据挂进市场观察

数据备齐后,真正落品种只差两步调用。CustomSymbolCreate() 负责建自定义品种,CustomRatesUpdate() 负责把准备好的报价 Rs 灌进去;后者不仅能初始化,还能在品种已存在时持续追加新数据。 有个坑要注意:如果品种名已存在,CustomSymbolCreate() 会返回 -1 而不是报错中断,程序照常往下走,报价更新由 CustomRatesUpdate() 接管。建好的品种会直接出现在市场观察窗口里,且以绿色高亮标识,方便你一眼确认是否生效。 下面这两行就是核心挂载逻辑,顺序不能反——先建(或确认)品种,再更新报价: [CODE] CustomSymbolCreate(Name); CustomRatesUpdate(Name,Rs); [/CODE] 逐行拆解:第一行用 Name 字符串创建自定义品种,重复调用返回 -1 属正常;第二行把 Rs 结构体内的历史/实时报价写入该品种,MT5 端即可在图表上调出验证。外汇与贵金属自定义品种同样受平台点差与流动性假象影响,回测结果仅代表历史数据拟合,实盘高风险。 挂完后直接右键品种开图表,肉眼看 K 线是否连续、时间轴是否对齐,比写日志更快定位数据缺口。

MQL5 / C++
  CustomSymbolCreate(Name);
  CustomRatesUpdate(Name,Rs);

「用外部文本文件接管品种规格」

早期做法是在 SetSpecifications() 里硬编码品种属性,覆盖 ENUM_SYMBOL_INFO_INTEGER、ENUM_SYMBOL_INFO_DOUBLE、ENUM_SYMBOL_INFO_STRING 三类枚举。源码里大多行被注释掉,只留 Sberbank 股票的典型参数:最小交易量、交易量增量、价格增量、点数大小,这几项不开就跑不了回测。 每次换品种都得改代码重编译,太磨人。后来改成读纯文本 Specifications.txt,MetaEditor 还能高亮参数。格式是逗号分隔,空行或前 10 字符内带 // 的就跳过,StringSplit() 切完丢给 SetProperties() 设属性;留了空格或制表符就用 StringTrimLeft()、StringTrimRight() 清掉。 下面这段是包含文件里的真实代码骨架,未注释部分直接定了自定义品种可见性与报价精度:SYMBOL_POINT 0.01、SYMBOL_VOLUME_MIN 1、SYMBOL_DIGITS 2、SYMBOL_SPREAD 2 点固定。外汇与贵金属自定义品种同样高风险,点值错一个数量级就可能让止损形同虚设,开 MT5 把这段贴进 mqh 验证最直观。

MQL5 / C++
class="type">void SetSpecifications(class="type">class="kw">string Name)
  {
class=class="str">"cmt">//---整数属性-------------------------------------
class=class="str">"cmt">//   CustomSymbolSetInteger(Name,SYMBOL_CUSTOM,true);
class=class="str">"cmt">//   CustomSymbolSetInteger(Name,SYMBOL_BACKGROUND_COLOR,clrGreen);
class=class="str">"cmt">// 其它整数属性
class=class="str">"cmt">//---双精度属性 --------------------------------------------------- 
class=class="str">"cmt">//   CustomSymbolSetDouble(Name,SYMBOL_BID,class="num">0);
class=class="str">"cmt">//   CustomSymbolSetDouble(Name,SYMBOL_BIDHIGH,class="num">0);
class=class="str">"cmt">//   CustomSymbolSetDouble(Name,SYMBOL_BIDLOW,class="num">0);
class=class="str">"cmt">// 其它双精度属性
class=class="str">"cmt">//---字符串属性-----------------------------------------------+
class=class="str">"cmt">//   CustomSymbolSetString(Name,SYMBOL_BASIS,"");
class=class="str">"cmt">//   CustomSymbolSetString(Name,SYMBOL_CURRENCY_BASE,"");
class=class="str">"cmt">//   CustomSymbolSetString(Name,SYMBOL_CURRENCY_PROFIT,"");
class=class="str">"cmt">// 其它字符串属性
}
   CustomSymbolSetInteger(Name,SYMBOL_CUSTOM,true);
   CustomSymbolSetInteger(Name,SYMBOL_BACKGROUND_COLOR,clrGreen);
   CustomSymbolSetInteger(Name,SYMBOL_SELECT,true);
   CustomSymbolSetInteger(Name,SYMBOL_VISIBLE,true);
   CustomSymbolSetDouble(name,SYMBOL_POINT,class="num">0.01);
   CustomSymbolSetDouble(name,SYMBOL_VOLUME_MIN,class="num">1);
   CustomSymbolSetDouble(name,SYMBOL_VOLUME_STEP,class="num">1);
   CustomSymbolSetInteger(name,SYMBOL_DIGITS,class="num">2);
   CustomSymbolSetInteger(name,SYMBOL_SPREAD,class="num">2);
   CustomSymbolSetInteger(name,SYMBOL_SPREAD_FLOAT,false);

常见问题

用StringToTime配合格式化截取子串转成时间,再赋给MqlRates.time,注意时区偏移按交易所+3处理。
用StringSplit按分号拆数组,先校验字段数等于预期再映射,少字段的行直接跳过避免越界。
可以,小布能读取你上传的文本文件,标出异常分隔与日期格式,并给出修正后的字段映射建议。
创建时需在MqlRates补vol字段且调用CustomSymbolSetInteger设VOLUME_MIN,否则面板默认隐藏量能。
确认文件里用键值对且回车换行结尾,再用CustomSymbolSetDouble重载,写错后缀名会被终端忽略。