继续迈进优化(第一部分):操控优化报告·进阶篇
(2/3)· 当滑动优化跑起来,手工翻报告只会漏掉稳健性信号,本篇给出第三方进程接管方案
回测摘要里的零成交与盈利因子
一段典型的 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 可能触发保证金告警,属高风险操作,须重做品种适配。
<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# 工程看字段布局。
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 跑一遍边界用例。
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 数量等于历史段数,外汇与贵金属回测属高风险,结果仅作概率参考。
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 + 1 并 break,保证每段历史只配一次前方样本,避免重复覆盖。
OptimisationResult 类通过隐式转换把 ReportItem 直接包成结果对象(SortBy 默认 0),又用显式转换拆回原始报告,调用时不用手写字段映射。
相等运算符被重载:逐个比对 BotParams 的键值,只要缺键或值不等就返回 false,否则 true;不等运算符直接取反。这套重载让两个优化结果能直接用 == 判断参数集是否一致,回测筛选时少写不少判断代码。
外汇与贵金属 EA 做 walk-forward 验证时,这类结构容易因样本区间重叠导致过拟合错觉,建议把 Forward[j].From < item.Till 的边界判断在 MT5 策略测试器里单独打印日志核对。
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>