开发多币种 EA 交易(第 9 部分):收集单一交易策略实例的优化结果·综合运用
(3/3)·从零散参数集到组装成型EA,最后一步决定前面八篇是否白做
「把优化事件收拢进一个 Handler 类」
做 MT5 多代理优化时,主终端和测试代理的事件容易散落在 EA 各处。这里建一个 CTesterHandler 类,把开始前、每次通过后、整体完成后的动作统一接管,主终端的 EA 事件函数只做薄薄一层转发。 代理侧每次跑完,在 OnTester 里调用 CTesterHandler::Tester(),把自定义优化标准值和策略参数描述串传进去。实数统计特征会被遍历并转成字符串,和参数描述一起写进一个文件,再在一个数据帧里发回主终端——因为帧只能传简单类型数组或预建文件,混传数字和字符串最省事的办法就是全字符串化。 主终端侧用 TesterPass() 收帧:循环捞未处理的帧,字符数组转字符串,按索引拼出参数名和值,组成 SQL 往 passes 表插行,最后在一个事务里批量执行。辅助方法 GetFrameInputs() 从 AlgoBook 拿来并按需改过,负责生成输入变量名值串。 代码落盘到 TesterHandler.mqh 后,开 MT5 起一次 2 代理优化,看 passes 表行数是否等于代理回传帧数,就能验证这套链路通没通。外汇与贵金属优化结果受点差和滑点影响大,回测表现不代表实盘概率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Optimization event handling class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CTesterHandler { class="kw">static class="type">class="kw">string s_fileName; class=class="str">"cmt">// File name for writing frame data class="kw">static class="type">void ProcessFrames(); class=class="str">"cmt">// Handle incoming frames class="kw">static class="type">class="kw">string GetFrameInputs(class="type">ulong pass); class=class="str">"cmt">// Get pass inputs class="kw">public: class="kw">static class="type">int TesterInit(); class=class="str">"cmt">// Handle the optimization start in the main terminal class="kw">static class="type">void TesterDeinit(); class=class="str">"cmt">// Handle the optimization completion in the main terminal class="kw">static class="type">void TesterPass(); class=class="str">"cmt">// Handle the completion of a pass on an agent in the main terminal class="kw">static class="type">void Tester(const class="type">class="kw">double OnTesterValue, const class="type">class="kw">string params); class=class="str">"cmt">// Handle completion of tester pass for agent }; class="type">class="kw">string CTesterHandler::s_fileName = "data.bin"; class=class="str">"cmt">// File name for writing frame data class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handling the optimization start in the main terminal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CTesterHandler::TesterInit(class="type">void) { class=class="str">"cmt">// Open / create a database DB::Open(); class=class="str">"cmt">// If failed to open it, we do not start optimization if(!DB::IsOpen()) { class="kw">return INIT_FAILED; } class=class="str">"cmt">// Close a successfully opened database DB::Close(); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handling the optimization completion in the main terminal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTesterHandler::TesterDeinit(class="type">void) { class=class="str">"cmt">// Handle the latest data frames received from agents ProcessFrames(); class=class="str">"cmt">// Close the chart with the EA running in frame collection mode ChartClose(); } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Handling the completion of a pass on an agent in the main terminal | class=class="str">"cmt">//+--------------------------------------------------------------------+
把回测代理的统计塞进自定义年化框架
在 MT5 多代理回测里,OnTester 是每趟测试 pass 结束时的钩子。上面这段把最大回撤、利润先抓出来,再按固定余额 10% 回撤反推仓位放大系数 coeff,最后乘上时间折算出年化拟合利润 fittedProfit,逻辑直接可抄进自己的 EA。 具体折算用 totalSeconds = TimeCurrent() - m_fromDate 算测试跨度,fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds。若某 pass 回撤极小,coeff 会放大到不正常倍数,年化数字会虚高,实盘外汇/贵金属高杠杆下这种估计只能当相对排序用。 CTesterHandler::Tester() 负责把 STAT_INITIAL_DEPOSIT、STAT_PROFIT 等枚举跑成字符串数组,再 APPEND 自定义值,用 FileOpen/FileWriteString 落盘后 FrameAdd 发回主终端。这样主端就能拿到每代理的参数与统计做横向筛。 别把正态当圣经:STAT_EQUITY_DD 是权益峰谷回撤,不是闭仓回撤,跨代理比的时候要统一口径,否则筛选出来的「优解」只是口径巧合。
class="type">void CTesterHandler::TesterPass(class="type">void) { class=class="str">"cmt">// Handle data frames received from the agent ProcessFrames(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTester event handler | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CVirtualAdvisor::Tester() { class=class="str">"cmt">// Maximum absolute drawdown class="type">class="kw">double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD); class=class="str">"cmt">// Profit class="type">class="kw">double profit = TesterStatistics(STAT_PROFIT); class=class="str">"cmt">// The ratio of possible increase in position sizes for the drawdown of class="num">10% of fixedBalance_ class="type">class="kw">double coeff = CMoney::FixedBalance() * class="num">0.1 / balanceDrawdown; class=class="str">"cmt">// Calculate the profit in annual terms class="type">long totalSeconds = TimeCurrent() - m_fromDate; class="type">class="kw">double fittedProfit = profit * coeff * class="num">365 * class="num">24 * class="num">3600 / totalSeconds ; class=class="str">"cmt">// Perform data frame generation on the test agent CTesterHandler::Tester(fittedProfit, ~((CVirtualStrategy *) m_strategies[class="num">0])); class="kw">return fittedProfit; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Handling completion of tester pass for agent | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTesterHandler::Tester(class="type">class="kw">double custom, class=class="str">"cmt">// Custom criteria class="type">class="kw">string params class=class="str">"cmt">// Description of EA parameters in the current pass ) { class=class="str">"cmt">// Array of names of saved statistical characteristics of the pass ENUM_STATISTICS statNames[] = { STAT_INITIAL_DEPOSIT, STAT_WITHDRAWAL, STAT_PROFIT, ... }; class=class="str">"cmt">// Array for values of statistical characteristics of the pass as strings class="type">class="kw">string stats[]; ArrayResize(stats, ArraySize(statNames)); class=class="str">"cmt">// Fill the array of values of statistical characteristics of the pass FOREACH(statNames, stats[i] = DoubleToString(TesterStatistics(statNames[i]), class="num">2)); class=class="str">"cmt">// Add the custom criterion value to it APPEND(stats, DoubleToString(custom, class="num">2)); class=class="str">"cmt">// Screen the quotes in the description of parameters just in case StringReplace(params, "&class="macro">#x27;", "\\&class="macro">#x27;"); class=class="str">"cmt">// Open the file to write data for the frame class="type">int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI); class=class="str">"cmt">// Write statistical characteristics FOREACH(stats, FileWriteString(f, stats[i] + ",")); class=class="str">"cmt">// Write a description of the EA parameters FileWriteString(f, StringFormat("&class="macro">#x27;%s&class="macro">#x27;", params)); class=class="str">"cmt">// Close the file FileClose(f); class=class="str">"cmt">// Create a frame with data from the recorded file and send it to the main terminal if(!FrameAdd("", class="num">0, class="num">0, s_fileName)) {
◍ 把回测帧落进本地数据库
在策略测试器里每跑完一个 pass,数据都暂时存在帧(frame)里。想自己留底做横向对比,就得在 CTesterHandler::ProcessFrames 里把帧读出来写进 SQLite,而不是只看 MT5 自带的优化报告。 函数开头先 DB::Open() 连库,然后声明一组变量:pass 是帧的索引号,data[] 是原始字节数组,values 和 inputs 分别是帧内容字符串与参数串,queries[] 用来攒 SQL。核心是一个 while(FrameNext(...)) 循环,把每一帧通过 CharArrayToString 转成字符串。 循环体内用 StringFormat 拼出 INSERT 语句,注意 TimeToString(TimeLocal(), TIME_DATE|TIME_SECONDS) 会把本地时间精确到秒写进表,方便你事后按时间排优化批次。所有 query 用 APPEND 塞进数组,最后 DB::ExecuteTransaction(queries) 一次性提交,再 DB::Close() 断库。 外汇与贵金属回测属高风险验证,历史帧数据只代表过去样本,参数表现可能在未来失效。直接把下面这段抄进你的 handler 类,跑一次优化就能在本地库里看到每 pass 的 values 与 inputs 明细。
class="type">void CTesterHandler::ProcessFrames(class="type">void) { class=class="str">"cmt">// Open the database DB::Open(); class=class="str">"cmt">// Variables for reading data from frames class="type">class="kw">string name; class=class="str">"cmt">// Frame name(not used) class="type">ulong pass; class=class="str">"cmt">// Frame pass index class="type">long id; class=class="str">"cmt">// Frame type ID(not used) class="type">class="kw">double value; class=class="str">"cmt">// Single frame value(not used) class="type">uchar data[]; class=class="str">"cmt">// Frame data array as a character array class="type">class="kw">string values; class=class="str">"cmt">// Frame data as a class="type">class="kw">string class="type">class="kw">string inputs; class=class="str">"cmt">// String with names and values of pass parameters class="type">class="kw">string query; class=class="str">"cmt">// A single SQL query class="type">class="kw">string class="type">class="kw">string queries[]; class=class="str">"cmt">// SQL queries for adding records to the database class=class="str">"cmt">// Go through frames and read data from them class="kw">while(FrameNext(pass, name, id, value, data)) { class=class="str">"cmt">// Convert the array of characters read from the frame into a class="type">class="kw">string values = CharArrayToString(data); class=class="str">"cmt">// Form a class="type">class="kw">string with names and values of the pass parameters inputs = GetFrameInputs(pass); class=class="str">"cmt">// Form an SQL query from the received data query = StringFormat("INSERT INTO passes " "VALUES(NULL, %d, %s,\n&class="macro">#x27;%s&class="macro">#x27;,\n&class="macro">#x27;%s&class="macro">#x27;);", pass, values, inputs, TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); class=class="str">"cmt">// Add it to the SQL query array APPEND(queries, query); } class=class="str">"cmt">// Execute all requests DB::ExecuteTransaction(queries); class=class="str">"cmt">// Close the database DB::Close(); }
「用短周期优化验证数据库一致性」
想确认这套存储逻辑真能落地,最直白的办法就是在 MT5 策略测试器里跑一轮轻量优化:参数少给几个、周期缩短,别一上来就压满算力。 优化跑完后,把策略测试器的结果面板和自建数据库里的记录并排看。按用户自定义标准排序后,两者给出的利润值序列完全一致——这说明写入和读取没有错位。 具体看最优那组:初始存款 10,000 美元,最大回撤控制在存款的 10%(即 1,000 美元),一年内预期利润可能超过 5,000 美元。外汇与贵金属品种波动剧烈,这类数字只是该次优化的特定输出,不预示实盘表现。 我们此刻不在乎绝对收益多少,重点在于——优化结果已经稳定落进数据库,后续调取、比对、再训练都有了地基。
别急着下结论
目前这套保存逻辑只覆盖单次优化过程:把通过筛选的 EA 参数写进本地数据库(终端数据文件夹内,由 CDatabase::Create() 生成),但还做不到把属于同一次优化的字符串组干净地提取出来。想往后走,得先动数据库结构,好在现有骨架让改动成本可控。 下一步值得在 MT5 里试的是自动拉起连续多轮优化,并给待调参数预分配不同选项组合——附带的 SimpleVolumesExpertSingle.mq5(9.92 KB)和 Database.mqh(13.37 KB)可直接丢进本地跑一遍,验证写入是否如预期。 外汇与贵金属 EA 开发天然高风险,自动化优化只是把人力换成算力,过拟合和品种跳空仍可能吞掉回测里的漂亮曲线。先让单机把单轮存准,再谈并行。