在 MetaTrader 5 中交易策略优化的可视化(基础篇)
MT5 优化结果也能画出来看
MetaTrader 5 的策略测试器跑完参数优化,默认只给表格和净值曲线,但优化过程的帧数据其实可以拿来单独渲染。2018 年 4 月 Anatoli Kazharski 在官方社区发过一组实操,原帖浏览量 4168、收藏 69,说明可视化优化这件事在量化交易者里一直有刚需。 思路不复杂:把每一帧优化产生的数据抽出来,用 MQL5 自己写图形界面画到图表上。这样你能直接看到参数空间里哪些区域收益陡、哪些区域过拟合,比盯冷冰冰的数字直观得多。 外汇和贵金属市场高杠杆、跳空频繁,任何可视化结论都只是概率参考,不能当成下单依据。开 MT5 跑一次两参数优化,把帧数据存下来,是验证这套方法的第一步。
◍ 为什么要在优化后叠加多条余额曲线
在 MT5 里跑参数优化时,优化图选项卡默认只给一张汇总图,很难判断某一组参数真实的权益走势。若只盯单张图,容易把曲线平滑但回撤隐蔽的过拟合组合误判为优解。 更实用的做法是在优化结束后,把多组被测参数的余额曲线同时铺开对比。我们在早先的可视化策略测试器文章里验证过这种叠加思路,但当时接口能力有限,只能做基础版。 现在借助 EasyAndFast 图形界面函数库的新版本,可以用 MQL 直接写出带 GUI 的优化辅助工具,把多测试任务的权益曲线同屏渲染。对习惯用该函数库的人来说,这套扩展能直接复用既有框架,不必从零搭界面。外汇与贵金属品种波动剧烈,叠加观察也只是降低误判概率,不表示某组参数实盘必优。
「用 EasyAndFast 搭 GUI 的工程骨架」
做 MT5 程序图形界面,如果手里有 EasyAndFast 这套库,基本不用从零画控件。它把 GUI 相关逻辑打包成单一块(GUI 函数库),接进项目不会把引用关系搅成一团乱麻,完整结构在库目录里能直接翻。 实际落地时,核心是一个 CProgram 应用类,它必须继承自图形库引擎的 CWndEvents,这样才能把窗口消息、鼠标事件接进来。类里面要声明 OnInitEvent / OnTickEvent / OnTradeEvent / OnTimerEvent,以及 OnTesterXXX 那一族方法——后者专门处理回测帧,优化重跑时画面刷新就靠它们。 界面上要摆的东西很固定:控件窗体、余额显示字段、重复显示速度调节字段、启动按钮、结果统计表、EA 外部参数表、余额曲线图、优化结果图、状态栏,还有重滚进度条。这些元素的类实例和创建方法,建议塞进 CreateFrameModeGUI.mqh 单独管,随着代码膨胀,分文件比全堆在 Program.mqh 里好梳理太多。 下面这段是 Program.mqh 的骨架示例,黄色高亮处就是接入引擎和测试器事件的关键行。开 MT5 建个空 EA,把 include 和类声明抄进去,编译能过就说明 GUI 底座立住了。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Program.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- 用于创建图形界面的库类 class="macro">#include <EasyAndFastGUI\WndEvents.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于开发应用程序的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CProgram : class="kw">public CWndEvents { }; class CProgram : class="kw">public CWndEvents { class="kw">public: class=class="str">"cmt">//--- 初始化/逆初始化 class="type">bool OnInitEvent(class="type">void); class="type">void OnDeinitEvent(class="kw">const class="type">int reason); class=class="str">"cmt">//--- "新逐笔报价" 事件处理器 class="type">void OnTickEvent(class="type">void); class=class="str">"cmt">//--- 交易事件处理器 class="type">void OnTradeEvent(class="type">void); class=class="str">"cmt">//--- 计时器 class="type">void OnTimerEvent(class="type">void); class=class="str">"cmt">//--- 测试器 class="type">class="kw">double OnTesterEvent(class="type">void); class="type">void OnTesterPassEvent(class="type">void); class="type">void OnTesterInitEvent(class="type">void); class="type">void OnTesterDeinitEvent(class="type">void); }; class=class="str">"cmt">//--- 包含应用程序类 class="macro">#include "Program.mqh" CProgram program; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit(class="type">void) { class=class="str">"cmt">//--- 初始化程序 if(!program.OnInitEvent()) {
把生命周期钩子全转交给程序类
EA 的主文件只做一件事:把 MT5 框架抛出来的各个系统事件,原样转交给一个封装好的 program 对象,自己不写任何业务逻辑。 初始化段里如果条件不满足,会先打印函数名加「 > 初始化失败!」,随后 return(INIT_FAILED);正常则落到 return(INIT_SUCCEEDED)。这两行决定了 EA 能否加载到图表,失败时在专家日志里能看到具体函数名,方便定位。 其余钩子都是单行转发:OnDeinit 传 reason 给 OnDeinitEvent,OnTick 调 OnTickEvent,OnTimer 调 OnTimerEvent,图表交互由 OnChartEvent 带着 id / lparam / dparam / sparam 转交 ChartEvent。回测相关的 OnTester 要求返回 double,所以写成 return(program.OnTesterEvent()),而 OnTesterInit 与 OnTesterPass 无返回值,仅做事件通知。 这种写法把「MT5 强制入口函数」和「策略实现」彻底隔离。你在 MT5 里新建 EA 时,照这个骨架挂一遍,就能把调试注意力全放在 program 类内部,而不必每次改策略都动主文件。外汇与贵金属品种波动剧烈,这类自动化加载逻辑若初始化判断疏漏,可能让 EA 在极端跳空时直接离线,实盘前务必在策略测试器跑足样本。
Print(__FUNCTION__," > 初始化失败!"); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { program.OnDeinitEvent(reason); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逐笔报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick(class="type">void) { program.OnTickEvent(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计数器函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer(class="type">void) { program.OnTimerEvent(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ChartEvent 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam) { program.ChartEvent(id,lparam,dparam,sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tester 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester(class="type">void) { class="kw">return(program.OnTesterEvent()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterInit 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterInit(class="type">void) { program.OnTesterInitEvent(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterPass 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterPass(class="type">void) { program.OnTesterPassEvent(); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 回测收尾与界面对象的类内布局
在 EA 结束优化回测时,终端会触发 OnTesterDeinit 钩子。下面这行把控制权转交给程序类,方便在退出前做资源清理或统计落盘: program.OnTesterDeinitEvent(); 真正的图形界面元素都封在 CProgram 类里,它继承自 CWndEvents 以接收窗口消息。类内 private 区先声明了成员:m_window1 是主窗体,m_status_bar 做底部状态栏,m_curves_total 与 m_sleep_ms 是两个文本输入框,分别用于设定曲线数量和刷新间隔毫秒数。 按钮 m_reply_frames 负责触发帧重绘请求,m_table_stat 与 m_table_param 两张表分别承载统计值和参数值,m_graph1、m_graph2 双图用于并行绘制不同维度的优化曲线,m_progress_bar 则反映当前进度。 public 区只暴露一个 CreateFrameModeGUI,作为构建整套优化监控界面的统一入口;其余 CreateWindow、CreateStatusBar、CreateTableStat 等十来个 private 方法各自只干一件事——按传入的 x_gap、y_gap 偏移量把对应控件摆进窗体。你在 MT5 里改界面布局,直接调这些方法的参数就能验证。
class=class="str">"cmt">//| TesterDeinit 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterDeinit(class="type">void) { program.OnTesterDeinitEvent(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class CProgram : class="kw">public CWndEvents { class="kw">private: class=class="str">"cmt">//--- 窗口 CWindow m_window1; class=class="str">"cmt">//--- 状态栏 CStatusBar m_status_bar; class=class="str">"cmt">//--- 输入字段 CTextEdit m_curves_total; CTextEdit m_sleep_ms; class=class="str">"cmt">//--- 按钮 CButton m_reply_frames; class=class="str">"cmt">//--- 表格 CTable m_table_stat; CTable m_table_param; class=class="str">"cmt">//--- 图形 CGraph m_graph1; CGraph m_graph2; class=class="str">"cmt">//--- 进度条 CProgressBar m_progress_bar; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 创建优化模式下处理帧的图形界面 class="type">bool CreateFrameModeGUI(class="type">void); class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 窗体 class="type">bool CreateWindow(class="kw">const class="type">class="kw">string text); class=class="str">"cmt">//--- 状态栏 class="type">bool CreateStatusBar(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap); class=class="str">"cmt">//--- 表格 class="type">bool CreateTableStat(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap); class="type">bool CreateTableParam(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap); class=class="str">"cmt">//--- 输入字段 class="type">bool CreateCurvesTotal(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap,class="kw">const class="type">class="kw">string text); class="type">bool CreateSleep(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap,class="kw">const class="type">class="kw">string text); class=class="str">"cmt">//--- 按钮 class="type">bool CreateReplyFrames(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap,class="kw">const class="type">class="kw">string text); class=class="str">"cmt">//--- 图形 class="type">bool CreateGraph1(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap); class="type">bool CreateGraph2(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap); class=class="str">"cmt">//--- 进度条
「帧模式下的图形界面装配」
在 MT5 优化结果回放(帧模式)里做可视化,核心是先判断运行环境再批量挂控件。程序入口 CProgram::CreateFrameModeGUI 第一道闸就是 MQLInfoInteger(MQL_FRAME_MODE),非帧模式直接返回 false,避免在普通图表上徒增开销。
窗体建好后才依次落控件:状态栏定在 (1,23),曲线总数标签在 (7,25),休眠参数框在 (145,25),回放按钮在 (255,25);下方统计表从 (2,50) 起、参数表从 (2,212) 起,两张图分别锚在 (200,50) 与 (200,159)。这些坐标都是像素偏移,改一处就可能和相邻控件重叠。
进度条函数 CreateProgressBar 接收 x_gap、y_gap 和 text 三个参数,头文件里只声明未展开,说明具体绘制逻辑在别的 mqh 中。想验证这套布局,直接在帧模式 EA 里 include 对应 mqh 并调用即可,外汇与贵金属品种回放时需注意点差跳空可能导致帧数据断裂。
class="type">bool CreateProgressBar(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap,class="kw">const class="type">class="kw">string text); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建控件元素的方法 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "CreateFrameModeGUI.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| CreateFrameModeGUI.mqh | class=class="str">"cmt">//| 版权所有 class="num">2018, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Program.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建图形界面 | class=class="str">"cmt">//| 用于分析优化结果并处理帧 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CProgram::CreateFrameModeGUI(class="type">void) { class=class="str">"cmt">//--- 仅在处理优化帧的模式下创建界面 if(!::MQLInfoInteger(MQL_FRAME_MODE)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 为控件元素创建窗体 if(!CreateWindow("Frame mode")) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 创建控件元素 if(!CreateStatusBar(class="num">1,class="num">23)) class="kw">return(class="kw">false); if(!CreateCurvesTotal(class="num">7,class="num">25,"Curves total:")) class="kw">return(class="kw">false); if(!CreateSleep(class="num">145,class="num">25,"Sleep:")) class="kw">return(class="kw">false); if(!CreateReplyFrames(class="num">255,class="num">25,"Replay frames")) class="kw">return(class="kw">false); if(!CreateTableStat(class="num">2,class="num">50)) class="kw">return(class="kw">false); if(!CreateTableParam(class="num">2,class="num">212)) class="kw">return(class="kw">false); if(!CreateGraph1(class="num">200,class="num">50)) class="kw">return(class="kw">false); if(!CreateGraph2(class="num">200,class="num">159)) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 进度条