100 个最佳优化递次(第 1 部分)。 开发优化分析器(基础篇)
◍ 优化分析器的骨架怎么搭
想把 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 方法收结构必须引用,所以作者自己造了接口。下面这段是核心枚举与类骨架。
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 对照着改就能跑。
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 仅作辅助,实盘仍属高风险。
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 属于高风险操作环境,界面逻辑错误可能干扰下单流程,建议在策略测试器里先跑通再上实盘。
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); }