继续迈进优化(第一部分):操控优化报告·进阶篇
🧰

继续迈进优化(第一部分):操控优化报告·进阶篇

(2/3)· 当滑动优化跑起来,手工翻报告只会漏掉稳健性信号,本篇给出第三方进程接管方案

案例拆解 第 2/3 篇
把优化结果直接当成交割单看,是多数人的习惯误区。MT5 自带报告字段有限,滑动优化里跨时段重叠的稳健性差异,靠肉眼比对基本抓不住。用结构化 XML 接管报告,才能把过滤和排序交给程序。

回测摘要里的零成交与盈利因子

一段典型的 MT5 策略测试摘要里,Trading_Days 节点下的单日记录可能全是零:DD、盈利笔数、亏损笔数三项均为 0,说明该交易日没有触发任何订单,外汇与贵金属品种在窄幅震荡时容易出现这种空白样本。 跳出逐日明细看整体,Payoff 与 Profit factor 同值 1.66020714285714,Average Profit factor 仅 0.830103571428571,Recovery factor 0.660207142857143 而平均恢复因子为负 -0.169896428571429。Total trades 只有 2 笔,PL 为 415.9305,DD 为 -630。 两个信号值得盯:总交易次数过低时,盈利因子再好看也可能是小样本偶然;平均恢复因子转负,意味着拉长周期后回撤修复能力倾向走弱。开 MT5 把这段 XML 摘要喂进自己的解析脚本,核对 2 笔交易对应的具体开平仓时间,比只看汇总数字更靠谱。外汇与贵金属杠杆高,这类低样本回测结论仅作参考,实盘风险显著。

◍ 回测结果里的参数落点

上面这段 XML 是 MT5 优化器导出的结果片段,记录了某次遗传优化后选中的一组参数组合。注意 Altman Z Score 系数为 0,说明该因子在这轮股票标的筛选里没被赋予权重,模型更依赖其他变量。 具体成交设定上:手数 _lot_ 为 1,标的 USymbol 是 SBER(俄储蓄银行),点差上限 Spread_in_percent 设为 3.0%,未开自动杠杆(UseAutoLevle=false),单标的最大持仓占比 max_per 为 174(百分比单位,可能对应跨合约合计敞口)。 手续费与滑点分开按品种给:股票侧 comission_stock=0.05、shift_stock=0;期货侧 comission_fut=4.0、shift_fut=0。把这些值直接抄进 EA 输入参数,可在 MT5 策略测试器里复现同一组优化解,验证其样本内表现是否稳健。外汇与贵金属品种点值结构不同,直接套用 max_per=174 可能触发保证金告警,属高风险操作,须重做品种适配。

MQL5 / C++
<Item Name="Altman Z Score">class="num">0</Item>
</Coefficients>
  <Item Name="_lot_">class="num">1</Item>
  <Item Name="USymbol">SBER</Item>
  <Item Name="Spread_in_percent">class="num">3.00000000</Item>
  <Item Name="UseAutoLevle">class="kw">false</Item>
  <Item Name="max_per">class="num">174</Item>
  <Item Name="comission_stock">class="num">0.05000000</Item>
  <Item Name="shift_stock">class="num">0.00000000</Item>
  <Item Name="comission_fut">class="num">4.00000000</Item>
  <Item Name="shift_fut">class="num">0.00000000</Item>
</Result>
</Optimisation_Results>
</Optimisation_Report>

「给优化日期范围写一套能排序去重的类」

滚动优化要把历史回测和正向测试的时间段配对,前提是日期范围本身能比较、能排序、能去重。原文把这段逻辑收进一个 DateBorders 类,并让它在 C# 里继承 IComparable,重载 ==、!=、>、<、>=、<= 这一组运算符。 相等判断先处理 null:用 is 关键字探空,否则直接比 From 和 Till 会抛 null reference exception。大于小于的逻辑是——若任一参数为 null 返回 False;否则先比第一个间隔,匹配再比第二个;开始日期相等时看结束日期,更古老的区间算“大于”。 光重载运算符不够。Visual Studio 编译时会提示你还要覆写 Equals、ToString、GetHashCode。关键点在于:不覆写 GetHashCode 时,两个 From、Till 完全相同的实例哈希码不同,IEnumerable.Distinct 就会误判为两条不同区间。ToString 输出 "From-Till" 字符串,GetHashCode 返回该字符串哈希码,行为就对齐了 String 类型,去重才稳。 配对历史与正向间隔用了一个静态方法:先按升序排好传入区间,再双层循环。历史循环开头把历史范围写进结果集,正向游标从第 i 个起跳,避免重复扫已处理项。表格里给的反例很直白——历史结束 2017.03.09,正向却从 2016.12.12 开始,这种错位直接跳过。最终字典里首条正向被丢、末条历史无配对得 null,程序据此知道哪段历史该接哪段正向参数。 ReportItem 外面套了一层包装器,多带一个用于多因子排序的字段(如利润+回报率合成单值)。它支持 ReportItem 到包装器的隐式转换、包装器到交易报告元素的显式转换,比手填字段省事。相等判定看机器人名和参数值,写文件时存在就追加。底下这段结构定义可直接粘进 C# 工程看字段布局。

