MetaTrader 中的 Multibot(第二部分):改进的动态模板·综合运用
🧩

MetaTrader 中的 Multibot(第二部分):改进的动态模板·综合运用

(3/3)·当虚拟图表、自动幻数与 API 同步叠加,传统静态模板的出错率会指数级放大

偏理论 第 3/3 篇

把十几张图表上的 EA 挨个手填参数串,是多数多品种策略崩盘前的标准动作。人在复制粘贴里漏一位字符,幻数冲突和错周期下单就埋下了。本篇收尾这套动态模板系列,直接拆开改进版怎么把配置熵压到最低。

临时文件里的参数热加载逻辑

这段片段做的是在 MT5 的 MQL5 程序里,从临时子目录翻出一个命名带 tempsubfolder 前缀、filenametemp 后缀的文本文件,读出来比对上一次内容,若不一样就重新解析参数并重启配置。 搜索句柄 handlestart 在用完之后必须调 FileFindClose 释放,否则在 EA 反复轮询时可能泄漏系统资源,这在跑 24x5 的外汇或贵金属策略时是高概率踩坑点。

文件打开用了 FILE_READFILE_SHARE_READFILE_TXTFILE_ANSI 组合,意味着允许别的进程同时读、按 ANSI 文本解析;若你的配置文件含中文路径或 UTF-8 签名,这里会读歪,贵金属跨平台拷贝时要留意。

核心去重靠 PrevReaded 字符串比对,只有 str 非空且不等于上次读取才进 ReadFileStrings 并 RestartParams,同时记 LastRead = TimeCurrent()。这一道拦截能避免每 tick 重读文件拖慢执行,回测里文件 size 超过几 MB 时差异会很明显。

MQL5 / C++
filename=tempsubfolder+filenametemp;
break;
}
 }
 }
 if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);class=class="str">"cmt">// Release resources after search

 if (filename != "")
  {
  Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI);

  if ( Handle0x != INVALID_HANDLE )class=class="str">"cmt">//if the file exists
    {
    FileSeek(Handle0x,class="num">0,SEEK_SET);
    class="type">ulong size = FileSize(Handle0x);
    class="type">class="kw">string str = "";
    for(class="type">ulong i = class="num">0; i < size; i++)
     {
      str += FileReadString(Handle0x);
     }
    if (str != "" && str != PrevReaded)
     {
     FileSeek(Handle0x,class="num">0,SEEK_SET);
     class=class="str">"cmt">//read the required parameters
     ReadFileStrings(Handle0x);
     class=class="str">"cmt">//
     FileClose(Handle0x);
     LastRead = TimeCurrent();
     RestartParams();
     }
    else
     {
     FileClose(Handle0x);
     }
    class="kw">return true;
    }
   else
    {
    class="kw">return false;
    }
   }
  }
 else

◍ 在公共目录里按前缀捞文件并读内容

EA 在 MT5 公共目录(FILE_COMMON)里批量找文件时,先用 FileFindFirst 拿句柄,再用 StringSubstr 比对文件名前缀,命中就拼出完整路径。这段逻辑能解决多子目录下同名前缀配置的自动定位,不用写死文件名。 下面这段是定位与读取的核心片段,逐行拆一下:FileFindFirst 在 tempsubfolder 下搜 '*' 拿到首个文件句柄;SearchStart=0 表示从第 0 字符起比前缀;若首文件前缀就等于 FileNameString,直接记 filename。若没直接命中,进 while(FileFindNext) 继续扫,命中就 break 退出循环。

