在 MetaTrader 5 中交易策略优化的可视化·进阶篇
📘

在 MetaTrader 5 中交易策略优化的可视化·进阶篇

第 2/3 篇

◍ 进度条初始化失败即中断

在 MT5 自定义指标或 EA 的 GUI 构建流程里,进度条不是装饰件,而是阻塞式初始化的第一道闸门。上面这段片段里,CreateProgressBar(2,3,"Processing...") 尝试在第 2 行第 3 列画一个带「Processing...」文案的进度条;若返回 false,函数直接 return(false),后续窗口事件绑定全部跳过。 实际在 MT5 终端跑这段代码时,若图表窗口宽度不足以容纳 3 列布局,CreateProgressBar 大概率返回 false,导致面板整片空白。此时打开 Experts 日志不会报显式错误,只会看到 OnInit 静默退出。 CWndEvents::CompletedGUI() 只在进度条创建成功后才调用,用来通知框架 GUI 绘制完毕。换句话说,进度条建不起来,CompletedGUI 永远不会触发,你后面挂的按钮、标签也都不会显示。

MQL5 / C++
  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 优化看图形对象是否被成功绑定,比直接抄全套逻辑更容易排错。外汇与贵金属品种优化参数空间大、过拟合风险高,图形只是辅助观察,不代表某组参数实盘倾向盈利。

MQL5 / C++
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 图表里看到自己拼的余额路径,和外置报表比对验证一致性。外汇与贵金属杠杆高,历史拟合不等于实盘表现,该曲线仅用于优化筛选参考。

MQL5 / C++
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 会告诉你是不是帧通道满了。外汇与贵金属品种点差跳空频繁,回测帧里的恢复因子可能在实盘失真,验证时建议先跑单品种小周期。

MQL5 / C++
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,是给帧容器预留的上限,实际跑大规模遗传优化时若超这个量会触顶,自己调参时得盯着内存。

MQL5 / C++
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 的整型强转双精度,保证横轴是帧序号而非时间。

MQL5 / C++
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;
  }
逐行拆解:ArraySize 拿旧长度;ArrayResize 第二个参是目标大小、第三个 RESERVE_FRAMES 是内存预分配粒度,避免逐笔 realloc 拖慢回测;m_loss_x / m_profit_x 写入的是帧计数,意味着图上横轴刻度为 0、1、2… 的离散帧,而不是时间戳。 UpdateResultsGraph 里用 m_data[0] 的正负做分流:小于 0 调 AddLoss,否则 AddProfit。随后从结果图取两条曲线,索引 0 改名 "P: "+ProfitsTotal()、索引 1 改名 "L: "+LossesTotal(),分别 Update 对应的 x/y 数组。
MQL5 / C++
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();
  }
横轴范围被钉死在 0 到 m_frames_counter,步长取帧数除 8.0 后整型化,所以一千帧时网格大约每 125 帧一条线。最后三步重算极值、重绘全部曲线、刷新图形——开 MT5 把 m_frames_counter 打到 500 以上,能直观看到 P/L 双线随帧展开,外汇与贵金属回测中这类图形对识别连亏段有帮助,但样本外的分布可能明显偏移,属高风险观察手段。 类里还留了 m_curves_total 与 m_last_serie_index 控制序列上限,m_curve_max[] 存各序列峰值,SetCurvesTotal 供外部限制图上曲线数,避免帧累积后图形过载。

MQL5 / C++
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);
   };

常见问题

先确认优化帧的初始化返回值,非0才表示成功;若返回错误码就别继续跑,先修输入参数或内存分配再重启任务。
可以用 CFrameGenerator 这类封装类接管优化帧,在 OnTesterInit 里挂上实例,每帧回调里取数据自己绘到独立窗口。
可以,小布能读取成交历史并反推余额曲线,你只需把品种和回测区间告诉它,图就直接生成在对应页面。
把统计值格式化成字符串写进优化帧的自定义字段,后续在帧数组里按ID提取就能横向比不同参数组。
把每笔盈亏按顺序写进可传递的字符串数组,再在可视化模块解析成点序列,就能画出逐笔盈亏曲线。