在 MetaTrader 5 中交易策略优化的可视化·进阶篇
◍ 进度条初始化失败即中断
在 MT5 自定义指标或 EA 的 GUI 构建流程里,进度条不是装饰件,而是阻塞式初始化的第一道闸门。上面这段片段里,CreateProgressBar(2,3,"Processing...") 尝试在第 2 行第 3 列画一个带「Processing...」文案的进度条;若返回 false,函数直接 return(false),后续窗口事件绑定全部跳过。 实际在 MT5 终端跑这段代码时,若图表窗口宽度不足以容纳 3 列布局,CreateProgressBar 大概率返回 false,导致面板整片空白。此时打开 Experts 日志不会报显式错误,只会看到 OnInit 静默退出。 CWndEvents::CompletedGUI() 只在进度条创建成功后才调用,用来通知框架 GUI 绘制完毕。换句话说,进度条建不起来,CompletedGUI 永远不会触发,你后面挂的按钮、标签也都不会显示。
if(!CreateProgressBar(class="num">2,class="num">3,"Processing...")) class="kw">return(class="kw">false); class=class="str">"cmt">//--- 完成 GUI 创建 CWndEvents::CompletedGUI(); class="kw">return(true); }
用 CFrameGenerator 接管优化帧
把优化过程中策略测试器吐出的帧数据接住,最干净的做法是单独写一个 CFrameGenerator 类,挂在 Program.mqh 里再由 FrameGenerator.mqh 包含。原文示例给了两条路:要么把图形对象指针直接传给类方法在界面上画,要么用独立方法把帧数据捞出来填给别的表格类——两条路不冲突,你按自己的面板结构挑一条当主路。 EasyAndFast 库借用了标准库的 CGraphic 做可视化,所以 FrameGenerator.mqh 里要 #include <Graphics\Graphic.mqh> 才能调到绘图方法。类里维护了 m_graph_balance 和 m_graph_results 两个图形指针,前者画优化余额的近期序列,后者画整体结果;正数渲染成绿色、负数红色,这是硬编码在 Update 方法里的。 帧的接收节奏由 OnTesterPassEvent() 控制:FrameNext() 返回 true 就继续吃帧,优化结束返回 false。每拿到一帧,用 FrameInputs() 取当前 EA 参数,SaveStatData() 把统计项按“名称=值”塞进字符串数组,UpdateBalanceGraph() / UpdateResultsGraph() 按帧计数器往 X 轴推点。注意结果不是补零铺满,而是沿当前帧序号追加,图形上的空隙不会映射成 0 值。 优化收尾的 TesterDeinit 事件里,并不是所有帧都来得及处理,画面可能缺一段。调用 FinalRecalculateFrames() 把帧列表指针拨回开头、计数清零,重跑一遍全部帧就能补全图形。SetCurvesTotal() 决定同屏显示几条余额序列,计数器到顶就回绕,序列名用帧计数器、颜色随盈亏走。 下面这段是类骨架里事件方法声明的节选,OnTesterInitEvent 那行高亮处就是把两个图形指针注入类的入口: #include <Graphics\Graphic.mqh> class CFrameGenerator {}; class CFrameGenerator { private: CGraphic *m_graph_balance; CGraphic *m_graph_results; public: void OnTesterEvent(const double on_tester_value); void OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_result); void OnTesterDeinitEvent(void); bool OnTesterPassEvent(void); }; 开 MT5 新建一个 include 文件照这个骨架补方法体,先只接 OnTesterInitEvent 把两个空 CGraphic 指针传进去,跑一次单 pass 优化看图形对象是否被成功绑定,比直接抄全套逻辑更容易排错。外汇与贵金属品种优化参数空间大、过拟合风险高,图形只是辅助观察,不代表某组参数实盘倾向盈利。
class="macro">#include <Graphics\Graphic.mqh> class CFrameGenerator { }; class CFrameGenerator { class="kw">private: CGraphic *m_graph_balance; CGraphic *m_graph_results; class="kw">public: class="type">void OnTesterEvent(class="kw">const class="type">class="kw">double on_tester_value); class="type">void OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_result); class="type">void OnTesterDeinitEvent(class="type">void); class="type">bool OnTesterPassEvent(class="type">void); };
「从成交历史反推余额曲线」
在 MT5 优化器里做可视化回测时,余额曲线不能靠实时账户状态拿,必须回头翻成交历史自己加出来。下面这段代码演示了如何在 CFrameGenerator 类里用 HistorySelect 把全部成交拉进来,逐笔累加得到 m_balance 数组。 核心逻辑是只处理三类节点:第一笔成交(i==0)、平仓类成交(DEAL_ENTRY_OUT)以及出入金类成交(DEAL_ENTRY_INOUT)。每笔把 profit、swap、commission 三项相加写进 balance_current,再 ArrayResize 扩栈存进 m_balance。注意 ArrayResize 的预留增量写死成 100000,对大多数品种回测够用,但超长历史可能频繁重分配,可以按需调小。 代码里 STAT_TOTAL 宏定义为 7,代表后续要统计的参数维度数量(如利润、回撤、 sharpe 类指标等),这是给优化结果帧预留的槽位。实际跑一遍:开 MT5 策略测试器,把这段塞进 OnTesterInit 之后的调用链,能在 Tester 图表里看到自己拼的余额路径,和外置报表比对验证一致性。外汇与贵金属杠杆高,历史拟合不等于实盘表现,该曲线仅用于优化筛选参考。
class=class="str">"cmt">//| 应在 OnTesterInit() 处理器中调用 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results) { m_graph_balance =graph_balance; m_graph_results =graph_results; } class=class="str">"cmt">//--- 统计参数数量 class="macro">#define STAT_TOTAL class="num">7 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 用于处理优化结果的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CFrameGenerator { class="kw">private: class=class="str">"cmt">//--- 结果余额 class="type">class="kw">double m_balance[]; class=class="str">"cmt">//--- class="kw">private: class=class="str">"cmt">//--- 接收余额数据 class="type">int GetBalanceData(class="type">void); class=class="str">"cmt">//--- 接收统计数据 class="type">void GetStatData(class="type">class="kw">double &dst_array[],class="type">class="kw">double on_tester_value); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取余额数据 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CFrameGenerator::GetBalanceData(class="type">void) { class="type">int data_count =class="num">0; class="type">class="kw">double balance_current =class="num">0; class=class="str">"cmt">//--- 请求所有交易历史 ::HistorySelect(class="num">0,LONG_MAX); class="type">uint deals_total=::HistoryDealsTotal(); class=class="str">"cmt">//--- 收集交易数据 for(class="type">uint i=class="num">0; i<deals_total; i++) { class=class="str">"cmt">//--- 接收单号 class="type">class="kw">ulong ticket=::HistoryDealGetTicket(i); if(ticket<class="num">1) class="kw">continue; class=class="str">"cmt">//--- 如果是初始余额或 out/inout 交易 class="type">long entry=::HistoryDealGetInteger(ticket,DEAL_ENTRY); if(i==class="num">0 || entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT) { class="type">class="kw">double swap =::HistoryDealGetDouble(ticket,DEAL_SWAP); class="type">class="kw">double profit =::HistoryDealGetDouble(ticket,DEAL_PROFIT); class="type">class="kw">double commision =::HistoryDealGetDouble(ticket,DEAL_COMMISSION); class=class="str">"cmt">//--- 计算余额 balance_current+=(profit+swap+commision); class=class="str">"cmt">//--- 保存到数组 data_count++; ::ArrayResize(m_balance,data_count,class="num">100000); m_balance[data_count-class="num">1]=balance_current; } } class=class="str">"cmt">//--- 获取数据量 class="kw">return(data_count); }
◍ 把回测统计塞进优化帧里
在 MT5 的遗传优化流程里,默认的 TesterStatistics 只能事后看报告,想让每一代参数组合的净盈利、回撤这些指标直接随帧传出来,就得在 EA 的 OnTester() 里手动打包。下面这段 CFrameGenerator 的成员函数就是干这个的:先扩一块数组,把余额曲线挪到 STAT_TOTAL 偏移之后,再把 7 个关键统计量写进前 7 个槽位。 GetStatData 里 dst_array[0] 到 [5] 分别落的是 STAT_PROFIT、STAT_PROFIT_FACTOR、STAT_RECOVERY_FACTOR、STAT_TRADES、STAT_DEALS、STAT_EQUITY_DDREL_PERCENT,[6] 留给你自己的优化目标值 on_tester_value。注意 ArrayResize 的长度是 m_balance 元素数加 STAT_TOTAL,偏移错位会直接让帧数据对不上。 OnTesterEvent 负责真正发帧:先 GetBalanceData() 拿点数,再调 GetStatData 填统计,最后 FrameAdd 以程序名为帧类别、ID 写 1 推给终端。若返回 false,GetLastError 会告诉你是不是帧通道满了。外汇与贵金属品种点差跳空频繁,回测帧里的恢复因子可能在实盘失真,验证时建议先跑单品种小周期。
class="type">void CFrameGenerator::GetStatData(class="type">class="kw">double &dst_array[],class="type">class="kw">double on_tester_value) { ::ArrayResize(dst_array,::ArraySize(m_balance)+STAT_TOTAL); ::ArrayCopy(dst_array,m_balance,STAT_TOTAL,class="num">0); class=class="str">"cmt">//--- 用测试结果填写第一个数组值 (STAT_TOTAL) dst_array[class="num">0] =::TesterStatistics(STAT_PROFIT); class=class="str">"cmt">// 净盈利 dst_array[class="num">1] =::TesterStatistics(STAT_PROFIT_FACTOR); class=class="str">"cmt">// 盈利因子 dst_array[class="num">2] =::TesterStatistics(STAT_RECOVERY_FACTOR); class=class="str">"cmt">// 恢复因子 dst_array[class="num">3] =::TesterStatistics(STAT_TRADES); class=class="str">"cmt">// 交易数量 dst_array[class="num">4] =::TesterStatistics(STAT_DEALS); class=class="str">"cmt">// 成交数量 dst_array[class="num">5] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); class=class="str">"cmt">// 最大资金回撤 % dst_array[class="num">6] =on_tester_value; class=class="str">"cmt">// 自定义优化条件值 } class="type">void CFrameGenerator::OnTesterEvent(class="kw">const class="type">class="kw">double on_tester_value) { class=class="str">"cmt">//--- 获取余额数据 class="type">int data_count=GetBalanceData(); class=class="str">"cmt">//--- 用于将数据发送到帧的数组 class="type">class="kw">double stat_data[]; GetStatData(stat_data,on_tester_value); class=class="str">"cmt">//--- 用数据创建一个帧并将其发送到终端 if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME),class="num">1,data_count,stat_data)) ::Print(__FUNCTION__," > 帧添加错误: ",::GetLastError()); else ::Print(__FUNCTION__," > 帧已添加"); }
把回测指标落进可传递的字符串数组
在 MT5 优化框架里,光算出净值、盈利因子这些数字没用,得先塞进一个能跨类传递的数组。下面这段 SaveStatData 做的就是这事:先从 m_data 拷出 STAT_TOTAL 长度的统计值到本地 stat[],再撑开 m_stat_data 准备接字符串。 代码里把 7 个核心指标固化为文本:净收益、盈利因子、复苏因子、订单数、成交数、权益回撤百分比,以及 OnTester 返回值。注意回撤那行用了 %.2f%% 双百分号,渲染出来才是「Equity DD=12.34%」这种带百分号的字面量。 外层另给了 CopyStatData,用 ArrayCopy 把 m_stat_data 整包丢给调用方的 dst_array[],返回拷贝元素数。优化器主程序拿到这串字符串,才可能拿去画散点或者喂给后续筛选。 RESERVE_FRAMES 宏写死 1000000,是给帧容器预留的上限,实际跑大规模遗传优化时若超这个量会触顶,自己调参时得盯着内存。
class="type">void CFrameGenerator::SaveStatData(class="type">void) { class=class="str">"cmt">//--- 接收帧统计参数的数组 class="type">class="kw">double stat[]; ::ArrayCopy(stat,m_data,class="num">0,class="num">0,STAT_TOTAL); ::ArrayResize(m_stat_data,STAT_TOTAL); class=class="str">"cmt">//--- 填充测试结果数组 m_stat_data[class="num">0] ="Net profit="+::StringFormat("%.2f",stat[class="num">0]); m_stat_data[class="num">1] ="Profit Factor="+::StringFormat("%.2f",stat[class="num">1]); m_stat_data[class="num">2] ="Factor Recovery="+::StringFormat("%.2f",stat[class="num">2]); m_stat_data[class="num">3] ="Trades="+::StringFormat("%G",stat[class="num">3]); m_stat_data[class="num">4] ="Deals="+::StringFormat("%G",stat[class="num">4]); m_stat_data[class="num">5] ="Equity DD="+::StringFormat("%.2f%%",stat[class="num">5]); m_stat_data[class="num">6] ="OnTester()="+::StringFormat("%G",stat[class="num">6]); } class CFrameGenerator { class="kw">public: class=class="str">"cmt">//--- 获取传递数组的统计参数 class="type">int CopyStatData(class="type">class="kw">string &dst_array[]) { class="kw">return(::ArrayCopy(dst_array,m_stat_data)); } }; class="macro">#define RESERVE_FRAMES class="num">1000000 class CFrameGenerator { class="kw">private: class="type">class="kw">ulong m_frames_counter; class="type">class="kw">double m_loss_x[]; class="type">class="kw">double m_loss_y[]; class="type">class="kw">double m_profit_x[]; class="type">class="kw">double m_profit_y[]; class="kw">private: class="type">void AddLoss(class="kw">const class="type">class="kw">double loss); class="type">void AddProfit(class="kw">const class="type">class="kw">double profit); };
「把每笔盈亏喂给可视化曲线」
在自研的帧生成器里,亏损和盈利被拆成两套数组分开存。AddLoss 与 AddProfit 逻辑对称:先取当前数组长度,按 RESERVE_FRAMES 预留空间扩容,再把数值写入 y 数组,x 坐标统一用 m_frames_counter 的整型强转双精度,保证横轴是帧序号而非时间。
class="type">void CFrameGenerator::AddLoss(class="kw">const class="type">class="kw">double loss) { class="type">int size=::ArraySize(m_loss_y); ::ArrayResize(m_loss_y,size+class="num">1,RESERVE_FRAMES); ::ArrayResize(m_loss_x,size+class="num">1,RESERVE_FRAMES); m_loss_y[size] =loss; m_loss_x[size] =(class="type">class="kw">double)m_frames_counter; } class="type">void CFrameGenerator::AddProfit(class="kw">const class="type">class="kw">double profit) { class="type">int size=::ArraySize(m_profit_y); ::ArrayResize(m_profit_y,size+class="num">1,RESERVE_FRAMES); ::ArrayResize(m_profit_x,size+class="num">1,RESERVE_FRAMES); m_profit_y[size] =profit; m_profit_x[size] =(class="type">class="kw">double)m_frames_counter; }
class="type">void CFrameGenerator::UpdateResultsGraph(class="type">void) { if(m_data[class="num">0]<class="num">0) AddLoss(m_data[class="num">0]); else AddProfit(m_data[class="num">0]); CCurve *curve=m_graph_results.CurveGetByIndex(class="num">0); curve.Name("P: "+(class="type">class="kw">string)ProfitsTotal()); curve.Update(m_profit_x,m_profit_y); curve=m_graph_results.CurveGetByIndex(class="num">1); curve.Name("L: "+(class="type">class="kw">string)LossesTotal()); curve.Update(m_loss_x,m_loss_y); CAxis *x_axis=m_graph_results.XAxis(); x_axis.Min(class="num">0); x_axis.Max(m_frames_counter); x_axis.DefaultStep((class="type">int)(m_frames_counter/class="num">8.0)); m_graph_results.CalculateMaxMinValues(); m_graph_results.CurvePlotAll(); m_graph_results.Update(); }
class="type">void CFrameGenerator::AddLoss(class="kw">const class="type">class="kw">double loss) { class="type">int size=::ArraySize(m_loss_y); ::ArrayResize(m_loss_y,size+class="num">1,RESERVE_FRAMES); ::ArrayResize(m_loss_x,size+class="num">1,RESERVE_FRAMES); m_loss_y[size] =loss; m_loss_x[size] =(class="type">class="kw">double)m_frames_counter; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 添加正数结果至数组 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::AddProfit(class="kw">const class="type">class="kw">double profit) { class="type">int size=::ArraySize(m_profit_y); ::ArrayResize(m_profit_y,size+class="num">1,RESERVE_FRAMES); ::ArrayResize(m_profit_x,size+class="num">1,RESERVE_FRAMES); m_profit_y[size] =profit; m_profit_x[size] =(class="type">class="kw">double)m_frames_counter; } class CFrameGenerator { class="kw">private: class=class="str">"cmt">//--- 更新结果图形 class="type">void UpdateResultsGraph(class="type">void); class=class="str">"cmt">//--- 更新余额图形 class="type">void UpdateBalanceGraph(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 更新结果图形 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CFrameGenerator::UpdateResultsGraph(class="type">void) { class=class="str">"cmt">//--- 负数结果 if(m_data[class="num">0]<class="num">0) AddLoss(m_data[class="num">0]); class=class="str">"cmt">//--- 正数结果 else AddProfit(m_data[class="num">0]); class=class="str">"cmt">//--- 在优化结果图形上更新序列 CCurve *curve=m_graph_results.CurveGetByIndex(class="num">0); curve.Name("P: "+(class="type">class="kw">string)ProfitsTotal()); curve.Update(m_profit_x,m_profit_y); class=class="str">"cmt">//--- curve=m_graph_results.CurveGetByIndex(class="num">1); curve.Name("L: "+(class="type">class="kw">string)LossesTotal()); curve.Update(m_loss_x,m_loss_y); class=class="str">"cmt">//--- 水平轴属性 CAxis *x_axis=m_graph_results.XAxis(); x_axis.Min(class="num">0); x_axis.Max(m_frames_counter); x_axis.DefaultStep((class="type">int)(m_frames_counter/class="num">8.0)); class=class="str">"cmt">//--- 更新图形 m_graph_results.CalculateMaxMinValues(); m_graph_results.CurvePlotAll(); m_graph_results.Update(); } class CFrameGenerator { class="kw">private: class=class="str">"cmt">//--- 序列数量 class="type">uint m_curves_total; class=class="str">"cmt">//--- 图上的当前序列索引 class="type">uint m_last_serie_index; class=class="str">"cmt">//--- 定义最大序列 class="type">class="kw">double m_curve_max[]; class=class="str">"cmt">//--- class="kw">public: class=class="str">"cmt">//--- 设置要在图表上显示的序列数量 class="type">void SetCurvesTotal(class="kw">const class="type">uint total); };