100 个最佳优化递次(第 1 部分)。 开发优化分析器(基础篇)
📘

100 个最佳优化递次(第 1 部分)。 开发优化分析器(基础篇)

第 1/3 篇

◍ 优化分析器的骨架怎么搭

想把 MT5 的策略优化从「跑完看报表」升级成「可回溯可比对」,第一步是给优化过程建一个分析器结构。原文给出的思路是把每轮递次优化的参数组合、净值曲线与回测指标落进本地数据库,而不是只依赖测试者终端的一次性结果输出。 这套结构至少包含四块:图形层负责把多组优化结果画成可叠加的曲线;数据库层存原始递次数据;计算层做夏普、回撤、递次间差异的统计;演播器层则按递次顺序回放,方便肉眼抓参数敏感度。2018 年那篇原帖里,作者用这套架子在 100 组递次优化里筛参数,终端测试者模块跑完一轮约产生数千行结果,手工比对基本不可行。 对外汇与贵金属这类高杠杆品种,优化结果过拟合概率偏高,任何递次筛选都只是「倾向」缩小候选集,不能直接当实盘依据。先把分析器跑通,后面才有资格谈哪 100 个递次值得留。

为什么需要给 MT5 优化补一层 GUI

手工交易时代那套场内手语早就进了博物馆,个人电脑把在线交易搬进书房后,算法单已经吃掉市场过半成交量,MT5 是其中最顺手的终端之一。但它原生的优化与评估流程,对家用机用户并不友好。 在不算强的家用 PC 上,一次 EA 优化可能拖成几天甚至几周。更糟的是跑完才发现参数根本不对——除了递次统计和几个评估比率,什么都没落着。原文提到的 EasyAndFastGUI 库纯用 MQL5 写成,目的就是补这块短板:把每轮优化的完备统计、条件过滤、与买入持有策略的对照,以及成交级历史落盘都做进去。 有些开发者迷恋「圣杯」,回测里月赚 1000% 以上的机器人不少见。可把机器人实际的手数管理(加仓多买、减仓少买)算进买入持有对照曲线后,这类夸张收益往往只是杠杆幻觉,并未真正跑赢市场。更客观的做法是单列「单手交易」PL 图——机器人和买入持有都按最小交易量绘线,剔除仓位变形干扰。 横盘策略在趋势段亏、震荡段赚,这类行情依赖型 EA 还得能按日期切片看分段比率;前瞻性测试在标准报告里常被画成历史曲线的延续,新手极易误读成「利润回吐后修复」。该库按优化类型(前瞻 / 历史)分开审数据,能少踩这类坑。外汇与贵金属杠杆高、滑点跳空频繁,任何优化结论都只是概率倾向,实盘前务必在 MT5 策略测试器复跑验证。

「解耦的优化分析器怎么搭模块」

