开发多币种 EA 交易 (第 13 部分):自动化第二阶段 分组选择·综合运用
EA 初始化时的参数装配与校验
多策略 EA 在 OnInit 里先向资金管理类灌入风控基线:用预期回撤的十分之一作为仓位占用比例,再锁定固定余额基准。这两行决定了后续所有虚拟子策略的杠杆暴露上限,外汇与贵金属品种在此处的高波动特性要求回撤参数必须实测而非拍脑袋。 输入端的 16 个索引 i1_ 到 i16_ 被整组塞进 indexes_ 数组,再按 count_ 截取前 N 个作为实际参与优化的集合。代码用 CHashSet 去重,若 count_ 不在 1~16 区间或存在重复索引,直接返回 INIT_PARAMETERS_INCORRECT,避免脏配置进入回测。 非优化模式下(MQL_OPTIMIZATION 为假)会重建任务数据库,随后从文件载入策略参数集;若 LoadParams 返回 NULL,终端会打印缺失 database892.sqlite 的报错并终止初始化。最后用 StringFormat 拼出 CVirtualAdvisor 的嵌套初始化串,把策略组、风险管理器一次性注入。 开 MT5 把 count_ 设成 17 跑一次,能立刻看到初始化失败日志,验证索引边界守卫是否生效。
class="type">int OnInit() { class=class="str">"cmt">// Set parameters in the money management class CMoney::DepoPart(expectedDrawdown_ / class="num">10.0); CMoney::FixedBalance(fixedBalance_); class=class="str">"cmt">// Array of all indices from the EA inputs class="type">int indexes_[] = {i1_, i2_, i3_, i4_, i5_, i6_, i7_, i8_, i9_, i10_, i11_, i12_, i13_, i14_, i15_, i16_ }; class=class="str">"cmt">// Array for indices to be involved in optimization class="type">int indexes[]; ArrayResize(indexes, count_); class=class="str">"cmt">// Copy the indices from the inputs into it FORI(count_, indexes[i] = indexes_[i]); class=class="str">"cmt">// Multiplicity for parameter set indices CHashSet<class="type">int> setIndexes; class=class="str">"cmt">// Add all indices to the multiplicity FOREACH(indexes, setIndexes.Add(indexes[i])); class=class="str">"cmt">// Report an error if if(count_ < class="num">1 || count_ > class="num">16 class=class="str">"cmt">// number of instances not in the range class="num">1 .. class="num">16 || setIndexes.Count() != count_ class=class="str">"cmt">// not all indexes are unique ) { class="kw">return INIT_PARAMETERS_INCORRECT; } class=class="str">"cmt">// If this is not an optimization, then you need to recreate the task database if(!MQLInfoInteger(MQL_OPTIMIZATION)) { CreateTaskDB(fileName_, idParentJob_); } class=class="str">"cmt">// Load strategy parameter sets class="type">class="kw">string strategiesParams = LoadParams(indexes); class=class="str">"cmt">// If nothing is loaded, report an error if(strategiesParams == NULL) { PrintFormat(__FUNCTION__" | ERROR: Can&class="macro">#x27;t load data from file %s.\n" "Check that it exists in data folder or in common data folder.", "database892.sqlite"); class="kw">return(INIT_PARAMETERS_INCORRECT); } class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for an EA with a group of several strategies class="type">class="kw">string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],%f\n" " ),\n" " class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%d,%.2f"
◍ 把参数拼成EA初始化字符串
这段片段处在EA初始化函数尾部,负责把策略参数、缩放系数与魔术码组装成一段可读的参数字符串,再交给虚拟持仓的EA对象处理。
字符串里用 %d,%s,%d\n 这类格式占位,对应后面传入的 scale_、magic_ 与 "SimpleVolumes" 标识;末尾的 useOnlyNewBars_ 控制是否只抓新K线信号,对外汇与贵金属策略回测时这个开关会明显改变成交密度。
PrintFormat 把最终 expertParams 打到日志,开MT5加载后能在专家标签页直接看到拼接结果;若返回 INIT_FAILED,多半是 NEW(expertParams) 没拿到实例,需检查前面 strategiesParams 是否越界。
高杠杆的外汇/贵金属品种下,虚拟持仓逻辑只是模拟,实盘映射仍有滑点与断连风险,参数验证完别直接当实盘配置抄。
" )\n"
" ,%d,%s,%d\n"
")",
strategiesParams, scale_,
class="num">0, class="num">0, class="num">0, class="num">0, class="num">0, class="num">0,
magic_, "SimpleVolumes", useOnlyNewBars_
);
PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);
class=class="str">"cmt">// Create an EA handling class="kw">virtual positions
expert = NEW(expertParams);
if(!expert) class="kw">return INIT_FAILED;
class="kw">return(INIT_SUCCEEDED);
}「把第一阶段作业派生成第二阶段」
第二阶段优化要在 stages 表插一行:id_stage 填 2,name 填 Second。接着用一条 SQL 把第一阶段所有 job 按相同 symbol 和 period 复制成第二阶段 job,并把 tester_inputs 里的 idParentJob_ 指向原 job 的 id_job,状态先置 Queued。 你可能会发现矛盾:第一阶段明明跑完了,主库里却还是 Queued。根因是任务完成后没回写 job 和 stage 状态。修法有两种——在优化 EA 里加完成检查,或给数据库挂触发器自动更新。后者更干净,但眼下手动分段跑就够了,不急着做。 第二阶段只用一个优化标准:平均规范化年利润,对应 optimization 字段索引 6。下面这条 SQL 一次性给所有 Second 阶段 job 建 task,建完把 Optimization.ex5 拖进图表等终端跑完即可。 耗时取决于 EA 复杂度、回测长度、品种/周期数及代理数。本项目里,2021–2022 两年数据、3 品种 × 3 周期、32 个代理,第二阶段全部任务约 5 小时跑完。外汇与贵金属优化属高风险验证,结果仅代表历史样本,实盘倾向需重测。
INSERT INTO jobs SELECT NULL, class="num">2 AS id_stage, j.symbol, j.period, &class="macro">#x27;idParentJob_=&class="macro">#x27; || j.id_job || &class="macro">#x27;||class="num">0||class="num">1||class="num">10||N&class="macro">#x27; AS tester_inputs, &class="macro">#x27;Queued&class="macro">#x27; AS status FROM jobs j JOIN stages s ON j.id_stage = s.id_stage WHERE s.name=&class="macro">#x27;First&class="macro">#x27;; INSERT INTO tasks SELECT NULL, j.id_job AS id_job, class="num">6 AS optimization, NULL AS start_date, NULL AS finish_date, &class="macro">#x27;Queued&class="macro">#x27; AS status FROM jobs j JOIN stages s ON j.id_stage = s.id_stage WHERE s.name=&class="macro">#x27;Second&class="macro">#x27;;
用逗号 ID 拼出一个组合 EA
把多品种、多周期各自跑出来的最优策略捏进同一个 EA,最省事的做法是加一个 passes_ 输入参数:里面填逗号分隔的 pass ID,初始化时让程序去主库把对应参数串捞出来,替换掉 EA 对象原本的初始化字符串。 具体取哪些 ID?可用 SQL 把名为“Second”阶段的任务拆开,按 custom_ontester 降序给每个任务内部分配行号 rn,只留 rn=1 的那一行——也就是该任务下优化值最高的通过。图 3 给出的列表里,像 734469、735755、736046、736121、761710、776928、786413、795381 这 8 个 ID 就是二阶段各作业最好结果。 把这些 ID 塞进 passes_ 后,组合 EA 存为当前目录的 SimpleVolumesExpert.mq5。回测里净值曲线在整个测试间隔增长率大致平稳,回撤处在可接受预期内;更重要的是,现在能用近乎自动的方式生成“跨三个交易品种、三个时间框架”的单一实例策略组。外汇与贵金属杠杆高,这类组合仍可能在实盘出现偏离回测的回撤,须以小规模验证。 别把正态当圣经:二阶段 rn=1 只是该任务内历史最优,换样本区间后同 ID 未必仍排第一,建议每次换数据重跑 SQL 再更新 passes_。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input group "::: Money management" sinput class="type">class="kw">double expectedDrawdown_ = class="num">10; class=class="str">"cmt">// - Maximum risk(%) sinput class="type">class="kw">double fixedBalance_ = class="num">10000; class=class="str">"cmt">// - Used deposit(class="num">0 - use all) in the account currency class="kw">input class="type">class="kw">double scale_ = class="num">1.00; class=class="str">"cmt">// - Group scaling multiplier class="kw">input group "::: Selection for the group" class="kw">input class="type">class="kw">string passes_ = "class="num">734469,class="num">735755,class="num">736046,class="num">736121,class="num">761710,class="num">776928,class="num">786413,class="num">795381"; class=class="str">"cmt">// - Comma-separated pass IDs class="type">ulong magic_ = class="num">27183; class=class="str">"cmt">// - Magic class="type">bool useOnlyNewBars_ = true; class=class="str">"cmt">// - Work only at bar opening class="type">class="kw">datetime fromDate = TimeCurrent(); CVirtualAdvisor *expert; class=class="str">"cmt">// EA object class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// Set parameters in the money management class CMoney::DepoPart(expectedDrawdown_ / class="num">10.0); CMoney::FixedBalance(fixedBalance_); class=class="str">"cmt">// Initialization class="type">class="kw">string with strategy parameter sets class="type">class="kw">string strategiesParams = NULL; class=class="str">"cmt">// If the connection to the main database is established, if(DB::Connect()) { class=class="str">"cmt">// Form a request to receive passes with the specified IDs class="type">class="kw">string query = StringFormat( "SELECT DISTINCT p.params" " FROM passes p" " WHERE id_pass IN(%s);" , passes_); class="type">int request = DatabasePrepare(DB::Id(), query); if(request != INVALID_HANDLE) { class=class="str">"cmt">// Structure for reading results class="kw">struct Row {
◍ 把数据库参数拼成多策略EA初始化串
从数据库读出每套策略的参数后,代码用 while(DatabaseReadBind) 把 row.params 逐个拼进 strategiesParams 字符串,以逗号分隔。若最终 strategiesParams 为 NULL,说明本次查询没拿到任何参数集,直接 return INIT_FAILED 中止 EA 初始化。 拿到参数后,StringFormat 按 CVirtualAdvisor 的嵌套结构拼装:策略组用 %s 塞入拼接好的参数,scale_ 作为浮点跟在策略组后;风险管理器暂时写死 0,0,0,0,0,0 六个数值占位;magic_、"SimpleVolumes" 与 useOnlyNewBars_ 控制订单识别与品种标记。 PrintFormat 会把完整 expertParams 打印到日志,便于在 MT5 终端核对拼接结果。最后 NEW(expertParams) 生成处理虚拟持仓的 EA 实例,失败同样返回 INIT_FAILED,成功才 INIT_SUCCEEDED。 SQL 片段 SELECT p.id_pass, j.symbol, j.period, p.custom_ontester, p.profit, p.profit_factor, p.sharpe_ratio, p.equity_dd, p.params FROM ( 表明回测结果按品种、周期与夏普比率等字段关联提取,外汇与贵金属品种在此类多策略框架下的持仓杠杆偏高,实盘复制前需自测爆仓概率。
class="type">class="kw">string params; } row; class=class="str">"cmt">// For all query result strings, concatenate initialization rows class="kw">while(DatabaseReadBind(request, row)) { strategiesParams += row.params + ","; } } DB::Close(); } class=class="str">"cmt">// If no parameter sets are found, abort the test if(strategiesParams == NULL) { class="kw">return INIT_FAILED; } class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for an EA with a group of several strategies class="type">class="kw">string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],%f\n" " ),\n" " class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%d,%.2f" " )\n" " ,%d,%s,%d\n" ")", strategiesParams, scale_, class="num">0, class="num">0, class="num">0, class="num">0, class="num">0, class="num">0, magic_, "SimpleVolumes", useOnlyNewBars_ ); PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams); class=class="str">"cmt">// Create an EA handling class="kw">virtual positions expert = NEW(expertParams); if(!expert) class="kw">return INIT_FAILED; class="kw">return(INIT_SUCCEEDED); } SELECT p.id_pass, j.symbol, j.period, p.custom_ontester, p.profit, p.profit_factor, p.sharpe_ratio, p.equity_dd, p.params FROM(
「用窗口函数挑出每任务的最优回测记录」
在 MT5 策略测试器的批量任务结果表里,同一个 id_task 往往会产生多遍 passes 记录。要只保留每任务里表现最好的一次,就得靠 ROW_NUMBER 窗口函数按 custom_ontester 降序打排名。 下面这段 SQL 先对 passes 表做子查询,按 id_task 分组、用 custom_ontester 倒序给每行编 rn;外层再连 tasks、jobs、stages,只取 rn=1 且阶段名为 'Second' 的行。这样能直接捞出每个任务在第二阶段的最优那一遍参数与指标。 实际跑的时候注意:stages.name 是写死的过滤条件,若你自己的任务流水线阶段命名不同,得改 'Second' 这个值,否则查出来是空表。外汇与贵金属回测结果受点差和滑点影响大,这类最优记录只代表历史环境,实盘存在较高风险。
SELECT p0.*,
ROW_NUMBER() OVER(PARTITION BY id_task ORDER BY custom_ontester DESC) AS rn
FROM passes p0
)
AS p
JOIN
tasks t ON t.id_task = p.id_task
JOIN
jobs j ON j.id_job = t.id_job
JOIN
stages s ON s.id_stage = j.id_stage
WHERE rn = class="num">1 AND
s.name = &class="macro">#x27;Second&class="macro">#x27;;一点提醒
这套两阶段优化目前只在相对较短的时间窗口里跑过,原作者也明确说延长测试间隔并重跑一遍才更稳妥。第二阶段还没接上聚类加速——第六篇里试过用聚类压缩优化规模,但 MQL5 里难落地,得靠 Python 或 R 补一段自动执行逻辑,开发量不小。 附件里 SimpleVolumesStage2.mq5 有 25.66 KB,Database.mqh 是 13.72 KB,读者直接开 MT5 把 ZIP 解到 MQL5/Files 下就能编译验证;外汇和贵金属策略优化涉及实盘映射,高风险,回测顺不代表 live 能复现。 先按完整路径把基础功能跑通,别被暂不需要的增强分散注意力。原计划的偏离常常发生在推进途中,留好余量比急着加特性更实际。