继续迈进优化(第一部分):操控优化报告·综合运用
📘

继续迈进优化(第一部分):操控优化报告·综合运用

第 3/3 篇

◍ 优化结果对象的相等性判定

在 MT5 策略优化器的自定义结果类里,重写 Equals 是为了让不同优化跑次产生的对象能被正确比对,避免默认引用比较带来的漏判。 下面这段实现先判断传入对象是不是 OptimisationResult 类型,是就走自定义 == 运算符,不是则回退到基类比较。 实际写 EA 框架时,若你用自定义结构缓存多组参数回测结果,这一步直接决定后续去重或排序逻辑是否可靠,建议开 MT5 新建类库验证一下类型分支。 外汇与贵金属品种波动跳空频繁,回测结果受点差影响大,对象比对仅解决代码层一致性,不预示任何实盘收益。

MQL5 / C++
class=class="str">"cmt">/// <param name="obj"></param>
class=class="str">"cmt">/// <returns></returns>
class="kw">public class="kw">override class="type">bool Equals(object obj)
{
    if (obj is OptimisationResult other)
    {
        class="kw">return this == other;
    }
    else
        class="kw">return base.Equals(obj);
}

「用 ReportWriter 把优化结果落盘成 XML」

要在 MT5 优化器跑完之后把报告拽出来二次处理,光靠终端面板不够,得让 DLL 直接写文件。ReportWriter 这个类只放静态方法,目的就是能直接 export 给 MQL5 调用,类本身标了 public,里面挂了一个 ReportItem 类型的静态字段,用来在调用间隙暂存 EA 参数和系数。 ClearReportItem() 干的事很直白:把 ReportItem 重新 new 一遍,等于清空临时仓库。当旧对象已经被销毁、你拿不到实例的时候,靠它重启数据累积流程,避免脏数据混进下一份报告。 文件落地之前先查是否存在,没有就建。CreateFileIfNotExists 会先写根标记 <Optimization_Report/>,再填创建时间,然后预置「无变化优化器设置」节点和一个已封闭的「优化结果分区」——这样出来的就是一个满足最小格式要求的空 XML,后续用 XmlDocument 读改写都顺手。 多核优化时线程会撞车。另一套写入方法用命名互斥体(named mutex)把写盘包在 try-catch 里,finally 里强制释放锁,否则一个线程抛异常不解锁,整个优化任务可能直接死锁卡死。 如果一次要写一串结果,扩展方法 IEnumerable<OptimisationResult> 比逐条写快得多:它建好 XmlWriter,按固定结构循环刷优化设置和结果,VaR、最大利润、回撤值都填进去,还内嵌一个按天输出五次的函数。数据直到 xmlWriter.Close() 才真正落盘,所以这是批量导出数组最省时的路径。外汇与贵金属优化本身高风险,回测好看不代表实盘概率同分布。 下面这段是类里最该先看明白的骨架,暂存字段和清空方法都在,后面 AppendBotParam、AppendMainCoef 的签名显示了能接住哪些参数。

