连续前行优化 (第三部分): 将机器人适配为自动优化器·进阶篇
(2/3)·前两篇铺好了零件,本篇把它们焊成一条能自跑的自动优化流水线
◍ 把自定义手续费写进回测报告
在 MT5 回测里, broker 默认点值往往不含你实盘那档加点或返佣结构,直接用默认设置跑出的净值曲线会偏乐观。下面这段 EA 骨架演示了如何把 custom_comission 与 custom_shift 两个输入参数落盘到 CSV,方便后续和真实账户对表。 核心逻辑放在 OnDeinit:只有当 MQLInfoInteger(MQL_TESTER)==1 即处于测试环境时才写文件,避免实盘误触发 IO。文件命名用 __FILE__ 去掉扩展名再拼 ' Report.csv',保证每次编译产物对应独立报告。 WRITE_BOT_PARAM 宏负责把数值参数写进同一份 CSV 表头区,WriteDetalesReport 则输出逐笔成交明细到 ' Deals Report.csv'。实盘验证时,把 custom_comission 设成你经纪商每手单边费用(比如 7.0 美元),custom_shift 设成滑点偏移点数,跑一遍 EURUSD 的 2023 年 H1 数据,就能看到手续费对 Sharpe 的侵蚀幅度。外汇与贵金属杠杆高,回测未含极端点差扩张,实盘偏差可能进一步扩大。
class="macro">#define ONLY_CUSTOM_COMISSION class="macro">#include <History manager/DealsHistory.mqh> input class="type">class="kw">double custom_comission = class="num">0; class=class="str">"cmt">// Custom commission; input class="type">int custom_shift = class="num">0; class=class="str">"cmt">// Custom shift; CCCM _comission_manager_; class="type">int OnInit() { _comission_manager_.add(_Symbol,custom_comission,custom_shift); class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(class="kw">const class="type">int reason) { if(MQLInfoInteger(MQL_TESTER)==class="num">1) { class="type">class="kw">string arr[]; StringSplit(__FILE__,&class="macro">#x27;.&class="macro">#x27;,arr); class="type">class="kw">string file_name = arr[class="num">0]+" Report.csv"; SaveReportToFile(file_name,&_comission_manager_); WRITE_BOT_PARAM(file_name,custom_comission); class=class="str">"cmt">// Custom commission; WRITE_BOT_PARAM(file_name,custom_shift); class=class="str">"cmt">// Custom shift; WriteDetalesReport(arr[class="num">0]+" Deals Report.csv", &_comission_manager_); } }
正文
<span class="keyword">void</span> <span class="functions">OnTick</span>() { <span class="comment">//---</span> } <span class="comment">//+------------------------------------------------------------------+</span> [/CODE]
「用 DLL 包装器把优化结果落盘成 XML」
连续前行优化最顺手的载体是 XML,但纯函数调用在 EA 里写起来别扭。把第一篇文章里的 C# DLL 用对象包一层,就得到 XmlHistoryWriter.mqh 里的 CXmlHistoryWriter,EA 端只管把参数丢进去,写入逻辑全藏 behind 对象里。 导入 ReportManager.dll 后,先声明 BotParams 结构存 name/value 键值对,再用两个宏降低动态数组操作成本:ADD_TO_ARR 负责扩数组并塞元素,APPEND_BOT_PARAM 则按变量名自动取 #Var 当键、把值强转 string 当值。枚举必须转 int 再转 string,否则和 .set 文件对不上。 多进程并行优化时,两个机器人可能同时写同一个 XML。CXmlHistoryWriter 里写死了 _mutex_name 字段,靠 C# DLL 的命名互斥锁走操作系统内核做同步,谁拿到锁谁写,避免报告互相覆盖。 文件名协议硬编码为『EA名_Report.xml』,且强制落在 MT5 的 Common 沙箱目录,不让你写去 PC 任意路径。Write() 是总入口,失败就吐日志,成功则依次调 append_bot_params、append_days_pl 等包装器,最后调 DLL 落盘。 想更懒一点就用 AutoLoader.mqh 的 CAutoUpLoader:它在 OnTick 里记起止时间,析构时自动委托 CXmlHistoryWriter 写报告。继承它还是全局实例化都行——继承时重载 OnTick 必须显式调基类,否则自动优化器拿不到时间基准。外汇与贵金属 EA 回测涉及高杠杆与滑点风险,并行优化结果仅作概率参考。
class="macro">#include "ReportCreator.mqh" class="macro">#class="kw">import "ReportManager.dll" class="kw">struct BotParams { class="type">class="kw">string name,value; }; class="macro">#define ADD_TO_ARR(arr, value) \ { class="type">int s = ArraySize(arr); ArrayResize(arr,s+class="num">1,s+class="num">1); arr[s] = value; } class="macro">#define APPEND_BOT_PARAM(Var,BotParamArr) \ { BotParams param; param.name = class="macro">#Var;\ param.value = (class="type">class="kw">string)Var;\ ADD_TO_ARR(BotParamArr,param); } class="type">void CopyBotParams(BotParams &dest[], class="kw">const BotParams &src[]) { class="type">int total = ArraySize(src); for(class="type">int i=class="num">0; i<total; i++) { ADD_TO_ARR(dest,src[i]); } } class CXmlHistoryWriter { class="kw">private: class="kw">const class="type">class="kw">string _path_to_file,_mutex_name; CReportCreator _report_manager; class="type">class="kw">string get_path_to_expert();class=class="str">"cmt">//
◍ 把回测结果落盘到公共目录的写入器
做 EA 批量回测时,最麻烦的是把每套参数的盈亏、回撤、星期分布写进同一个文件供后续比对。下面这个类把这件事封死了:构造时传入文件名和互斥名,落盘路径直接拼到 TerminalInfoString(TERMINAL_COMMONDATA_PATH) 下,也就是所有终端账号共享的 Common 数据目录,不会散落在某个品种子目录里。 无参文件名的重载更省事——路径自动变成 MQL_PROGRAM_NAME + "_Report.xml",EA 跑完在自己程序名下生成报告,多品种并行也不会互相覆盖。 get_path_to_expert() 里用 StringSplit 按 '\\' 拆 MQL_PROGRAM_PATH,从命中 "Experts" 那段开始拼相对目录。这意味着你若把脚本挪到 Scripts 或 Indicators 下,这段逻辑会拿不到预期路径,调用前最好先打印 arr 数组确认层级。外汇与贵金属回测含高杠杆风险,落盘数据仅用于策略对比,不代表实盘倾向。
class="type">void append_bot_params(class="kw">const BotParams ¶ms[]);class=class="str">"cmt">// class="type">void append_main_coef(PL_detales &pl_detales, TotalResult &totalResult);class=class="str">"cmt">// class="type">class="kw">double get_average_coef(CoefChartType type); class="type">void insert_day(PLDrawdown &day,ENUM_DAY_OF_WEEK day);class=class="str">"cmt">// class="type">void append_days_pl();class=class="str">"cmt">// class="kw">public: CXmlHistoryWriter(class="type">class="kw">string file_name,class="type">class="kw">string mutex_name, CCCM *_comission_manager);class=class="str">"cmt">// CXmlHistoryWriter(class="type">class="kw">string mutex_name,CCCM *_comission_manager); ~CXmlHistoryWriter(class="type">void) {_report_manager.Clear();} class=class="str">"cmt">// class="type">void Write(class="kw">const BotParams ¶ms[],class="type">class="kw">datetime start_test,class="type">class="kw">datetime end_test);class=class="str">"cmt">// }; CXmlHistoryWriter::CXmlHistoryWriter(class="type">class="kw">string file_name, class="type">class="kw">string mutex_name, CCCM *_comission_manager) : _mutex_name(mutex_name), _path_to_file(TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name), _report_manager(_comission_manager) { } CXmlHistoryWriter::CXmlHistoryWriter(class="type">class="kw">string mutex_name, CCCM *_comission_manager) : _mutex_name(mutex_name), _path_to_file(TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+MQLInfoString(MQL_PROGRAM_NAME)+"_"+"Report.xml"), _report_manager(_comission_manager) { } class="type">class="kw">string CXmlHistoryWriter::get_path_to_expert(class="type">void) { class="type">class="kw">string arr[]; StringSplit(MQLInfoString(MQL_PROGRAM_PATH),&class="macro">#x27;\\&class="macro">#x27;,arr); class="type">class="kw">string relative_dir=NULL; class="type">int total= ArraySize(arr); class="type">bool save= class="kw">false; for(class="type">int i=class="num">0; i<total; i++) { if(save) { if(relative_dir== NULL) relative_dir=arr[i]; else relative_dir+="\\"+arr[i]; } if(StringCompare("Experts",arr[i])==class="num">0)
把回测结果落盘成 XML 的实现落点
这段 CXmlHistoryWriter::Write 负责把策略回测的汇总指标写进 XML 报告。它先尝试创建报告管理器,失败就直接 Print 报错并返回,不会继续污染文件。 拿到 totalResult 与 pl_detales 后,依次追加机器人参数、核心系数、VaR 三档分位(90/95/99)以及均值和标准差,再写最大盈亏与回撤细节。VaR_99 这类绝对风险值对贵金属跨周期回测尤其敏感,杠杆放大后尾部亏损可能远超正态假设。 最后用 MutexWriter 加锁写文件,传入账户币种、杠杆(AccountInfoInteger(ACCOUNT_LEVERAGE))、品种、周期与起止时间;若返回的 error_msg 非空,则打印错误阻断。外汇与贵金属回测高风险,实盘前务必在 MT5 用真实点差重跑验证。
save=true; } class="kw">return relative_dir; } class="type">void CXmlHistoryWriter::append_bot_params(class="kw">const BotParams ¶ms[]) { class="type">int total= ArraySize(params); for(class="type">int i=class="num">0; i<total; i++) { ReportWriter::AppendBotParam(params[i].name,params[i].value); } } class="type">void CXmlHistoryWriter::Write(class="kw">const BotParams ¶ms[],class="type">class="kw">datetime start_test,class="type">class="kw">datetime end_test) { if(!_report_manager.Create()) { Print("##################################"); Print("Can`t create report:"); Print("###################################"); class="kw">return; } TotalResult totalResult; _report_manager.GetTotalResult(totalResult); PL_detales pl_detales; _report_manager.GetPL_detales(pl_detales); append_bot_params(params); append_main_coef(pl_detales,totalResult); ReportWriter::AppendVaR(totalResult.total.VaR_absolute.VAR_90, totalResult.total.VaR_absolute.VAR_95, totalResult.total.VaR_absolute.VAR_99, totalResult.total.VaR_absolute.Mx, totalResult.total.VaR_absolute.Std); ReportWriter::AppendMaxPLDD(pl_detales.total.profit.totalResult, pl_detales.total.drawdown.totalResult, pl_detales.total.profit.orders, pl_detales.total.drawdown.orders, pl_detales.total.profit.dealsInARow, pl_detales.total.drawdown.dealsInARow); append_days_pl(); class="type">class="kw">string error_msg=ReportWriter::MutexWriter(_mutex_name,get_path_to_expert(),AccountInfoString(ACCOUNT_CURRENCY), _report_manager.GetBalance(), (class="type">int)AccountInfoInteger(ACCOUNT_LEVERAGE), _path_to_file, _Symbol,(class="type">int)Period(), start_test, end_test); if(StringCompare(error_msg,"")!=class="num">0) { Print("##################################"); Print("Error class="kw">while creating(*.xml) report file:"); Print("_________________________________________"); Print(error_msg);