MQL5 中的策略可视化:在标准图表中展示优化结果(基础篇)
「在MT5图表里直接画出优化结果」
MQL5 做策略优化后,结果通常只躺在优化报告表格里,想边盯盘边对照参数表现,得自己把数据搬回图表。Artyom Trishkin 在 2026 年 2 月 18 日发布的示例里,给出了一套把优化结果渲染进标准图表的思路,原文示例帖收获了 412 次查看与 2 条讨论,说明这类需求在量化交易者中并不冷门。 核心做法是放弃用外部报表,转而在 EA 内部建一套轻量 GUI 类:选项卡控件切不同优化维度,表格类列具体参数组合,进度条类显示回测推进,统计图表类画分布,帧查看器类负责按帧重绘。把这些类挂到 EA 的 OnChartEvent 里,优化跑完就能在图表上直接翻看,不用切窗口。 外汇与贵金属品种波动受杠杆和流动性影响大,这类可视化只解决‘看得清’,不解决‘算得准’,参数过拟合风险依旧存在,任何图形结论都只能当作概率参考。
把普通优化器改成可视化面板
在 MT5 里跑 EA 优化时,原生策略测试器只给一张结果表,想横向比几组参数得来回切。有个思路是在 EA 里挂一小段标准处理代码,把常用优化器变成带图的可视面板,这件事值得自己动手验一遍。 接上这段逻辑后,EA 会弹出一个含五个标签页的窗口。第一个标签页实时画每次通过的余额线;其余四个标签页在优化跑完才可用,分别按夏普比率、净利润、利润因子、恢复因子各挑前三名通过,画它们的余额曲线并配两张表——一张是该项优化结果,一张是那次通过的 EA 输入参数,还带重放和前三切换开关。 实现上,标签页靠一组控件类拼出来,本文直接给现成类文件,后面再拆。表格类借了按交易品种统计的仪表盘文章里的成品,稍改就能往单元格塞文字。框架代码参考了优化相关旧文,但旧文没讲帧模式 EA 怎么跑,这块得自己啃。
◍ 代理与终端之间的帧通信机制
MT5 策略测试器的优化是多线程的:本地代理数量默认等于 CPU 核心数,每个代理其实是 metaster64.exe 的副本,在 Windows 任务管理器里能直接看到。每个代理都跑独立的交易与软件环境,彼此隔离,也和客户端隔离;每次 EA 实例测试完,最终结果帧会从代理回传终端。 如果 EA 里写了 OnTesterInit()、OnTesterDeinit() 或 OnTesterPass() 中任意一个处理函数,它会被终端自动加载到一张独立图表上,以「帧模式」运行。这张图上的 EA 不交易,只做服务操作,OnInit / OnTick 这类常规 handler 全部不触发。注意 OnTesterInit 和 OnTesterDeinit 必须成对出现,不能只写其中一个。 这三个事件只在优化过程中生效,单次测试跑不起来。代理侧每次跑完一 pass,会触发 OnTester(),用 FrameAdd() 把本次通过的编号、输入参数值和结果打成帧发往终端;终端图表的 EA 在 OnTesterPass() 里收帧、读数据、画余额曲线,再做后续排序。 实际落地时,开 MT5 按 F4 写一段带这三个 handler 的 EA,优化时切到「帧模式」图表,就能实时看到每 pass 的权益走向,而不是等全部跑完才翻报告。外汇与贵金属品种波动剧烈,优化结果仅代表历史样本表现,实盘存在显著回撤风险。
「搭选项卡控件先把类骨架立起来」
做可视化优化面板,第一步是把控件类体系搭好。实际放出来的 Controls.mqh 里共实现了 10 个类、对应 8 个独立控件:从 CBaseCanvas 绘图基类,到 CPanel、CLabel、CButton,再到双态的 CButtonTriggered、CTabButton,以及管理多页的 CTab、CTabControl。控件对象建完必须逐个调 Create() 并给坐标和尺寸,否则不参与事件循环。 用户交互靠图表事件回传。CButton / CButtonTriggered / CTabButton 点击时发 CHARTEVENT_CLICK,lparam、dparam 是光标 XY,sparam 是按钮名;CButtonSwitch 则点哪颗子按钮就带按钮 ID 回传。选项卡切换没单独注册事件,靠捕 TabButton 名称反查页码或向 TabControl 要选中索引,能少写一层回调。 表格部分要复用旧文 Dashboard.mqh 里的 CTable 类(原文件第 12–285 行),复制到 \MQL5\Experts\FrameViewer\Table.mqh 并小改:私有段加文本宽高色变量,公有段加读写方法和画到画布的函数,末尾再挂一个继承自表格的 CTableDataControl,统一管表数据。 本次重头戏是把优化结果当场画出来。文末附 4 个 MQH:specialchart.mqh(7.61 KB,画每趟余额线)、colorprogressbar.mqh(4.86 KB,绿盈红亏进度条)、simpletable.mqh(10.74 KB,左旁两表显参数)、framegenerator.mqh(14.88 KB,代理与终端交换数据的主类)。任选 EA 如 Moving Averages.mq5,尾部插代码块、改名 Moving Averages With Frames.mq5 编译跑优化即可见效果。 FrameViewer.mqh 里把类聚到一个文件,绘图对象大多套两个 CCanvas:底图一个、前景一个。跟踪鼠标或定位用 CRect 结构(\MQL5\Include\Controls\Rect.mqh),可移位取界。帧类 CFrame 负责缓存每趟回传数据,Compare() 比较实数时必须先 NormalizeDouble 到两位再比——内部值如 1.085686399864 与 1.081254322375 表显 1.09/1.08,不归一化会漏掉真最优。 生命周期走四个处理函数:OnTesterInit 建界面脱离图表;OnTester 收尾数据 FrameAdd 发终端;OnTesterPass 接帧画余额填表入列;OnTesterDeinit 改标题印回放钮。外汇/贵金属优化高风险,回测优参过拟合概率不低,MT5 跑通再上实盘。
把优化帧塞进独立图表的实现骨架
EA 跑完优化后以帧模式常驻浮动图表,终端里所有交互都收口在 OnChartEvent():按钮和鼠标事件驱动流程,标签页切换与切换按钮点击各自走独立处理函数,逻辑几乎一致,差别只在标签页 ID。 非活动页的控件要靠 ControlObjectsView 隐藏、活动页再显示,避免图表被多余按钮糊住。重放时从 FrameFirst() 到 FrameNext() 循环遍历,把每帧数据刷进表格和图表;想抽指定编号的帧,也只能线性搜一遍已存帧,找到再打印——目前为减代码量就这么干,没另写帧对象直取接口。 三个最佳通过结果用 FillArrayBestFrames() 填数组(长度固定 3),内部靠 FrameSearchLess() 在 CArrayObj 排序列表里找属性值更小的帧。之后 DrawFrameData() 从帧取数填表并画余额线,统计表和参数表先画空壳、再回头灌数据。 把 CFrameViewer 类挂到任意 EA 上,就能在单独图表看优化进度。外汇与贵金属优化结果受点差和滑点扰动,回测优异的帧在实盘可能显著衰减,接入前建议在 MT5 用历史数据跑一遍验证。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Table.mqh | class=class="str">"cmt">//| Copyright class="num">2025, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2025, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include <Canvas\Canvas.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Table cell class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTableCell : class="kw">public CObject { class="kw">private: class="type">int m_row; class=class="str">"cmt">// Row class="type">int m_col; class=class="str">"cmt">// Column class="type">int m_x; class=class="str">"cmt">// X coordinate class="type">int m_y; class=class="str">"cmt">// Y coordinate class="type">int m_w; class=class="str">"cmt">// Width class="type">int m_h; class=class="str">"cmt">// Height class="type">class="kw">string m_text; class=class="str">"cmt">// Text in the cell
◍ 单元格对象的取值与设值接口
在自绘表格控件里,每一个单元格都被封装成一个 C++ 风格的结构体,用私有成员存坐标、尺寸和文字,再靠公开的 setter / getter 暴露给外部逻辑。 下面的片段给出了典型的设值方法:行号、列号、像素坐标 x/y 都接收 uint 再强转 int 存入成员;宽高同样走 SetWidth / SetHeight 或一次性 SetSize。文字内容由 SetText 直接赋值给 m_text 字符串成员。 color m_fore_color; // 单元格内文字颜色 public: public: //--- Methods for setting values void SetRow(const uint row) { this.m_row=(int)row; } void SetColumn(const uint col) { this.m_col=(int)col; } void SetX(const uint x) { this.m_x=(int)x; } void SetY(const uint y) { this.m_y=(int)y; } void SetXY(const uint x,const uint y) { this.m_x=(int)x; this.m_y=(int)y; } void SetWidth(const uint w) { this.m_w=(int)w; } void SetHeight(const uint h) { this.m_h=(int)h; } void SetSize(const uint w,const uint h) { this.m_w=(int)w; this.m_h=(int)h; } void SetText(const string text) { this.m_text=text; } //--- Methods for getting values int Row(void) const { return this.m_row; } int Column(void) const { return this.m_col; } 取数接口更短平快:Row() 和 Column() 以 const 方法返回内部 m_row / m_col 的 int 值,调用时不会改动对象状态。在 MT5 里新建一个继承自该结构的类,编译后拖进 EURUSD 的 M5 图,就能看到自绘面板按设定的行列号落位,外汇品种波动快、杠杆高,面板坐标算错可能叠层重叠,验证前先开模拟盘跑一遍。
class="type">color m_fore_color; class=class="str">"cmt">// Text class="type">color in the cell class="kw">public: class="kw">public: class=class="str">"cmt">//--- Methods for setting values class="type">void SetRow(const class="type">uint row) { this.m_row=(class="type">int)row; } class="type">void SetColumn(const class="type">uint col) { this.m_col=(class="type">int)col; } class="type">void SetX(const class="type">uint x) { this.m_x=(class="type">int)x; } class="type">void SetY(const class="type">uint y) { this.m_y=(class="type">int)y; } class="type">void SetXY(const class="type">uint x,const class="type">uint y) { this.m_x=(class="type">int)x; this.m_y=(class="type">int)y; } class="type">void SetWidth(const class="type">uint w) { this.m_w=(class="type">int)w; } class="type">void SetHeight(const class="type">uint h) { this.m_h=(class="type">int)h; } class="type">void SetSize(const class="type">uint w,const class="type">uint h) { this.m_w=(class="type">int)w; this.m_h=(class="type">int)h; } class="type">void SetText(const class="type">class="kw">string text) { this.m_text=text; } class=class="str">"cmt">//--- Methods for getting values class="type">int Row(class="type">void) const { class="kw">return this.m_row; } class="type">int Column(class="type">void) const { class="kw">return this.m_col; }