MQL5 / C++
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// temporary data keeper
class=class="str">"cmt">/// </summary>
class="kw">private class="kw">static ReportItem ReportItem;
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// clearing the temporary data keeper
class=class="str">"cmt">/// </summary>
class="kw">public class="kw">static class="type">void ClearReportItem()
{
    ReportItem = new ReportItem();
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Add robot parameters
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="name">Parameter name</param>
class=class="str">"cmt">/// <param name="value">Parameter value</param>
class="kw">public class="kw">static class="type">void AppendBotParam(class="type">class="kw">string name, class="type">class="kw">string value);
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Add the main list of coefficients
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="payoff"></param>
class=class="str">"cmt">/// <param name="profitFactor"></param>
class=class="str">"cmt">/// <param name="averageProfitFactor"></param>
class=class="str">"cmt">/// <param name="recoveryFactor"></param>
class=class="str">"cmt">/// <param name="averageRecoveryFactor"></param>
class=class="str">"cmt">/// <param name="totalTrades"></param>
class=class="str">"cmt">/// <param name="pl"></param>
class=class="str">"cmt">/// <param name="dd"></param>
class=class="str">"cmt">/// <param name="altmanZScore"></param>
class="kw">public class="kw">static class="type">void AppendMainCoef(class="type">class="kw">double payoff,
                              class="type">class="kw">double profitFactor,
                              class="type">class="kw">double averageProfitFactor,
                              class="type">class="kw">double recoveryFactor,
                              class="type">class="kw">double averageRecoveryFactor,
                              class="type">int totalTrades,
                              class="type">class="kw">double pl,

回测统计接口的写入方法拆解

这段接口定义暴露了 MT5 回测报告生成器的核心写入入口,做自定义复盘工具时可以直接复用这几个静态方法。 AppendVaR 接收 Q_90、Q_95、Q_99 三个分位数以及均值 Mx 与标准差 Std,对应 90%、95%、99% 置信度的在险价值。外汇与贵金属杠杆品种下,VaR 只反映历史波动概率,极端跳空时实际亏损可能超出该区间,属高风险敞口。 AppendMaxPLDD 把总利润 profit、最大回撤 dd、盈利/亏损笔数、连胜连亏次数一次性写入。连胜与连亏字段对判断策略衰减点很有用,例如 consecutiveLose 突然高于历史均值 3 倍时,策略可能进入失效期。 AppendDay 按 day 索引落盘单日盈亏与交易笔数,适合做日历效应分析。 CreateFileIfNotExists 先判 File.Exists 直接 return,避免重复建文件;随后用 XmlTextWriter 并以 Formatting.Indented 输出缩进 XML,方便人工核对。 开 MT5 把这段挂到自己的 EA 报告模块里,改 pathToFile 就能拿到结构化回测档案,比默认报告更利于二次挖掘。

MQL5 / C++
class="kw">public class="kw">static class="type">void AppendVaR(class="type">class="kw">double Q_90, class="type">class="kw">double Q_95,
                      class="type">class="kw">double Q_99, class="type">class="kw">double Mx, class="type">class="kw">double Std);
class="kw">public class="kw">static class="type">void AppendMaxPLDD(class="type">class="kw">double profit, class="type">class="kw">double dd,
                      class="type">int totalProfitTrades, class="type">int totalLoseTrades,
                      class="type">int consecutiveWins, class="type">int consecutiveLose);
class="kw">public class="kw">static class="type">void AppendDay(class="type">int day,
                      class="type">class="kw">double profit, class="type">class="kw">double dd,
                      class="type">int numberOfProfitTrades,
                      class="type">int numberOfLoseTrades);
class="kw">private class="kw">static class="type">void CreateFileIfNotExists(class="type">class="kw">string pathToBot, class="type">class="kw">string currency, class="type">class="kw">double balance, class="type">int leverage, class="type">class="kw">string pathToFile)
{
    if (File.Exists(pathToFile)
        class="kw">return;
    class="kw">using (var xmlWriter = new XmlTextWriter(pathToFile, null))
    {
        class=class="str">"cmt">// set document format
        xmlWriter.Formatting = Formatting.Indented;

◍ 把优化报告落地成 XML 节点

在 MT5 里导出 EA 优化结果,最稳的做法是用 XmlTextWriter 自己拼结构,而不是依赖终端自带的杂乱报表。上面这段逻辑先设缩进为 1 个制表符,再写文档头,根节点叫 Optimisation_Report,并带一个 Created 属性记录当下时间,格式是 dd.MM.yyyy HH:mm:ss,例如实测跑出过 24.10.2019 19:10:08 这样的时间戳。 根节点下分两块:Optimiser_Settings 写机器人路径、存款、杠杆;Optimisation_Results 先留空壳等后续塞pass。存款项通过 WriteItem 写时顺带挂了 Currency 属性,杠杆只写数值不挂属性,这种差异在回看 XML 时一眼能看出哪些字段是带元数据的。 WriteItem 是复用核心:它统一生成 <Item Name=...>值</Item> 结构,属性用 Dictionary 传,为空就只写纯值。你在 EA 里接这套,只要改 pathToBot、balance、currency、leverage 几个变量,就能一键生成可机读的优化纪要,比手动抄表不容易错。 外汇与贵金属优化受点差和滑点影响大,同份 XML 在实盘可能偏离回测,高杠杆品种尤甚,导出后建议人工抽查两三个 pass 的数值。

MQL5 / C++
xmlWriter.IndentChar = &class="macro">#x27;	&class="macro">#x27;;
xmlWriter.Indentation = class="num">1;
xmlWriter.WriteStartDocument();
class=class="str">"cmt">// Create document root
class="macro">#region Document root
xmlWriter.WriteStartElement("Optimisation_Report");
class=class="str">"cmt">// Write the creation date
  xmlWriter.WriteStartAttribute("Created");
  xmlWriter.WriteString(DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));
  xmlWriter.WriteEndAttribute();
class="macro">#region Optimiser settings section
class=class="str">"cmt">// Optimizer settings
xmlWriter.WriteStartElement("Optimiser_Settings");
class=class="str">"cmt">// Path to the robot
WriteItem(xmlWriter, "Bot", pathToBot);
class=class="str">"cmt">// Deposit
WriteItem(xmlWriter, "Deposit", balance.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Currency", currency } });
class=class="str">"cmt">// Leverage
WriteItem(xmlWriter, "Leverage", leverage.ToString());
xmlWriter.WriteEndElement();
class="macro">#endregion
class="macro">#region Optimization results section
class=class="str">"cmt">// the root node of the optimization results list
xmlWriter.WriteStartElement("Optimisation_Results");
xmlWriter.WriteEndElement();
class="macro">#endregion
xmlWriter.WriteEndElement();
class="macro">#endregion
xmlWriter.WriteEndDocument();
xmlWriter.Close();
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Write element to a file
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="writer">Writer</param>
class=class="str">"cmt">/// <param name="Name">Element name</param>
class=class="str">"cmt">/// <param name="Value">Element value</param>
class=class="str">"cmt">/// <param name="Attributes">Attributes</param>
class="kw">private class="kw">static class="type">void WriteItem(XmlTextWriter writer, class="type">class="kw">string Name, class="type">class="kw">string Value, Dictionary<class="type">class="kw">string, class="type">class="kw">string> Attributes = null)
{
    writer.WriteStartElement("Item");
    writer.WriteStartAttribute("Name");
    writer.WriteString(Name);
    writer.WriteEndAttribute();
    if (Attributes != null)
    {
        foreach(var item in Attributes)
        {
            writer.WriteStartAttribute(item.Key);
            writer.WriteString(item.Value);
            writer.WriteEndAttribute();
        }
    }
    writer.WriteString(Value);
    writer.WriteEndElement();
}

「把回测配置落进 XML 节点」

做 MT5 策略优化报告自动化时,常需要把机器人路径、入金、杠杆这些参数写成结构化 XML,方便后续比对多组跑分。上面那段样例节点里,Deposit 写了 1000、Leverage 写了 1,意味着这是一笔无杠杆的千元账户测试,外汇或贵金属这类高波动品种在这种设定下爆仓概率会显著低于满杠杆。 FillInAttributes 负责把键值对塞进节点的属性列:先 xmlDoc.CreateAttribute 建属性,再赋 Value,最后 item.Attributes.Append 挂上去。AppendSection 和 WriteItem 的区别在于后者多一步 item.InnerText = value,也就是写带文本内容的子项而非空壳分区。 直接在 EA 里复用这三个静态方法,你能把每轮优化的 Optimisation_Results 自动归档,不用手动抄表。注意外汇和贵金属交易本身高风险,回测无杠杆不代表实盘可照搬。

MQL5 / C++
class="kw">private class="kw">static class="type">void FillInAttributes(XmlNode item, XmlDocument xmlDoc, Dictionary<class="type">class="kw">string, class="type">class="kw">string> Attributes)
{
   if (Attributes != null)
   {
      foreach(var attr in Attributes)
      {
         XmlAttribute attribute = xmlDoc.CreateAttribute(attr.Key);
         attribute.Value = attr.Value;
         item.Attributes.Append(attribute);
      }
   }
}
class="kw">private class="kw">static class="type">void AppendSection(XmlDocument xmlDoc, class="type">class="kw">string xpath_parentSection,
                                 class="type">class="kw">string sectionName, Dictionary<class="type">class="kw">string, class="type">class="kw">string> Attributes = null)
{
   XmlNode section = xmlDoc.SelectSingleNode(xpath_parentSection);
   XmlNode item = xmlDoc.CreateElement(sectionName);
   FillInAttributes(item, xmlDoc, Attributes);
   section.AppendChild(item);
}
class="kw">private class="kw">static class="type">void WriteItem(XmlDocument xmlDoc, class="type">class="kw">string xpath_parentSection, class="type">class="kw">string name,
                             class="type">class="kw">string value, Dictionary<class="type">class="kw">string, class="type">class="kw">string> Attributes = null)
{
   XmlNode section = xmlDoc.SelectSingleNode(xpath_parentSection);
   XmlNode item = xmlDoc.CreateElement(name);
   item.InnerText = value;
   FillInAttributes(item, xmlDoc, Attributes);
   section.AppendChild(item);
}

把回测结果写进 XML 报告树的实操

做 MT5 策略批量回测后,最麻烦的不是跑优化,而是把每轮结果的品种、周期、起止时间落盘成可比对的结构。下面这段静态方法就是干这个的:先确保文件存在,再往 Optimisation_Report/Optimisation_Results 节点下挂新一轮 Result。 调用时传入 pathToBot、currency、balance、leverage 等 9 个参数,其中 leverage 是 int,balance 是 double,StartDT 与 FinishDT 用 ulong 存时间戳。注意原文注释把 StartDT 拼错成 'dare',实机复制时别照抄错字。 写入逻辑靠 AppendSection 逐级拼 XPath:先建 Result,再在里面建 Coefficients,其下再挂 VaR、Max_PL_DD、Trading_Days;Trading_Days 里继续拆成 Mn/Tu/We/Th/Fr 五个工作日节点。这样一份 XML 就能按'品种→结果→系数→逐日'四层展开,方便小布后续做跨品种胜率归因。 外汇与贵金属杠杆品种回测含高杠杆风险,XML 里记录的 leverage 仅作存档,不代表实盘可盲用同倍数。

MQL5 / C++
class=class="str">"cmt">/// <param name="leverage">Leverage</param>
class=class="str">"cmt">/// <param name="pathToFile">Path to file</param>
class=class="str">"cmt">/// <param name="symbol">Symbol</param>
class=class="str">"cmt">/// <param name="tf">Timeframe</param>
class=class="str">"cmt">/// <param name="StartDT">Trading start dare</param>
class=class="str">"cmt">/// <param name="FinishDT">Trading end date</param>
class="kw">public class="kw">static class="type">void Write(class="type">class="kw">string pathToBot, class="type">class="kw">string currency, class="type">class="kw">double balance,
                      class="type">int leverage, class="type">class="kw">string pathToFile, class="type">class="kw">string symbol, class="type">int tf,
                      class="type">class="kw">ulong StartDT, class="type">class="kw">ulong FinishDT)
{
   class=class="str">"cmt">// Create the file if it does not yet exist
   CreateFileIfNotExists(pathToBot, currency, balance, leverage, pathToFile);

   ReportItem.Symbol = symbol;
   ReportItem.TF = tf;
   class=class="str">"cmt">// Create a document and read the file class="kw">using it
   XmlDocument xmlDoc = new XmlDocument();
   xmlDoc.Load(pathToFile);
   class="macro">#region Append result section
   class=class="str">"cmt">// Write a request to class="kw">switch to the optimization results section 
   class="type">class="kw">string xpath = "Optimisation_Report/Optimisation_Results";
   class=class="str">"cmt">// Add a new section with optimization results
   AppendSection(xmlDoc, xpath, "Result",
                  new Dictionary<class="type">class="kw">string, class="type">class="kw">string>
                  {
                      { "Symbol", symbol },
                      { "TF", tf.ToString() },
                      { "Start_DT", StartDT.ToString() },
                      { "Finish_DT", FinishDT.ToString() }
                  });
   class=class="str">"cmt">// Add section with optimization results
   AppendSection(xmlDoc, $"{xpath}/Result[last()]", "Coefficients");
   class=class="str">"cmt">// Add section with VaR
   AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "VaR");
   class=class="str">"cmt">// Add section with total PL / DD
   AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "Max_PL_DD");
   class=class="str">"cmt">// Add section with trading results by days
   AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "Trading_Days");
   class=class="str">"cmt">// Add section with trading results on Monday
   AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Mn");
   class=class="str">"cmt">// Add section with trading results on Tuesday
   AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Tu");
   class=class="str">"cmt">// Add section with trading results on Wednesday
   AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "We");
   class=class="str">"cmt">// Add section with trading results on Thursday
   AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Th");
   class=class="str">"cmt">// Add section with trading results on Friday

◍ 把优化结果写进 XML 报告节点

在 MT5 优化报告导出逻辑里,机器人参数和核心评价系数都要落到同一个 XML 文档的末位 Result 节点下。下面这段 C# 风格代码展示了如何分批写入:先遍历 BotParams 把每个参数以 Item 节点挂到 Optimisation_Results/Result[last()],再用 WriteItem 把 Payoff、ProfitFactor、RecoveryFactor 等系数写进 Coefficients 子节点。 注意系数覆盖的维度:除了常见的 ProfitFactor、TotalTrades、DD,还包含 Altman Z Score 与 VaR(代码里只示例了 Q_90 即 90% 置信度的在险价值)。外汇与贵金属品种回测中,VaR 对极端行情下的潜在回撤有参考意义,但历史分布不保证未来,杠杆交易高风险。 实操上,你可以直接复制这段区域化代码块(#region 分段)到自己的报告生成器里,把 ReportItem 对象换成实际回测结果结构体,就能在每次优化后自动追加一行完整评估记录,省去手工抄表。

MQL5 / C++
class="macro">#region Append Bot params
class=class="str">"cmt">// Iterate through bot parameters
foreach(var item in ReportItem.BotParams)
{
  class=class="str">"cmt">// Write the selected robot parameter
  WriteItem(xmlDoc, "Optimisation_Report/Optimisation_Results/Result[last()]",
            "Item", item.Value, new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", item.Key } });
}
class="macro">#endregion
class="macro">#region Append main coef
class=class="str">"cmt">// Set path to node with coefficients
xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients";
class=class="str">"cmt">// Save coefficients
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.Payoff.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Payoff" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.ProfitFactor.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Profit factor" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AverageProfitFactor.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Average Profit factor" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.RecoveryFactor.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Recovery factor" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AverageRecoveryFactor.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Average Recovery factor" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.TotalTrades.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Total trades" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.PL.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "PL" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.DD.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "DD" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AltmanZScore.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Altman Z Score" } });
class="macro">#endregion
class="macro">#region Append VaR
class=class="str">"cmt">// Set path to node with VaR
xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/VaR";
class=class="str">"cmt">// Save VaR results
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_90.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "class="num">90" } });

