如何在 MetaTrader 5 中创建并测试自定义 MOEX(莫斯科证券交易所) 品种·进阶篇
「解析外部数据里的分隔符与日期串」
把 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 策略测试器里跑通。
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 字段单位,否则夏普率可能虚高。
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 线是否连续、时间轴是否对齐,比写日志更快定位数据缺口。
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 验证最直观。
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);