使用伪模板替代 C++ 模板·进阶篇
用脚本替你重写类型代码
MQL5 没有 C++ 那种编译期模板,所谓伪模板是靠外部脚本模拟预处理器来干活。代价是要多移动一份处理文件,好处是比语言原生确定的类型组合更灵活,能按分析需要随意换类型。 具体做法是用一个“模板”脚本读入指定源文件,保留原有数据结构,扫描出占位模板后,用目标类型逐次替换。类型要重写几种,代码就整段重生成几次,每次吐出一个新版本。 这里有个硬约束:因为走的是重写机制而不是真泛型,要替换的类型数量直接等于脚本重写遍数。脚本会在待分析代码全量做字符串替换,本质是把手工复制改类型的活交给机器跑。外汇与贵金属杠杆高,这类自动生成代码务必先在 MT5 策略测试器小样本验证再上实盘。
「脚本怎么读模板并替换类型」
做这套代码生成器,先定四个输入:待处理文件名、要被重写的类型清单、模板占位符名,以及一对起止标记。开始标记不可省,结束标记可缺省——缺了就一路读到文件尾;标记名前后不能带空格,MetaEditor 格式化时常偷偷塞进制表符或空格,所以读取时一律 StringTrimLeft / StringTrimRight 再比对。 文件以 FILE_SHARE_READ 模式开,扩展名写死成 mqh,本质就是文本档,重命名成 txt 照样能看。被处理文件必须带 templat 扩展名,否则原文件会被直接覆盖、信息全丢。 读文件用动态数组逐行存,不揉成一整行,否则换行没了、后续正则替换会乱。读完后数组长度恰好等于起止标记之间的行数。 替换逻辑里作者没用标准 StringReplace,而是自写 StringReplac(多一个 e):它会看 find 前面是不是单词一部分,是就用大写 replacement 替,不是才原样替。这样除了换类型,还能顺手把重写后的变量名也带上类型标识。 下面这段是 ReadFile 的核心:定位 //start point 后逐行灌进 fdates[],撞到 //end point 或文件尾停止。
class="kw">input class="type">class="kw">string folder="Example templat";class=class="str">"cmt">//name of file for processing class="kw">input class="type">class="kw">string type="class="type">long;class="type">class="kw">double;class="type">class="kw">datetime;class="type">class="kw">string" ;class=class="str">"cmt">//names of custom types, separator ";" class="type">class="kw">string TEMPLAT="_XXX_";class=class="str">"cmt">// class="kw">template name class="macro">#define startread "class=class="str">"cmt">//start point" class="macro">#define endread "class=class="str">"cmt">//end point" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| downloading file | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ReadFile() { class="type">class="kw">string subfolder="Templates"; class="type">int han=FileOpen(subfolder+"\\"+folder+".mqh",FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI,"\r"); if(han!=INVALID_HANDLE) { class="type">class="kw">string temp=""; class=class="str">"cmt">//--- scrolling file to the starting point do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);} class="kw">while(startread!=temp); class="type">class="kw">string text=""; class="type">int size; class=class="str">"cmt">//--- reading the file to the array until a class="kw">break point or the end of the file class="kw">while(!FileIsEnding(han)) { temp=text=FileReadString(han); class=class="str">"cmt">// deleting symbols of tabulation to check the end StringTrimLeft(temp);StringTrimRight(temp); if(endread==temp)class="kw">break; class=class="str">"cmt">// flushing data to the array if(text!="") { size=ArraySize(fdates); ArrayResize(fdates,size+class="num">1); fdates[size]=text; } } FileClose(han); } else {
◍ 多模板文件重写的循环代价
这段 WriteFile 逻辑在文件已存在时走 else 分支:先把 fdates 拷进临时数组 tempfdates,再按 type_dates 的每种类型逐轮替换并写盘。 关键在三层嵌套 for:外层 j 遍历 count_type 种模板类型,内层两个 i 循环分别做拷贝和 Replace,最后再一个 i 循环执行 FileWrite。若 count_type=5、count=200,单此函数就会触发 5×(200+200+200)=3000 次数组访问与写操作,MT5 回测里文件 IO 很容易成为瓶颈。 Replace 函数本身很轻,只做边界判断加 StringReplac 把 TEMPLAT 常量替换成 type_dates[j],但注意它是按引用传 temp_m[],改的是同一块内存,所以每轮 j 之前必须重新从 fdates 灌数据,否则上一轮替换结果会污染下一轮。 开 MT5 把 count_type 和 count 打 Print 出来,若发现写文件耗时占比异常,优先把 FileWrite 合并到一次 flush,而不是每种类型重写整文件。外汇与贵金属品种波动剧烈,这类日志/缓存文件逻辑若卡顿,可能拖累实时信号响应。
Print("File open failed"+subfolder+"\\"+folder+".mqh, error",GetLastError()); flagnew=true; } } class="type">void WriteFile(class="type">int count) { ... if(han!=INVALID_HANDLE) { if(flagnew)class=class="str">"cmt">// if the file cannot be read { ... } else {class=class="str">"cmt">// if the file exists ArrayResize(tempfdates,count); class="type">int count_type=ArraySize(type_dates); class=class="str">"cmt">//--- the cycle rewrites the contents of the file for each type of the type_dates class="kw">template for(class="type">int j=class="num">0;j<count_type;j++) { for(class="type">int i=class="num">0;i<count;i++) class=class="str">"cmt">// copy data into the temporary array tempfdates[i]=fdates[i]; for(class="type">int i=class="num">0;i<count;i++) class=class="str">"cmt">// replace templates with types Replace(tempfdates,i,j); for(class="type">int i=class="num">0;i<count;i++) FileWrite(han,tempfdates[i]); class=class="str">"cmt">// flushing array in the file } } ... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| replacing templates with types | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Replace(class="type">class="kw">string &temp_m[],class="type">int i,class="type">int j) { if(i>=ArraySize(temp_m))class="kw">return; if(j<ArraySize(type_dates)) StringReplac(temp_m[i],TEMPLAT,type_dates[j]);class=class="str">"cmt">// replacing templat with types }
读标记卡壳后的三处脚本改造
早期版本在脚本里读标记经常对不上行,根因是文件字符串左右带了制表符和空格。ReadFile() 里加了两句 StringTrimLeft() 和 StringTrimRight(),把 temp 两端空白剥掉再比对 startread,循环才能准确定位起点,这段代码是增强版才有的。 输出文件名原先带 ' templat' 后缀,不能直接当可用文件用。现在用 StringDel() 从 folder 里删掉这个样本:StringReplace 把 ' templat' 替换成空串,非首次运行就得到干净的输出名,首次运行仍保留原样去建预模板。 预模板机制靠全局变量 flagnew 把关。读取出错时 flagnew 置 true,WriteFile() 里走 if(flagnew) 分支,往 han 写 #define、起止标记行,在 Files/Templates 没有目标文件时先生成空预模板;正常时走 else 删后缀出正式文件。 连接第二个模板不用改核心逻辑,把靠近 OnStart() 的那几个函数照第一个模板复制一份即可,理论上能挂任意多个。外汇与贵金属脚本自动化高风险,参数和路径先在 MT5 策略测试器外的文件目录手验一遍再跑。
class="type">class="kw">string temp=""; class=class="str">"cmt">//--- scrolling the file to the start point do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);} class="kw">while(startread!=temp); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Deleting the &class="macro">#x27;find&class="macro">#x27; class="kw">template from the &class="macro">#x27;text&class="macro">#x27; class="type">class="kw">string | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string StringDel(class="type">class="kw">string text,class="kw">const class="type">class="kw">string find) { class="type">class="kw">string str=text; StringReplace(str,find,""); class="kw">return(str); } class="type">class="kw">string newfolder; if(flagnew)newfolder=folder;class=class="str">"cmt">// if it is the first start, create an empty file of pre-class="kw">template else newfolder=StringDel(folder," templat");class=class="str">"cmt">// or create the output file according to the class="kw">template class=class="str">"cmt">//class="macro">#define _XXX_ class="type">long class=class="str">"cmt">//this is the start point _XXX_ class=class="str">"cmt">//this is the end point if(flagnew)class=class="str">"cmt">// if the file couldn&class="macro">#x27;t be read {class=class="str">"cmt">// fill the class="kw">template file with the pre-class="kw">template FileWrite(han,"class="macro">#define "+TEMPLAT+" "+type_dates[class="num">0]); FileWrite(han," "); FileWrite(han,startread); FileWrite(han," "+TEMPLAT); FileWrite(han,endread); Print("Creating pre-class="kw">template "+subfolder+"\\"+folder+".mqh"); }
「用伪模板逐类型调试的实操路径」
启动脚本时先填好 'Example templat' 文件名,自定义数据类型用 ';' 分隔。点 OK 后会在 Templates 目录生成 'Example templat.mqh',日志里能看到创建事件;再跑一次脚本,因为目录和文件已存在,打开文件报错的信息就不再打印,替换按指定模板执行。
把预模板里一行 _XXX_ Value_XXX_; 按传入类型数量展开,实测会从 1 行变成 4 行(long/double/datetime/string 各一)。第二个模板嵌进示例文本后,处理方式和第一个一致;若某类型在第一个模板里找不到对应,就不生成任何代码。末尾多一个空格空行,是用来标记“一类处理完、切下一类”的边界。
调试大段代码时,预模板留了注释行 //#define _XXX_ long。去掉注释,_XXX_ 就变成真实类型,编译器按 long 解释模板,相当于单类型跑通逻辑。
不能一次性调试所有类型,但可以把一种类型调通后,改 define 里的类型名逐个过一遍。注意文件要移到调用目录或 Include 目录才方便调试——这就是伪模板在调试便捷性上的固有短板。
class=class="str">"cmt">//this_is_the_start_point _XXX_ Value_XXX_; class=class="str">"cmt">//this_is_the_end_point class="type">long ValueLONG; class="type">class="kw">double ValueDOUBLE; class="type">class="kw">datetime ValueDATETIME; class="type">class="kw">string ValueSTRING; class=class="str">"cmt">//this_is_the_start_point _XXX_ Value_XXX_; _XXX_ Type_XXX_; class=class="str">"cmt">//this_is_the_end_point class="type">long ValueLONG; class="type">long TypeLONG; class="type">class="kw">double ValueDOUBLE; class="type">class="kw">double TypeDOUBLE; class="type">class="kw">datetime ValueDATETIME; class="type">class="kw">datetime TypeDATETIME; class="type">class="kw">string ValueSTRING; class="type">class="kw">string TypeSTRING; class=class="str">"cmt">//this_is_the_start_point _XXX_ Value_XXX_(_xxx_ ind){class="kw">return((_XXX_)ind);}; _XXX_ Type_XXX_(_xxx_ ind){class="kw">return((_XXX_)ind);}; class=class="str">"cmt">//this_is_the_end_button class="type">long ValueLONG(class="type">int ind){class="kw">return((class="type">long)ind);}; class="type">long TypeLONG(class="type">int ind){class="kw">return((class="type">long)ind);}; class="type">class="kw">double ValueDOUBLE(class="type">class="kw">float ind){class="kw">return((class="type">class="kw">double)ind);}; class="type">class="kw">double TypeDOUBLE(class="type">class="kw">float ind){class="kw">return((class="type">class="kw">double)ind);}; class="type">class="kw">datetime ValueDATETIME(class="type">int ind){class="kw">return((class="type">class="kw">datetime)ind);}; class="type">class="kw">datetime TypeDATETIME(class="type">int ind){class="kw">return((class="type">class="kw">datetime)ind);}; class="type">class="kw">string ValueSTRING(class="type">class="kw">string ind){class="kw">return((class="type">class="kw">string)ind);}; class="type">class="kw">string TypeSTRING(class="type">class="kw">string ind){class="kw">return((class="type">class="kw">string)ind);};
◍ 别急着下结论
伪模板思路在 MQL5 里确实能省下不少重复敲代码的时间,但它只是个起点,不是终点。上面那段字符串示例就暴露了问题:解析器面对 double a=1.5; 和 double a = 1.5 , b = 2.5 ; 这类写法时,规则怎么定全看你自己,没有强制标准。
社区里早有人点出,MQL5 后来已经原生支持函数模板,老办法在严谨工程里容易留坑。外汇和贵金属杠杆高、滑点凶,拿这类实验性脚本直接跑实盘,风险倾向偏大,建议先在策略测试器里用历史数据验证逻辑。
代码层能验证的一件事:下面这些字符串都是同一类赋值意图,但空格和逗号处理差异很大,你的解析函数若不覆盖全,就可能漏掉变量。
str="double a=1.5;"
void eval(str);
Print(a); // a=1.5
"double a=1.5;"
"double a =1.5;"
"double a = 1.5;"
"double a=1.5,b=2.5;"
"double a =1.5,b =2.5;"
"double a = 1.5,b = 2.5;"
"double a = 1.5 , b = 2.5 ;"
真要落地,开 MT5 把这段贴进脚本,自己写个 eval 跑一遍,比看讨论区吵架有用。思路碰撞归碰撞,验证过才算自己的。
str="class="type">class="kw">double a=class="num">1.5;" class="type">void eval(str); Print(a); class=class="str">"cmt">// a=class="num">1.5 "class="type">class="kw">double a=class="num">1.5;" "class="type">class="kw">double a =class="num">1.5;" "class="type">class="kw">double a = class="num">1.5;" "class="type">class="kw">double a=class="num">1.5,b=class="num">2.5;" "class="type">class="kw">double a =class="num">1.5,b =class="num">2.5;" "class="type">class="kw">double a = class="num">1.5,b = class="num">2.5;" "class="type">class="kw">double a = class="num">1.5 , b = class="num">2.5 ;"