「把 VaR 与最大盈亏序列写进优化报告 XML」

在 MT5 优化报告导出逻辑里,VaR 节点需要落四个分位:95%、99% 分位数值,以及均值 Mx 与标准差 Std。下面这段直接调用 WriteItem 把 ReportItem 里的 VaR 子结构按 Name 属性写进对应 Item,跑完优化你在生成的 XML 里搜 Q_95、Q_99 就能核对分位计算是否生效。 紧跟着的 Max_PL_DD 区块把单结果的最大利润、最大回撤、各自总交易次数、连续赢亏次数全塞进 Result[last()]/Coefficients/Max_PL_DD。注意 Profit.TotalTrades 与 DD.TotalTrades 分别统计盈利和亏损笔数,连续亏损字段 ConsecutivesTrades 挂在 DD 下,外汇和贵金属品种因点差跳空,这个连续亏数值往往在极端行情会被放大,属高风险特征。 Trading_Days 段用 switch 把周一至周四映射到 Mn/Tu/We/Th 子节点,xpath 每次拼到 Trading_Days 后面。你改 EA 报告模块时,若想加周五周六,照此补 case 即可,不补则那两天数据不会进 XML。

MQL5 / C++
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_95.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "class="num">95" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_99.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "class="num">99" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Mx.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Mx" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Std.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Std" } });
class="macro">#endregion
class="macro">#region Append max PL and DD
class=class="str">"cmt">// Set path to node with total PL / DD
xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/Max_PL_DD";
class=class="str">"cmt">// Save coefficients
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.Value.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Profit" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.Value.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "DD" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.TotalTrades.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Total Profit Trades" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.TotalTrades.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Total Lose Trades" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.ConsecutivesTrades.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Consecutive Wins" } });
WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.ConsecutivesTrades.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Consecutive Lose" } });
class="macro">#endregion
class="macro">#region Append Days
foreach(var item in ReportItem.OptimisationCoefficients.TradingDays)
{
  class=class="str">"cmt">// Set path to specific day node
  xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/Trading_Days";
  class=class="str">"cmt">// Select day
  class="kw">switch (item.Key)
  {
    case DayOfWeek.Monday: xpath += "/Mn"; class="kw">break;
    case DayOfWeek.Tuesday: xpath += "/Tu"; class="kw">break;
    case DayOfWeek.Wednesday: xpath += "/We"; class="kw">break;
    case DayOfWeek.Thursday: xpath += "/Th"; class="kw">break;

用命名互斥锁守住回测落盘

把多品种、多周期回测结果写进同一个 XML 汇总文件时,最怕几个 EA 实例同时 Save 把节点写穿。上面这段在周五分支拼完 xpath 后,连续 WriteItem 落了四项:Profit 金额、DD 金额、盈利笔数、亏损笔数,最后 xmlDoc.Save 一次性刷盘并 ClearReportItem 清空内存态。 真正拦并发的是 MutexWriter:以 mutexName 建命名互斥体,WaitOne 阻塞到拿到锁,try 内调 Write 写文件,catch 把异常消息塞进 ans,finally 前 ReleaseMutex 放锁。这样同机多终端跑同套参数采集,文件损坏概率会明显下降。 外汇与贵金属回测涉及高杠杆,并发写文件若丢节点,后续统计样本会偏。开 MT5 把这段直接挂到你自己的报告器外层,用不同 symbol 各起一个 mutexName 验证是否还会偶发空 Result。

MQL5 / C++
case DayOfWeek.Friday: xpath += "/Fr"; class="kw">break;
}
class=class="str">"cmt">// Save results
WriteItem(xmlDoc, xpath, "Item", item.Value.Profit.Value.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Profit" } });
WriteItem(xmlDoc, xpath, "Item", item.Value.DD.Value.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "DD" } });
WriteItem(xmlDoc, xpath, "Item", item.Value.Profit.Trades.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Number Of Profit Trades" } });
WriteItem(xmlDoc, xpath, "Item", item.Value.DD.Trades.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Name", "Number Of Lose Trades" } });
}
class="macro">#endregion
class=class="str">"cmt">// Rewrite the file with the changes
xmlDoc.Save(pathToFile);
class=class="str">"cmt">// Clear the variable which stored results written to a file
ClearReportItem();
}
"{xpath}/Result[last()]/Coefficients"
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Write to file class="kw">while locking class="kw">using a named mutex
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="mutexName">Mutex name</param>
class=class="str">"cmt">/// <param name="pathToBot">Path to the bot</param>
class=class="str">"cmt">/// <param name="currency">Deposit currency</param>
class=class="str">"cmt">/// <param name="balance">Balance</param>
class=class="str">"cmt">/// <param name="leverage">Leverage</param>
class=class="str">"cmt">/// <param name="pathToFile">Path to file</param>
class=class="str">"cmt">/// <param name="symbol">Symbol</param>
class=class="str">"cmt">/// <param name="tf">Timeframe</param>
class=class="str">"cmt">/// <param name="StartDT">Trading start dare</param>
class=class="str">"cmt">/// <param name="FinishDT">Trading end date</param>
class=class="str">"cmt">/// <returns></returns>
class="kw">public class="kw">static class="type">class="kw">string MutexWriter(class="type">class="kw">string mutexName, class="type">class="kw">string pathToBot, class="type">class="kw">string currency, class="type">class="kw">double balance,
                      class="type">int leverage, class="type">class="kw">string pathToFile, class="type">class="kw">string symbol, class="type">int tf,
                      class="type">class="kw">ulong StartDT, class="type">class="kw">ulong FinishDT)
{
  class="type">class="kw">string ans = "";
  class=class="str">"cmt">// Mutex lock
  Mutex m = new Mutex(class="kw">false, mutexName);
  m.WaitOne();
  try
  {
    class=class="str">"cmt">// write to file
    Write(pathToBot, currency, balance, leverage, pathToFile, symbol, tf, StartDT, FinishDT);
  }
  class="kw">catch (Exception e)
  {
    class=class="str">"cmt">// Catch error if any
    ans = e.Message;
  }
  class=class="str">"cmt">// Release the mutex
  m.ReleaseMutex();
  class=class="str">"cmt">// Return error text
  class="kw">return ans;
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// The method adds current parameter to the existing file or creates a new file with the current parameter
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="pathToBot">Relative path to the robot from the Experts folder</param>

◍ 把回测指标落盘到报告文件

做 EA 优化报告时,光在内存里算完指标没用,最终得把关键数据写进文件才能横向比对多组参数。下面这段公共方法就是干这个活的:传入机器人路径、入金币种、余额、杠杆和输出文件路径,内部按顺序拼装参数与核心系数。 方法先遍历 report.BotParams,把每一对键值通过 AppendBotParam 写进报告;随后一次性追加主系数块,覆盖 payoff、profit factor、平均 profit factor、recovery factor、平均 recovery factor、总交易次数、PL、DD 以及 Altman Z 分数共 10 项。外汇与贵金属回测中,Altman Z 低于 1.8 通常提示策略样本稳健性偏弱,属高风险信号。 风险价值部分用 AppendVaR 写入分位数 Q_90 / Q_95 / Q_99 再加均值 Mx 与标准差 Std,相当于给出了尾部亏损的量化边界。最后 AppendMaxPLDD 把最大 profit factor 对应的回撤、最大回撤及其各自交易笔数、最长连胜与连亏笔数一并落盘,方便你直接在 MT5 里打开文件核对哪组参数更抗揍。 让小布替你跑这套 把 pathToFile 指向一个 CSV,跑完优化直接 grep MaxDD 那行,能省掉手动翻 MT5 报表的功夫。

MQL5 / C++
class=class="str">"cmt">/// <param name="currency">Deposit currency</param>
class=class="str">"cmt">/// <param name="balance">Balance</param>
class=class="str">"cmt">/// <param name="leverage">Leverage</param>
class=class="str">"cmt">/// <param name="pathToFile">Path to file</param>
class="kw">public class="type">void WriteResult(class="type">class="kw">string pathToBot,
                      class="type">class="kw">string currency, class="type">class="kw">double balance,
                      class="type">int leverage, class="type">class="kw">string pathToFile)
{
   try
   {
      foreach(var param in report.BotParams)
      {
         ReportWriter.AppendBotParam(param.Key, param.Value);
      }
      ReportWriter.AppendMainCoef(GetResult(ReportManager.SortBy.Payoff),
                                  GetResult(ReportManager.SortBy.ProfitFactor),
                                  GetResult(ReportManager.SortBy.AverageProfitFactor),
                                  GetResult(ReportManager.SortBy.RecoveryFactor),
                                  GetResult(ReportManager.SortBy.AverageRecoveryFactor),
                                  (class="type">int)GetResult(ReportManager.SortBy.TotalTrades),
                                  GetResult(ReportManager.SortBy.PL),
                                  GetResult(ReportManager.SortBy.DD),
                                  GetResult(ReportManager.SortBy.AltmanZScore));
      ReportWriter.AppendVaR(GetResult(ReportManager.SortBy.Q_90), GetResult(ReportManager.SortBy.Q_95),
                             GetResult(ReportManager.SortBy.Q_99), GetResult(ReportManager.SortBy.Mx),
                             GetResult(ReportManager.SortBy.Std));
      ReportWriter.AppendMaxPLDD(GetResult(ReportManager.SortBy.ProfitFactor), GetResult(ReportManager.SortBy.MaxDD),
                                 (class="type">int)GetResult(ReportManager.SortBy.MaxProfitTotalTrades),
                                 (class="type">int)GetResult(ReportManager.SortBy.MaxDDTotalTrades),
                                 (class="type">int)GetResult(ReportManager.SortBy.MaxProfitConsecutivesTrades),
                                 (class="type">int)GetResult(ReportManager.SortBy.MaxDDConsecutivesTrades));
      foreach(var day in report.OptimisationCoefficients.TradingDays)
      {
         ReportWriter.AppendDay((class="type">int)day.Key, day.Value.Profit.Value, day.Value.Profit.Value,

「把优化结果落盘成 XML 报告」

在 MT5 策略测试器跑完参数优化后,最怕结果只停在内存里。下面这段静态方法把 IEnumerable<OptimisationResult> 直接序列化成带缩进的 XML 文件,方便你用外部脚本或小布盯盘做横向比对。 文件存在就先删再写,避免旧报告污染新跑批。XmlTextWriter 的 Formatting 设为 Indented、IndentChar 用制表符、Indentation=1,生成的文档层级一眼能看清,不用再开格式化工具。 根节点叫 Optimisation_Report,并写入 Created 属性(dd.MM.yyyy HH:mm:ss 本地时间)。Optimiser_Settings 区记录了 Bot 路径、Deposit(含 Currency 属性)、Leverage 三项,都是回测可复现的关键元数据。 Optimisation_Results 区对每个 item 写 Result 子节点,首行属性就是 Symbol。你复制这段代码到自己的报告类里,改一下 pathToFile,就能在每次优化后自动留痕,外汇与贵金属品种的高杠杆回测尤其该保留这类原始记录。

MQL5 / C++
class="kw">public class="kw">static class="type">void ReportWriter(this IEnumerable<OptimisationResult> results, class="type">class="kw">string pathToBot,
                      class="type">class="kw">string currency, class="type">class="kw">double balance,
                      class="type">int leverage, class="type">class="kw">string pathToFile)
{
  class=class="str">"cmt">// Delete the file if it exists
  if (File.Exists(pathToFile))
    File.Delete(pathToFile);
  class=class="str">"cmt">// Create writer 
  class="kw">using (var xmlWriter = new XmlTextWriter(pathToFile, null))
  {
    class=class="str">"cmt">// Set document format
    xmlWriter.Formatting = Formatting.Indented;
    xmlWriter.IndentChar = &class="macro">#x27;\t&class="macro">#x27;;
    xmlWriter.Indentation = class="num">1;
    xmlWriter.WriteStartDocument();
    class=class="str">"cmt">// The root node of the document
    xmlWriter.WriteStartElement("Optimisation_Report");
    class=class="str">"cmt">// Write attributes
    WriteAttribute(xmlWriter, "Created", DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));
    class=class="str">"cmt">// Write optimizer settings to file
    class="macro">#region Optimiser settings section
    xmlWriter.WriteStartElement("Optimiser_Settings");
    WriteItem(xmlWriter, "Bot", pathToBot); class=class="str">"cmt">// path to the robot
    WriteItem(xmlWriter, "Deposit", balance.ToString(), new Dictionary<class="type">class="kw">string, class="type">class="kw">string> { { "Currency", currency } }); class=class="str">"cmt">// Currency and deposit
    WriteItem(xmlWriter, "Leverage", leverage.ToString()); class=class="str">"cmt">// Leverage
    xmlWriter.WriteEndElement();
    class="macro">#endregion
    class=class="str">"cmt">// Write optimization results to the file
    class="macro">#region Optimisation result section
    xmlWriter.WriteStartElement("Optimisation_Results");
    class=class="str">"cmt">// Loop through optimization results
    foreach(var item in results)
    {
      class=class="str">"cmt">// Write specific result
      xmlWriter.WriteStartElement("Result");
      class=class="str">"cmt">// Write attributes of this optimization pass
      WriteAttribute(xmlWriter, "Symbol", item.report.Symbol); class=class="str">"cmt">// Symbol

把优化结果落进 XML 的写法

做 EA 批量优化后,把每一遍的参数与统计指标序列化到 XML,是后续用 AIGC 做横向筛选的前提。下面这段 C# 风格代码(MQL5 脚本里常用)演示了如何把一个优化项的时间框架、起止日期和结果块写进文件。 时间框架用 TF 属性记录,起止日期通过 DTToUnixDT() 转成 Unix 时间戳再存,避免时区歧义。结果块交给 WriteResultItem() 处理,外层用 WriteEndElement() 收口,最后 WriteEndDocument()Close() 保证文件完整。 在 WriteResultItem 里,VaR 区块固定输出 90/95/99 三分位加上均值 Mx 与标准差 Std——这三个分位对应外汇回测里常用的尾部风险观察窗。Max_PL_DD 区块则把最大利润、最大回撤、连胜连败次数都写成独立节点,方便小布这类工具直接按字段抓数。 实盘外汇与贵金属波动剧烈,这类统计仅基于历史优化区间,未来表现可能偏离,使用时须自担高风险。

MQL5 / C++
WriteAttribute(xmlWriter, "TF", item.report.TF.ToString()); class=class="str">"cmt">// Timeframe
WriteAttribute(xmlWriter, "Start_DT", item.report.DateBorders.From.DTToUnixDT().ToString()); class=class="str">"cmt">// Optimization start date
WriteAttribute(xmlWriter, "Finish_DT", item.report.DateBorders.Till.DTToUnixDT().ToString()); class=class="str">"cmt">// Optimization end date
class=class="str">"cmt">// Write optimization result
WriteResultItem(item, xmlWriter);
xmlWriter.WriteEndElement();
}
xmlWriter.WriteEndElement();
class="macro">#endregion
xmlWriter.WriteEndElement();
xmlWriter.WriteEndDocument();
xmlWriter.Close();
}
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Write a specific optimization pass
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="resultItem">Optimization pass value</param>
class=class="str">"cmt">/// <param name="writer">Writer</param>
class="kw">private class="kw">static class="type">void WriteResultItem(OptimisationResult resultItem, XmlTextWriter writer)
{
class=class="str">"cmt">// Write coefficients
class="macro">#region Coefficients
writer.WriteStartElement("Coefficients");
class=class="str">"cmt">// Write VaR
class="macro">#region VaR
writer.WriteStartElement("VaR");
WriteItem(writer, "class="num">90", resultItem.GetResult(SortBy.Q_90).ToString()); class=class="str">"cmt">// Quantile class="num">90
WriteItem(writer, "class="num">95", resultItem.GetResult(SortBy.Q_95).ToString()); class=class="str">"cmt">// Quantile class="num">95
WriteItem(writer, "class="num">99", resultItem.GetResult(SortBy.Q_99).ToString()); class=class="str">"cmt">// Quantile class="num">99
WriteItem(writer, "Mx", resultItem.GetResult(SortBy.Mx).ToString()); class=class="str">"cmt">// Average for PL
WriteItem(writer, "Std", resultItem.GetResult(SortBy.Std).ToString()); class=class="str">"cmt">// Standard deviation for PL
writer.WriteEndElement();
class="macro">#endregion
class=class="str">"cmt">// Write PL / DD parameters - extreme points
class="macro">#region Max PL DD
writer.WriteStartElement("Max_PL_DD");
WriteItem(writer, "Profit", resultItem.GetResult(SortBy.MaxProfit).ToString()); class=class="str">"cmt">// Total profit
WriteItem(writer, "DD", resultItem.GetResult(SortBy.MaxDD).ToString()); class=class="str">"cmt">// Total loss
WriteItem(writer, "Total Profit Trades", ((class="type">int)resultItem.GetResult(SortBy.MaxProfitTotalTrades)).ToString()); class=class="str">"cmt">// Total number of winning trades
WriteItem(writer, "Total Lose Trades", ((class="type">int)resultItem.GetResult(SortBy.MaxDDTotalTrades)).ToString()); class=class="str">"cmt">// Total number of losing trades
WriteItem(writer, "Consecutive Wins", ((class="type">int)resultItem.GetResult(SortBy.MaxProfitConsecutivesTrades)).ToString()); class=class="str">"cmt">// Winning trades in a row 
WriteItem(writer, "Consecutive Lose", ((class="type">int)resultItem.GetResult(SortBy.MaxDDConsecutivesTrades)).ToString()); class=class="str">"cmt">// Losing trades in a row
writer.WriteEndElement();
class="macro">#endregion
class=class="str">"cmt">// Write trading results by days
class="macro">#region Trading_Days
class=class="str">"cmt">// The method writing trading results

◍ 把星期维度写进回测报告

做 EA 回测统计时,光看总盈亏不够,按交易日拆分才能看出周一和周五的行为差异。下面这段逻辑就是把周一到周五的平均日盈利、平均日回撤、盈利笔数与亏损笔数分别落盘到 XML 节点里。 AddDay 函数接收星期缩写和四项数据,用 writer.WriteStartElement(Day) 开节点,再调 WriteItem 写四个子项,最后 WriteEndElement 收口。调用处对 Mn/Tu/We/Th/Fr 各跑一次,数据来自 resultItem.GetResult 按 SortBy 枚举取的日均指标。 除了星期维度,代码还顺手写了 Payoff、Profit factor、Recovery factor 等全局系数。注意 Total trades 用 (int) 强转后再 ToString,避免浮点格式污染报表。外汇与贵金属回测含高杠杆风险,这些系数仅描述历史样本,样本外表现可能偏离。 开 MT5 把这段嵌进你的报告生成器,先验证周五 Fr 节点的平均日回撤是否真高于周三 We,再决定要不要对周末前仓位做约束。

MQL5 / C++
class="type">void AddDay(class="type">class="kw">string Day, class="type">class="kw">double Profit, class="type">class="kw">double DD, class="type">int ProfitTrades, class="type">int DDTrades)
  {
    writer.WriteStartElement(Day);
    WriteItem(writer, "Profit", Profit.ToString()); class=class="str">"cmt">// Profits
    WriteItem(writer, "DD", DD.ToString()); class=class="str">"cmt">// Losses
    WriteItem(writer, "Number Of Profit Trades", ProfitTrades.ToString()); class=class="str">"cmt">// Number of profitable trades
    WriteItem(writer, "Number Of Lose Trades", DDTrades.ToString()); class=class="str">"cmt">// Number of losing trades
    writer.WriteEndElement();
  }
  writer.WriteStartElement("Trading_Days");
  class=class="str">"cmt">// Monday
  AddDay("Mn", resultItem.GetResult(SortBy.AverageDailyProfit_Mn),
         resultItem.GetResult(SortBy.AverageDailyDD_Mn),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Mn),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Mn));
  class=class="str">"cmt">// Tuesday
  AddDay("Tu", resultItem.GetResult(SortBy.AverageDailyProfit_Tu),
         resultItem.GetResult(SortBy.AverageDailyDD_Tu),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Tu),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Tu));
  class=class="str">"cmt">// Wednesday
  AddDay("We", resultItem.GetResult(SortBy.AverageDailyProfit_We),
         resultItem.GetResult(SortBy.AverageDailyDD_We),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_We),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyDDTrades_We));
  class=class="str">"cmt">// Thursday
  AddDay("Th", resultItem.GetResult(SortBy.AverageDailyProfit_Th),
         resultItem.GetResult(SortBy.AverageDailyDD_Th),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Th),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Th));
  class=class="str">"cmt">// Friday
  AddDay("Fr", resultItem.GetResult(SortBy.AverageDailyProfit_Fr),
         resultItem.GetResult(SortBy.AverageDailyDD_Fr),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Fr),
         (class="type">int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Fr));
  writer.WriteEndElement();
  class="macro">#endregion
  class=class="str">"cmt">// Write other coefficients
  WriteItem(writer, "Payoff", resultItem.GetResult(SortBy.Payoff).ToString());
  WriteItem(writer, "Profit factor", resultItem.GetResult(SortBy.ProfitFactor).ToString());
  WriteItem(writer, "Average Profit factor", resultItem.GetResult(SortBy.AverageProfitFactor).ToString());
  WriteItem(writer, "Recovery factor", resultItem.GetResult(SortBy.RecoveryFactor).ToString());
  WriteItem(writer, "Average Recovery factor", resultItem.GetResult(SortBy.AverageRecoveryFactor).ToString());
  WriteItem(writer, "Total trades", ((class="type">int)resultItem.GetResult(SortBy.TotalTrades)).ToString());

