在 MetaTrader 5 中交易策略优化的可视化·综合运用
📊

在 MetaTrader 5 中交易策略优化的可视化·综合运用

(3/3)·单一优化图看不出策略强弱?本篇把图形界面、帧数据处理与结果同屏一次性串起来

进阶 第 3/3 篇
只盯着优化器自带的单一图形做参数筛选,容易漏掉回撤结构和曲线稳定性。把多组余额曲线叠在同一画面里对比,才可能看出哪组参数只是运气好。

在优化回传里实时画出余额曲线

做 MT5 批量优化时,默认报告只给一张最终散点图,过程中每个 pass 的余额序列其实是被丢弃的。下面这段类方法把每一帧的余额数组抓出来,直接塞进图表对象的曲线缓冲里,让你在优化跑完前就能肉眼看哪组参数更平滑。 核心入口是 OnTesterPassEvent,它靠 FrameNext 拉取当前帧,再用 FrameInputs 把该 pass 的 EA 输入参数存进 m_param_data。拿到数据后调 UpdateBalanceGraph 刷新图形,m_frames_counter 自增标记已处理帧数。 UpdateBalanceGraph 里有个细节容易踩坑:曲线索引 m_last_serie_index 是循环复用的。一旦 m_last_serie_index 达到 m_curves_total 就归零,所以图表上永远只保留最近 N 条序列,早前的会被覆盖。 水平轴范围按所有序列里最长那条定,x_axis.Max(x_max) 中 x_max 来自 m_curve_max 数组的 fmax 比较,DefaultStep 取 x_max/8.0 向下整。外汇与贵金属优化涉及高杠杆与滑点,回测曲线好看不代表实盘概率占优,请用小资金验证。