这套优化分析器刻意不绑定任何具体 EA,只在 MQL5 图形界面构建细节上借用了官方 EA 开发模板当底座。程序体量到了数千行,所以拆成多个模块再下沉到类,机器人模板仅仅作为启动入口,真正的逻辑散布在各模块里。 要跑起来,你至少需要三样东西:交易算法、Sqlite3 的 DLL、以及经过改写的图形界面库。EA 本身怎么写都行,OOP、单函数、从 DLL 导入均可,只要套用 MQL5 向导的模板。每次优化递次结束,类会把数据写进数据库,这部分独立于应用,因为库是在策略测试器里跑机器人时生成的。 数据库和计算模块被塞进 Include 目录,两个地方复用——分析的机器人和这套应用都用。它们干大部分活,再经演播器类接图形界面。演播器是胶水:每个模块在 GUI 里有自己的函数,它接按钮事件、重定向逻辑、把返回数据画图填表。 图形部分不碰概念逻辑,只画窗口、转发事件。程序以 MQL5 项目形态编写,方便结构化开发;项目里还藏了一个专写此程序的排序类,服务“优化选择”选项卡,按标准裁剪数据采样。 通用排序类是独立补充,算法取自第三方网站的冒泡式选择排序(俄文资料)。它提供模板 Sort 方法,能对任何类型包括类与结构排序,比较逻辑须放在实现 ICustomComparer<T> 的分离类里——传统 IComparer 的 Compare 不按引用传结构,而 MQL5 方法收结构必须引用,所以作者自己造了接口。下面这段是核心枚举与类骨架。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| E-num 排序风格                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum SortMethod
  {
   Sort_Ascending,class=class="str">"cmt">// 升序
   Sort_Descendinglyclass=class="str">"cmt">// 降序
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 针对所传递数据类型进行排序的类                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CGenericSorter
  {
class="kw">public:
  class=class="str">"cmt">// 默认构造函数
                     CGenericSorter(){method=Sort_Descendingly;}
  class=class="str">"cmt">// 排序方法
  class="kw">template<class="kw">typename T>
  class="type">void              Sort(T &out[],ICustomComparer<T>*comparer);
  class=class="str">"cmt">// 选择排序类型
  class="type">void Method(SortMethod _method){method=_method;}
  class=class="str">"cmt">// 获取排序方法
  SortMethod Method(){class="kw">return method;}
class="kw">private:
  class=class="str">"cmt">// 排序方法
  SortMethod        method;
  };

◍ EasyAndFastGUI 的坑与窗口分层套路

用 EasyAndFastGUI 搭 MT5 图形界面时先排一颗雷:ComboBox 在重新填充时会清掉不完整的内部变量,原库的 CListView::Clear 方法(ListView.mqh 第 600 行,路径 Include\EasyAndFastGUI\Controls)不补三行就会偶发“数组超出范围”并强关程序。修复就是在 Clear 里加 m_item_index_focus = WRONG_VALUE; m_prev_selected_item = WRONG_VALUE; m_prev_item_index_focus = WRONG_VALUE; 开发者俄语建议里就是这么写的,照抄即可。 窗口容器类要从 CWndEvents 派生,所有控件不直接在 CreateGUI 里 new,而是调子方法批量建,最后一句必须跑 CWndEvents::CompletedGUI() 把界面画出来。本例把界面切了 4 个选项卡容器:main_tab 隔离优化数据表,tab_up_1 放初始设置和过滤,tab_up_2 三个子页做报告/灵敏度/图形,tab_down 五个子页四个看 EA 交易报告、一个存文件。 选项卡创建逻辑高度雷同,差异仅在内容。以主选项卡为例,固定三步:把指针交给容器知道归属、拼控件创建串、塞进通用控件列表。11 类控件都用同一个工厂方法依次生成,比如 CTextLabel 建完先记所属容器与选项卡,再填初始数据,最后进全局数组。 事件模型别绕晕:按钮或列表交互的处理方法收 (id, lparam),id 是事件类型、lparam 是对象 ID,先判事件种类再比 lparam。私有声明事件后在重载 OnEvent 里分发,机器人模板的 OnEvent 一路把消息递给 GUI 再转 presenter。下面这段就是窗口管理类的骨架,直接开 MT5 建个 include 对照着改就能跑。

MQL5 / C++
class=class="str">"cmt">//--- 初始/逆初
  class="type">void                OnDeinitEvent(class="kw">const class="type">int reason){CWndEvents::Destroy();};
  class=class="str">"cmt">//--- 图表事件处理程序
  class="kw">virtual class="type">void        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">//
class CWindowManager : class="kw">public CWndEvents
  {
class="kw">public:
                      CWindowManager(class="type">void){presenter = NULL;};
                      ~CWindowManager(class="type">void){};
  class=class="str">"cmt">//===============================================================================  
  class=class="str">"cmt">// 调用方法和事件 :
  class=class="str">"cmt">//===============================================================================
  class=class="str">"cmt">//--- 初始/逆初
  class="type">void                OnDeinitEvent(class="kw">const class="type">int reason){CWndEvents::Destroy();};
  class=class="str">"cmt">//--- 图表事件处理程序
  class="kw">virtual class="type">void        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">//--- 创建程序的图形界面
  class="type">bool                CreateGUI(class="type">void);
class="kw">private:
 class=class="str">"cmt">//--- 主窗口
  CWindow             m_window;
  }
class="type">bool CWindowManager::CreateWindow(class="kw">const class="type">class="kw">string text)
  {
class=class="str">"cmt">//--- 将窗口指针添加到窗口数组
  CWndContainer::AddWindow(m_window);
class=class="str">"cmt">//--- 坐标
  class="type">int x=(m_window.X()>class="num">0) ? m_window.X() : class="num">1;

给分析面板定窗体尺寸与选项卡骨架

在 MT5 自定义面板里,窗体坐标不能为 0,否则部分版本会贴边无法拖拽。代码用三元运算把 Y 轴兜底成 1:当 m_window.Y() 小于 0 时取 1,保证窗体一定可见。 窗体属性里 XSize 设为 WINDOW_X_SIZE+25、Alpha 设 200(0~255 区间,偏半透明),同时开了 IsMovable 和 CollapseButtonIsUsed,关掉 ResizeMode 与 FullscreenButtonIsUsed,意味着这个面板能移动、能折叠,但不让用户手动拉变形。 选项卡部分用 CTabs 声明了 4 组:main_tab 主卡、tab_up_1 放设置与结果表、tab_up_2 放统计与参数、tab_down 放统计与导出。主卡里 tabs_width 数组先全部初始化成 45,再把前两项覆盖为 120,对应 "Analysis" 和 "Optimisation Data" 两个标签,其余留默认宽。 main_tab 开了 AutoXResizeMode 与 AutoYResizeMode,右侧留 3 像素、底部留 3 像素偏移,跟随父窗体自动伸缩;SelectedTab 若读到 WRONG_VALUE 就默认选第 0 个。循环里用 tabs_names[i] 为空时补 "Tab "+序号,避免空标签崩溃。复制这段代码到你的 CWindowManager 类里,编译后开一个 EURUSD 图表就能看到浮动面板雏形,外汇与贵金属波动剧烈,这类自绘 UI 仅作辅助,实盘仍属高风险。

MQL5 / C++
class="type">int y=(m_window.Y()>class="num">0) ? m_window.Y() : class="num">1;
class=class="str">"cmt">//--- 属性
  m_window.XSize(WINDOW_X_SIZE+class="num">25);
  m_window.YSize(WINDOW_Y_SIZE);
  m_window.Alpha(class="num">200);
  m_window.IconXGap(class="num">3);
  m_window.IconYGap(class="num">2);
  m_window.IsMovable(true);
  m_window.ResizeMode(class="kw">false);
  m_window.CloseButtonIsUsed(true);
  m_window.FullscreenButtonIsUsed(class="kw">false);
  m_window.CollapseButtonIsUsed(true);
  m_window.TooltipsButtonIsUsed(class="kw">false);
  m_window.RollUpSubwindowMode(true,true);
  m_window.TransparentOnlyCaption(true);
class=class="str">"cmt">//--- 设置工具提示
  m_window.GetCloseButtonPointer().Tooltip("Close");
  m_window.GetFullscreenButtonPointer().Tooltip("Fullscreen/Minimize");
  m_window.GetCollapseButtonPointer().Tooltip("Collapse/Expand");
  m_window.GetTooltipButtonPointer().Tooltip("Tooltips");
class=class="str">"cmt">//--- 创建窗体
  if(!m_window.CreateWindow(m_chart_id,m_subwin,text,x,y))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//---
  class="kw">return(true);
  }
class=class="str">"cmt">//--- 选项卡
  CTabs          main_tab; class=class="str">"cmt">// 主选项卡
  CTabs          tab_up_1; class=class="str">"cmt">// 设置和结果表格的选项卡
  CTabs          tab_up_2; class=class="str">"cmt">// 统计数据和参数选择的选项卡,以及共用图形
  CTabs          tab_down; class=class="str">"cmt">// 统计信息及上传到文件的选项卡
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 主选卡                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CWindowManager::CreateTab_main(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
  {
class=class="str">"cmt">//--- 将指针保存到主元素
  main_tab.MainPointer(m_window);
class=class="str">"cmt">//--- 选卡宽度数组
  class="type">int tabs_width[TAB_MAIN_TOTAL];
  ::ArrayInitialize(tabs_width,class="num">45);
  tabs_width[class="num">0]=class="num">120;
  tabs_width[class="num">1]=class="num">120;
class=class="str">"cmt">//---
  class="type">class="kw">string tabs_names[TAB_UP_1_TOTAL]={"Analysis","Optimisation Data"};
class=class="str">"cmt">//--- 属性
  main_tab.XSize(WINDOW_X_SIZE-class="num">23);
  main_tab.YSize(WINDOW_Y_SIZE);
  main_tab.TabsYSize(TABS_Y_SIZE);
  main_tab.IsCenterText(true);
  main_tab.PositionMode(TABS_LEFT);
  main_tab.AutoXResizeMode(true);
  main_tab.AutoYResizeMode(true);
  main_tab.AutoXResizeRightOffset(class="num">3);
  main_tab.AutoYResizeBottomOffset(class="num">3);
class=class="str">"cmt">//---
  main_tab.SelectedTab((main_tab.SelectedTab()==WRONG_VALUE)? class="num">0 : main_tab.SelectedTab());
class=class="str">"cmt">//--- 添加指定属性的选项卡
  for(class="type">int i=class="num">0; i<TAB_MAIN_TOTAL; i++)
     main_tab.AddTab((tabs_names[i]!="")? tabs_names[i]: "Tab "+class="type">class="kw">string(i+class="num">1),tabs_width[i]);
class=class="str">"cmt">//--- 创建一个控件元素
  if(!main_tab.CreateTabs(x_gap,y_gap))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 将对象添加到公共对象数组
  CWndContainer::AddToElementsArray(class="num">0,main_tab);
  class="kw">return(true);
  }
class="type">bool CWindowManager::CreateLable(class="kw">const class="type">class="kw">string text,

「把标签挂进选项卡并拉起整窗 GUI」

上面这段是 CWindowManager 里两个关键函数的拼装逻辑:一个负责把文本标签绑定到指定选项卡,另一个负责把整套界面控件一次性建出来。 先看标签挂载函数,它接收 x/y 偏移、选项卡引用、标签引用、目标 tab 索引和标签宽度。先把标签的主指针指向选项卡,再用 tab_link.AddToElementsArray 把标签塞进对应选项卡的元素数组,设置好 X 尺寸后调用 CreateTextLabel 创建,失败直接 return false,成功再归入常规元素数组。 [CODE] const int x_gap, const int y_gap, CTabs &tab_link, CTextLabel &lable_link, int tabIndex, int lable_x_size) { //--- 将指针保存到主元素 lable_link.MainPointer(tab_link); //--- 分配到选卡 tab_link.AddToElementsArray(tabIndex,lable_link); //--- 设置 lable_link.XSize(lable_x_size); //--- 创建 if(!lable_link.CreateTextLabel(text,x_gap,y_gap)) return false; //--- 将对象添加到常规对象数组 CWndContainer::AddToElementsArray(0,lable_link); return true; } [/CODE] 逐行拆解:前两个 const int 是标签相对选项卡的偏移量;CTabs 和 CTextLabel 引用用来跨对象联动;tabIndex 决定标签进哪个选项卡;lable_x_size 控制标签横向像素宽。MainPointer 建立从属关系,AddToElementsArray 完成挂载,CreateTextLabel 才真正在图上画出标签。 CreateGUI 则是总装线:先建标题为 "Optimisation Selection" 的窗口,再依次建主选项卡(偏移 120,20)和三个子选项卡(如 CreateTab_up_2 用 indent 变量算纵向位置,indent = WINDOW_Y_SIZE - (TAB_UP_1_BOTTOM_OFFSET + TABS_Y_SIZE - TABS_Y_SIZE))。之后连续调十几个 Create_all_* 函数把标签、按钮、下拉框、日历、编辑框、表格、单选、分隔线、图表、复选框全建出来,任一步返回 false 就中断。最后 CWndEvents::CompletedGUI() 触发界面完成事件。 [CODE] bool CWindowManager::CreateGUI(void) { //--- 创建窗口 if(!CreateWindow("Optimisation Selection")) return(false); //--- 创建选项卡 if(!CreateTab_main(120,20)) return false; if(!CreateTab_up_1(3,44)) return(false); int indent=WINDOW_Y_SIZE-(TAB_UP_1_BOTTOM_OFFSET+TABS_Y_SIZE-TABS_Y_SIZE); if(!CreateTab_up_2(3,indent)) return(false); if(!CreateTab_down(3,33)) return false; //--- 创建控件 if(!Create_all_lables()) return false; if(!Create_all_buttons()) return false; if(!Create_all_comboBoxies()) return false; if(!Create_all_dropCalendars()) return false; if(!Create_all_textEdits()) return false; if(!Create_all_textBoxies()) return false; if(!Create_all_tables()) return false; if(!Create_all_radioButtons()) return false; if(!Create_all_SepLines()) return false; if(!Create_all_Charts()) return false; if(!Create_all_CheckBoxies()) return false; // 显示窗口 CWndEvents::CompletedGUI(); return(true); } [/CODE] 在 MT5 里接这套框架时,注意 CreateTab_up_2 的 indent 计算公式里 TABS_Y_SIZE - TABS_Y_SIZE 恒为 0,实际等效于 WINDOW_Y_SIZE - TAB_UP_1_BOTTOM_OFFSET,若以后改了选项卡高度逻辑,这行得同步重算,否则标签可能叠到别的元素上。外汇和贵金属图表上挂这种自定义 GUI 属于高风险操作环境,界面逻辑错误可能干扰下单流程,建议在策略测试器里先跑通再上实盘。

MQL5 / C++
class="kw">const class="type">int x_gap,
class="kw">const class="type">int y_gap,
CTabs &tab_link,
CTextLabel &lable_link,
class="type">int tabIndex,
class="type">int lable_x_size)
{
class=class="str">"cmt">//--- 将指针保存到主元素
  lable_link.MainPointer(tab_link);
class=class="str">"cmt">//--- 分配到选卡
  tab_link.AddToElementsArray(tabIndex,lable_link);
class=class="str">"cmt">//--- 设置
  lable_link.XSize(lable_x_size);
class=class="str">"cmt">//--- 创建
  if(!lable_link.CreateTextLabel(text,x_gap,y_gap))
     class="kw">return class="kw">false;
class=class="str">"cmt">//--- 将对象添加到常规对象数组
  CWndContainer::AddToElementsArray(class="num">0,lable_link);
  class="kw">return true;
}
class="type">bool CWindowManager::CreateGUI(class="type">void)
  {
class=class="str">"cmt">//--- 创建窗口
  if(!CreateWindow("Optimisation Selection"))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 创建选项卡
  if(!CreateTab_main(class="num">120,class="num">20))
     class="kw">return class="kw">false;
  if(!CreateTab_up_1(class="num">3,class="num">44))
     class="kw">return(class="kw">false);
  class="type">int indent=WINDOW_Y_SIZE-(TAB_UP_1_BOTTOM_OFFSET+TABS_Y_SIZE-TABS_Y_SIZE);
  if(!CreateTab_up_2(class="num">3,indent))
     class="kw">return(class="kw">false);
  if(!CreateTab_down(class="num">3,class="num">33))
     class="kw">return class="kw">false;
class=class="str">"cmt">//--- 创建控件 
  if(!Create_all_lables())
     class="kw">return class="kw">false;
  if(!Create_all_buttons())
     class="kw">return class="kw">false;
  if(!Create_all_comboBoxies())
     class="kw">return class="kw">false;
  if(!Create_all_dropCalendars())
     class="kw">return class="kw">false;
  if(!Create_all_textEdits())
     class="kw">return class="kw">false;
  if(!Create_all_textBoxies())
     class="kw">return class="kw">false;
  if(!Create_all_tables())
     class="kw">return class="kw">false;
  if(!Create_all_radioButtons())
     class="kw">return class="kw">false;
  if(!Create_all_SepLines())
     class="kw">return class="kw">false;
  if(!Create_all_Charts())
     class="kw">return class="kw">false;
  if(!Create_all_CheckBoxies())
     class="kw">return class="kw">false;
class=class="str">"cmt">// 显示窗口
  CWndEvents::CompletedGUI();
  class="kw">return(true);
  }

常见问题

先把优化逻辑和界面展示拆开,单独写个分析器模块挂 GUI,别在测试器里硬改;先定窗体尺寸和选项卡骨架再填内容。
用现成的轻量 GUI 库做分层,先建主窗体再往里塞选项卡,标签挂进对应选项卡容器,最后统一拉起整窗。
可以,把分析器模块接进小布盯盘的 AIGC 看板,打开对应品种页就能直接调参并看优化分布。
分数据接收、计算、GUI 三层;最容易漏的是给面板定好固定窗体尺寸,不然标签会挤变形。
多半是选项卡没激活或事件没绑到主窗体;检查拉起整窗 GUI 的调用顺序,确保先建窗体再挂标签。