「把回测指标和机器人参数落盘」

这段片段负责把单次优化结果里的关键数值写进 XML 输出流,方便后续用脚本或「小布盯盘」做横向比对。前三行分别取出盈利(PL)、最大回撤(DD)和 Altman Z 分数——后者的意义在于用财务破产预警思路给策略稳健度打一个分,Z 低于 1.8 通常意味着样本内过拟合概率偏高。 随后用 #region Bot params 圈出一段循环,把 report.BotParams 里的键值对逐个通过 WriteItem 写出去。这意味着你在 MT5 策略测试器里自定义的输入参数(如均线周期、挂单距离)都会原样留痕,不必再回测完手动抄表。 实际用法:把这段代码接在你自己的 COptimizationResult 处理流程末尾,跑一轮多品种网格优化,就能直接拿到带参数和 Z 分的 CSV/XML,外汇与贵金属品种波动大、滑点敏感,这类留痕对排查「漂亮曲线」尤其有用,但历史分高不代表未来实盘能复制,杠杆市场仍属高风险。

MQL5 / C++
  WriteItem(writer, "PL", resultItem.GetResult(SortBy.PL).ToString());
  WriteItem(writer, "DD", resultItem.GetResult(SortBy.DD).ToString());
  WriteItem(writer, "Altman Z Score", resultItem.GetResult(SortBy.AltmanZScore).ToString());
  writer.WriteEndElement();
  class="macro">#endregion
  class=class="str">"cmt">// Write robot coefficients
  class="macro">#region Bot params
  foreach(var item in resultItem.report.BotParams)
  {
    WriteItem(writer, item.Key, item.Value);
  }
  class="macro">#endregion

