连续前行优化 (第八部分): 程序改进和修复·进阶篇
(2/3)·旧版分阶段手填日期拖慢回测节奏,新内核弃用互斥体后速度翻数倍
EA 里把回测框架接进交易类
在 MT5 写多品种回测 EA 时,先把 CAutoUploader 这个类挂进来,它负责在 OnInit 之外独立管理历史读取与参数拷贝。类里给了两个构造函数重载:一个吃自定义过滤回调 TCustomFilter,另一个不吃,默认用 EmptyCustomCoefCallback 返回 0,两者都把 From、Till 初始化为 0。 下面是类尾部与构造实现里最该盯的两段。空回调只是占位,真正过滤逻辑后面再注入;而 CopyBotParams 在构造时就完成,意味着你改 _params[] 必须赶在 new CAutoUploader 之前。 [CODE] double EmptyCustomCoefCallback() {return 0;} CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[], TCustomFilter filter) : comission_manager(_comission_manager), mutexName(_mutexName), From(0), Till(0), custom_filter(filter) { CopyBotParams(params,_params); } CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[]) : comission_manager(_comission_manager), mutexName(_mutexName), From(0), Till(0), custom_filter(EmptyCustomCoefCallback) { CopyBotParams(params,_params); } [/CODE] 逐行拆:第1行定义空回调,固定返 0;第2至9行是带 filter 的构造,成员初始化列表里把回调存进 custom_filter;第10行 CopyBotParams 把外部参数数组拷进类内。第12至18行是无 filter 版本,第16行直接把 EmptyCustomCoefCallback 当默认过滤塞进去。 主 EA 文件里这么引:#include <History manager/AutoLoader.mqh> 拿到类声明,再声明 CAutoUploader * auto_optimiser 指针。输入参数给了 ma_fast=10、ma_slow=50、_sl_=20、_tp_=60、_lot_=1,以及 _comission_=0、_shift_=0,外汇贵金属盘口滑点和点差会随流动性跳变,实盘前务必把 _comission_ 按经纪商标准重填,否则回测权益曲线可能偏乐观。
class="type">class="kw">double EmptyCustomCoefCallback() {class="kw">return class="num">0;} CAutoUploader::CAutoUploader(CCCM *_comission_manager,class="type">class="kw">string _mutexName,BotParams &_params[], TCustomFilter filter) : comission_manager(_comission_manager), mutexName(_mutexName), From(class="num">0), Till(class="num">0), custom_filter(filter) { CopyBotParams(params,_params); } CAutoUploader::CAutoUploader(CCCM *_comission_manager,class="type">class="kw">string _mutexName,BotParams &_params[]) : comission_manager(_comission_manager), mutexName(_mutexName), From(class="num">0), Till(class="num">0), custom_filter(EmptyCustomCoefCallback) { CopyBotParams(params,_params); }
「自定义系数与回测报告落点」
这段片段里最该盯住的是 CulculateMyCustomCoef() 这个桩函数:当前它直接 return 0,意味着你自定义的优化权重系数暂时不生效,跑 OnTester 时也是原样返回 0。想接自己的评分逻辑,改这里就行,但别指望不填就出差异化结果。 OnInit 中把 _comission_manager_ 和这个系数函数一起塞进 CAutoUploader 实例,标识名写死成 "SimpleMAMutex",说明这套自动上传器是绑定具体策略命名的,复制代码时若策略名不对,后续报告归类可能乱掉。 ReportWriter 类用静态 List<OptimisationResult> 攒每次优化的结果,AppendToReportData 接收 symbol、tf、起止 Unix 时间,转成可读 DateBorders 后入列。WriteReportData 会调 ReportData.ReportWriter 把整批落盘到 pathToFile,异常时返回 e.Message 而不是抛错——实盘前你最好先在小资金账户验证路径可写,外汇与贵金属杠杆波动大,报表写失败可能让你漏掉关键回测结论。 一个易踩的坑:ClearReportData 在 try 和 catch 后各调了一次,正常分支里写入后即清,异常分支清了又清,逻辑冗余但不致命;若你改造成异步写,得防 List 被提前清空。
CCCM _comission_manager_; class=class="str">"cmt">// Comission manager(Article class="num">2) class="type">class="kw">double CulculateMyCustomCoef() { class="kw">return class="num">0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class=class="str">"cmt">// Add Instance CAutoUploader class (Article3) auto_optimiser = new CAutoUploader(&_comission_manager_,"SimpleMAMutex",params,CulculateMyCustomCoef); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">class="kw">double OnTester() { class="kw">return(CulculateMyCustomCoef()); } class="kw">public class ReportWriter { class="kw">private class="kw">static ReportItem ReportItem; class="kw">private class="kw">static List<OptimisationResult> ReportData = new List<OptimisationResult>(); class="kw">public class="kw">static class="type">void AppendToReportData(class="type">class="kw">string symbol, class="type">int tf, class="type">class="kw">ulong StartDT, class="type">class="kw">ulong FinishDT) { ReportItem.Symbol = symbol; ReportItem.TF = tf; ReportItem.DateBorders = new DateBorders(StartDT.UnixDTToDT(), FinishDT.UnixDTToDT()); ReportData.Add(ReportItem); ClearReportItem(); } class="kw">public class="kw">static class="type">void ClearReportItem() { ReportItem = new ReportItem(); } class="kw">public class="kw">static class="type">void ClearReportData() { ReportData.Clear(); } class="kw">public class="kw">static class="type">class="kw">string WriteReportData(class="type">class="kw">string pathToBot, class="type">class="kw">string currency, class="type">class="kw">double balance, class="type">int laverage, class="type">class="kw">string pathToFile) { try { ReportData.ReportWriter(pathToBot, currency, balance, laverage, pathToFile); ClearReportData(); } catch (Exception e) { class="kw">return e.Message; } ClearReportData(); class="kw">return ""; } } class="kw">private class="kw">static SortMethod GetSortMethod(SortBy sortBy) { class="kw">switch (sortBy) { case SortBy.Payoff: class="kw">return SortMethod.Increasing;
◍ 排序方向里藏着的风险偏好
这段 switch 逻辑决定了每个指标在排行榜上是升序还是降序,背后其实是筛选器的风险立场。盈利因子、恢复因子、总交易次数这类越大越好的量,统一走 Increasing;而回撤(DD)、Altman Z 分数、各分位亏损(Q_90/Q_95/Q_99)、标准差(Std)都走 Decreasing,说明系统默认把「少亏、稳结构」放在优先位。 注意 Q_90 到 Q_99 三个分位全部降序,意味着极端尾部亏损越小的品种越靠前。外汇与贵金属杠杆高,尾部风险可能在单日吞掉数月利润,这类排序倾向对实盘仓位分配有参考价值。 把下面代码直接丢进 MT5 的 EA 或脚本里,改一个 case 的返回值就能反转某指标的排序逻辑,比如把 SortBy.DD 改成 Increasing,排行榜就会变成「回撤越大越靠前」的逆向视图,用来排查哪些品种被常规筛选掩盖了。
case SortBy.ProfitFactor: class="kw">return SortMethod.Increasing; case SortBy.AverageProfitFactor: class="kw">return SortMethod.Increasing; case SortBy.RecoveryFactor: class="kw">return SortMethod.Increasing; case SortBy.AverageRecoveryFactor: class="kw">return SortMethod.Increasing; case SortBy.PL: class="kw">return SortMethod.Increasing; case SortBy.DD: class="kw">return SortMethod.Decreasing; case SortBy.AltmanZScore: class="kw">return SortMethod.Decreasing; case SortBy.TotalTrades: class="kw">return SortMethod.Increasing; case SortBy.Q_90: class="kw">return SortMethod.Decreasing; case SortBy.Q_95: class="kw">return SortMethod.Decreasing; case SortBy.Q_99: class="kw">return SortMethod.Decreasing; case SortBy.Mx: class="kw">return SortMethod.Increasing; case SortBy.Std: class="kw">return SortMethod.Decreasing; case SortBy.MaxProfit: class="kw">return SortMethod.Increasing; case SortBy.MaxDD: class="kw">return SortMethod.Decreasing; case SortBy.MaxProfitTotalTrades: class="kw">return SortMethod.Increasing; case SortBy.MaxDDTotalTrades: class="kw">return SortMethod.Decreasing; case SortBy.MaxProfitConsecutivesTrades: class="kw">return SortMethod.Increasing; case SortBy.MaxDDConsecutivesTrades: class="kw">return SortMethod.Decreasing; case SortBy.AverageDailyProfit_Mn: class="kw">return SortMethod.Increasing; case SortBy.AverageDailyDD_Mn: class="kw">return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_Mn: class="kw">return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_Mn: class="kw">return SortMethod.Decreasing; case SortBy.AverageDailyProfit_Tu: class="kw">return SortMethod.Increasing; case SortBy.AverageDailyDD_Tu: class="kw">return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_Tu: class="kw">return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_Tu: class="kw">return SortMethod.Decreasing; case SortBy.AverageDailyProfit_We: class="kw">return SortMethod.Increasing; case SortBy.AverageDailyDD_We: class="kw">return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_We: class="kw">return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_We: class="kw">return SortMethod.Decreasing; case SortBy.AverageDailyProfit_Th: class="kw">return SortMethod.Increasing; case SortBy.AverageDailyDD_Th: class="kw">return SortMethod.Decreasing; case SortBy.AverageDailyProfitTrades_Th: class="kw">return SortMethod.Increasing; case SortBy.AverageDailyDDTrades_Th: class="kw">return SortMethod.Decreasing;
排序方向映射里的隐藏坑
在 MT5 的指标或 EA 辅助代码里,常会用一个私有静态方法把排序枚举转成实际升降序。上面这段 GetSortingDirection 就是典型实现:Custom、Payoff、ProfitFactor 等绝大多数指标都走 Ascending,而 AltmanZScore 和 Std 刻意走 Descending,说明高分或低波动更被看重。
注意原文里三处被标红的逻辑——DD(回撤)、Q_90/Q_95/Q_99(分位回撤)以及 MaxDD 全部返回了 OrderBy.Ascending。这意味着代码把「回撤越小越靠前」写成了升序,但如果你在 UI 上误以为这些字段是「越大越优」,筛选结果会完全反掉。外汇与贵金属杠杆高,回撤排序错一位可能让观察样本偏向高危品种。
另有一个手误点:AverageRecoveryFactor 那行写成了 Or-derBy.Ascending(带连字符),编译会直接报错。复制这段代码到 MetaEditor 时先改掉这个连字符,否则整个排序模块起不来。
class="kw">private class="kw">static OrderBy GetSortingDirection(SortBy sortBy) { class="kw">switch (sortBy) { case SortBy.Custom: class="kw">return OrderBy.Ascending; case SortBy.Payoff: class="kw">return OrderBy.Ascending; case SortBy.ProfitFactor: class="kw">return OrderBy.Ascending; case SortBy.AverageProfitFactor: class="kw">return OrderBy.Ascending; case SortBy.RecoveryFactor: class="kw">return OrderBy.Ascending; case SortBy.AverageRecoveryFactor: class="kw">return OrderBy.Ascending; case SortBy.PL: class="kw">return OrderBy.Ascending; case SortBy.DD: class="kw">return OrderBy.Ascending; case SortBy.AltmanZScore: class="kw">return OrderBy.Descending; case SortBy.TotalTrades: class="kw">return OrderBy.Ascending; case SortBy.Q_90: class="kw">return OrderBy.Ascending; case SortBy.Q_95: class="kw">return OrderBy.Ascending; case SortBy.Q_99: class="kw">return OrderBy.Ascending; case SortBy.Mx: class="kw">return OrderBy.Ascending; case SortBy.Std: class="kw">return OrderBy.Descending; case SortBy.MaxProfit: class="kw">return OrderBy.Ascending; case SortBy.MaxDD: class="kw">return OrderBy.Ascending; case SortBy.MaxProfitTotalTrades: class="kw">return OrderBy.Ascending; case SortBy.MaxDDTotalTrades: class="kw">return OrderBy.Descending; case SortBy.MaxProfitConsecutivesTrades: class="kw">return OrderBy.Ascending; case SortBy.MaxDDConsecutivesTrades: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfit_Mn: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDD_Mn: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_Mn: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_Mn: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfit_Tu: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDD_Tu: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_Tu: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_Tu: class="kw">return OrderBy.Descending;
「周中分日排序方向与负值平移处理」
这段代码暴露了多周期评分引擎里一个容易忽略的细节:按周三、周四、周五切分的日均利润与日均回撤,排序方向被硬编码了。We/Th/Fr 后缀的 AverageDailyProfit 一律升序,AverageDailyDD 一律降序,意味着系统默认「利润越高越好、回撤越低越好」的朴素偏好,改排序逻辑时若只动主开关不碰这里,结果会悄悄偏离预期。 负值平移那段是数值归一化的前置保险。当某指标最小值 mm.Min 小于 0 时,所有 value 和 mm.Max 都加上 Abs(mm.Min),把区间整体抬到非负轴;否则后续 value/mm.Max 会出现负分母或反向缩放。外汇与贵金属策略回测里,日均回撤类指标常出负值,这一步不补,排序系数可能直接算飞。 系数计算区分了升降序:降序用 (1 - value/mm.Max)*coef,升序用 value/mm.Max*coef,coef 是维度权重。最后 OrderBy 与 OrderByDescending 的兜底,先按外部 order 参数走,再按 GetSortingDirection 对首排序旗标校验一次——两套返回语句并存,说明旧分支没清干净,实盘复制时建议只留后者避免歧义。
case SortBy.AverageDailyProfit_We: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDD_We: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_We: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_We: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfit_Th: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDD_Th: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_Th: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_Th: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfit_Fr: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDD_Fr: class="kw">return OrderBy.Descending; case SortBy.AverageDailyProfitTrades_Fr: class="kw">return OrderBy.Ascending; case SortBy.AverageDailyDDTrades_Fr: class="kw">return OrderBy.Descending; class="kw">default: class="kw">throw new ArgumentException($"Unaxpected Sortby variable {sortBy}"); } } 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 (GetSortingDirection(item.Key) == OrderBy.Descending) { 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; } } 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))); if (order == GetSortingDirection(sortingFlags.ElementAt(class="num">0))) 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)));
◍ 把优化报告自动回传塞进 EA 只要三行
新的参数加载逻辑放在 AutoUploader2.mqh 里,核心是一个只有静态方法的 CAutoUploader2 类。没有构造函数、不需要实例化,EA 里直接调静态方法就能跑,比旧方案少了约 13 行样板代码(第四篇示例从 16 行降到 3 行)。 类内部用静态字段记住日期范围 From/Till、帧名 frame_name、帧 ID frame_id、输出文件名 file_name,以及几个回调指针(on_tick、on_tester、on_tester_deinit、on_tester_init)。回调都包在预处理器条件里,你没定义对应宏就不会编译进去,不会报未实现。 EA 在 OnTesterInit 里调 CAutoUploader2::OnTesterInit,优化启动时把实时时间写进 From/Till;每次报价触发 on_tick 回调(或空函数)。测试器跑完,OnTester 里保存帧或写文件,OnTesterDeinit 里读全部帧、按名称+ID 过滤、循环取数据写报告,最后调自定义回调。 有个坑:自动优化器要能重启终端,但旧办法在配置文件标 required=true 会在 OnTesterDeinit 完成前关掉终端,导致帧来不及处理。解法是加一个输入变量(由优化器改、人手别动),为 true 时才从 MQL5 调终端关闭,否则不关。所有帧处理完 EA 从图表移除。 接法上,除了 include 该文件,只需在 EA 里引 3 行:定义 CUSTOM_ON_TICK 宏(用已有 OnTick)、把自定义系数函数传给 SetCustomCoefCallback、其余 OnTester/OnTesterInit/OnTesterDeinit 全在包含文件里兜底。想要自定义系数计算,传 CulculateMyCustomCoef 进 SetCustomCoefCallback 即可,外汇和贵金属品种回测请留意点差和佣金假设偏差可能放大结果不确定性。
class CAutoUploader2 { class="kw">private: CAutoUploader2() {} class="kw">static CCCM comission_manager; class="kw">static class="type">class="kw">datetime From,Till; class="kw">static TCustomFilter on_tester; class="kw">static TCallback on_tick, on_tester_deinit; class="kw">static TOnTesterInit on_tester_init; class="kw">static class="type">class="kw">string frame_name; class="kw">static class="type">long frame_id; class="kw">static class="type">class="kw">string file_name; class="kw">static class="type">bool FillInData(Data &data); class="kw">static class="type">void UploadData(class="kw">const Data &data, class="type">class="kw">double custom_coef, class="kw">const BotParams ¶ms[]); class="kw">public: class="kw">static class="type">void OnTick(); class="kw">static class="type">class="kw">double OnTester(); class="kw">static class="type">int OnTesterInit(); class="kw">static class="type">void OnTesterDeinit(); class="kw">static class="type">void SetUploadingFileName(class="type">class="kw">string name); class="kw">static class="type">void SetCallback(TCallback callback, ENUM_CALLBACK_TYPE type); class="kw">static class="type">void SetCustomCoefCallback(TCustomFilter custom_filter_callback); class="kw">static class="type">void SetOnTesterInit(TOnTesterInit on_tester_init_callback); class="kw">static class="type">void AddComission(class="type">class="kw">string symbol,class="type">class="kw">double comission,class="type">class="kw">double shift);