MQL5 编程基础: 文件·进阶篇
📂

MQL5 编程基础: 文件·进阶篇

(2/3)· 只会整文件读写不够用,定点改行和拆分隔符才是 EA 参数外挂的真门槛

偏理论 第 2/3 篇
不少交易者把历史行情或财经日历塞进 txt 就以为万事大吉,真到要改某一行参数或按逗号拆字段时,脚本直接读崩。MQL5 的文件函数看似琐碎,其实每一类操作都对应一个具体坑位,提前摸清能省掉大量半夜排错时间。

改不了行就整文件重写

MT5 里用 FileOpen 以读写模式打开文本文件,指针能自由移动,但只能往末尾追加,没法原地改某一行。文本文件的“行”只是编辑器概念,底层是连续字节流,换行在 Windows 下占两个字符。 直接把指针挪到第二行开头写 "AB",FileWrite 会在末尾补换行符(2 字节)。原第二行被截掉 4 个字符,剩下 "-2",文件变成 "AB" 和 "-2" 两行而非替换。写更长的 "Line-12345" 会连带吞掉第三行部分数据——证明原地改行在 MQL5 文本文件上不靠谱。 正解是整文件读、改、写新文件再替换:建临时文件,逐行读源文件,遇到目标行写入新内容,其余原样转发。下面这段脚本演示了无数组方案,先生成不重名的临时文件再流转。 删除旧文件可用 FileDelete,改名用 FileMove(第四参数设 FILE_REWRITE 时连删除都省了)。也可用 FileCopy 后手动删临时文件。外汇/贵金属 EA 做配置持久化时留意:文本改写必须走整文件重写,否则概率出现静默数据损坏。