把优化报告读进内存的读取器怎么写

做 MT5 批量优化后处理时,光跑完参数不够,得把 .xml 优化报告读出来才能筛结果。原文给出的做法是封装一个 ReportReader 类,从文件加载后只保留需要的字段,避免把整棵文档树长期挂在内存里。 这个类继承了 IDisposable,实例建议包在 using 块里。超出 using 作用域时自动调 Dispose 清掉 XmlDocument,实测能把单次读取后的托管内存占用压到最低,不会随报告数量堆积。 读取时用 IEnumerator 遍历 Optimization_Results 分区下带 <Result/> 的节点,双精度定界符强制设为 '.',绕开俄文/中文系统下逗号做小数点的本地化坑。ResetReader 负责把枚举器拨回头部,方便重复扫。 下面这段是类的骨架,几个关键字段已经标出:document 存整份报告、enumerator 管行游标、ReportItem 暴露当前行、优化器设置(机器人路径、余额、币种、杠杆)在构造时填好。

MQL5 / C++
<span class="keyword">class="kw">public</span> <span class="keyword">class</span> ReportReader : <span style="background-class="type">class="kw">color:rgb(class="num">204, class="num">204, class="num">204);">IDisposable</span>
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// Constructor</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;param name="path"&gt;Path to file&lt;/param&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">public</span> ReportReader(<span class="keyword">class="type">class="kw">string</span> path);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// Binary number format provider</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span><span style="background-class="type">class="kw">color:rgb(class="num">222, class="num">188, class="num">133);">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">private</span> readonly NumberFormatInfo formatInfo = <span class="keyword">new</span> NumberFormatInfo { NumberDecimalSeparator = <span class="class="type">class="kw">string">"."</span> };</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="preprocessor">class="macro">#region </span>DataKeepers
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// Presenting the report file in OOP format</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="background-class="type">class="kw">color:rgb(class="num">232, class="num">183, class="num">215);"><span class="keyword">class="kw">private</span> readonly XmlDocument document = <span class="keyword">new</span> XmlDocument();</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// Collection of document nodes(rows in excel table)</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="background-class="type">class="kw">color:rgb(class="num">164, class="num">192, class="num">228);"><span class="keyword">class="kw">private</span> readonly System.Collections.IEnumerator enumerator;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="preprocessor">class="macro">#endregion
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// The read current report item</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);"><span class="keyword">class="kw">public</span> ReportItem? ReportItem { get; <span class="keyword">class="kw">private</span> set; } = null;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">246, class="num">200);"><span class="preprocessor">class="macro">#region </span>Optimiser settings
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// Path to the robot</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">public</span> <span class="keyword">class="type">class="kw">string</span> RelativePathToBot { get; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// Balance</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">public</span> <span class="keyword">class="type">class="kw">double</span> Balance { get; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// Currency</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">public</span> <span class="keyword">class="type">class="kw">string</span> Currency { get; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// Leverage</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">/// &lt;/summary&gt;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">public</span> <span class="keyword">class="type">int</span> Leverage { get; }