MQL5 / C++
class=class="str">"cmt">//| 设置要在图表上显示的序列数量                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::SetCurvesTotal(class="kw">const class="type">uint total)
  {
   m_curves_total=total;
   ::ArrayResize(m_curve_max,total);
   ::ArrayInitialize(m_curve_max,class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 更新余额图                                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::UpdateBalanceGraph(class="type">void)
  {
class=class="str">"cmt">//--- 接收当前帧的余额值的数组
   class="type">class="kw">double serie[];
   ::ArrayCopy(serie,m_data,class="num">0,STAT_TOTAL,::ArraySize(m_data)-STAT_TOTAL);
class=class="str">"cmt">//--- 发送数组以便在余额图上显示
   CCurve *curve=m_graph_balance.CurveGetByIndex(m_last_serie_index);
   curve.Name((class="type">class="kw">string)m_frames_counter);
   curve.Color((m_data[class="num">0]>=class="num">0)? ::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed));
   curve.Update(serie);
class=class="str">"cmt">//--- 获取序列大小
   class="type">int serie_size=::ArraySize(serie);
   m_curve_max[m_last_serie_index]=serie_size;
class=class="str">"cmt">//--- 定义具有最大元素数两的序列
   class="type">class="kw">double x_max=class="num">0;
   for(class="type">uint i=class="num">0; i<m_curves_total; i++)
      x_max=::fmax(x_max,m_curve_max[i]);
class=class="str">"cmt">//--- 水平轴属性
   CAxis *x_axis=m_graph_balance.XAxis();
   x_axis.Min(class="num">0);
   x_axis.Max(x_max);
   x_axis.DefaultStep((class="type">int)(x_max/class="num">8.0));
class=class="str">"cmt">//--- 更新结果图
   m_graph_balance.CalculateMaxMinValues();
   m_graph_balance.CurvePlotAll();
   m_graph_balance.Update();
class=class="str">"cmt">//--- 增加序列计数器
   m_last_serie_index++;
class=class="str">"cmt">//--- 如果达到极限,请将序列计数器设置为零
   if(m_last_serie_index>=m_curves_total)
      m_last_serie_index=class="num">0;
  }
class CFrameGenerator
  {
class="kw">private:
   class=class="str">"cmt">//--- EA 参数
   class="type">class="kw">string            m_param_data[];
   class="type">uint              m_par_count;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在优化过程中接收数据帧并显示图形                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CFrameGenerator::OnTesterPassEvent(class="type">void)
  {
class=class="str">"cmt">//--- 获取新帧后,尝试从中提取数据
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      class=class="str">"cmt">//--- 获取所形成帧的 EA 输入参数
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      class=class="str">"cmt">//--- 将结果统计参数保存到数组中
      SaveStatData();
      class=class="str">"cmt">//--- 更新结果和余额图
      UpdateResultsGraph();
      UpdateBalanceGraph();
      class=class="str">"cmt">//--- 增加处理帧的计数器
      m_frames_counter++;
      class="kw">return(true);
     }
class=class="str">"cmt">//---

「帧数据回收与终算的落地写法」

在 MT5 优化器跑完一整轮参数后,CFrameGenerator 必须先把手里四块动态数组清空,否则上一轮残留的盈亏坐标会混进新图。ArraysFree 直接调全局 ArrayFree 把 m_loss_y、m_loss_x、m_profit_y、m_profit_x 全部释放,这一步放在 FinalRecalculateFrames 开头,保证重算前画布是干净的。 FinalRecalculateFrames 的逻辑很直白:FrameFirst 把帧指针拨回开头,ArraysFree 清场,m_frames_counter 归零,然后 while(FrameNext(...)) 逐帧读 m_data[0],负数进 AddLoss、非负数进 AddProfit,同时帧计数器自增。循环结束时就拿到了本轮全部样本的分布。 图形更新那段给了可验证的锚点:X 轴 Max 设为 m_frames_counter,DefaultStep 用 (int)(m_frames_counter/8.0) 算,也就是把横轴大致切成 8 格。你在 MT5 策略测试器里跑完优化,打开结果图应该看到横轴刻度约 8 段,曲线名带 "P: " 和 "L: " 前缀显示总盈总亏。 OnTesterDeinitEvent 是挂接点,必须在 OnTesterDeinit() 里调用,它跑完 FinalRecalculateFrames 后把 m_frames_counter 存进 m_frames_total 并清零,顺手把 m_last_serie_index 置 0。外汇与贵金属品种测试受点差滑点影响大,这类重算结果仅反映历史样本分布,实盘存在较高风险,参数过拟合概率不低。

MQL5 / C++
  class="kw">return(class="kw">false);
  }
class CFrameGenerator
  {
class="kw">private:
  class=class="str">"cmt">//--- 释放数组
  class="type">void              ArraysFree(class="type">void);
  class=class="str">"cmt">//--- 优化后所有帧的最终数据重新计算
  class="type">void              FinalRecalculateFrames(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 释放数组                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::ArraysFree(class="type">void)
  {
  ::ArrayFree(m_loss_y);
  ::ArrayFree(m_loss_x);
  ::ArrayFree(m_profit_y);
  ::ArrayFree(m_profit_x);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 优化后所有帧的最终数据重新计算                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::FinalRecalculateFrames(class="type">void)
  {
class=class="str">"cmt">//--- 将帧指针设置到开始
  ::FrameFirst();
class=class="str">"cmt">//--- 重置计数器和数组
  ArraysFree();
  m_frames_counter=class="num">0;
class=class="str">"cmt">//--- 启动循环遍历帧
  class="kw">while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
    {
    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">//--- 增加处理帧的计数器
    m_frames_counter++;
    }
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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 应在 OnTesterDeinit() 处理器中调用                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CFrameGenerator::OnTesterDeinitEvent(class="type">void)
  {
class=class="str">"cmt">//--- 优化后最终重新计算所有帧的数据
  FinalRecalculateFrames();
class=class="str">"cmt">//--- 记住总帧数并将计数器设置为零
  m_frames_total     =m_frames_counter;
  m_frames_counter   =class="num">0;
  m_last_serie_index =class="num">0;
  }

◍ 把优化数据锁进界面与帧生成器

图形界面在测试初始化阶段由 CProgram::OnTesterInitEvent() 建好,随后必须让界面处于不可操作状态,否则优化过程中的用户输入会污染结果。具体靠 IsAvailableGUI() 与 IsLockedGUI() 两个私有方法控制窗口、休眠毫秒框、曲线数量框和重演按钮的可用与锁定。 帧生成器初始化时接收两个图形指针(m_graph1、m_graph2),用来把每帧优化结果画出来。统计表和参数表共用一套更新逻辑,以 '=' 分隔参数名与值,循环拆成两元素数组后写进单元格,方法分别是 UpdateStatTable() 和 UpdateParamTable()。 OnTesterPassEvent() 里收到帧生成器返回正数时才刷表;优化结束的 CalculateProfitsAndLosses() 会算正负结果占比并推到状态栏。注意当前 MT5 版本在优化完成时无法开启定时器,所以光标跟踪和计时器初始化虽写了,暂停滚动仍只能靠 Sleep 输入字段手动调。 用户改输入字段或点重演帧按钮,会触发 UpdateBalanceGraph() 重设序列数并刷新图;CFrameGenerator::ReplayFrames() 从零指针重放帧,返回 true 表示有帧、false 表示放完。外汇与贵金属优化空间大、过拟合概率高,建议先在历史数据跑小样本验证界面逻辑再上实盘参数。

MQL5 / C++
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 界面可用性
   class="type">void                IsAvailableGUI(class="kw">const class="type">bool state);
   class="type">void                IsLockedGUI(class="kw">const class="type">bool state);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 优化开始事件                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnTesterInitEvent(class="type">void)
  {
class=class="str">"cmt">//--- 创建图形界面
   if(!CreateFrameModeGUI())
     {
      ::Print(__FUNCTION__," > 不能创建图形界面!");
      class="kw">return;
     }
class=class="str">"cmt">//--- 令界面无法访问
   IsLockedGUI(class="kw">false);
class=class="str">"cmt">//--- 初始化帧生成器
   m_frame_gen.OnTesterInitEvent(m_graph1.GetGraphicPointer(),m_graph2.GetGraphicPointer());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 界面可用性                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::IsAvailableGUI(class="kw">const class="type">bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsAvailable(state);
   m_curves_total.IsAvailable(state);
   m_reply_frames.IsAvailable(state);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 阻塞界面                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::IsLockedGUI(class="kw">const class="type">bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsLocked(!state);
   m_curves_total.IsLocked(!state);
   m_reply_frames.IsLocked(!state);
  }
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 更新统计表
   class="type">void                UpdateStatTable(class="type">void);
   class=class="str">"cmt">//--- 更新参数表
   class="type">void                UpdateParamTable(class="type">void);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

把优化帧塞进统计表与盈亏比率

回测通关事件 OnTesterPassEvent 里只做一件事:让帧生成器先消化测试结果,成功就调 UpdateStatTable 和 UpdateParamTable。这两行是 GUI 实时刷新的触发点,漏掉任一个,面板数据就会停在上一帧。 UpdateStatTable 先通过 CopyStatData 拿到字符串数组,总长度由返回值 total 决定。每条记录用 '=' 切分,StringSplit 返回 2 才认为合法;CurrentFrame 大于 1 时只写第二列(数值),否则连标题带数值一起写第 0、1 列。 盈亏比率在 CalculateProfitsAndLosses 里算:losses 和 profits 分别取自帧生成器的 LossesTotal、ProfitsTotal,各自除以 FramesTotal 再乘 100,保留两位小数。状态栏会显示类似「Profits: 63 (31.50%) / Losses: 137 (68.50%)」——外汇与贵金属回测中这类胜率分布偏向亏损侧是常见现象,仅反映样本内概率,实盘高风险依旧。 若帧总数为 0 直接 return,避免除零。开 MT5 把这段挂到自己的 CWndEvents 派生类里,跑一次优化就能在状态栏看到真实盈亏占比。

MQL5 / C++
class="type">void CProgram::UpdateStatTable(class="type">void)
  {
class=class="str">"cmt">//--- 获取统计表的数据数组
   class="type">class="kw">string stat_data[];
   class="type">int total=m_frame_gen.CopyStatData(stat_data);
   for(class="type">int i=class="num">0; i<total; i++)
     {
      class=class="str">"cmt">//--- 分成两行并输入到表格
      class="type">class="kw">string array[];
      if(::StringSplit(stat_data[i],&class="macro">#x27;=&class="macro">#x27;,array)==class="num">2)
        {
         if(m_frame_gen.CurrentFrame()>class="num">1)
            m_table_stat.SetValue(class="num">1,i,array[class="num">1],class="num">0,true);
         else
           {
            m_table_stat.SetValue(class="num">0,i,array[class="num">0],class="num">0,true);
            m_table_stat.SetValue(class="num">1,i,array[class="num">1],class="num">0,true);
           }
        }
     }
class=class="str">"cmt">//--- 更新表格
   m_table_stat.Update();
  }
class="type">void CProgram::OnTesterPassEvent(class="type">void)
  {
class=class="str">"cmt">//--- 处理获得的测试结果并显示图形
   if(m_frame_gen.OnTesterPassEvent())
     {
      UpdateStatTable();
      UpdateParamTable();
     }
  }
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   class=class="str">"cmt">//--- 计算正数和负数结果的比率
   class="type">void              CalculateProfitsAndLosses(class="type">void);
  }
class="type">void CProgram::CalculateProfitsAndLosses(class="type">void)
  {
class=class="str">"cmt">//--- 如果没有帧,退出
   if(m_frame_gen.FramesTotal()<class="num">1)
      class="kw">return;
class=class="str">"cmt">//--- 负数和正数结果的数量
   class="type">int losses  =m_frame_gen.LossesTotal();
   class="type">int profits =m_frame_gen.ProfitsTotal();
class=class="str">"cmt">//--- 百分比
   class="type">class="kw">string pl =::DoubleToString(((class="type">class="kw">double)losses/(class="type">class="kw">double)m_frame_gen.FramesTotal())*class="num">100,class="num">2);
   class="type">class="kw">string pp =::DoubleToString(((class="type">class="kw">double)profits/(class="type">class="kw">double)m_frame_gen.FramesTotal())*class="num">100,class="num">2);;
class=class="str">"cmt">//--- 显示在状态栏中
   m_status_bar.SetValue(class="num">1,"Profits: "+(class="type">class="kw">string)profits+" ("+pp+"%)"+" / Losses: "+(class="type">class="kw">string)losses+" ("+pl+"%)");

「优化收尾时把净值曲线重绘出来」

策略优化跑完,MT5 会触发 OnTesterDeinitEvent。这里先让框架做收尾(m_frame_gen.OnTesterDeinitEvent),再把 GUI 锁打开(IsLockedGUI(true)),随后算盈利与亏损次数的比值(CalculateProfitsAndLosses),最后用 CWndEvents::InitializeCore() 把界面核心重新拉起,否则优化后面板可能点不动。 净值图不是优化完就自动刷新的。UpdateBalanceGraph 先读 m_curves_total 的数值决定画几条曲线,再把旧曲线按索引从后往前全删掉(graph.CurveRemoveByIndex),接着用空 data 数组补回对应数量的 CURVE_LINES,最后 CurvePlotAll + Update 才出图。 界面交互上,自定义按钮点击(CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)和输入框结束编辑(CHARTEVENT_CUSTOM+ON_END_EDIT)都会判断 lparam 是否等于 m_curves_total.Id(),是就重绘曲线。也就是说你在面板上改“曲线数量”那个框,松手即触发重画,不用重启 EA。 外汇与贵金属品种做这类优化回测波动剧烈、滑点随机,重画出的曲线仅反映历史参数表现,实盘复制可能显著偏离。

MQL5 / C++
m_status_bar.GetItemPointer(class="num">1).Update(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 优化完成事件                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnTesterDeinitEvent(class="type">void)
  {
class=class="str">"cmt">//--- 优化完成
   m_frame_gen.OnTesterDeinitEvent();
class=class="str">"cmt">//--- 令界面可访问
   IsLockedGUI(true);
class=class="str">"cmt">//--- 计算正数和负数结果的比率
   CalculateProfitsAndLosses();
class=class="str">"cmt">//--- 初始化图形界面核心
   CWndEvents::InitializeCore();
  }
class CProgram : class="kw">public CWndEvents
  {
class="kw">private:
   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 CProgram::UpdateBalanceGraph(class="type">void)
  {
class=class="str">"cmt">//--- 设置工作序列的数量
   class="type">int curves_total=(class="type">int)m_curves_total.GetValue();
   m_frame_gen.SetCurvesTotal(curves_total);
class=class="str">"cmt">//--- 删除序列
   CGraphic *graph=m_graph1.GetGraphicPointer();
   class="type">int total=graph.CurvesTotal();
   for(class="type">int i=total-class="num">1; i>=class="num">0; i--)
      graph.CurveRemoveByIndex(i);
class=class="str">"cmt">//--- 添加序列
   class="type">class="kw">double data[];
   for(class="type">int i=class="num">0; i<curves_total; i++)
      graph.CurveAdd(data,CURVE_LINES,"");
class=class="str">"cmt">//--- 更新结果图
   graph.CurvePlotAll();
   graph.Update();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理器                                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnEvent(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)
  {
class=class="str">"cmt">//--- 按钮触发事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      class=class="str">"cmt">//--- 更改图表上的序列数量
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         class="kw">return;
        }
      class="kw">return;
     }
class=class="str">"cmt">//--- 在输入字段中输入数值的事件
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      class=class="str">"cmt">//--- 更改图表上的序列数量
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         class="kw">return;
        }
      class="kw">return;
     }
  }
class CFrameGenerator
  {
class="kw">public:
   class=class="str">"cmt">//--- 循环遍历帧

◍ 优化后逐帧重演怎么落地

MT5 策略测试器跑完遗传优化,结果不是一次性吐出,而是按帧(frame)流式回放。CFrameGenerator::ReplayFrames() 就是这套重演的引擎:每次调用推进一帧,返回 true 表示还有后续,返回 false 表示全部帧走完。 帧计数器小于 1 时先释放旧数组并调用 ::FrameFirst() 回到开头;随后 ::FrameNext() 取出当前帧的 pass、name、id、value、data,再用 ::FrameInputs() 拿该帧对应的 EA 输入参数,SaveStatData() 落统计,UpdateResultsGraph() 和 UpdateBalanceGraph() 刷两张图。m_frames_counter 每帧加 1。 外层 ViewOptimizationResults() 用 while(m_frame_gen.ReplayFrames() && !::IsStopped()) 把帧一帧帧播完。暂停时长由 m_sleep_ms 控件值决定,转成 int 后传给 ::Sleep(pause),默认节奏由你调滑块控制。进度条实时显示 Replay frames: 当前/总数,例如跑 5000 帧优化时就是 1/5000 到 5000/5000。 重演结束清零 m_frames_counter 与 m_last_serie_index,调用 CalculateProfitsAndLosses() 算盈亏帧比,最后把 GUI 解禁。外汇与贵金属品种优化重演受点差滑点影响大,同一帧在不同经纪商环境可能倾向不同结果,建议本地多券商数据各跑一遍验证。

MQL5 / C++
class="type">bool CFrameGenerator::ReplayFrames(class="type">void)
  {
class=class="str">"cmt">//--- 将帧指针设置为开头
   if(m_frames_counter<class="num">1)
     {
       ArraysFree();
       ::FrameFirst();
     }
class=class="str">"cmt">//--- 启动循环遍历帧
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
       class=class="str">"cmt">//--- 获取 EA 输入,哪个帧已经形成
       ::FrameInputs(m_pass,m_param_data,m_par_count);
       class=class="str">"cmt">//--- 将统计结果参数保存到数组中
       SaveStatData();
       class=class="str">"cmt">//--- 更新结果和余额图
       UpdateResultsGraph();
       UpdateBalanceGraph();
       class=class="str">"cmt">//--- 增加处理帧的计数器
       m_frames_counter++;
       class="kw">return(true);
     }
class=class="str">"cmt">//--- 循环完成
   m_frames_counter   =class="num">0;
   m_last_serie_index =class="num">0;
   class="kw">return(class="kw">false);
  }

class="type">void CProgram::ViewOptimizationResults(class="type">void)
  {
class=class="str">"cmt">//--- 令界面不可访问
   IsAvailableGUI(class="kw">false);
class=class="str">"cmt">//--- 暂停
   class="type">int pause=(class="type">int)m_sleep_ms.GetValue();
class=class="str">"cmt">//--- 播放帧
   class="kw">while(m_frame_gen.ReplayFrames() && !::IsStopped())
     {
       class=class="str">"cmt">//--- 更新表格
       UpdateStatTable();
       UpdateParamTable();
       class=class="str">"cmt">//--- 更新进度条
       m_progress_bar.Show();
       m_progress_bar.LabelText("Replay frames: "+class="type">class="kw">string(m_frame_gen.CurrentFrame())+"/"+class="type">class="kw">string(m_frame_gen.FramesTotal()));
       m_progress_bar.Update((class="type">int)m_frame_gen.CurrentFrame(),(class="type">int)m_frame_gen.FramesTotal());
       class=class="str">"cmt">//--- 暂停
       ::Sleep(pause);
     }
class=class="str">"cmt">//--- 计算正数和负数结果的比率
   CalculateProfitsAndLosses();
class=class="str">"cmt">//--- 隐藏进度条
   m_progress_bar.Hide();
class=class="str">"cmt">//--- 令界面可用
   IsAvailableGUI(true);
   m_reply_frames.MouseFocus(class="kw">false);
   m_reply_frames.Update(true);
  }

在自定义事件里捞回优化结果

EA 跑完框架优化后,结果不会自己跳出来,得靠图表自定义事件把数据端喂回来。上面这段把按钮点击映射成 CHARTEVENT_CUSTOM+ON_CLICK_BUTTON,一旦 lparam 对上 m_reply_frames.Id(),就调 ViewOptimizationResults() 把缓存的优化帧刷到面板上。 这种写法把「算完」和「看结果」拆成两个时刻,实盘里你能边盯价边等回测帧,不必阻塞主线程。MT5 的事件 id 从 CHARTEVENT_CUSTOM(1000) 起算,自定义偏移量撞车会静默丢失事件,调试时建议先 Print(id) 确认区间。 外汇与贵金属杠杆高、滑点跳变频繁,回测帧里的参数在活盘可能只具备概率优势,别直接当下单依据。

MQL5 / C++
class="type">void CProgram::OnEvent(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)
  {
class=class="str">"cmt">//--- 按下按钮的事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
    {
      class=class="str">"cmt">//--- 查看优化结果 
      if(lparam==m_reply_frames.Id())
        {
         ViewOptimizationResults();
         class="kw">return;
        }
      class=class="str">"cmt">//--- 
      ...
      class="kw">return;
    }
  }

「原版EA跑出来到底什么样」

用 MT5 标准库里自带的 Moving Average EA 做基线验证,代码按原样加载,不调参不改装。策略类写在 Program.mqh,FormatString.mqh 仅作行格式化辅助函数,不属于任何类,在依赖图里以黑箭头标记。 测试器直接挂 EURUSD H1,区间 2017.01.01–2018.01.01。跑完优化可见结果信息量够用,但 95.23% 的优化组合是负收益;把样本区间拉长,亏损比例还会继续恶化。做策略开发时,多数参数组合应为正才是可用前提,否则这个算法大概率只会持续失血,不该上实盘。 换 MACD Sample.mq5 再测,它本来就是类封装,小改后直接挂进同一套应用框架。品种周期不变,时间范围扩到 2010.01.01–2018.01.01,优化结果反转:正面组合占到 90.89%。 外汇/贵金属品种跳空与滑点风险高,回测顺滑不等于实盘能复现。参数优化在大数据量下可能耗时很久,不必盯屏;跑完点「重演帧」用加速模式回看,序列上限设 25 条就能快速扫完结果。

◍ 画得少,看得清

基于 EasyAndFast 函数库搭的图形界面,把优化帧的接收和分析做成了可视化的现成环境,省掉了自己写面板的麻烦。 目前这套方案有两个绕不开的坑:帧处理模式跑完优化后计时器起不来,限制了同界面的后续用法;从图表撤掉 EA 时 OnDeinit() 里的逆初始化不触发,事件链容易乱。这两点大概率要等 MT5 后续版本才补。 外汇和贵金属品种波动大、杠杆高,用这类工具做优化验证时先在小周期样本里跑通事件逻辑,比直接上实盘稳妥。

把多曲线同屏交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到优化后的多曲线对比视图,你只需判断哪组参数更耐打。

常见问题

单次优化图只给最终态,重复播放帧数据能还原参数在每一遍测试里的演进,倾向暴露过拟合段。
可以,小布盯盘的品种页已内置多曲线同屏模块,导入测试报告即可生成对比图,省去自己写GUI。
它把控件窗体、表格和绘图封装成类,开发者在CProgram里调方法即可,不必从零处理WinAPI层。
把解析和绘图拆到独立文件,控制重复显示速度字段,降低重绘频率,概率上能缓解卡顿。