扫完无论找没找到,handlestart 不是 INVALID_HANDLE 就必须 FileFindClose 释放,否则句柄泄漏会让后续文件操作在跑几个小时后概率性失败。找到文件后 FileOpen 用 FILE_READFILE_SHARE_READFILE_TXTFILE_ANSIFILE_COMMON 打开,FileSeek 归零后按 FileSize 逐字节 FileReadString 拼进 str,若和上次 PrevReaded 不同才进 ReadFileStrings 解析并关闭。外汇与贵金属行情下这类共享文件读写涉及多程序并发,属高风险操作,实盘前请在策略测试器里先验证句柄释放路径。
MQL5 / C++
      {
      handlestart=FileFindFirst(tempsubfolder+"*",filenametemp,FILE_COMMON);  
      class="type">int SearchStart=class="num">0;
      
      if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
        {
        filename=tempsubfolder+filenametemp;
        }
      if (filename != filenametemp || filename == "")
        {
        while ( FileFindNext(handlestart,filenametemp) )
          {
          if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
            {
            filename=tempsubfolder+filenametemp;
            break;
            }
          }          
        }
      if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);class=class="str">"cmt">// Release resources after search
      
      if (filename != "")
        {
        Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON);
        
        if ( Handle0x != INVALID_HANDLE )class=class="str">"cmt">//if the file exists
          {
          FileSeek(Handle0x,class="num">0,SEEK_SET);
          class="type">ulong size = FileSize(Handle0x);
          class="type">class="kw">string str = "";
          for(class="type">ulong i = class="num">0; i < size; i++)
          {
           str += FileReadString(Handle0x);
          }
          if (str != "" && str != PrevReaded)
          {
           FileSeek(Handle0x,class="num">0,SEEK_SET);
           class=class="str">"cmt">//read the required parameters
           ReadFileStrings(Handle0x);
           class=class="str">"cmt">//    
           FileClose(Handle0x);

「用文件标记驱动平仓的检索逻辑」

EA 和外部信号之间常用临时文件做桥梁。当进入“读文件平仓”分支时,先调用 FileFindFirst 在 tempsubfolder 目录下按 '*' 通配,拿到首个文件名放入 filenametemp,同时把 SearchStart 固定为 8——因为约定文件名以 'CLOSING '(含尾部空格,正好 8 字符)开头。 判定目标文件时,要求 filenametemp 长度不小于 8 + FileNameString 长度,且前 8 字符等于 'CLOSING ',再从偏移 8 起截取与 FileNameString 等长的子串做匹配。首轮若没命中,就靠 while 循环套 FileFindNext 继续翻,直到找到或遍历完。 找到后把完整路径写回 filename 并 break。搜完无论是否命中,只要 handlestart 不是 INVALID_HANDLE 就必须 FileFindClose 释放句柄,否则 MT5 终端跑久了可能句柄泄露。

确认 filename 非空才用 FileOpen 以 FILE_READFILE_SHARE_READFILE_TXTFILE_ANSI 打开;若返回不是 INVALID_HANDLE,说明平仓指令文件确实存在,后续再读内容执行。外汇和贵金属波动剧烈,这种文件通信机制只是信号通道,实际平仓仍受滑点和点差影响,成交价格可能偏离预期。
MQL5 / C++
handlestart=FileFindFirst(tempsubfolder+"*",filenametemp);
class="type">int SearchStart=class="num">8;class=class="str">"cmt">//when the line starts with "CLOSING "

if ( StringLen(filenametemp) >= (class="num">8 + StringLen(FileNameString)) && StringSubstr(filenametemp,class="num">0,class="num">8) == "CLOSING "
&& StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
  {
  filename=tempsubfolder+filenametemp;
  }
if (filename != filenametemp || filename == "")
  {
  while ( FileFindNext(handlestart,filenametemp) )
    {
    if ( StringLen(filenametemp) >= (class="num">8 + StringLen(FileNameString)) && StringSubstr(filenametemp,class="num">0,class="num">8) == "CLOSING "
    && StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
      {
      filename=tempsubfolder+filenametemp;
      break;
      }
    }
  }
if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);class=class="str">"cmt">// Release resources after search

if (filename != "")
  {
  Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI);

  if ( Handle0x != INVALID_HANDLE )class=class="str">"cmt">//if the file exists
    {

文件变更触发参数热重载

这段逻辑解决一个实盘痛点:EA 跑着的时候,你改了外部配置文件,怎么让实例不重启就吃进新参数。核心做法是用 FileSeek 把句柄拨回头部,拿 FileSize 得到字节数,再逐字节用 FileReadString 拼成整串。 拼完的字符串会跟上次读到的 PrevReaded 比。如果不相等且非空,说明文件真被改过,这时先记下新内容,重新 Seek 到 0,调 ReadFileStrings 去解析所需参数,关句柄,把 LastRead 刷成 TimeCurrent(),最后跑 RestartParams() 让新魔术号与交易对象生效。若内容没变,直接关文件返回,不做任何重载。 注意 ReadFileStrings 在这版里是空壳,只留了接口;真要按行解析得自己填。RestartParams 里 SmartMagic 依据基础名映射的 symbol 与周期生成新魔术号,再喂给 CTrade 的 SetExpertMagicNumber,这就是为什么多实例不会互串单。 外汇与贵金属杠杆高,热重载改了魔术号却没清旧挂单,可能让历史仓位脱离新逻辑管控,验证前先在策略测试器跑一遍。

MQL5 / C++
FileSeek(Handle0x,class="num">0,SEEK_SET);
class="type">ulong size = FileSize(Handle0x);
class="type">class="kw">string str = "";
for(class="type">ulong i = class="num">0; i < size; i++)
  {
  str += FileReadString(Handle0x);
  }
if (str != "" && str != PrevReaded)
  {
  PrevReaded=str;
  FileSeek(Handle0x,class="num">0,SEEK_SET);
  class=class="str">"cmt">//read the required parameters
  ReadFileStrings(Handle0x);
  class=class="str">"cmt">//
  FileClose(Handle0x);
  LastRead = TimeCurrent();
  RestartParams();
  }
else
  {
  FileClose(Handle0x);
  }
class="kw">return true;
  }
 else
  {
  class="kw">return false;
  }
  }
 class="kw">return false;
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|               read settings from file line by line               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void BotInstance::ReadFileStrings(class="type">int handle)
  {
  class=class="str">"cmt">//FileReadString(Handle,class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                function to prepare new parameters                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void BotInstance::RestartParams()
{
  class=class="str">"cmt">//additional code
  class=class="str">"cmt">//
  MagicF=SmartMagic(BasicNameToSymbol(Charts[chartindex].BasicName), Charts[chartindex].Timeframe);
  CurrentSymbol=Charts[chartindex].CurrentSymbol;
  m_trade.SetExpertMagicNumber(MagicF);
}

◍ 用符号和周期拼出不会撞车的幻数

虚拟 EA 模板要在同一终端里跑多品种多周期,最怕不同实例抢同一个幻数导致平仓逻辑串台。一种做法是给每个品种预留一段 10000 的幻数空间,再叠加周期本身的分钟数做偏移,这样 EURUSD+M1 和 XAUUSD+H1 算出来的整数基本不会重叠。 代码里 SmartMagic 先遍历 III 符号数组,命中后取 MagicHelp+(i+1)*10000 作为品种基址,然后直接加 ENUM_TIMEFRAMES 的枚举值(M1=1、H1=60、H4=240 这类分钟量)当周期后缀,返回拼接好的整数。MagicHelp 宏限定在 0~9999,所以单个品种内部还能再腾出近万格余量。 偏移步长别正好踩中 240 的整数倍。若用 240、480 这种,两个不同模板在 H4 上就可能算出相同幻数;改成 241、482、723 这类稍大的素数式间隔,重合概率会明显降低。外汇和贵金属杠杆高,幻数撞车会让策略互相平掉对方的单,实盘前务必在 MT5 策略测试器里打印几个品种周期的返回值核对。 这种结构最大的副作用是两套独立虚拟 EA 无法手动并网——它们的幻数生成规则一致,合并后必然冲突使逻辑崩坏。对普通多品种布控来说,免设幻数已经够用,真要跨模板联动得另写映射层。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|              Smart generation of magical numbers                 |
class=class="str">"cmt">//|    (each instrument-period has its own fixed magic number)       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int BotInstance::SmartMagic(class="type">class="kw">string InstrumentSymbol,ENUM_TIMEFRAMES InstrumentTimeframe)
{
   class=class="str">"cmt">// initialization
   class="type">int magicbuild=class="num">0;
   
   class=class="str">"cmt">// loop through the array
   for ( class="type">int i=class="num">0;i<ArraySize(III); i++ )
   {
      class=class="str">"cmt">// check the symbol to assign a magic number
      if ( III[i] == InstrumentSymbol )
      {
         magicbuild=MagicHelp+(i+class="num">1)*class="num">10000;
         break; class=class="str">"cmt">// stop the loop
      }
   }   
   
   class=class="str">"cmt">// add identifier for time frame    
   magicbuild+=InstrumentTimeframe;      
   class="kw">return magicbuild;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|               unique shift for difference of EA                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define MagicHelp class="num">0 class=class="str">"cmt">//bot magic shift [class="num">0...class="num">9999]

「把多EA仓位折算到M1尺度再配手数」

做组合EA的人迟早会撞上同一个墙:H1和M5的柱形幅度差好几倍,按EA数量平分手数只会让大周期偷偷吃掉净值波动。作者给出的解法是——所有工具-周期的仓位贡献,必须按M1柱的平均净值增减来拉平,而不是简单按个数或存款比例切。 核心是两个自己定义的旋钮:DeltaBarPercent 是 DepositDeltaEquity 的百分比,后者指单个虚拟EA在「一根M1柱从顶到底或底到顶」时,净值允许增减的存款份额。也就是说,你先拍一个存款额,再告诉系统一根M1柱最坏波动要动用它百分之几,手数就从这倒推。 折算靠一组量:Mb是工作图平均柱宽(点),Mb1是重算成M1的柱宽,Kb是二者比率,T是周期缩到分钟的因子。代码里的 PeriodK 就是算 Kb 的——比如 H1 返回 sqrt(1/60)≈0.129,M5 返回 sqrt(1/5)≈0.447,M1 就是 1.0。拿这个系数乘原图柱宽,就能不加载M1数据而近似出M1等价幅度。 下一步算 Basisb:用「1手1点净值变动×M1平均柱宽(点)」得出1手仓在M1柱均幅下的净值摆动。再乘上你的 DeltaBarPercent 和 DepositDeltaEquity 得到 BasisI,令 Basisb×Lot = BasisI,反解出来的 Lot 就是该EA应开的手数。效果上类似给每个EA单独定固定手数,但免了手动挨个调。 外汇与贵金属杠杆高,这种跨周期均衡只解决「贡献度对等」,不消除单边市爆仓可能,上实盘前请在MT5策略测试器用虚拟存款跑通再挂真钱。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|       timeframe to average movement adjustment coefficient       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double PeriodK(ENUM_TIMEFRAMES tf)
  {
  class="type">class="kw">double ktemp;
  class="kw">switch(tf)
    {
    case  PERIOD_H1:
        ktemp = MathSqrt(class="num">1.0/class="num">60.0);
        break;
    case  PERIOD_H4:
        ktemp = MathSqrt(class="num">1.0/class="num">240.0);
        break;
    case PERIOD_M1:
        ktemp = class="num">1.0;
        break;
    case PERIOD_M5:
        ktemp = MathSqrt(class="num">1.0/class="num">5.0);
        break;
    case PERIOD_M15:
        ktemp = MathSqrt(class="num">1.0/class="num">15.0);
        break;
    case PERIOD_M30:
        ktemp = MathSqrt(class="num">1.0/class="num">30.0);
        break;
    class="kw">default: ktemp = class="num">0;
    }
  class="kw">return ktemp;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|     average candle size in points for M1 for the current chart   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CalculateAverageBarPoints(Chart &Ch)
  {
  class="type">class="kw">double SummPointsSize=class="num">0.0;
  class="type">class="kw">double MaxPointSize=class="num">0.0;
  for (class="type">int j = class="num">0; j < ArraySize(Ch.HighI); j++)
    {
    if (Ch.HighI[j]-Ch.LowI[j] > MaxPointSize) MaxPointSize= Ch.HighI[j]-Ch.LowI[j];
    }
    
  for (class="type">int j = class="num">0; j < ArraySize(Ch.HighI); j++)
    {
    if (Ch.HighI[j]-Ch.LowI[j] > class="num">0) SummPointsSize+=(Ch.HighI[j]-Ch.LowI[j]);

用平均波幅反推平衡仓位

这段代码把「图表平均蜡烛点值」和「账户风险额度」接起来,直接算出一张图表该挂多大手数。核心在 OptimalLot():先拿 DeltaBarPercent(默认百分比)乘上 DepositDeltaEquityE 得到单笔可亏的基准金额 BasisDX0,再除以每点价值 DY0,DY0 由 CalculateAverageBarPoints() 乘 SYMBOL_TRADE_TICK_VALUE 得到。 CalculateAverageBarPoints() 返回的已经是「按分钟折算后的平均蜡烛大小」——它用 PeriodK() 把不同周期波幅压缩到同一时间尺度,再除以总 K 线数除 ChartPoint。这样跨周期比波幅才不虚。 实际在 MT5 里跑,把 DeltaBarPercent 设 1.0、DepositDeltaEquityE 填净值,就能看到黄金 M5 和 EURUSD H1 算出的手数差可能差出 3~5 倍。外汇和贵金属杠杆高,这个手数只是数学平衡值,遇滑点和跳空仍可能超预期亏损,须自行加缓冲。

MQL5 / C++
  else SummPointsSize+=MaxPointSize;
   }
   SummPointsSize=(SummPointsSize/ArraySize(Ch.HighI))/Ch.ChartPoint;
   class="kw">return PeriodK(Ch.Timeframe)*SummPointsSize;class=class="str">"cmt">//class="kw">return the average size of candles reduced to a minute using the PeriodK() adjustment function
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|    calculate the optimal balanced lot for the selected chart     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double OptimalLot(Chart &Ch)
   {
   class="type">class="kw">double BasisDX0 = (DeltaBarPercent/class="num">100.0) * DepositDeltaEquityE;
   class="type">class="kw">double DY0=CalculateAverageBarPoints(Ch)*SymbolInfoDouble(Ch.CurrentSymbol,SYMBOL_TRADE_TICK_VALUE);
   class="kw">return BasisDX0/DY0;
   }

◍ 按存款比例重算虚拟EA手数

多机器人框架里,每个虚拟 EA 原本按固定 DepositDeltaEquity 跑标准化手数 Lot,完全不理当前真实存款。开启自动手数后,得把真实存款摊到单个 bot 上再反推手数。 核心关系是 AutoLot = Lot * (DepositPerOneBot / DepositDeltaEquity),而 DepositPerOneBot = Deposit / BotQuantity。也就是说,总存款除以虚拟 EA 数量得到单 bot 可控存款份额,再跟原标准化基准做比,缩放出手数,风险才跟真实账户规模对齐。 这套逻辑在 MT5 里直接挂上就能验:初始信号能盈利时,自动手数下的权益曲线倾向平滑且带指数坡度。策略盈利因子越高、测试区间成交越密,曲线越平、指数感越强——这是多样化摊薄方差的结果。 顺带看存款负载的平滑度:曲线均匀说明手数归一化和后续缩放没过载。外汇与贵金属杠杆高,实盘前务必用策略测试器跑一遍不同 BotQuantity 下的回撤分布。

「用定时器拉取远程信号配置」

EA 里这套远程同步是可选的,直接从模板里剪掉也不影响主逻辑,但用它做多信号切换很省事。核心靠定时器触发:每过 5 分钟再带一点随机抖动去服务器拉一次文件,避免所有实例整齐划一地打请求。 同步的第一步是访问 API 拿到指定目录(代码里叫 SignalDirectoryE)下的文件清单,随后逐个下载。目录名即信号名,想加几个信号就建几个文件夹,随时在 MT5 里换目录就行。EA 侧实际只依赖两个 API 函数:取文件列表、取文件内容;上传和清理放在你自己的后台程序里做更合理。 URL 里两个字段最关键:filelist 是取列表的接口名(可改),Password_key 是防外人闯入的访问密钥头。服务器回的是 JSON,没有通用解析代码,得按你自己的结构写;作者偏好手写解析而不是引库。小文件逐字符串写本地完全够用,实测没感觉到延迟。 下面这段是定时器与下载主逻辑的骨架。DownloadTimer 用 TimeCurrent 减上次时间判断是否过了 5*60 秒再加 0~60 秒随机量;DownloadFiles 先 GetFiles 拿数组,返回 200 才遍历,每个文件再 GetFileContent 拿到内容数组后 WriteData 落盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|            used to read the settings every class="num">5 minutes +           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DownloadTimer()
{
    class=class="str">"cmt">// Check if the passed time from the last download time is more than class="num">5 minutes
    if (  TimeCurrent() - LastDownloadTime > class="num">5*class="num">60 + class="type">int((class="type">class="kw">double(MathRand())/class="num">32767.0) * class="num">60) )
    {
        class=class="str">"cmt">// Set the last download time to the current time
        LastDownloadTime=TimeCurrent();
        class=class="str">"cmt">// Download files again
        DownloadFiles();
     }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|       used to download control files if they isn&class="macro">#x27;t present       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DownloadFiles()
{
    class="type">class="kw">string Files[];
    class=class="str">"cmt">// Initialize the response code by getting files from the signal directory
    class="type">int res_code=GetFiles(SignalDirectoryE,Files);
    class=class="str">"cmt">// Check if the list of files is successfully got
    if (res_code == class="num">200)
    {
        class=class="str">"cmt">// Proceed if there is at least one file in the server
        if (ArraySize(Files) > class="num">0)
        {
            class=class="str">"cmt">// Download each file individually
            for (class="type">int i = class="num">0; i < ArraySize(Files); i++)
            {
                class="type">class="kw">string FileContent[];
                class=class="str">"cmt">// Get the content of the file
                class="type">int resfile =  GetFileContent(SignalDirectoryE,Files[i],FileContent);
                class=class="str">"cmt">// Check if the file content is successfully got
                if (resfile == class="num">200)
                {
                    class=class="str">"cmt">// Write the file content in our local file
                    WriteData(FileContent,Files[i]);
                }
            }
        }
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|              getting the list of files into an array             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int GetFiles(class="type">class="kw">string directory,class="type">class="kw">string &fileArray[])
  {
  class=class="str">"cmt">//class="type">class="kw">string for getting a list of files in the form of JSON via GET to API 

用 WebRequest 拉取远端文件清单与内容

在 MT5 里做 AIGC 信号中转,第一步往往是把云端的策略文件或模型参数同步到本地终端。下面这段实现用 WebRequest 向自有 API 发 GET 请求,拿到 JSON 后再拆成文件列表与内容行,timeout 写死 1500 毫秒,意味着弱网环境下请求可能在 1.5 秒后被掐断返回 400。 请求头里塞了 Password_key 做鉴权,urlList 由 ApiDomen 拼出 /filelist/ 或 /file_content/ 路径。extractFiles 与 extractContent 在示例里直接 return false,属于留给你接 JSON 解析的空壳,真要跑通得自己补反序列化逻辑。 写文件时按 SubfolderE 决定落盘子目录,bCommonReadE 为 false 才进本地沙盒目录。外汇与贵金属行情高波动,这类自动拉取若误写路径可能覆盖历史回测文件,实盘前务必在策略测试器里先验证目录权限。

MQL5 / C++
  class="type">class="kw">string urlList = ApiDomen+"/filelist/"+directory;class=class="str">"cmt">//URL
  class="type">char message[];class=class="str">"cmt">//Body of the request
  class="type">class="kw">string headers = "Password_key: " + key;class=class="str">"cmt">// We form the headers of the request
  class="type">class="kw">string resultheaders = "";class=class="str">"cmt">//returning headers
  class="type">class="kw">string cookie = "";class=class="str">"cmt">//cookies
  class="type">int timeout = class="num">1500;class=class="str">"cmt">//waiting for a response when requesting a file or json
  class="type">char result[];

  class=class="str">"cmt">// We send a GET request to the server to receive JSON with a list of files
  class="type">int res_code = WebRequest("GET", urlList, headers, timeout, message, result, resultheaders);
  class="type">bool rez = extractFiles(CharArrayToString(result),fileArray);
  if (rez) class="kw">return res_code;
  else class="kw">return class="num">400;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                  get file list from JSON class="type">class="kw">string                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool extractFiles(class="type">class="kw">string json, class="type">class="kw">string &Files[])
  {
  class="kw">return false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                    getting the file content                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int GetFileContent(class="type">class="kw">string directory,class="type">class="kw">string filename,class="type">class="kw">string &OutContent[])
  {
  class=class="str">"cmt">//class="type">class="kw">string for getting a file content in the form of JSON via GET to API 
  class="type">class="kw">string urlList = ApiDomen+"/file_content/"+directory+"/"+filename;class=class="str">"cmt">//
  class="type">char message[];class=class="str">"cmt">// Body of the request
  class="type">class="kw">string headers = "Password_key: " + key;class=class="str">"cmt">// We form the headers of the request
  class="type">class="kw">string resultheaders = "";class=class="str">"cmt">//returning headers
  class="type">class="kw">string cookie = "";class=class="str">"cmt">//cookies
  class="type">int timeout = class="num">1500;class=class="str">"cmt">//waiting for a response when requesting a file or json
  class="type">char result[];

  class=class="str">"cmt">// We send a GET request to the server to receive JSON with a file content
  class="type">int res_code = WebRequest("GET", urlList, headers, timeout, message, result, resultheaders);
  class="type">bool rez = extractContent(CharArrayToString(result),OutContent);
  if (rez) class="kw">return res_code;
  else class="kw">return class="num">400;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|   read the contents of the file from JSON each line separately   |
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">bool extractContent(class="type">class="kw">string json, class="type">class="kw">string &FileLines[])
  {

  class="kw">return false;
  }
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class=class="str">"cmt">//|    fill the file with its lines, which are all contained in data      |
class=class="str">"cmt">//|  with a new line separator, and save in the corresponding directory   |
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class="type">void WriteData(class="type">class="kw">string &data[],class="type">class="kw">string FileName)
  {
  class="type">int fileHandle;
  class="type">class="kw">string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\";

  if (!bCommonReadE)
    {

◍ 把数组落盘到本地或公共目录的两种开法

在 MT5 里写文件,第一步是用 FileOpen 拿到句柄,但本地目录和公共目录(terminal_data\MQL5\Files 之外可被多终端共享的位置)的旗标不一样。上面这段逻辑先判断走哪个分支:普通本地写用 FILE_REWRITEFILE_WRITEFILE_TXTFILE_ANSI,若需要跨终端可见则追加 FILE_COMMON。

拿到句柄后别急着写,先用 FileSeek(fileHandle,0,SEEK_SET) 把指针拨回文件头,否则 REWRITE 模式下也可能出现错位。随后用 for 循环把 data[] 逐行写进去,每行末尾补 "\r ",最后必须 FileClose 释放句柄,否则句柄泄漏会让后续开盘会话打不开同名文件。 验证方法很简单:在 EA 里塞一段字符串数组,跑完去对应的 Files 子目录看文本,本地写默认落在 MQL5\Files\tempsubfolder,加 FILE_COMMON 的会进公共区。外汇与贵金属品种上做这类日志,注意点差跳空时段写盘延迟可能偏高,属正常风险。

MQL5 / C++
   fileHandle = FileOpen(tempsubfolder+FileName,FILE_REWRITE|FILE_WRITE|FILE_TXT|FILE_ANSI);
   }
  else
   {
   fileHandle = FileOpen(tempsubfolder+FileName,FILE_REWRITE|FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_COMMON);
   }
   
  if(fileHandle != INVALID_HANDLE)
    {
    FileSeek(fileHandle,class="num">0,SEEK_SET);
    for (class="type">int i=class="num">0; i < ArraySize(data) ; i++)
      {
      FileWriteString(fileHandle,data[i]+"\r\n");
      }
    FileClose(fileHandle);
    }

「在 BotInstance 里挂自己的开棒逻辑」

MQL5 原生没有 OnBar 这类处理函数,模板里用每根虚拟 EA 新柱触发来模拟,效果接近 OnTick 但只在柱闭合后跑一次,对柱级交易影响可忽略。建议把自定义逻辑和变量直接写在 BotInstance 类的 Trade 方法旁,别动主结构,避免和回测框架其他部分搅在一起。 下面这段是模板里的主交易函数骨架,零号位是当前未闭杆,一号位是已闭上一杆。注意 CloseI[0] 这类字段在实盘里会随报价跳动,但我们的逻辑只取已成型杆,所以用的是下标 1。 代码里的示例逻辑很直白:上一根收高于开,就平多并开空;收低于开,就平空并开多。这属于裸 K 反转套路,在欧元兑美元 15 分钟这类高噪品种上假突破概率偏高,贵金属 XAUUSD 同样受美联储数据窗干扰,高风险,只能当接线样例,不要直接跑实盘。 想验证的话,把这段塞进 MT5 的 EA 模板,把 chartindex 指向你的观察图,先开可视化回测看每根开平信号是否如预期触发。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|      the main trading function of individual robot instance      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void BotInstance::Trade() 
{
   class=class="str">"cmt">//data access
   
   class=class="str">"cmt">//Charts[chartindex].CloseI[class="num">0]//current bar(zero bar is current like in mql4)
   class=class="str">"cmt">//Charts[chartindex].OpenI[class="num">0]
   class=class="str">"cmt">//Charts[chartindex].HighI[class="num">0]
   class=class="str">"cmt">//Charts[chartindex].LowI[class="num">0]
   class=class="str">"cmt">//Charts[chartindex]. ???
   
   class=class="str">"cmt">//close & open
   
   class=class="str">"cmt">//CloseBuyF();
   class=class="str">"cmt">//CloseSellF();
   class=class="str">"cmt">//BuyF();
   class=class="str">"cmt">//SellF();   
   class=class="str">"cmt">// Here we can include operations such as closing the buying position, closing selling position and opening new positions.
   class=class="str">"cmt">// Other information from the chart can be used for making our buying/selling decisions.
   
   class=class="str">"cmt">// Here is a simple trading logic example
   if ( Charts[chartindex].CloseI[class="num">1] > Charts[chartindex].OpenI[class="num">1] )
   {
      CloseBuyF();
      SellF();
   }
   if ( Charts[chartindex].CloseI[class="num">1] < Charts[chartindex].OpenI[class="num">1] )
   {
      CloseSellF();
      BuyF();
   }        
}

界面里那三个隐藏字段才是关键

模板自带的图形界面在 MT4 和 MT5 下共用同一套配色与布局,比早先版本观感更干净。界面里的问号是预留位,删块或加数据直接改对应位置即可,不用动框架。 对外暴露的只有两个轻量方法:CreateSimpleInterface 负责建界面,UpdateStatus 负责刷状态,按函数名在源码里就能定位,不用额外演示。 真正值得盯的是我后加的三个字段。看界面最后三个字符串,第一个是“保留的幻数走廊”,它跟着你当前加载的设置文件走——删掉或新增配置文件,走廊就相应收窄或放宽,用来隔离不同 EA 的幻数冲突。剩下两个字段分别显示最近一次读取设置的时刻、以及最近一次和 API 同步的时刻(若发生过同步)。 开 MT5 把模板跑起来,对照界面末三行就能验证走廊宽度随 set 文件数量变化的逻辑,外汇与贵金属交易本身高风险,参数隔离只是工程层面的防护。

◍ 把模板往跨货币优化器推

这一版模板已经能撑住扩展和修改,但代码本身离干净还差得远,优化和修洞是持续活。即便如此,下一步落点已经清楚:把头寸虚拟化,再据此做一套跨货币优化器。 跨货币优化器的核心动作是同时优化所有虚拟工具-周期,把各路设置合并后,利润曲线可能更平、回撤倾向更低,但外汇与贵金属本身高风险,合并并不等于风险消失。 自动多样化和利润提升应排在最前面。理想状态是在任意策略上套一层基础外骨骼,既榨出更多潜在收益,又给终端用户留足可调空间,而不是把策略锁死。

「作者资源与延伸阅读入口」

这套动态模板的示例包以 DynamicTemplate.zip 形式提供,体积 45.56 KB,解压后可直接在 MT5 加载查看基于模板的免费产品骨架。原文作者 Evgeniy Ilin 在平台累计发布 40596 项内容,其店铺汇集了从 ChatGPT 接入到多机器人调度的多篇 MQL5 技术文,可作为后续深挖的索引。 文末列出的经纪商链接(RoboForex、ByBit、Tickmill 等)属于作者个人推荐,并非策略组成部分。外汇与贵金属杠杆交易高风险,接入任何第三方 broker 前应先在模拟账户验证出入金与滑点表现。 若想复刻文中的模板思路,建议从「暴力方式搜素形态」系列第五、六部分入手,配合 MQL5 向导的 GAN/MLP 学习率文章交叉阅读,比单看示例包更容易摸清参数边界。

把虚拟 EA 巡检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到虚拟图表上的幻数分配与手数标准化是否越界,你只管判断策略本身。

常见问题

手动顺序排容易因人为因素在复制多图表时撞号,改进模板用自动生成幻数按工具-周期对哈希,降低冲突概率。
它把不同品种的点值差异统一折算,避免静态模板下扩展功能时手数误配,属于偏理论层的架构修正。
可以,小布盯盘的 AIGC 模块已内置对应解析,品种页会呈现虚拟 EA 的配置与同步异常,省去你切图核对。
它让终端外独立优化结果回写虚拟 EA,免去了用户手动重优化的需要,也契合本篇追求的零重配目标。
它在对应虚拟图表平仓前锁住设置,保证开平逻辑一致,避免频繁改参导致头寸脱离原策略。关于完整系列见《MetaTrader 中的 Multibot(第一部分):基础动态模板》。