◍ 从优化报告 XML 里抠出交易设定

MT5 的优化报告本质是 XML,ReportReader 构造函数直接把文件 load 进内存,再按节点路径取参数。Created 字段用 dd.MM.yyyy HH:mm:ss 格式解析,例如 21.03.2024 14:32:07,这一步若格式不对会抛异常,开 MT5 跑完优化后最好手动核对报告头时间。 读取存款与杠杆时,代码从 Optimiser_Settings 下按 Name 属性定位:Balance 用 Replace(",", ".") 处理小数逗号,Leverage 转 Int32。若你的终端语言环境把小数点写成逗号,这处替换就是必须的,否则 Convert.ToDouble 会失败。 Dispose() 只做 document.RemoveAll() 清空节点,ResetReader() 负责把枚举器归位。想在 EA 里复用报告解析,直接抄这段构造逻辑,比自己写正则稳。外汇与贵金属品种波动剧烈,杠杆参数只是历史记录,实盘放大倍数需自行评估高风险。

MQL5 / C++
class="kw">public ReportReader(class="type">class="kw">string path)
{
  class=class="str">"cmt">// load the document
  document.Load(path);
  class=class="str">"cmt">// Get file creation date
  Created = DateTime.ParseExact(document["Optimisation_Report"].Attributes["Created"].Value, "dd.MM.yyyy HH:mm:ss", null);
  class=class="str">"cmt">// Get enumerator
  enumerator = document["Optimisation_Report"]["Optimisation_Results"].ChildNodes.GetEnumerator();
  class=class="str">"cmt">// Parameter receiving function
  class="type">class="kw">string xpath(class="type">class="kw">string Name) { class="kw">return $"/Optimisation_Report/Optimiser_Settings/Item[@Name=&class="macro">#x27;{Name}&class="macro">#x27;]"; }
  class=class="str">"cmt">// Get path to the robot
  RelativePathToBot = document.SelectSingleNode(xpath("Bot")).InnerText;
  class=class="str">"cmt">// Get balance and deposit currency
  XmlNode Deposit = document.SelectSingleNode(xpath("Deposit"));
  Balance = Convert.ToDouble(Deposit.InnerText.Replace(",", "."), formatInfo);
  Currency = Deposit.Attributes["Currency"].Value;
  class=class="str">"cmt">// Get leverage
  Leverage = Convert.ToInt32(document.SelectSingleNode(xpath("Leverage")).InnerText);
}

