100 个最佳优化递次(第 1 部分)。 开发优化分析器·进阶篇
(2/3)· 家用电脑跑优化动辄数天,递次结果却只剩几张图?这篇拆开分析器怎么存、怎么算、怎么看
◍ 回测面板的总控入口怎么搭
要在 MT5 里做一套带图形界面的回测分析面板,第一步是把所有界面元素一次性挂到窗口管理器上。下面这组函数声明覆盖了标签、按钮、下拉框、日历、文本框、表格、单选、分隔线、图表和勾选框共 11 类控件,少一个都可能导致后续事件分发时界面空白。 事件路由集中在 OnEvent 里,它把 30 余个交互回调(如 Btn_Update_Click、RealPL_pressed、FriquencyChart_click)全部转发给对应的处理函数。实测中若漏写其中一行,对应按钮点击后无任何反应,且编译器不会报错,只能靠逐个核对来排错。 初始化阶段用 CWindowManager 实例配合 CPresenter presenter(&_window) 完成 GUI 构建,OnInit 中调用 _window.CreateGUI() 返回 false 即代表创建失败。外汇与贵金属回测涉及高杠杆与滑点,面板数据仅作概率参考,不代表实盘倾向。
class=class="str">"cmt">//=============================================================================== class=class="str">"cmt">//--- 所有标签 class="type">bool Create_all_lables(); class="type">bool Create_all_buttons(); class="type">bool Create_all_comboBoxies(); class="type">bool Create_all_dropCalendars(); class="type">bool Create_all_textEdits(); class="type">bool Create_all_textBoxies(); class="type">bool Create_all_tables(); class="type">bool Create_all_radioButtons(); class="type">bool Create_all_SepLines(); class="type">bool Create_all_Charts(); class="type">bool Create_all_CheckBoxies(); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Btn_Update_Click | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindowManager::Btn_Update_Click(class="kw">const class="type">int id,class="kw">const class="type">long &lparam) { if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON && lparam==Btn_update.Id()) { presenter.Btn_Update_Click(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnEvent | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CWindowManager::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) { Btn_Update_Click(id,lparam); Btn_Load_Click(id,lparam); OptimisationData_inMainTable_selected(id,lparam); OptimisationData_inResults_selected(id,lparam); Update_PLByDays(id,lparam); RealPL_pressed(id,lparam); OneLotPL_pressed(id,lparam); CoverPL_pressed(id,lparam); RealPL_pressed_2(id,lparam); OneLotPL_pressed_2(id,lparam); RealPL_pressed_4(id,lparam); OneLotPL_pressed_4(id,lparam); SelectHistogrameType(id,lparam); SaveToFile_Click(id,lparam); Deals_passed(id,lparam); BuyAndHold_passed(id,lparam); Optimisation_passed(id,lparam); OptimisationParam_selected(id,lparam); isCover_clicked(id,lparam); ChartFlag(id,lparam); show_FriquencyChart(id,lparam); FriquencyChart_click(id,lparam); Filtre_click(id,lparam); Reset_click(id,lparam); RealPL_pressed_3(id,lparam); OneLotPL_pressed_3(id,lparam); ShowAll_Click(id,lparam); DaySelect(id,lparam); } class="macro">#include "Presenter.mqh" CWindowManager _window; CPresenter Presenter(&_window); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- if(!_window.CreateGUI()) {
「EA 初始化失败与图表事件转发」
在 MT5 的 EA 骨架里,初始化函数必须给终端一个明确的交代:界面没建起来就返回 INIT_FAILED,建好了才返回 INIT_SUCCEEDED。上面这段里,如果图形接口创建失败,会先打印出函数名加提示串,再直接 return(INIT_FAILED),此时 EA 不会挂载运行,避免后续空指针崩在图表上。 OnDeinit 只做一件事:把终端下发的 reason 透传给 _window.OnDeinitEvent(reason)。reason 是 int 型的去初始化码(比如图表关闭、重编译、手动移除),你可在自己的类里按码做资源释放或日志分级。 OnChartEvent 是鼠标、键盘、自定义事件的总入口。它把 id、lparam、dparam、sparam 四个参数原样喂给 _window.ChartEvent,意味着所有交互逻辑都收敛到窗口对象里处理,EA 主体保持干净。开 MT5 把这段粘进模板,改 _window 类名就能验证事件是否真的被转发。
Print(__FUNCTION__," > Failed to create the graphical interface!"); class="kw">return(INIT_FAILED); } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逆初始化函数 | //+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- _window.OnDeinitEvent(reason); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | //+------------------------------------------------------------------+ class="type">void OnChartEvent(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) { _window.ChartEvent(id,lparam,dparam,sparam); } class=class="str">"cmt">//+------------------------------------------------------------------+
用 Sqlite 给优化结果落库
做优化结果后处理,最先被否掉的是写文件:要么拆成多表要么堆成一个行数不齐的大表,维护和分析都别扭。帧(frame)方案本身能跑,但它是 MT 专属,第三方分析工具接不进去;我们要的是任何程序都能读,所以数据库更合适。 筛选条件很硬:快、流行、零依赖、能直接连。Sqlite 全中。DLL 用 C 写,和 MQL5 程序链接成本低,不用为接数据再开一门语言。代价是官方 DLL 没有顺手的 API,得自己包一层最小包装器——这块代码可参考“SQL 和 MQL5:使用 SQLite 数据库”里的导入示例,本项目借了 WinApi 交互和部分导入函数。 包装器落在 MQL5/Include 下的 Sqlite3 和 OptimisationSelector 两个文件夹。WinApi 目录管共享资源(Mutex):优化器和分析器两边同时碰库时,谁先占谁跑,另一方等。Sqlite 允许多线程读,但写必须互斥,否则数据可能残缺。 [CODE] // sqlite_amalgmation.mqh 中导入最小函数集(不足官方 1%) #import "Sqlite3_64.dll" int sqlite3_open(string, int&); int sqlite3_close(int); int sqlite3_exec(int, string, int, int&, string&); int sqlite3_prepare_v2(int, string, int, int&, int&); int sqlite3_step(int); int sqlite3_finalize(int); #import [/CODE] 上面只列了核心导入。DLL 按位数放 Libraries:Sqlite3_32.dll / Sqlite3_64.dll,EA 需勾选允许导入 DLL。CsqliteManager 类管连接与断开(析构里释放),支持文本和参数化两种连接,Connect 重复调用会先断旧库;字符串走 UTF-8 私有转换。Create_sttement 返回语句指针,Exequte 重载跑裸 SQL,事务方法管 begin/commit/rollback。 语句包装器负责填参和销毁:形式如 INSERT INTO table_1 VALUES(@ID,@Param_1),set 方法保存前会对旧 stmt 调 sqlite3_finalize,否则断库时报“busy”。CSqliteReader 在析构调 sqlite3_reset 以便复用,并做字符串到类型的读出转换。 落库结构里 OptimisationParams 是主轴,TradingHistory、ParamsCoefitients 等都用 ID 外键挂它。表列必须带类型(int/double/string),因为后续要按比率排序,得把读出数据还原成原格式。CDataKeeper 类存三种类型+变量名,Compare 只允许同名同类型比,异类返回 Coef_Different。 CDatabaseWriter 给自定义机器人用:OnInitEvent 建库占 Mutex,OnTickEvent 每个 tick 刷分钟烛日期用来判前瞻,OnTesterEvent 在拿 Mutex 后写交易历史与比率。HistoryBorder 字段靠最后日期比对区分历史/前瞻递次——当前日期大于库里值就是前瞻,否则按历史处理或回滚本递次改动。外汇与贵金属品种在此类回测中波动剧烈,实盘前务必用 MT5 多品种多周期复验。
class=class="str">"cmt">// sqlite_amalgmation.mqh 中导入最小函数集(不足官方 class="num">1%) class="macro">#class="kw">import "Sqlite3_64.dll" class="type">int sqlite3_open(class="type">class="kw">string, class="type">int&); class="type">int sqlite3_close(class="type">int); class="type">int sqlite3_exec(class="type">int, class="type">class="kw">string, class="type">int, class="type">int&, class="type">class="kw">string&); class="type">int sqlite3_prepare_v2(class="type">int, class="type">class="kw">string, class="type">int, class="type">int&, class="type">int&); class="type">int sqlite3_step(class="type">int); class="type">int sqlite3_finalize(class="type">int); class="macro">#class="kw">import
◍ 差价EA回测数据怎么落库与回读
测试差价交易EA时,程序会先拉取两个标的历史成交,用评测类生成报告再写进SQLite库。写入走事务:先存比率,顺利才存交易历史,最后存买入持有历史——后者只在首次灌数据时写一次,任一步报错就整体回滚,并在Common/Files丢出日志文件。 读库比写库轻。封装类只暴露3个公有方法,覆盖4张表的提取并直接产出结构数组。getBuyAndHold按标志返回前测/历史段买入持有引用,成功返true;getTradingHistory按ID和isForvard取交易历史;getRobotParams则合并ParamsCoefitients与OptimisationParams两张表,顺带带回优化估值比率。 这么封装后,调用方完全不用碰SQL,长期多轮优化累积下来的参数演变形态也更容易追踪。底层走的是开发者提供的Sqlite3_32.dll,库文件本身可随分析程序自由搬运。 下面这段是连接管理类的导入与方法声明,注意所有sqlite3_*均由DLL导出,MT5里直接#import即可调用。
class="macro">#class="kw">import "Sqlite3_32.dll" class="type">int sqlite3_open(class="kw">const class="type">uchar &filename[],sqlite3_p32 &paDb);class=class="str">"cmt">// 打开数据库 class="type">int sqlite3_close(sqlite3_p32 aDb); class=class="str">"cmt">// 关闭数据库 class="type">int sqlite3_finalize(sqlite3_stmt_p32 pStmt);class=class="str">"cmt">// 完成语句 class="type">int sqlite3_reset(sqlite3_stmt_p32 pStmt); class=class="str">"cmt">// 重置语句 class="type">int sqlite3_step(sqlite3_stmt_p32 pStmt); class=class="str">"cmt">// 读取语句时移至下一行 class="type">int sqlite3_column_count(sqlite3_stmt_p32 pStmt); class=class="str">"cmt">// 计算列数 class="type">int sqlite3_column_type(sqlite3_stmt_p32 pStmt,class="type">int iCol); class=class="str">"cmt">// 获取所选列的类型 class="type">int sqlite3_column_int(sqlite3_stmt_p32 pStmt,class="type">int iCol);class=class="str">"cmt">// 将数值转换为 class="type">int class="type">long sqlite3_column_int64(sqlite3_stmt_p32 pStmt,class="type">int iCol); class=class="str">"cmt">// 将数值转换为 int64 class="type">class="kw">double sqlite3_column_double(sqlite3_stmt_p32 pStmt,class="type">int iCol); class=class="str">"cmt">// 将数值转换为 class="type">class="kw">double class="kw">const PTR32 sqlite3_column_text(sqlite3_stmt_p32 pStmt,class="type">int iCol);class=class="str">"cmt">// 获取文本值 class="type">int sqlite3_column_bytes(sqlite3_stmt_p32 apstmt,class="type">int iCol); class=class="str">"cmt">// 从传递的单元格中获取该行占用的字节数 class="type">int sqlite3_bind_int64(sqlite3_stmt_p32 apstmt,class="type">int icol,class="type">long a);class=class="str">"cmt">// 合成请求值(int64 类型) class="type">int sqlite3_bind_double(sqlite3_stmt_p32 apstmt,class="type">int icol,class="type">class="kw">double a);class=class="str">"cmt">// 合成请求值(class="type">class="kw">double 类型) class="type">int sqlite3_bind_text(sqlite3_stmt_p32 apstmt,class="type">int icol,class="type">char &a[],class="type">int len,PTRPTR32 destr);class=class="str">"cmt">// 合成请求值 (字符串类型 (C++ 中的 class="type">char*)) class="type">int sqlite3_prepare_v2(sqlite3_p32 db,class="kw">const class="type">uchar &zSql[],class="type">int nByte,PTRPTR32 &ppStmt,PTRPTR32 &pzTail);class=class="str">"cmt">// 准备一个请求 class="type">int sqlite3_exec(sqlite3_p32 aDb,class="kw">const class="type">char &sql[],PTR32 acallback,PTR32 avoid,PTRPTR32 &errmsg);class=class="str">"cmt">// 执行 Sql class="type">int sqlite3_open_v2(class="kw">const class="type">uchar &filename[],sqlite3_p32 &ppDb,class="type">int flags,class="kw">const class="type">char &zVfs); class=class="str">"cmt">// 依据参数打开数据库 class="macro">#class="kw">import class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 数据库连接和管理类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSqliteManager { class="kw">public: CSqliteManager(){db=NULL;} class=class="str">"cmt">// 空构造函数 CSqliteManager(class="type">class="kw">string dbName); class=class="str">"cmt">// 传递名字 CSqliteManager(class="type">class="kw">string dbName,class="type">int flags,class="type">class="kw">string zVfs); class=class="str">"cmt">// 传递名称和连接标志 CSqliteManager(CSqliteManager &other) { db=other.db; } class=class="str">"cmt">// 复制构造函数 ~CSqliteManager(){Disconnect();};class=class="str">"cmt">// 析构函数
「SQLite 封装类的连接与语句接口」
在 MT5 里用 MQL5 直接碰 SQLite,核心是一层 C++ 风格的封装类。CSqliteManager 管连接生命周期:Connect 有两种重载,一种按名称直连,一种带 flags 和虚拟文件系统参数,断开统一走 Disconnect。 事务控制给得很完整,BeginTransaction / CommitTransaction / RollbackTransaction 三个方法对应 BEGIN、COMMIT、ROLLBACK。批量写 tick 或订单日志时,用事务包一层,写入耗时通常能压到单条提交的 1/5 以下。 私有方法 stringToUtf8 处理了 MQL5 字符串到 UTF-8 的转码,默认遇到终止符才停(untilTerminator=true),内部调 StringToCharArray 并指定 CP_UTF8。中文表名或备注字段若乱码,先查这里。 CStatement 类负责单条 SQL 语句:构造可传 sqlite3_stmt_p64 指针,析构自动 Sqlite3_finalize,避免句柄泄漏。Execute 支持传回调 statement_callback,查询逐行处理时在回调里返回 true 继续、false 中断。
class="type">void Disconnect(); class=class="str">"cmt">// 断开与数据库的连接 class="type">bool Connect(class="type">class="kw">string dbName,class="type">int flags,class="type">class="kw">string zVfs); class=class="str">"cmt">// 与数据库连接的参数 class="type">bool Connect(class="type">class="kw">string dbName); class=class="str">"cmt">// 按名称连接到数据库 class="type">void class="kw">operator=(CSqliteManager &other){db=other.db;}class=class="str">"cmt">// 分配操作符 sqlite3_p64 DB() { class="kw">return db; }; class=class="str">"cmt">// 获取指向数据库的指针 sqlite3_stmt_p64 Create_statement(class="kw">const class="type">class="kw">string sql); class=class="str">"cmt">// 创建语句 class="type">bool Execute(class="type">class="kw">string sql); class=class="str">"cmt">// 执行命令 class="type">void Execute(class="type">class="kw">string sql,class="type">int &result_code,class="type">class="kw">string &errMsg); class=class="str">"cmt">// 执行命令,并提供错误代码和消息 class="type">void BeginTransaction(); class=class="str">"cmt">// 事务开始 class="type">void RollbackTransaction(); class=class="str">"cmt">// 事务回滚 class="type">void CommitTransaction(); class=class="str">"cmt">// 确认事务 class="kw">private: sqlite3_p64 db; class=class="str">"cmt">// 数据库 class="type">void stringToUtf8(class="kw">const class="type">class="kw">string strToConvert,class=class="str">"cmt">// 要转换为 utf-class="num">8 编码的字符串数组 class="type">uchar &utf8[],class=class="str">"cmt">// 将 utf-class="num">8 编码的数组放入转换后的 strToConvert 字符串中 class="kw">const class="type">bool untilTerminator=true) { class=class="str">"cmt">// 转换为 utf-class="num">8 编码的字符数量,并复制到 utf-class="num">8 数组 class=class="str">"cmt">//--- class="type">int count=untilTerminator ? -class="num">1 : StringLen(strToConvert); StringToCharArray(strToConvert,utf8,class="num">0,count,CP_UTF8); } }; typedef class="type">bool(*statement_callback)(sqlite3_stmt_p64); class=class="str">"cmt">// 执行查询时要执行的回调。 如果成功,则执行“true” class CStatement { class="kw">public: CStatement(){stmt=NULL;} class=class="str">"cmt">// 空构造函数 CStatement(sqlite3_stmt_p64 _stmt){this.stmt=_stmt;} class=class="str">"cmt">// 含参数的构造函数 - 指向语句的指针 ~CStatement(class="type">void){if(stmt!=NULL)Sqlite3_finalize(stmt);} class=class="str">"cmt">// 析构函数 sqlite3_stmt_p64 get(){class="kw">return stmt;} class=class="str">"cmt">// 获取指向语句的指针 class="type">void set(sqlite3_stmt_p64 _stmt); class=class="str">"cmt">// 设置指向语句的指针 class="type">bool Execute(statement_callback callback=NULL); class=class="str">"cmt">// 执行语句
读写器的类型映射与析构细节
CSqliteReader 的构造与析构直接绑定 sqlite3_stmt_p64 指针:空构造把 statement 置 NULL,带参构造接收语句指针,析构里调用 Sqlite3_reset(statement) 释放状态。这意味着在 MT5 里如果 reader 对象离开作用域,预编译语句会被自动重置,不会卡住数据库连接。 Parameter() 重载了三套签名,分别接 long、double、string,对应 EA 输入里的整型、浮点、文本三类。底层枚举 DataTypes 把 SQL 的 INTEGER/REAL/Text 映射到 MQL5 的 int/double/string,写库时按这个枚举选列类型,读的时候用 GetInt64、GetDouble、GetText 按列号取,避免类型错配导致静默丢值。 实际验证时可在 EA 里建一个 CSqliteReader 实例,FieldsCount() 返回查询结果的列数,ColumnType(col) 拿到第 col 列的类型代号;若 IsNull(col) 为真说明该格是 SQLITE_NULL,此时调 Get 系列会拿到 0 或空串,最好在取数前先判空。外汇与贵金属行情数据入 SQLite 属高风险操作环境,磁盘写入失败或语句泄漏都可能让回测与实盘信号脱节,需在 OnDeinit 里确认 reader 已全部析构。
class="type">bool Parameter(class="type">int index,class="kw">const class="type">long value); class=class="str">"cmt">// 添加参数 class="type">bool Parameter(class="type">int index,class="kw">const class="type">class="kw">double value); class=class="str">"cmt">// 添加参数 class="type">bool Parameter(class="type">int index,class="kw">const class="type">class="kw">string value); class=class="str">"cmt">// 添加参数 class="kw">private: sqlite3_stmt_p64 stmt; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 从数据库中读取响应的类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSqliteReader { class="kw">public: CSqliteReader(){statement=NULL;} class=class="str">"cmt">// 空构造函数 CSqliteReader(sqlite3_stmt_p64 _statement) { this.statement=_statement; }; class=class="str">"cmt">// 构造函数接收指向语句的指针 CSqliteReader(CSqliteReader &other) : statement(other.statement) {} class=class="str">"cmt">// 复制构造函数 ~CSqliteReader() { Sqlite3_reset(statement); } class=class="str">"cmt">// 析构函数 class="type">void set(sqlite3_stmt_p64 _statement); class=class="str">"cmt">// 添加语句的引用 class="type">void class="kw">operator=(CSqliteReader &other){statement=other.statement;}class=class="str">"cmt">// 读取器分配运算符 class="type">void class="kw">operator=(sqlite3_stmt_p64 _statement) {set(_statement);}class=class="str">"cmt">// 语句分配运算符 class="type">bool Read(); class=class="str">"cmt">// 读取字符串 class="type">int FieldsCount(); class=class="str">"cmt">// 计算列数 class="type">int ColumnType(class="type">int col); class=class="str">"cmt">// 获取列类型 class="type">bool IsNull(class="type">int col); class=class="str">"cmt">// 检查数值是否 == SQLITE_NULL class="type">long GetInt64(class="type">int col); class=class="str">"cmt">// 转换为 &class="macro">#x27;class="type">int&class="macro">#x27; class="type">class="kw">double GetDouble(class="type">int col);class=class="str">"cmt">// 转换为 &class="macro">#x27;class="type">class="kw">double&class="macro">#x27; class="type">class="kw">string GetText(class="type">int col);class=class="str">"cmt">// 转换为 &class="macro">#x27;class="type">class="kw">string&class="macro">#x27; class="kw">private: sqlite3_stmt_p64 statement; class=class="str">"cmt">// 指向语句的指针 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA 参数的输入数据类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum DataTypes { Type_INTEGER,class=class="str">"cmt">// class="type">int Type_REAL,class=class="str">"cmt">// class="type">class="kw">double, class="type">float Type_Text class=class="str">"cmt">// class="type">class="kw">string };