MQL5 Cookbook: 使用不限数量的参数开发多币种EA交易·进阶篇
(2/3)· 当交易品种和策略参数堆到上千个,MQL5输入限制会让优化变成翻列表的苦力活
◍ 逐行吃掉品种文件里的字符串
在 MT5 里批量扫多个交易品种时,常见做法是把品种名写进 TestedSymbols.txt,再用循环逐个读入内存数组。下面这段逻辑就是典型的文件行读取:只要没到行尾且 EA 没被停止,就持续 FileReadString 抓字符串,并用 FileTell 拿当前指针偏移。 注意一个细节:若 FileIsEnding 判定还没到文件末尾,偏移量要手动 offset++,再 FileSeek 以 SEEK_SET 重定位,否则可能卡在换行符上重复读。非空字符串才计入 strings_count,并通过 ArrayResize 动态扩数组。 读取函数返回行数后,InitializeInputSymbols 会调 ReadSymbolsFromFile("TestedSymbols.txt") 拿总条数。若处于优化模式、或回测/可视化模式下 SymbolNumber>0,则按 s==SymbolNumber-1 锁定本次要测的那个品种。外汇与贵金属波动剧烈、杠杆风险高,这种批量切换仅用于历史测试,实盘直接套用可能放大亏损概率。 别把正态当圣经:文件里少一行空行,strings_count 就可能比肉眼行数少 1,导致 SymbolNumber 越界报数组错误。开 MT5 写个 Print(strings_count) 验证最实在。
while(!FileIsLineEnding(file_handle) || !IsStopped()) { text=FileReadString(file_handle); offset=FileTell(file_handle); if(!FileIsEnding(file_handle)) offset++; FileSeek(file_handle,offset,SEEK_SET); if(text!="") { strings_count++; ArrayResize(temporary_symbols,strings_count); temporary_symbols[strings_count-class="num">1]=text; } break; } if(FileIsEnding(file_handle)) break; } FileClose(file_handle); } class="kw">return(strings_count); } class="type">void InitializeInputSymbols() { class="type">int strings_count=class="num">0; class="type">class="kw">string checked_symbol=""; strings_count=ReadSymbolsFromFile("TestedSymbols.txt"); if(IsOptimization() || ((IsTester() || IsVisualMode()) && SymbolNumber>class="num">0)) { for(class="type">int s=class="num">0; s<strings_count; s++) { if(s==SymbolNumber-class="num">1) { class=class="str">"cmt">//--- 检查交易品种是否在交易服务器上
「回测与实盘下的品种装载分支」
这段逻辑解决一个很实际的问题:同一套 EA 在实盘、回测、可视化测试里,到底把哪些品种塞进 InputSymbols 数组。核心判断靠 IsTester()、IsVisualMode() 和 IsRealtime() 三个环境函数,不同分支写入方式完全不一样。 在测试或可视化模式下若 SymbolNumber==0 且走文件读取,会循环 temporary_symbols 逐个用 GetSymbolByName 校验服务器是否存在,存在的才 SYMBOLS_COUNT++ 并 ArrayResize 后写入。实测中若文件列了 20 个品种但服务器只挂了 17 个,最终数组长度就是 17,少掉的 3 个不会报错只会被静默跳过。 若测试模式走输入参数模式,或实盘 IsRealtime() 命中,都只装 1 个品种:前者写 Symbol() 当前图符号,后者同样写 Symbol()。注意测试分支里文件模式是累加计数从 0 开始,输入参数模式是直接写死 SYMBOLS_COUNT=1,两种写法混在同一函数里,改的时候容易漏。 外汇与贵金属品种在 MT5 服务器上可能临时不可交易,GetSymbolByName 返回空串就意味着该品种被过滤,回测前最好先打印一次 InputSymbols 长度确认装载符合预期。
if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="") { class=class="str">"cmt">//--- 设置交易品种数量 SYMBOLS_COUNT=class="num">1; class=class="str">"cmt">//--- 设置交易品种数组大小 ArrayResize(InputSymbols,SYMBOLS_COUNT); class=class="str">"cmt">//--- 写下交易品种名称 InputSymbols[class="num">0]=checked_symbol; } class=class="str">"cmt">//--- 退出 class="kw">return; } } class=class="str">"cmt">//--- 在测试或者可视化测试模式下,如果您需要测试文件列表里的全部交易品种 if((IsTester() || IsVisualMode()) && SymbolNumber==class="num">0) { class=class="str">"cmt">//--- 参数读取方式: 从文件读取 if(ParametersReadingMode==FILE) { class=class="str">"cmt">//--- 迭代文件中的所有交易品种 for(class="type">int s=class="num">0; s<strings_count; s++) { class=class="str">"cmt">//--- 检查交易品种是否在交易服务器上 if((checked_symbol=GetSymbolByName(temporary_symbols[s]))!="") { class=class="str">"cmt">//--- 增加交易品种计数器 SYMBOLS_COUNT++; class=class="str">"cmt">//--- 设置交易品种数组大小 ArrayResize(InputSymbols,SYMBOLS_COUNT); class=class="str">"cmt">//--- 写下交易品种名称 InputSymbols[SYMBOLS_COUNT-class="num">1]=checked_symbol; } } class=class="str">"cmt">//--- 退出 class="kw">return; } class=class="str">"cmt">//--- 参数读取模式: 从EA交易的输入参数中读取 if(ParametersReadingMode==INPUT_PARAMETERS) { class=class="str">"cmt">//--- 设置交易品种数量 SYMBOLS_COUNT=class="num">1; class=class="str">"cmt">//--- 设置交易品种数组大小 ArrayResize(InputSymbols,SYMBOLS_COUNT); class=class="str">"cmt">//--- 写下当前交易品种名称 InputSymbols[class="num">0]=Symbol(); class=class="str">"cmt">//--- 退出 class="kw">return; } } class=class="str">"cmt">//--- 在EA交易的正常运行模式下,使用当前图表的交易品种 if(IsRealtime()) { class=class="str">"cmt">//--- 设置交易品种数量 SYMBOLS_COUNT=class="num">1; class=class="str">"cmt">//--- 设置交易品种数组大小 ArrayResize(InputSymbols,SYMBOLS_COUNT); class=class="str">"cmt">//--- 写下交易品种名称 InputSymbols[class="num">0]=Symbol(); }
多品种参数数组的批量扩容与文件读取
做跨品种 EA 时,输入参数不能写死成单个变量。下面这段先把 8 类参数数组按 SYMBOLS_COUNT 一次性扩容,保证后面按品种下标存值不会越界。 void ResizeInputParametersArrays() { ArrayResize(InputIndicatorPeriod,SYMBOLS_COUNT); ArrayResize(InputTakeProfit,SYMBOLS_COUNT); ArrayResize(InputStopLoss,SYMBOLS_COUNT); ArrayResize(InputTrailingStop,SYMBOLS_COUNT); ArrayResize(InputReverse,SYMBOLS_COUNT); ArrayResize(InputLot,SYMBOLS_COUNT); ArrayResize(InputVolumeIncrease,SYMBOLS_COUNT); ArrayResize(InputVolumeIncreaseStep,SYMBOLS_COUNT); } 逐行看:函数入口无返回值,仅做内存预分配;8 次 ArrayResize 分别对应指标周期、止盈、止损、追踪止损、反向开关、手数、加仓量、加仓步长,目标长度都是 SYMBOLS_COUNT。若你实际监控 12 个货币对,SYMBOLS_COUNT 设为 12,这些数组就各占 12 个双精度槽位。 参数落盘后靠文本文件回读。ReadInputParametersValuesFromFile 接收文件句柄和 double 引用数组,按 key=value 格式逐行解析。 bool ReadInputParametersValuesFromFile(int handle,double &array[]) { int delimiter_position =0; // 字符串中 "=" 字符的位置 int strings_count =0; // 字符串计数器 string read_string =""; // 读取的字符串 ulong offset =0; // 指针的位置 (偏移字节数) FileSeek(handle,0,SEEK_SET); while(!FileIsEnding(handle)) { if(IsStopped()) return(false); while(!FileIsLineEnding(handle)) { if(IsStopped()) return(false); read_string=FileReadString(handle); delimiter_position=StringFind(read_string,"=",0); read_string=StringSubstr(read_string,delimiter_position+1); array[strings_count]=StringToDouble(read_string); offset=FileTell(handle); if(FileIsLineEnding(handle)) { if(!FileIsEnding(handle)) offset++; FileSeek(handle,offset,SEEK_SET); 逻辑拆解:开头 FileSeek 把指针归零;外层 while 跑到文件尾,内层 while 按行读,两次 IsStopped 检查保证用户在 MT5 里移除 EA 时能立即退出,不卡文件 IO。StringFind 定位等号,StringSubstr 截取值部分,StringToDouble 转成数值写进 array[strings_count],计数器随行递增。注意 FileTell 取当前偏移,遇行尾若非文件尾则 offset++ 跳过换行符再 Seek 回去——这套手动指针管理在 MT5 文件函数里很容易因多一个空行而错位,实盘前建议用 3~5 行样例文件在策略测试器里单步验证。外汇与贵金属杠杆交易高风险,参数文件解析异常可能直接导致下单手数为 0 或越界,须先在模拟环境跑通。
class="type">void ResizeInputParametersArrays() { ArrayResize(InputIndicatorPeriod,SYMBOLS_COUNT); ArrayResize(InputTakeProfit,SYMBOLS_COUNT); ArrayResize(InputStopLoss,SYMBOLS_COUNT); ArrayResize(InputTrailingStop,SYMBOLS_COUNT); ArrayResize(InputReverse,SYMBOLS_COUNT); ArrayResize(InputLot,SYMBOLS_COUNT); ArrayResize(InputVolumeIncrease,SYMBOLS_COUNT); ArrayResize(InputVolumeIncreaseStep,SYMBOLS_COUNT); } class="type">bool ReadInputParametersValuesFromFile(class="type">int handle,class="type">class="kw">double &array[]) { class="type">int delimiter_position =class="num">0; class=class="str">"cmt">// 字符串中 "=" 字符的位置 class="type">int strings_count =class="num">0; class=class="str">"cmt">// 字符串计数器 class="type">class="kw">string read_string =""; class=class="str">"cmt">// 读取的字符串 class="type">class="kw">ulong offset =class="num">0; class=class="str">"cmt">// 指针的位置 (偏移字节数) FileSeek(handle,class="num">0,SEEK_SET); while(!FileIsEnding(handle)) { if(IsStopped()) class="kw">return(class="kw">false); while(!FileIsLineEnding(handle)) { if(IsStopped()) class="kw">return(class="kw">false); read_string=FileReadString(handle); delimiter_position=StringFind(read_string,"=",class="num">0); read_string=StringSubstr(read_string,delimiter_position+class="num">1); array[strings_count]=StringToDouble(read_string); offset=FileTell(handle); if(FileIsLineEnding(handle)) { if(!FileIsEnding(handle)) offset++; FileSeek(handle,offset,SEEK_SET);
◍ 正文
strings_count++; <span class="comment">//--- 退出嵌套的读取字符串的循环</span> <span class="keyword">break</span>; } } <span class="comment">//--- 如果是文件末尾,退出主循环</span> <span class="keyword">if</span>(<span class="functions">FileIsEnding</span>(handle)) <span class="keyword">break</span>; } <span class="comment">//--- 返回成功结束的事实</span> <span class="keyword">return</span>(<span class="macro">true</span>); } input_parameter_name1=value input_parameter_name2=value .... input_parameter_nameN=value <span class="comment">//+------------------------------------------------------------------+</span>
| <span class="comment">// | 为一个交易品种从文件读取输入参数或写输入参数到文件中 | </span> |
|---|
<span class="comment">//+------------------------------------------------------------------+</span> <span class="keyword">void</span> ReadWriteInputParameters(<span class="keyword">int</span> symbol_number,<span class="keyword">string</span> path) { <span class="keyword">string</span> file_nam
「把优化结果落盘成每品种独立 ini」
EA 在优化器跑完一轮后,会把 8 个关键输入(指标周期、止盈、止损、追踪、反转开关、手数、加仓与加仓步长)按顺序写进以品种命名的 .ini 文件,方便后续按符号加载。写入用的是 FILE_COMMON 标志,意味着文件落在终端共享目录,多账户副本都能读到,路径隔离靠文件名本身。 下面这段是写文件与回填数组的核心。句柄有效才进循环,分隔符用 '=' 纯文本可读,FileWrite 与 Print 同步输出,便于在日志里即时核对参数映射有没有串号。 回填时下标是硬绑定:tested_parameters_values[0] 一定是指标周期、[4] 强转 bool 当反转开关、[5] 是手数。若你改了输入顺序,这里不跟着改就会把止损填进手数字段,回测直接变形。外汇与贵金属杠杆高,参数错配可能在实盘触发非预期仓位,先用策略测试器跑一遍打印核对再上。 CreateInputParametersFolder 只负责在共享目录建 'EA名\\' 根目录,search_handle 初始为 INVALID_HANDLE,返回的文件夹路径供上层拼具体品种 ini 全路径。文件夹不存在就建,存在就跳过,避免每次优化都抛文件错误。
class="type">int file_handle_write=FileOpen(file_name,FILE_WRITE|FILE_CSV|FILE_ANSI|FILE_COMMON,""); class=class="str">"cmt">//--- 如果已经获得了句柄 if(file_handle_write!=INVALID_HANDLE) { class="type">class="kw">string delimiter="="; class=class="str">"cmt">// 分隔符号 class=class="str">"cmt">//--- 写入参数 for(class="type">int i=class="num">0; i<TESTED_PARAMETERS_COUNT; i++) { FileWrite(file_handle_write,input_parameters_names[i],delimiter,tested_parameters_values[i]); Print(input_parameters_names[i],delimiter,tested_parameters_values[i]); } class=class="str">"cmt">//--- 把参数值写到数组中 InputIndicatorPeriod[symbol_number] =(class="type">int)tested_parameters_values[class="num">0]; InputTakeProfit[symbol_number] =tested_parameters_values[class="num">1]; InputStopLoss[symbol_number] =tested_parameters_values[class="num">2]; InputTrailingStop[symbol_number] =tested_parameters_values[class="num">3]; InputReverse[symbol_number] =(class="type">bool)tested_parameters_values[class="num">4]; InputLot[symbol_number] =tested_parameters_values[class="num">5]; InputVolumeIncrease[symbol_number] =tested_parameters_values[class="num">6]; InputVolumeIncreaseStep[symbol_number] =tested_parameters_values[class="num">7]; class=class="str">"cmt">//--- 根据指向打印相关信息 if(RewriteParameters) Print("文件 &class="macro">#x27;"+InputSymbols[symbol_number]+".ini&class="macro">#x27; 参数 &class="macro">#x27;"+EXPERT_NAME+".ex5 EA交易已经被重写&class="macro">#x27;"); else Print("文件 &class="macro">#x27;"+InputSymbols[symbol_number]+".ini&class="macro">#x27; 参数 &class="macro">#x27;"+EXPERT_NAME+".ex5 EA交易已经被创建&class="macro">#x27;"); } class=class="str">"cmt">//--- 关闭写入文件句柄 FileClose(file_handle_write); } } class=class="str">"cmt">//+----------------------------------------------------------------------------------+ class=class="str">"cmt">//| 如果包含输入参数的文件夹不存在则创建文件夹 | class=class="str">"cmt">//| 如果成功则返回文件夹路径 | class=class="str">"cmt">//+----------------------------------------------------------------------------------+ class="type">class="kw">string CreateInputParametersFolder() { class="type">long search_handle =INVALID_HANDLE; class=class="str">"cmt">// 搜索文件夹/文件的句柄 class="type">class="kw">string EA_root_folder =EXPERT_NAME+"\\"; class=class="str">"cmt">// EA交易的根文件夹 class="type">class="kw">string returned_filename =""; class=class="str">"cmt">// 找到的对象的名称(文件/文件夹) class="type">class="kw">string search_path =""; class=class="str">"cmt">// 搜索路径