MQL5 / C++
class="kw">public class="kw">struct ReportItem
{
    class="kw">public Dictionary<class="type">class="kw">string, class="type">class="kw">string> BotParams; class=class="str">"cmt">// List of robot parameters
    class="kw">public Coefficients OptimisationCoefficients; class=class="str">"cmt">// Robot coefficients
    class="kw">public class="type">class="kw">string Symbol; class=class="str">"cmt">// Symbol
    class="kw">public class="type">int TF; class=class="str">"cmt">// Timeframe
    class="kw">public DateBorders DateBorders; class=class="str">"cmt">// Date range
}
class="kw">public Dictionary<DayOfWeek, DailyData> TradingDays;
class="kw">public class DateBorders : IComparable
{
    class=class="str">"cmt">/// <summary>
    class=class="str">"cmt">/// Constructor
    class=class="str">"cmt">/// </summary>
    class=class="str">"cmt">/// <param name="from">Range beginning date</param>
    class=class="str">"cmt">/// <param name="till">Range ending date</param>
    class="kw">public DateBorders(DateTime from, DateTime till)
    {
        if (till <= from)
            class="kw">throw new ArgumentException("Date &class="macro">#x27;Till&class="macro">#x27; is less or equal to date &class="macro">#x27;From&class="macro">#x27;");
        From = from;
        Till = till;
    }
    class=class="str">"cmt">/// <summary>
    class=class="str">"cmt">/// From
    class=class="str">"cmt">/// </summary>
    class="kw">public DateTime From { get; }
    class=class="str">"cmt">/// <summary>
    class=class="str">"cmt">/// To
    class=class="str">"cmt">/// </summary>

给时间区间写一套比较算子

在 MT5 里做时段过滤(比如只跑伦敦盘 08:00–17:00 的信号),常会封装一个 DateBorders 结构存 From/Till。光有字段不够,排序和去重得靠重载运算符才顺手。 相等判断先处理 null:两个都是 null 视为相等,只有一个为 null 直接不等,否则比 From 和 Till 是否双双相同。不等运算符直接复用 == 取反,一行表达式就够了。 大于和小于的逻辑是「先比起点,起点相同再比终点」:任一为 null 返回 false,避免空引用崩在回测初始化阶段;From 相等时看 Till 谁更晚。 大于等于、小于等于就是 == 配合 > 或 < 的短路组合。把这截代码贴进 EA 的 DateBorders 类,你就能直接写 if(sessionA < sessionB) 来做时段优先级判定,外汇与贵金属波动受时段切换影响明显,这类边界比较出错容易在重大数据日发布重复信号,属高风险环节,建议先在策略测试器用 2023 年全年的 EURUSD 1H 跑一遍边界用例。

MQL5 / C++
class="kw">public class="kw">static class="type">bool class="kw">operator ==(DateBorders b1, DateBorders b2)
{
  class="type">bool ans;
  if (b2 is null && b1 is null) ans = true;
  else if (b2 is null || b1 is null) ans = class="kw">false;
  else ans = b1.From == b2.From && b1.Till == b2.Till;
  class="kw">return ans;
}
class="kw">public class="kw">static class="type">bool class="kw">operator !=(DateBorders b1, DateBorders b2) => !(b1 == b2);
class="kw">public class="kw">static class="type">bool class="kw">operator >(DateBorders b1, DateBorders b2)
{
  if (b1 == null || b2 == null)
    class="kw">return class="kw">false;
  if (b1.From == b2.From)
    class="kw">return (b1.Till > b2.Till);
  else
    class="kw">return (b1.From > b2.From);
}
class="kw">public class="kw">static class="type">bool class="kw">operator <(DateBorders b1, DateBorders b2)
{
  if (b1 == null || b2 == null)
    class="kw">return class="kw">false;
  if (b1.From == b2.From)
    class="kw">return (b1.Till < b2.Till);
  else
    class="kw">return (b1.From < b2.From);
}
class="kw">public class="kw">static class="type">bool class="kw">operator >=(DateBorders b1, DateBorders b2) => (b1 == b2 || b1 > b2);
class="kw">public class="kw">static class="type">bool class="kw">operator <=(DateBorders b1, DateBorders b2) => (b1 == b2 || b1 < b2);

◍ 给日期区间类补上比较与配对逻辑

在 MT5 里写自定义回测区间类 DateBorders 时,若想直接扔进 List 做排序或字典键,得先覆写 object 的三个基础方法。Equals 里先判类型再走 == 重载,避免基类按引用比较导致相同区间被判不等。 GetHashCode 直接吃 ToString 的哈希,而 ToString 返回 "From-Till" 这样的字符串。这意味着两个区间只要起止时间一致,哈希就一致,字典寻址才不会漏。 [CODE]…[/CODE] CompareTo 是给 Sort 用的:空对象排最后,自身与传入相等返 0,小于返 -1,大于返 1;若传入不是 DateBorders 直接抛 ArgumentException。 CompareHistoryToForward 是实战落点:先对 History、Forward 两个列表各 Sort(),再遍历历史区间,用字典把历史区间映射 forward 区间。若 ans 已含该 key 就 continue 跳过,否则加进字典;当 Forward 元素不够时(Forward.Count <= i)对应值为 null。开 MT5 把这段贴进 EA 工具类,跑一遍 2023.01-2023.06 与 2023.07-2023.12 两组区间,能看到字典 key 数量等于历史段数,外汇与贵金属回测属高风险,结果仅作概率参考。

MQL5 / C++
class="macro">#region class="kw">override base methods(from object)
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Overloading of equality comparison
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="obj">Element to compare to</param>
class=class="str">"cmt">/// <returns></returns>
class="kw">public class="kw">override class="type">bool Equals(object obj)
{
    if (obj is DateBorders other)
        class="kw">return this == other;
    else
        class="kw">return base.Equals(obj);
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Cast the class to a class="type">class="kw">string and class="kw">return its hash code
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <returns>String hash code</returns>
class="kw">public class="kw">override class="type">int GetHashCode()
{
    class="kw">return ToString().GetHashCode();
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Convert the current class to a class="type">class="kw">string
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <returns>String From date - To date</returns>
class="kw">public class="kw">override class="type">class="kw">string ToString()
{
    class="kw">return $"{From}-{Till}";
}
class="macro">#endregion
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Compare the current element with the passed one
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="obj"></param>
class=class="str">"cmt">/// <returns></returns>
class="kw">public class="type">int CompareTo(object obj)
{
    if (obj == null) class="kw">return class="num">1;
    if (obj is DateBorders borders)
    {
        if (this == borders)
            class="kw">return class="num">0;
        else if (this < borders)
            class="kw">return -class="num">1;
        else
            class="kw">return class="num">1;
    }
    else
    {
        class="kw">throw new ArgumentException("object is not DateBorders");
    }
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Method for comparing forward and historical optimizations
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="History">Array of historical optimization</param>
class=class="str">"cmt">/// <param name="Forward">Array of forward optimizations</param>
class=class="str">"cmt">/// <returns>Sorted list historical - forward optimization</returns>
class="kw">public class="kw">static Dictionary<DateBorders, DateBorders> CompareHistoryToForward(List<DateBorders> History, List<DateBorders> Forward)
{
    class=class="str">"cmt">// array of comparable optimizations
    Dictionary<DateBorders, DateBorders> ans = new Dictionary<DateBorders, DateBorders>();
    class=class="str">"cmt">// Sort the passed parameters
    History.Sort();
    Forward.Sort();
    class=class="str">"cmt">// Create a historical optimization loop
    class="type">int i = class="num">0;
    foreach(var item in History)
    {
        if(ans.ContainsKey(item))
            class="kw">continue;
        ans.Add(item, null); class=class="str">"cmt">// Add historical optimization
        if (Forward.Count <= i)

「结果配对与类型转换的收口逻辑」

前方优化循环跑完后,代码用 ans[item] = Forward[j] 把历史优化项与对应的前方验证结果绑定,同时把游标 i 推到 j + 1break,保证每段历史只配一次前方样本,避免重复覆盖。 OptimisationResult 类通过隐式转换把 ReportItem 直接包成结果对象(SortBy 默认 0),又用显式转换拆回原始报告,调用时不用手写字段映射。 相等运算符被重载:逐个比对 BotParams 的键值,只要缺键或值不等就返回 false,否则 true;不等运算符直接取反。这套重载让两个优化结果能直接用 == 判断参数集是否一致,回测筛选时少写不少判断代码。 外汇与贵金属 EA 做 walk-forward 验证时,这类结构容易因样本区间重叠导致过拟合错觉,建议把 Forward[j].From < item.Till 的边界判断在 MT5 策略测试器里单独打印日志核对。

MQL5 / C++
class="kw">continue; class=class="str">"cmt">// If the array of forward optimization is less than the index, class="kw">continue the loop
class=class="str">"cmt">// Forward optimization loop
for (class="type">int j = i; j < Forward.Count; j++)
{
  class=class="str">"cmt">// If the current forward optimization is contained in the results array, skip
  if (ans.ContainsValue(Forward[j]) ||
  Forward[j].From < item.Till)
  {
    class="kw">continue;
  }
  class=class="str">"cmt">// Compare forward and historical optimization
  ans[item] = Forward[j];
  i = j + class="num">1;
  class="kw">break;
}
class="kw">return ans;
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Optimization pass report
class=class="str">"cmt">/// </summary>
class="kw">public ReportItem report;
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Sorting factor
class=class="str">"cmt">/// </summary>
class="kw">public class="type">class="kw">double SortBy;
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// The class="kw">operator of implicit type conversion from optimization pass to the current type
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="item">Optimization pass report</param>
class="kw">public class="kw">static implicit class="kw">operator OptimisationResult(ReportItem item)
{
  class="kw">return new OptimisationResult { report = item, SortBy = class="num">0 };
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// The class="kw">operator of explicit type conversion from current to the optimization pass structure
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="optimisationResult">current type</param>
class="kw">public class="kw">static explicit class="kw">operator ReportItem(OptimisationResult optimisationResult)
{
  class="kw">return optimisationResult.report;
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Overloading of the equality comparison class="kw">operator
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="result1">Parameter class="num">1 to compare</param>
class=class="str">"cmt">/// <param name="result2">Parameter class="num">2 to compare</param>
class=class="str">"cmt">/// <returns>Comparison result</returns>
class="kw">public class="kw">static class="type">bool class="kw">operator ==(OptimisationResult result1, OptimisationResult result2)
{
  foreach(var item in result1.report.BotParams)
  {
    if (!result2.report.BotParams.ContainsKey(item.Key))
      class="kw">return class="kw">false;
    if (result2.report.BotParams[item.Key] != item.Value)
      class="kw">return class="kw">false;
  }
  class="kw">return true;
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Overloading of the inequality comparison class="kw">operator
class=class="str">"cmt">/// </summary>
class=class="str">"cmt">/// <param name="result1">Parameter class="num">1 to compare</param>
class=class="str">"cmt">/// <param name="result2">Parameter class="num">2 to compare</param>
class=class="str">"cmt">/// <returns>Comparison result</returns>
class="kw">public class="kw">static class="type">bool class="kw">operator !=(OptimisationResult result1, OptimisationResult result2)
{
  class="kw">return !(result1 == result2);
}
class=class="str">"cmt">/// <summary>
class=class="str">"cmt">/// Overloading of the basic type comparison class="kw">operator
class=class="str">"cmt">/// </summary>
把报告解析交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到跨时段优化结果的重叠区与衰减提示,你只需判读方向。

常见问题

标准报告字段少于回测栏自生成报告,且不支持自定义数据模块,难以在多个平移时段内做统一过滤与排序,程序化读取也受限。
目前小布盯盘的 AIGC 诊断内置了常见优化结果结构解析,若你用本篇的 DLL 包装器导出 XML,对应品种页可自动映射重叠优化区与系数衰减。
存款货币写入 Currency 属性仅作标识,过滤逻辑基于 Item 模块的 Name 与数值,货币本身不参与排序,但跨币种混合报告需先分组。
DLL 可被 MQL5 与 C# 双向调用,避免进程间通信开销,也让下一篇的 MQL5 抓取代码能直接复用同一套解析函数库。