「遍历 XML 报告节点填充机器人参数」

在 MQL5 里用枚举器逐节点读取回测报告时,先判断 MoveNext() 是否成功,才能安全访问 Current。若返回 true,说明当前 XmlNode 有效,可继续向下解析子节点。 对 result.ChildNodes 做 foreach 遍历,只处理 Name 为 "Item" 的节点,把它的 Name 属性和 InnerText 塞进 BotParams 字典。这样一套报告里的每一条参数都能被程序化提取,不用手工抄。 最后函数返回 ans,即是否还有后续节点。开 MT5 把这段接进你的报告解析类,跑一次多品种回测,看 BotParams 数量是否和 XML 里 Item 节点数一致(例如 12 个参数项应得 12 条记录)。外汇与贵金属回测含跳空与滑点,结果仅作概率参考,实盘前务必小仓验证。

MQL5 / C++
class="kw">return class="kw">false;
class=class="str">"cmt">// Read the next item
class="type">bool ans = enumerator.MoveNext();
if (ans)
{
   class=class="str">"cmt">// Current node
   XmlNode result = (XmlNode)enumerator.Current;
   class=class="str">"cmt">// current report item
   ReportItem = new ReportItem[...]
   class=class="str">"cmt">// Fill the robot parameters
   foreach(XmlNode item in result.ChildNodes)
   {
      if (item.Name == "Item")
         ReportItem.Value.BotParams.Add(item.Attributes["Name"].Value, item.InnerText);
   }
}
class="kw">return ans;

多系数归一化排序怎么落地

做优化报告后处理时,单条件过滤好写,多因子排序才是坑。若依次对盈利因子、回报因子各排一次序,最终只剩最后一个参数的顺序,前面的排序会被覆盖。 原文给了一组样本:盈利因子取值 0~2,回报因子取值 -2~9,跨度宽且含负值。直接除以最大值归一化会失效,因为回报因子有负。处理办法是把整列按负数值取模平移,使最小变成 0,再除以新最大值,得到 [0;1] 区间内的归一化值,顺序不变。 归一化后按权重合计,权重 = 1/n(n 为因子数)。例如两因子时各取 0.5 加权,得到可排序的合计列。若某因子要求降序,用 1 减去其归一值即可交换极大极小。 代码层把过滤做成 IEnumerable 扩展。Compare 函数借 int 标志位(最多 8 个)顺序判断大于、小于、等于,避免堆砌 if-else;Sort 函数遍历传入的所有标志,调用 GetResult 取参,不符即舍。 只排一个参数时不需归一化,直接返回;多参数才建最大/最小值字典加速,再走双循环做加权合计,最后丢给 List.OrderBy 或 OrderByDescending。降序委托可自定义,留默认就走内置。 别把正态当圣经 回报因子出现负值是常态,硬套“除以最大值”会直接废掉整列排序,先平移再归一才是稳的。 让小布替你跑这套 把下面枚举和过滤骨架贴进 MT5 的 C# 优化后处理脚本,改两个因子名就能验证加权排序是否符合直觉,外汇与贵金属优化结果波动大,高风险,仅作方法验证。

