在 MetaTrader 5 中交易策略优化的可视化(基础篇)
📘

在 MetaTrader 5 中交易策略优化的可视化(基础篇)

第 1/3 篇

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 底座立住了。

MQL5 / C++
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 在极端跳空时直接离线,实盘前务必在策略测试器跑足样本。

MQL5 / C++
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 里改界面布局,直接调这些方法的参数就能验证。

MQL5 / C++
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 并调用即可,外汇与贵金属品种回放时需注意点差跳空可能导致帧数据断裂。

MQL5 / C++
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">//--- 进度条

常见问题

在优化结束后把每组的余额序列存下来,用 GUI 把多条曲线画在同一坐标系,斜率和回撤一眼能分。
用现成的界面库做底层,把窗口、按钮、画布先声明好,再只填业务钩子,避免从零写消息循环。
小布可读取你的优化结果,自动生成叠加曲线与回撤标注,你只管看分歧点,不用手动画图。
单组最优容易过拟合,叠曲线能看参数稳定性,群集平滑的批次比尖峰组更抗实盘。
把坐标和层级写进界面类的初始化方法,按生命周期统一创建销毁,别在循环里零散推对象。