MQL5 / C++
<span class="keyword">class="type">void</span> <span class="functions">OnStart</span>(){
&nbsp;&nbsp; <span class="keyword">class="type">int</span> h=<span class="functions">FileOpen</span>(<span class="class="type">class="kw">string">"test.txt"</span>,<span class="macro">FILE_READ</span>|<span class="macro">FILE_WRITE</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_TXT</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(h==<span class="macro">INVALID_HANDLE</span>){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Alert</span>(<span class="class="type">class="kw">string">"打开文件错误"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> str=<span class="functions">FileReadString</span>(h);
&nbsp;&nbsp; <span class="functions">FileWrite</span>(h,<span class="class="type">class="kw">string">"AB"</span>);
&nbsp;&nbsp; <span class="functions">FileClose</span>(h);
&nbsp;&nbsp; <span class="functions">Alert</span>(<span class="class="type">class="kw">string">"完成"</span>);
}
<span class="keyword">class="type">void</span> <span class="functions">OnStart</span>(){
&nbsp;&nbsp; <span class="keyword">class="type">int</span> h=<span class="functions">FileOpen</span>(<span class="class="type">class="kw">string">"test.txt"</span>,<span class="macro">FILE_READ</span>|<span class="macro">FILE_WRITE</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_TXT</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(h==<span class="macro">INVALID_HANDLE</span>){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Alert</span>(<span class="class="type">class="kw">string">"打开文件错误"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> str=<span class="functions">FileReadString</span>(h);
&nbsp;&nbsp; <span class="functions">FileWrite</span>(h,<span class="class="type">class="kw">string">"Line-class="num">12345"</span>);
&nbsp;&nbsp; <span class="functions">FileClose</span>(h);
&nbsp;&nbsp; <span class="functions">Alert</span>(<span class="class="type">class="kw">string">"完成"</span>);
}
<span class="keyword">class="type">class="kw">string</span> TmpFileName(<span class="keyword">class="type">class="kw">string</span> Name,<span class="keyword">class="type">class="kw">string</span> Ext){
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">string</span> fn=Name+<span class="class="type">class="kw">string">"."</span>+Ext; <span class="comment">class=class="str">"cmt">// 形成名字</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span> n=<span class="number">class="num">0</span>;
&nbsp;&nbsp; <span class="keyword">while</span>(<span class="functions">FileIsExist</span>(fn)){ <span class="comment">class=class="str">"cmt">// 如果文件存在</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;n++;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fn=Name+<span class="functions">IntegerToString</span>(n)+<span class="class="type">class="kw">string">"."</span>+Ext; <span class="comment">class=class="str">"cmt">// 在名字里添加数字</span>
&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(fn);
}
<span class="keyword">class="type">int</span> h=<span class="functions">FileOpen</span>(<span class="class="type">class="kw">string">"test.txt"</span>,<span class="macro">FILE_READ</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_TXT</span>);
<span class="keyword">class="type">class="kw">string</span> tmpName=TmpFileName(<span class="class="type">class="kw">string">"test"</span>,<span class="class="type">class="kw">string">"txt"</span>);
<span class="keyword">class="type">int</span> tmph=<span class="functions">FileOpen</span>(tmpName,<span class="macro">FILE_WRITE</span>|<span class="macro">FILE_ANSI</span>|<span class="macro">FILE_TXT</span>);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> cnt=<span class="number">class="num">0</span>;
&nbsp;&nbsp; <span class="keyword">while</span>(!<span class="functions">FileIsEnding</span>(h)){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;cnt++;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> str=<span class="functions">FileReadString</span>(h);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(cnt==<span class="number">class="num">2</span>){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 替换此行</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">FileWrite</span>(tmph,<span class="class="type">class="kw">string">"New line-class="num">2"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 无更改重写</span>

「用临时文件规避写中断」

MT5 里直接改写正在被读取的配置文件容易在异常退出时搞坏原文件。上面这段把数据先写进临时句柄 tmph,原句柄 h 关闭后再用 FileMove 把临时文件顶替成 test.txt,等于一次原子替换。 注意 FileMove 的最后一个参数:传 0 时若目标已存在会失败,传 FILE_REWRITE 则强制覆盖。代码里连续写了两行不同参数的 FileMove,实际只有带 FILE_REWRITE 的那行会生效,前一行在文件已存在时纯属冗余调用。 想验证就开 MT5 的 MQEditor,把这段塞进一个脚本的尾部,先在终端目录手动建一个 test.txt,跑完看修改时间和内容是否被 tmpName 的内容替换。外汇与贵金属品种上做文件缓存时务必意识到:EA 异常终止概率不低,临时文件方案能降低配置损坏风险,但不消除平台崩溃本身的交易暴露。

MQL5 / C++
   FileWrite(tmph,str);
   }
   }
FileClose(tmph);
FileClose(h);
FileDelete("test.txt");
FileMove(tmpName,class="num">0,"test.txt",class="num">0);
FileMove(tmpName,class="num">0,"test.txt",FILE_REWRITE);
FileCopy(tmpName,class="num">0,"test.txt",FILE_REWRITE);
FileDelete(tmpName);

◍ 把文本文件灌进字符串数组的写法

做文件类工具时,常要把一个 txt 里的每一行读出来塞进数组备用。下面这个函数接收文件名和字符串数组的引用,打开失败就返回 false,成功则把非空行填进数组并返回 true。 读行的细节上有两处值得留意:每行先用 StringTrimLeft 和 StringTrimRight 削掉两端空格,再判断是不是空串,空行直接跳过不进数组。数组不是来一行扩一个元素,而是每次不够了就 ArrayResize 加 1024 个位置,批量扩容比逐元素增长快得多,最后按实际行数 cnt 收缩到刚好大小。 这套逻辑在 MT5 里跑起来很直接,外汇或贵金属相关的文本信号、参数表如果走文件交互,用这种读法能少踩空行和频繁重分配的坑,但文件读写本身失败率不低,实盘前先在策略测试器里用样本文件验证。 代码逐行拆解: bool ReadFileToArray(string FileName,string & Lines[]) — 定义函数,数组按引用传,便于原地填充。 ResetLastError(); — 先清掉之前的错误码,保证后面抓的是本次操作的错。

int h=FileOpen(FileName,FILE_READFILE_ANSIFILE_TXT); — 以只读、ANSI、文本模式打开文件,拿句柄 h。

if(h==INVALID_HANDLE){ ... return(false); } — 句柄无效就打印文件名和错误号并返回失败。 int cnt=0; — 用 cnt 当实际读入行数计数器。 while(!FileIsEnding(h)){ ... } — 没到文件尾就循环。 string str=FileReadString(h); — 读一行进 str。 StringTrimLeft(str); StringTrimRight(str); — 去左、右空格。 if(str!=""){ ... } — 非空才处理。 if(cnt>=ArraySize(Lines)){ ArrayResize(Lines,ArraySize(Lines)+1024); } — 容量不够一次性加 1024。 Lines[cnt]=str; cnt++; — 存入数组并计数。 ArrayResize(Lines,cnt); — 收尾时把数组缩到真实行数。 FileClose(h); return(true); — 关文件,返回成功。

MQL5 / C++
class="type">bool ReadFileToArray(class="type">class="kw">string FileName,class="type">class="kw">string & Lines[]){
  ResetLastError();
  class="type">int h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_TXT);
  if(h==INVALID_HANDLE){
    class="type">int ErrNum=GetLastError();
    printf("打开文件错误 %s # %i",FileName,ErrNum);
    class="kw">return(class="kw">false);
  }
  class="type">int cnt=class="num">0; class=class="str">"cmt">// 使用变量作为文件行数计数器
  while(!FileIsEnding(h)){
    class="type">class="kw">string str=FileReadString(h); class=class="str">"cmt">// 从文件里读取下一行
    class=class="str">"cmt">// 删除左右两侧的空格, 检测并避免使用空行
    StringTrimLeft(str);
    StringTrimRight(str);
    if(str!=""){
      if(cnt>=ArraySize(Lines)){ class=class="str">"cmt">// 数组填充完毕
        ArrayResize(Lines,ArraySize(Lines)+class="num">1024); class=class="str">"cmt">// 增加 class="num">1024 个元素的数组大小
      }
      Lines[cnt]=str; class=class="str">"cmt">// 发送读取的行至数组
      cnt++; class=class="str">"cmt">// 增加读取行数计数
    }
  }
  ArrayResize(Lines,cnt);
  FileClose(h);
  class="kw">return(true);
}

用 FILE_CSV 写出带分隔符的文件

纯文本之外,MT5 还支持带分隔符的文本文件,常见扩展名是 .csv。本质仍是文本,可用记事本直接改,但行内字段靠一个约定字符切分,不一定非得是逗号。 和 FILE_TXT 不同,FILE_CSV 模式下 FileWrite() 不会把多个变量硬拼成一行,而是在参数之间自动插入分隔符;读的时候 FileReadString() 也只读到分隔符或行尾,不再是整行通吃。 创建时把 FileOpen 的标签从 FILE_TXT 换成 FILE_CSV,第三个参数就是分隔符符号。下面这段脚本用分号做分隔符,循环写 10 行,每行 3 个字段,跑完在终端目录生成 test.csv。 验证方式很简单:把脚本挂到 MT5 跑一次,打开生成的 test.csv,应看到 Line-1-1;Line-1-2;Line-1-3 这样的 10 行结构,字段间严格以分号隔开。

MQL5 / C++
class="type">int h=FileOpen("test.csv",FILE_WRITE|FILE_ANSI|FILE_CSV,";");
  for(class="type">int i=class="num">1;i<=class="num">10;i++){
    class="type">class="kw">string str="Line-"+IntegerToString(i)+"-";
    FileWrite(h,str+"class="num">1",str+"class="num">2",str+"class="num">3");
  }

「用 FileIsLineEnding 切分 CSV 行字段」

MT5 里读带分隔符的文本,核心在 FileOpen 的 FILE_CSV 标志加上分隔符参数。上面这段用分号 ";" 作列分隔,脚本每读到一个字符串就弹窗,但单看输出分不清哪几个字段属于同一行。 FileIsLineEnding() 就是干这个的:它返回 true 时说明当前文件指针刚越过一行结尾。在循环里加一句判断,为真就 Alert("---"),消息窗口里字段就被 "---" 切成一组一组,对应原文件物理行。 实测把 sTestFileReadToAlertCSV 改出第二版(ReadFileToAlertCSV2),用 test.csv 跑完,弹窗序列是「=== 开始 ===」→ 若干字段 → "---" → 下一组字段,图例 8 展示的就是这种分组效果。外汇或贵金属 EA 做外部信号导入时,这种行边界识别不可省,否则容易把上一根 K 线的附加数据并到下一根。 下面代码可直接贴进 MT5 脚本编译验证,注意 FileOpen 第三个参数必须和你的 csv 实际分隔符一致,否则列会整段读串。

MQL5 / C++
class="type">int h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_CSV,";");
class="type">void OnStart(){
   ReadFileToAlertCSV("test.csv");
}
class="type">void ReadFileToAlertCSV2(class="type">class="kw">string FileName){
   class="type">int h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_CSV,";");
   if(h==INVALID_HANDLE){
      Alert("打开文件错误");
      class="kw">return;
   }  
   Alert("=== 开始 ===");  
   while(!FileIsEnding(h)){
      class="type">class="kw">string str=FileReadString(h);
      Alert(str);
      if(FileIsLineEnding(h)){
         Alert("---");
      }
   }
   FileClose(h);
}

◍ 把分号分隔的 CSV 直接灌进结构数组

处理 MT5 里的 csv 日志或导出的 tick 数据,常需要把整文件按行、按字段拆开。下面这套思路是把每行映射成一个结构元素,结构里再挂一个字符串数组装字段,行和字段都按实际数量回缩,不浪费内存。 外层 while(!FileIsEnding(h)) 每跑一圈就是一行:先读第一个字段塞进 field[0],并一次性把该行的字段数组预开 64 个位置;行内剩余字段走 while(!FileIsLineEnding(h)),不够了每次再扩 64。一行读完立刻 ArrayResize 到真实字段数,全部读完再把行数组缩到 lcnt。 预扩步长值得记一下:行数组每次吃紧加 1024,字段数组每次加 64。若你的 csv 是每分钟 K 线导出、字段固定 6 列,64 的初值基本够用;若是逐笔成交明细、单行字段可能破百,第一次进循环就会触发 +64 的重分配。 脚本 sTestFileReadFileToArrayCSV 把 test.csv 读进数组后走 printf 打印,消息窗口能看到图例里那种行列对齐的结果。外汇与贵金属数据导入涉及杠杆品种,历史文件若含跳空缺口,字段数异常时要人工核对,这类操作属高风险验证动作。

MQL5 / C++
class="kw">struct SLine{
  class="type">class="kw">string line[];
};
class="type">bool ReadFileToArrayCSV(class="type">class="kw">string FileName,SLine & Lines[]){
  ResetLastError();
  class="type">int h=FileOpen(FileName,FILE_READ|FILE_ANSI|FILE_CSV,";");
  if(h==INVALID_HANDLE){
    class="type">int ErrNum=GetLastError();
    printf("打开文件错误 %s # %i",FileName,ErrNum);
    class="kw">return(class="kw">false);
  }  
  class="type">int lcnt=class="num">0; class=class="str">"cmt">// 计算行数的变量
  class="type">int fcnt=class="num">0; class=class="str">"cmt">// 计算行内字段数的变量
  while(!FileIsEnding(h)){
    class="type">class="kw">string str=FileReadString(h);
    class=class="str">"cmt">// 新行 (结构数组的新元素)
    if(lcnt>=ArraySize(Lines)){ class=class="str">"cmt">// 结构数组完整填充
      ArrayResize(Lines,ArraySize(Lines)+class="num">1024); class=class="str">"cmt">// 增加 class="num">1024 个元素的数组大小
    }
    ArrayResize(Lines[lcnt].field,class="num">64);class=class="str">"cmt">// 改变结构的数组大小
    Lines[lcnt].field[class="num">0]=str; class=class="str">"cmt">// 分配第一个字段值
    class=class="str">"cmt">// 开始读取行内其它字段
    fcnt=class="num">1; class=class="str">"cmt">// 直到行数组中的一个元素被占用
      while(!FileIsLineEnding(h)){ class=class="str">"cmt">// 读取行内剩余字段
        str=FileReadString(h);
        if(fcnt>=ArraySize(Lines[lcnt].field)){ class=class="str">"cmt">// 字段数组完整填充
          ArrayResize(Lines[lcnt].field,ArraySize(Lines[lcnt].field)+class="num">64); class=class="str">"cmt">// 数组增加 class="num">64 个元素大小
        }    
        Lines[lcnt].field[fcnt]=str; class=class="str">"cmt">// 分配下一个字段值
        fcnt++; class=class="str">"cmt">// 增加行计数器
      }
    ArrayResize(Lines[lcnt].field,fcnt); class=class="str">"cmt">// 根据实际字段数量更改字段数组的大小
    lcnt++; class=class="str">"cmt">// 增加行计数器
  }
  ArrayResize(Lines,lcnt); class=class="str">"cmt">// 根据实际行数更改结构 (行) 数组
  FileClose(h);
  class="kw">return(true);
}
把重复文件巡检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 读写文件的异常告警,你只需专注策略逻辑本身。

常见问题

文件函数要求先以对应标志打开,指定行改动通常要整文件重排或建临时文件再替换,直接写会破坏偏移和结构。
字段会错位,数组维度对不上,后续计算可能把字符串当数值处理,结果完全不可信。
ANSI 与 UNICODE 混用会导致乱码或截断,FileRead 系列函数对编码敏感,需先确认文件实际格式。
可以,小布盯盘的品种页内置了 AIGC 诊断,能捕捉文件打开失败、编码异常等常见报错并提示。
按需拆分只保留结构化字段,避免冗余字符串驻留,大文件下数组膨胀更可控。