MQL5 / C++
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Filtering type
class=class="str">"cmt">/// </summary>
[Flags]
class="kw">public enum CompareType
{
    GraterThan = class="num">1, class=class="str">"cmt">// greater than
    LessThan = class="num">2, class=class="str">"cmt">// less than
    EqualTo = class="num">4 class=class="str">"cmt">// equal
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Optimization filtering method
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="results">Current collection</param>
class=class="str">"cmt">/// <param name="compareData">Collection of coefficients and filtering types</param>
class=class="str">"cmt">/// <returns>Filtered collection</returns>
class="kw">public class="kw">static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,
                                            IDictionary<SortBy, KeyValuePair<CompareType, class="type">class="kw">double>> compareData)
{
    class=class="str">"cmt">// Result sorting function
    class="type">bool Compare(class="type">class="kw">double _data, KeyValuePair<CompareType, class="type">class="kw">double> compareParams)
    {
        class=class="str">"cmt">// Comparison result
        class="type">bool ans = class="kw">false;
        class=class="str">"cmt">// Comparison for equality
        if (compareParams.Key.HasFlag(CompareType.EqualTo))
        {
            ans = compareParams.Value == _data;
        }
        class=class="str">"cmt">// Comparison for &class="macro">#x27;greater than current&class="macro">#x27;
        if (!ans && compareParams.Key.HasFlag(CompareType.GraterThan))
        {
            ans = _data > compareParams.Value;
        }
        class=class="str">"cmt">// Comparison for &class="macro">#x27;less than current&class="macro">#x27;
        if (!ans && compareParams.Key.HasFlag(CompareType.LessThan))
        {
            ans = _data < compareParams.Value;
        }
        class="kw">return ans;
    }
    class=class="str">"cmt">// Sorting condition
    class="type">bool Sort(OptimisationResult x)
    {

◍ 多参数优化结果的归一化边界处理

在 MT5 优化结果后处理里,当传入的排序标志多于一个时,不能直接用 OrderBy 排,因为不同系数量纲不同(例如盈利因子可能在 1.0~3.0,而回撤百分比在 0~40),直接加权会失真。 代码先对 sortingFlags 去重,若数量为 0 直接返回 null;若只有 1 个标志,就按升序或降序走简单的 OrderBy / OrderByDescending。 多标志时,先建一个 Dictionary<SortBy, MinMax> 边界表,把每个标志对应的最大最小值初始化为 double.MinValue 和 double.MaxValue,再遍历全部 results 用 Math.Max / Math.Min 刷新。这套边界后面用来做归一化,权重求和才合理。 外汇与贵金属 EA 回测优化高风险,归一化只是让排序可比,不表示某组参数实盘就倾向盈利。

MQL5 / C++
foreach(var item in compareData)
{
  class=class="str">"cmt">// Compare the passed parameter with the current one
  if (!Compare(x.GetResult(item.Key), item.Value))
    class="kw">return class="kw">false;
}
class="kw">return true;
}
class=class="str">"cmt">// Filtering
class="kw">return results.Where(x => Sort(x));
}
class="kw">public class="kw">static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,
                                                            OrderBy order, IEnumerable<SortBy> sortingFlags,
                                                            Func<SortBy, SortMethod> sortMethod = null)
{
  class=class="str">"cmt">// Get the unique list of flags for sorting
  sortingFlags = sortingFlags.Distinct();
  class=class="str">"cmt">// Check flags
  if (sortingFlags.Count() == class="num">0)
    class="kw">return null;
  class=class="str">"cmt">// If there is one flag, sort by this parameter
  if (sortingFlags.Count() == class="num">1)
  {
    if (order == OrderBy.Ascending)
      class="kw">return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(class="num">0)));
    else
      class="kw">return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(class="num">0)));
  }
  class=class="str">"cmt">// Form minimum and maximum boundaries according to the passed optimization flags
  Dictionary<SortBy, MinMax> Borders = sortingFlags.ToDictionary(x => x, x => new MinMax { Max = class="type">class="kw">double.MinValue, Min = class="type">class="kw">double.MaxValue });
  class="macro">#region create Borders min max dictionary
  class=class="str">"cmt">// Loop through the list of optimization passes
  for (class="type">int i = class="num">0; i < results.Count(); i++)
  {
    class=class="str">"cmt">// Loop through sorting flags
    foreach(var item in sortingFlags)
    {
      class=class="str">"cmt">// Get the value of the current coefficient
      class="type">class="kw">double value = results.ElementAt(i).GetResult(item);
      MinMax mm = Borders[item];
      class=class="str">"cmt">// Set the minimum and maximum values
      mm.Max = Math.Max(mm.Max, value);
      mm.Min = Math.Min(mm.Min, value);
      Borders[item] = mm;
    }
  }
  class="macro">#endregion
  class=class="str">"cmt">// The weight of the weighted sum of normalized coefficients

「把多目标优化结果归一化再排序」

做参数寻优时最头疼的是多个指标量纲不同:有的越大越好,有的越小越好,直接比没意义。下面这段逻辑先把每个边界区间归一化到 0~1,再按指定方向折成统一打分,最后整体排序。 核心在前面的 coef = 1.0 / Borders.Count,它把总权重均摊到每个优化目标上。若某个指标最小值小于 0,代码会把整段数据平移掉负轴,避免归一化出现负值扭曲。 排序方向由 SortMethod 决定:递减目标用 (1 - value/mm.Max) 让高分靠近 0 权重小、低分权重高;递增目标则直接用 value/mm.Max。两者乘 coef 后累加进 data.SortBy,形成单值可比分数。 末尾按 order 参数走 OrderBy 或 OrderByDescending,返回的已是按综合分排好的序列。在 MT5 里接这套,能把几十组 EA 回测结果按你定的偏好压成一张表,外汇与贵金属品种波动大、滑点敏感,回测结论仅作概率参考,实盘前务必小仓位验证。

MQL5 / C++
class="type">class="kw">double coef = (class="num">1.0 / Borders.Count);
class=class="str">"cmt">// Convert the list of optimization results to the List type array
class=class="str">"cmt">// Since it is faster to work with
List<OptimisationResult> listOfResults = results.ToList();
class=class="str">"cmt">// Loop through optimization results
for (class="type">int i = class="num">0; i < listOfResults.Count; i++)
{
  class=class="str">"cmt">// Assign value to the current coefficient
  OptimisationResult data = listOfResults[i];
  class=class="str">"cmt">// Zero the current sorting factor
  data.SortBy = class="num">0;
  class=class="str">"cmt">// Loop through the formed maximum and minimum borders
  foreach(var item in Borders)
  {
    class=class="str">"cmt">// Get the current result value
    class="type">class="kw">double value = listOfResults[i].GetResult(item.Key);
    MinMax mm = item.Value;
    class=class="str">"cmt">// If the minimum is below zero, shift all data by the negative minimum value
    if (mm.Min < class="num">0)
    {
      value += Math.Abs(mm.Min);
      mm.Max += Math.Abs(mm.Min);
    }
    class=class="str">"cmt">// If the maximum is greater than zero, calculate
    if (mm.Max > class="num">0)
    {
      class=class="str">"cmt">// Calculate the coefficient according to the sorting method
      if ((sortMethod == null ? GetSortMethod(item.Key) : sortMethod(item.Key)) == SortMethod.Decreasing)
      {
        class=class="str">"cmt">// Calculate the coefficient to sort in descending order
        data.SortBy += (class="num">1 - value / mm.Max) * coef;
      }
      else
      {
        class=class="str">"cmt">// Calculate the coefficient to sort in ascending order
        data.SortBy += value / mm.Max * coef;
      }
    }
  }
  class=class="str">"cmt">// Replace the value of the current coefficient with the sorting parameter
  listOfResults[i] = data;
}
class=class="str">"cmt">// Sort according to the passed sorting type
if (order == OrderBy.Ascending)
  class="kw">return listOfResults.OrderBy(x => x.SortBy);
else
  class="kw">return listOfResults.OrderByDescending(x => x.SortBy);

多因素排序的边界与下一步

这套机制已经把自定义 xml 的读写、C# 集合扩展的排序过滤都跑通了,核心是多因素排序——标准终端测试器给不了这种按系数序列排结果的能力。 但它有个硬边界:只能在同一个系列里比,不同时间间隔各自用独立系数,加权合计没法横向比。做跨区间择优时得自己先归一化再判。 下一篇会转到算法转换,把自动优化器的逻辑接进来。外汇和贵金属波动大、杠杆高,这类工程化工具只解决筛选效率,不替你扛回撤风险。

常见问题

在回测框架里对优化结果对象做相等性判定,比对参数组合与统计指标是否完全一致,一致即视为同一结果,可避免重复落盘。
用 ReportWriter 类把优化结果对象序列化成 XML 节点,逐字段写入回测配置、统计接口数据与结果树,最后统一落盘。
小布可读取你导出的优化 XML 并做横向对比、异常标注和参数排序,你只需把文件丢进对应品种页。
调用回测统计接口的取值方法,把每个指标作为子节点挂到报告树对应位置,再随整体结构一起写入 XML。
把回测配置单独作为一个 XML 节点块,在写入结果树前先落盘品种、周期、点差等基础设置,防止后期对不上。