开发多币种 EA 交易(第 21 部分):准备重要实验并优化代码·综合运用
(3/3)· 当参数失效争论不休时,用三年回测重优化实验看清EA真实存活概率
很多交易者把一次优化跑出的漂亮曲线当成圣杯,却从没验证过参数在随后每个月重优化后是否还能活下来。另一些人知道该定期重优化,但手动切周期、导报告、比对回撤,三天就放弃了。本篇把这套繁琐实验直接写进EA框架,让机器替你问出最关键的问题:参数到底能信多久。
◍ 用 EA 脚本往数据库灌项目与三阶段任务
这里不写独立脚本,而是做一个行为像脚本的 EA:所有逻辑塞进 OnInit(),跑完第一次报价就自动卸载。好处是同一套代码既能挂图表手动跑,也能丢进策略测试器做优化器批量跑,方便后续反复调参。 输入参数先求能用:projectName_ 默认 SimpleVolumes、projectVersion_ 默认 1.20;symbols_ 传 "GBPUSD;EURUSD;EURGBP"、timeframes_ 传 "H1;M30;M15",分号分隔。代码里会按交易品种×时间周期做笛卡尔积,上面这组默认值直接推出 3×3 = 9 个单一实例组合。 九组实例不是一步到位,而是拆成三阶段往项目表灌:第一阶段逐组合做初筛优化,第二阶段从初筛里挑“好”组合(count_=8,即每组收 8 个单实例),第三阶段把九组结果拼成一条库初始化字符串。每个阶段、作业、任务都写进对应的表,最后把项目状态置为 Queued,数据库触发器会自动把下属阶段和任务全改为排队。 fromDate_ / toDate_ 两个日期输入专门用来框定优化区间,默认 2018-01-01 到 2023-01-01。后面可以用不同区间反复跑,模拟周期性重优化,观察重优化后多久实盘表现会退化到和样本内结果趋同——外汇与贵金属市场高波动、过拟合风险大,这种区间回测仅作方法验证,不代表未来概率。 CreateJobs() 是重头戏:先 StringToTimeframe() 把字符串周期转成 ENUM_TIMEFRAMES,再双循环铺开组合。第一阶段每组合硬编码 3 个优化任务,依据是单组合遗传算法几乎总能收敛到不同参数,10–20 千次 pass 积累已够选;第二阶段每组合只 1 个任务、用用户标准收 8 实例;第三阶段 1 个作业收尾写库。任务数和标准目前写死,以后可挪到 input。 数据库结构做成 db.schema.sql 资源嵌进 Database.mqh,Connect() 改逻辑:指定名数据库不存在就自动按资源里的 SQL 建库,老 ExecuteFile() 废弃。代码落盘为 CreateProject.mq5 即可在 MT5 里编译试运行。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ input group "::: Database" sinput class="type">class="kw">string fileName_ = "article.class="num">16373.db.sqlite"; class=class="str">"cmt">// - Main database file input group "::: Project parameters" sinput class="type">class="kw">string projectName_ = "SimpleVolumes"; class=class="str">"cmt">// - Name sinput class="type">class="kw">string projectVersion_ = "class="num">1.20"; class=class="str">"cmt">// - Version sinput class="type">class="kw">string symbols_ = "GBPUSD;EURUSD;EURGBP"; class=class="str">"cmt">// - Symbols sinput class="type">class="kw">string timeframes_ = "H1;M30;M15"; class=class="str">"cmt">// - Timeframes input class="type">class="kw">datetime fromDate_ = D&class="macro">#x27;class="num">2018-class="num">01-class="num">01&class="macro">#x27;; class=class="str">"cmt">// - Start date input class="type">class="kw">datetime toDate_ = D&class="macro">#x27;class="num">2023-class="num">01-class="num">01&class="macro">#x27;; class=class="str">"cmt">// - End date class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// Connect to the database DB::Connect(fileName_); class=class="str">"cmt">// Create a project CreateProject(projectName_, projectVersion_, StringFormat("%s - %s",
「用 EA 把优化任务写进本地数据库」
这段 MQL5 代码展示了一个典型做法:把参数优化项目拆成「项目—阶段—作业」三层,在 OnInit 里一次性灌进本地 SQLite,再由云代理或本地代理跑。OnTick 直接 ExpertRemove,说明这 EA 只负责建任务、不盯盘。 CreateProject 用 StringFormat 拼 INSERT,RETURNING rowid 拿回项目 ID 存进 id_project;CreateStages 里写死三阶段,第一阶段是单实例优化(single instance optimization),注释里标得很清楚。外汇与贵金属杠杆高,这类批量优化结果仅作概率参考,实盘前务必在 MT5 策略测试器复核。 想验证的话,把 DB::Insert / DB::Close 这几个封装换成你自己的 sqlite 包装,编译挂到图表,初始化日志会打印出完整 SQL;改 CreateStages 里的 name 字段就能区分不同实验批次。
TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE) ); class=class="str">"cmt">// Create project stages CreateStages(); class=class="str">"cmt">// Creating jobs and tasks CreateJobs(); class=class="str">"cmt">// Queueing the project for execution QueueProject(); class=class="str">"cmt">// Close the database DB::Close(); class=class="str">"cmt">// Successful initialization class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tick handling | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">// Since all work is done in OnInit(), class="kw">delete the EA ExpertRemove(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a project | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateProject(class="type">class="kw">string name, class="type">class="kw">string ver, class="type">class="kw">string desc = "") { class="type">class="kw">string query = StringFormat("INSERT INTO projects " " VALUES(NULL,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;%s&class="macro">#x27;,NULL,&class="macro">#x27;Done&class="macro">#x27;) RETURNING rowid;", name, ver, desc); PrintFormat(__FUNCTION__" | %s", query); id_project = DB::Insert(query); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create three stages | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateStages() { class=class="str">"cmt">// Stage class="num">1 - single instance optimization class="type">class="kw">string query1 = StringFormat("INSERT INTO stages VALUES(" "NULL," class=class="str">"cmt">// id_stage "%I64u," class=class="str">"cmt">// id_project "%s," class=class="str">"cmt">// id_parent_stage "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// name "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// expert
回测记录的 SQL 写入字段映射
在 MT5 策略优化器里跑完一轮测试后,如果想把结果落库做横向对比,核心是把每一趟回测的上下文参数写进数据表。下面这段格式化字符串就是向 stages 表插记录的字段模板,覆盖了品种、周期、优化开关到杠杆与执行模式等 15 个维度。
注意 profit_in_pips 与 optimization_criterion 是两个独立整型字段:前者记盈亏点数,后者记本次优化所采用的评价准则编号(如 0=余额最大、1=夏普等),回测时若只盯余额而忽略准则编号,后续筛参会混淆。
id_parent_stage 这里硬编码为 "NULL",说明本条是首轮(name 标为 "First");当你做多阶段优化(如先粗筛再精细)时,应把上阶段 rowid 填进来才能串起血缘。外汇与贵金属回测受点差跳空影响大,落库后务必标注 status 字段区分「中断 / 完成」,高风险品种建议单独建项目 id 隔离。
&class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// symbol &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// period %d, class=class="str">"cmt">// optimization %d, class=class="str">"cmt">// model &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// from_date &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// to_date %d, class=class="str">"cmt">// forward_mode &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// forward_date %d, class=class="str">"cmt">// deposit &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// currency %d, class=class="str">"cmt">// profit_in_pips %d, class=class="str">"cmt">// leverage %d, class=class="str">"cmt">// execution_mode %d, class=class="str">"cmt">// optimization_criterion &class="macro">#x27;%s&class="macro">#x27; class=class="str">"cmt">// status ) RETURNING rowid;, id_project, class=class="str">"cmt">// id_project "NULL", class=class="str">"cmt">// id_parent_stage "First" class=class="str">"cmt">// name
◍ 用脚本拉起 GBPUSD 的 H1 批量回测
在 MT5 终端里做批量验证,最省事的办法是直接用代码把回测参数一次性喂给策略测试器,而不是在 GUI 里手动点选。下面这段配置把简易成交量策略跑在英镑兑美元小时图上,属于典型的第一阶段压力测试。 配置里写死了 expert 为 SimpleVolumesStage1.ex5,品种 GBPUSD,周期 H1,优化标志与模型都设为 2(对应启用遗传优化与基于收盘价的 OHLC 模式)。起始和结束日期用 TimeToString 格式化为纯日期字符串传入,forward_mode 为 0 表示不做样本外前向检验。 本金设了 1000000 USD、杠杆 200 倍、profit_in_pips 为 0(以金额而非点数统计盈亏)。这套参数在外汇品种上属于高杠杆场景,价格跳空与滑点可能显著侵蚀回测结果,实盘前务必在 MT5 里用相同设置跑一遍并比对成交日志。
"SimpleVolumesStage1.ex5", class=class="str">"cmt">// expert "GBPUSD", class=class="str">"cmt">// symbol "H1", class=class="str">"cmt">// period class="num">2, class=class="str">"cmt">// optimization class="num">2, class=class="str">"cmt">// model TimeToString(fromDate_, TIME_DATE), class=class="str">"cmt">// from_date TimeToString(toDate_, TIME_DATE), class=class="str">"cmt">// to_date class="num">0, class=class="str">"cmt">// forward_mode "class="num">0", class=class="str">"cmt">// forward_date class="num">1000000, class=class="str">"cmt">// deposit "USD", class=class="str">"cmt">// currency class="num">0, class=class="str">"cmt">// profit_in_pips class="num">200 class=class="str">"cmt">// leverage
「把遗传优化拆成两段写进库」
在 MT5 里做 EA 遗传优化,光跑完测试器不够,想把每一代种群落盘就得自己拼 SQL。下面这段把第一阶段(全量种群生成)先插进 stages 表,execution_mode 填 0 代表同步执行,optimization_criterion 用 7 指代某种自定义适应度,status 直接写死 'Done' 方便后续筛选。 插完别忘用 DB::Insert 拿回自增主键 id_stage1,不拿这个 id 第二阶段就没法用 id_parent_stage 挂接父子关系。代码里 PrintFormat 打出 query1 是为了在专家日志里肉眼核对语句,实盘前建议开着看看有没有字段错位。 第二阶段挑“好单体组”时,表结构字段一下多到 14 个:从 id_project、id_parent_stage 到 from_date、to_date、forward_date 全是格式化占位符。forward_mode 和 deposit 用 %d 接整数,日期类全用 '%%s' 包成字符串,漏了单引号 SQLite 会直接报语法错。外汇与贵金属品种回测波动大,这类落库方案仅作分析用,实盘前请充分认知杠杆高风险。
class="num">0, class=class="str">"cmt">// execution_mode class="num">7, class=class="str">"cmt">// optimization_criterion "Done" class=class="str">"cmt">// status ); PrintFormat(__FUNCTION__" | %s", query1); id_stage1 = DB::Insert(query1); class=class="str">"cmt">// Stage class="num">2 - selection of a good group of single specimens class="type">class="kw">string query2 = StringFormat("INSERT INTO stages VALUES(" "NULL," class=class="str">"cmt">// id_stage "%I64u," class=class="str">"cmt">// id_project "%d," class=class="str">"cmt">// id_parent_stage "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// name "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// expert "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// symbol "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// period "%d," class=class="str">"cmt">// optimization "%d," class=class="str">"cmt">// model "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// from_date "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// to_date "%d," class=class="str">"cmt">// forward_mode "&class="macro">#x27;%s&class="macro">#x27;," class=class="str">"cmt">// forward_date "%d," class=class="str">"cmt">// deposit
把第二阶段任务写进 SQLite 的插入语句
这段代码负责把多品种量价策略的第二阶段跑批配置,作为一条记录插进本地 SQLite 的 tasks 表,并取回自增 rowid 以便后续串接。注意它硬编码了 expert 字段为 SimpleVolumesStage2.ex5、symbol 为 GBPUSD、周期 H1,意味着这套配置只服务于英镑美元一小时图上的次级过滤。 插入串里用 %s / %d 占位,对应货币对名、盈利点数、杠杆、执行模式、优化准则与状态文本;末尾 RETURNING rowid 是 SQLite 3.35+ 才支持的语法,老版本 MT5 内置 SQL 引擎若不支持就会报语法错。 实际参数中 optimization 与 model 都传了常量 2,对应控制面板里「开启优化」与「所用报价模型」的第二档选项;from_date 由 TimeToString(fromDate_, TIME_DATE) 格式化成纯日期字符串,不含时分秒,回测起点精度被限制在日级。 在 MT5 里验证时,先确认你的 terminal 数据目录下的 SQLite 版本,直接把这段拼好的 SQL 丢进 SQLITE.EXE 跑一次,看 rowid 能否正常返回,能返回才说明第二阶段任务被调度系统认领。
&class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// currency %d, class=class="str">"cmt">// profit_in_pips %d, class=class="str">"cmt">// leverage %d, class=class="str">"cmt">// execution_mode %d, class=class="str">"cmt">// optimization_criterion &class="macro">#x27;%s&class="macro">#x27; class=class="str">"cmt">// status ) RETURNING rowid;, id_project, class=class="str">"cmt">// id_project id_stage1, class=class="str">"cmt">// id_parent_stage "Second", class=class="str">"cmt">// name "SimpleVolumesStage2.ex5", class=class="str">"cmt">// expert "GBPUSD", class=class="str">"cmt">// symbol "H1", class=class="str">"cmt">// period class="num">2, class=class="str">"cmt">// optimization class="num">2, class=class="str">"cmt">// model TimeToString(fromDate_, TIME_DATE), class=class="str">"cmt">// from_date
◍ 把回测任务写进 stages 表的参数细节
这段代码片段在做一件事:把一次策略回测的任务配置,以一行记录的形式插进本地库的 stages 表。注意 toDate_ 用了 TimeToString 配 TIME_DATE 参数,只存到「日」精度,意味着回测区间的截止边界按自然日切,不保留时分秒。 参数里 deposit 写死 1000000、currency 为 USD、leverage 取 200,说明这套框架默认以百万美元本金、200 倍杠杆的标准账户跑优化,实际接自己经纪商条件时要改这三个值。optimization_criterion 设为 7,对应框架内部第 7 号评价准则(通常是某种风险调整收益类指标),换准则直接改这个数字即可。 forward_mode 为 0 且 forward_date 传 "0",表示未启用前向样本外测试;profit_in_pips 为 0 代表不以点数计利。execution_mode 为 0 即默认成交模拟。status 写 "Done" 是标记该阶段已落库。 Stage 3 的 query3 用 StringFormat 拼 INSERT,id_stage 留 NULL 让自增接管,id_project 和 id_parent_stage 用 %I64u、%d 占位,说明项目与主阶段之间是 64 位整型外键关联。复制时确认你的 DB::Insert 能返回自增 id,否则后续级联会断。
TimeToString(toDate_, TIME_DATE), class=class="str">"cmt">// to_date class="num">0, class=class="str">"cmt">// forward_mode "class="num">0", class=class="str">"cmt">// forward_date class="num">1000000, class=class="str">"cmt">// deposit "USD", class=class="str">"cmt">// currency class="num">0, class=class="str">"cmt">// profit_in_pips class="num">200, class=class="str">"cmt">// leverage class="num">0, class=class="str">"cmt">// execution_mode class="num">7, class=class="str">"cmt">// optimization_criterion "Done" class=class="str">"cmt">// status ); PrintFormat(__FUNCTION__" | %s", query2); id_stage2 = DB::Insert(query2); class=class="str">"cmt">// Stage class="num">3 - saving the initialization class="type">class="kw">string of the final EA to the library class="type">class="kw">string query3 = StringFormat("INSERT INTO stages VALUES(" "NULL," class=class="str">"cmt">// id_stage "%I64u," class=class="str">"cmt">// id_project "%d," class=class="str">"cmt">// id_parent_stage
「回测记录落库的字段映射」
把 MT5 策略测试器的每次优化结果写进 SQLite,需要明确每个占位符对应的业务字段。下面这段 SQL 片段就是插入一条回测任务时,按顺序排列的字段模板。 name 与 expert 区分项目名和 EA 名,symbol、period 锁定品种与周期;optimization 和 model 用整型标记是否开启优化及所用模型(如 0/1 或具体枚举值)。from_date、to_date 是回测区间,forward_mode 与 forward_date 控制前向检验。 deposit、currency 记录初始入金与账户币种,profit_in_pips 以点数记收益,leverage 为杠杆倍数。execution_mode 与 optimization_criterion 决定成交模拟方式与优化目标,status 用字符串标记任务状态(如 'done')。末尾 RETURNING rowid 拿回自增主键,便于把 id_project、id_stage2 作为父子阶段外键挂接。 在 MT5 里跑完一组黄金 XAUUSD 的 H1 优化后,照这个字段序导出的话,一条记录大约占用 17 个列,漏填 forward_date 会导致前向分析模块读不到样本切分点。
&class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// name &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// expert &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// symbol &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// period %d, class=class="str">"cmt">// optimization %d, class=class="str">"cmt">// model &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// from_date &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// to_date %d, class=class="str">"cmt">// forward_mode &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// forward_date %d, class=class="str">"cmt">// deposit &class="macro">#x27;%s&class="macro">#x27;, class=class="str">"cmt">// currency %d, class=class="str">"cmt">// profit_in_pips %d, class=class="str">"cmt">// leverage %d, class=class="str">"cmt">// execution_mode %d, class=class="str">"cmt">// optimization_criterion &class="macro">#x27;%s&class="macro">#x27; class=class="str">"cmt">// status ) RETURNING rowid;, id_project, class=class="str">"cmt">// id_project id_stage2, class=class="str">"cmt">// id_parent_stage
把回测任务写死进代码里
在 MQL5 里批量跑历史回测,不一定非得手动点 MT5 的策略测试器界面。用代码直接构造一组参数传给框架,就能让 EA 在指定品种和周期上自动跑起来,省去重复手填的麻烦。 下面这段配置把任务钉死在 GBPUSD 的 H1 周期:存入库的名字叫 Save to library,调用的 expert 是 SimpleVolumesStage3.ex5,初始入金 1000000 美元、杠杆 200 倍,优化开关设为 0(即不优化只跑单组)。 日期区间用 TimeToString(fromDate_, TIME_DATE) 和 TimeToString(toDate_, TIME_DATE) 转成日期字符串传入,forward_mode 填 0 表示不做前向检验,profit_in_pips 为 0 代表以货币而非点值统计利润。复制去改 fromDate_ / toDate_ 两个变量,就能在 MT5 里复现这段英镑小时的单轮回测。
"Save to library", class=class="str">"cmt">// name "SimpleVolumesStage3.ex5", class=class="str">"cmt">// expert "GBPUSD", class=class="str">"cmt">// symbol "H1", class=class="str">"cmt">// period class="num">0, class=class="str">"cmt">// optimization class="num">2, class=class="str">"cmt">// model TimeToString(fromDate_, TIME_DATE), class=class="str">"cmt">// from_date TimeToString(toDate_, TIME_DATE), class=class="str">"cmt">// to_date class="num">0, class=class="str">"cmt">// forward_mode "class="num">0", class=class="str">"cmt">// forward_date class="num">1000000, class=class="str">"cmt">// deposit "USD", class=class="str">"cmt">// currency class="num">0, class=class="str">"cmt">// profit_in_pips class="num">200 class=class="str">"cmt">// leverage
◍ 把多品种多周期拆成批量优化任务
做跨品种策略扫描时,手动一个个挂任务太慢。下面这段逻辑演示了如何把分号分隔的品种串和时间周期串拆成数组,再嵌套循环灌进任务表。 代码里先按 ';' 用 StringSplit 把 symbols_ 和 timeframes_ 拆开,时间周期字符串再经 StringToTimeframe 转成 ENUM_TIMEFRAMES 枚举。这样 EURUSD;XAUUSD 配 M1;M5 就能自动展开成 4 组组合。 第一阶段循环里,每个 symbol+timeframe 组合会插入一条 jobs 记录,并立刻拿到 rowid 存进 id_jobs1 数组。每个 job 再固定挂 3 条 tasks 子任务(循环 i<3),任务类型参数写死为 6,状态标 'Done' 等待代理领取。 在 MT5 里跑这套,你能直接验证:把 symbols_ 设为 "EURUSD;XAUUSD"、timeframes_ 设为 "M1;M5",数据库 jobs 表会多出 4 行、tasks 表多出 12 行。外汇与贵金属杠杆高,批量挂任务前先确认代理算力,避免爆队列。
class=class="str">"cmt">// Array of symbols for strategies class="type">class="kw">string symbols[]; StringSplit(symbols_, &class="macro">#x27;;&class="macro">#x27;, symbols); class=class="str">"cmt">// Array of timeframes for strategies ENUM_TIMEFRAMES timeframes[]; class="type">class="kw">string sTimeframes[]; StringSplit(timeframes_, &class="macro">#x27;;&class="macro">#x27;, sTimeframes); FOREACH(sTimeframes, APPEND(timeframes, StringToTimeframe(sTimeframes[i]))); class=class="str">"cmt">// Stage class="num">1 FOREACH(symbols, { for(class="type">int j = class="num">0; j < ArraySize(timeframes); j++) { class=class="str">"cmt">// Use the optimization parameters class="kw">template for the first stage class="type">class="kw">string params = StringFormat(paramsTemplate1, ""); class=class="str">"cmt">// Request to create the first stage job for a given symbol and timeframe class="type">class="kw">string query = StringFormat("INSERT INTO jobs " " VALUES(NULL,%I64u,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;Done&class="macro">#x27;) " " RETURNING rowid;", id_stage1, symbols[i], IntegerToString(timeframes[j]), params); class="type">ulong id_job = DB::Insert(query); class=class="str">"cmt">// Add the created job ID to the array APPEND(id_jobs1, id_job); class=class="str">"cmt">// Create three tasks for this job for(class="type">int i = class="num">0; i < class="num">3; i++) { query = StringFormat("INSERT INTO tasks " " VALUES(NULL,%I64u,%d,NULL,NULL,&class="macro">#x27;Done&class="macro">#x27;);", id_job, class="num">6); DB::Execute(query); } } });
「两阶段参数模板与任务入库写法」
| 把 EA 优化拆成多阶段跑,核心是先定义好每阶段的参数模板字符串,再循环塞进数据库 jobs 表。下面这段给出了第一阶段和第二阶段的模板实例,格式是「参数名=初值 | 步长 | 最小 | 最大 | 是否优化」。 |
|---|
第一模板里 signalPeriod_ 从 12 起步、步长 12、最大 240 且开启优化(Y),说明信号周期会在 12/24/36…240 之间网格搜;stopLevel_ 初值 16000、步长 200、最大 20000,明显是按点差整数位给止损空间。第二阶段模板用 idParentJob_=%s 承接上一阶段任务 ID,并硬性卡 minTrades_=40、minSharpeRatio_=0.7,低于这两条的父任务结果不会进下一轮。 代码里 FOREACH 套 for 的双层循环,把 symbols 和 timeframes 两两组合,用 StringFormat 把 paramsTemplate2 补全后 INSERT 进 jobs 表,RETURNING rowid 拿回任务主键存进 id_jobs2 数组;每个 job 再补一条 tasks 记录、写死 agent 类型 6。外汇与贵金属杠杆高,这种多阶段批跑若父阶段过滤过松,子阶段可能放大过拟合概率,建议先单品种手动验模板。 别把步长当越小越好 第一阶段 signalDeviation_ 步长 0.1、最大才 2.0,若品种波动率大,0.1 微调基本无意义,反而拖长优化时长;按品种 ATR 量级重设步长更划算。
class=class="str">"cmt">// Template of optimization parameters at the first stage class="type">class="kw">string paramsTemplate1 = "; === Open signal parameters\n" "signalPeriod_=class="num">212||class="num">12||class="num">40||class="num">240||Y\n" "signalDeviation_=class="num">0.1||class="num">0.1||class="num">0.1||class="num">2.0||Y\n" "signaAddlDeviation_=class="num">0.8||class="num">0.1||class="num">0.1||class="num">2.0||Y\n" "; === Pending order parameters\n" "openDistance_=class="num">10||class="num">0||class="num">10||class="num">250||Y\n" "stopLevel_=class="num">16000||class="num">200.0||class="num">200.0||class="num">20000.0||Y\n" "takeLevel_=class="num">240||class="num">100||class="num">10||class="num">2000.0||Y\n" "ordersExpiration_=class="num">22000||class="num">1000||class="num">1000||class="num">60000||Y\n" "; === Capital management parameters\n" "maxCountOfOrders_=class="num">3||class="num">3||class="num">1||class="num">30||N\n"; class=class="str">"cmt">// Template of optimization parameters for the second stage class="type">class="kw">string paramsTemplate2 = "idParentJob_=%s\n" "useClusters_=false||false||class="num">0||true||N\n" "minCustomOntester_=class="num">500.0||class="num">0.0||class="num">0.000000||class="num">0.000000||N\n" "minTrades_=class="num">40||class="num">40||class="num">1||class="num">400||N\n" "minSharpeRatio_=class="num">0.7||class="num">0.7||class="num">0.070000||class="num">7.000000||N\n" "count_=class="num">8||class="num">8||class="num">1||class="num">80||N\n"; class=class="str">"cmt">// Stage class="num">2 class="type">int k = class="num">0; FOREACH(symbols, { for(class="type">int j = class="num">0; j < ArraySize(timeframes); j++) { class=class="str">"cmt">// Use the optimization parameters class="kw">template for the second stage class="type">class="kw">string params = StringFormat(paramsTemplate2, IntegerToString(id_jobs1[k])); class=class="str">"cmt">// Request to create a second stage job for a given symbol and timeframe class="type">class="kw">string query = StringFormat("INSERT INTO jobs " " VALUES(NULL,%I64u,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;Done&class="macro">#x27;) " " RETURNING rowid;", id_stage2, symbols[i], IntegerToString(timeframes[j]), params); class="type">ulong id_job = DB::Insert(query); class=class="str">"cmt">// Add the created job ID to the array APPEND(id_jobs2, id_job); k++; class=class="str">"cmt">// Create one task for this job query = StringFormat("INSERT INTO tasks " " VALUES(NULL,%I64u,%d,NULL,NULL,&class="macro">#x27;Done&class="macro">#x27;);", id_job, class="num">6); DB::Execute(query); } }); class=class="str">"cmt">// Template of optimization parameters at the third stage class="type">class="kw">string paramsTemplate3 = "groupName_=%s\n" "passes_="; class=class="str">"cmt">// Stage class="num">3 class=class="str">"cmt">// Use the optimization parameters class="kw">template for the third stage
把第三阶段任务写进数据库并排队
在分布式回测框架里,第三阶段任务要靠 SQL 写入 jobs 与 tasks 两张表才能被调度器认领。下面这段逻辑把 GBPUSD 的 D1 周期任务挂到指定 stage3 节点下,参数串由项目名、版本号和截止日期拼成。 代码先用 StringFormat 把 paramsTemplate3 填成可读的任务标识,例如 ProjectA_v.1.2_2024.05.01 这种格式;随后向 jobs 表插一行并 RETURNING rowid 拿回自增主键,再往 tasks 表插一条 agent 编号为 0 的空任务,状态直接标 Done 仅作占位。 QueueProject 函数则把 projects 表里对应 id_project 的状态改成 Queued,调度线程扫到这个标记才会拉起执行。外汇与贵金属回测涉及高杠杆与滑点风险,实盘前请在 MT5 策略测试器用相同 schema 验证插入语句是否兼容你的 SQLite 版本。 把 db.schema.sql 以 #resource 方式编进 EX5,能保证数据库结构随程序分发而不依赖外部文件路径,省去部署时的手动拷贝。
class="type">class="kw">string params = StringFormat(paramsTemplate3, projectName_ + "_v." + projectVersion_ + "_" + TimeToString(toDate_, TIME_DATE)); class=class="str">"cmt">// // Request to create a third stage job class="type">class="kw">string query = StringFormat("INSERT INTO jobs " " VALUES(NULL,%I64u,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;%s&class="macro">#x27;,&class="macro">#x27;Done&class="macro">#x27;) " " RETURNING rowid;", id_stage3, "GBPUSD", "D1", params); class="type">ulong id_job = DB::Insert(query); class=class="str">"cmt">// Create one task for this job query = StringFormat("INSERT INTO tasks " " VALUES(NULL,%I64u,%d,NULL,NULL,&class="macro">#x27;Done&class="macro">#x27;);", id_job, class="num">0); DB::Execute(query); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Queueing the project for execution | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void QueueProject() { class="type">class="kw">string query = StringFormat("UPDATE projects SET status=&class="macro">#x27;Queued&class="macro">#x27; WHERE id_project=%d;", id_project); DB::Execute(query); } class=class="str">"cmt">// Import sql file for creating DB structure class="macro">#resource "db.schema.sql" as class="type">class="kw">string dbSchema
◍ 四项目并行填充库的实操节奏
做数据库填充时,不建议一次性铺太多任务,把并行项目压到 4 个就够了。具体做法是在 MT5 任意图表上挂 4 次 EA 脚本,除结束日期外其余参数全留默认,每次把测试区间的结束日期往后推一个月,形成连续四段样本。 项目表会落 4 条记录,每个项目自带 4 个阶段,其中创建时自动多出一个叫“单一测试器通过”的阶段,留着在自动优化传送带之外手动跑单次策略测试用。作业表同步写入对应作业,启动执行后大约 4 天出结果——性能已尽量调过,不算快但资源占用还能接受。 验证时直接查 strategy_groups 组库表,看 id_pass 去 passes 表对初始化字符串;或者把通过 ID 换成 SimpleVolumesStage3.ex5 第三阶段的输入,在 2018.01.01–2023.01.01 这种区间丢进测试器重跑。图 3 里 id_pass=876663 的通过结果就是这么来的,外汇与贵金属品种波动属性差异大,重跑前先确认点差与杠杆设置,这类跨周期验证高风险,结论只代表该样本区间的概率倾向。 接下来对这批结果做细拆,留到后续篇幅展开。
「优化输送机的阶段阻塞与热替换假想」
当前自动优化输送机被拆成三个阶段串行执行:必须等第一阶段全部跑完,第二阶段才启动;第二阶段收尾后,第三阶段才跟上。这种强依赖让任务排队时间被拉长,对想快速迭代参数的交易者并不友好。 若将来给最终 EA 的初始化字符串做“热”替换——也就是在账户上让 EA 持续交易的同时,并行把优化结果喂进去——可以把更新拆得更碎、更频繁。这有可能改善样本外表现,但目前只是待验证的假设,没有回测数字支撑。 现阶段的 EA 脚本只针对这一套模型策略生成优化项目;换策略至少要改第一阶段输入参数字符串的模板。模板现在还写死在源码里,因为放 EA 输入项里直接填很别扭。后续更可行的做法是搞一套任务描述格式,让脚本从文件读取建项任务。 外汇与贵金属市场高波动、高杠杆,本文涉及的输送机与脚本仅基于历史数据做研究性测试,不预示未来收益,任何照搬都需自担风险。
把这条线请下神坛
整套多币种 EA 的源码在 MQL5/Experts/Article.16373 目录下收口,共 40 个文件:从 Advisor.mqh 1.04 基类到 VirtualOrder.mqh 1.08 虚拟订单类,版本号跨度显示这套框架迭代了 21 个章节才稳定。 SimpleVolumesExpert.mq5 停在 1.20,是并行跑多组模型策略的实体;SimpleVolumesStage1 到 Stage3 分别承担单实例优化、组优化和组库落盘,三者版本号 1.18 / 1.02 / 1.02 说明后期改动已很小。 第三阶段 EA 启动必须指定前序优化管道产出的通行证 ID,这是作者在评论区亲口确认的限制——意味着你不能直接拿 ZIP 里的文件无脑跑,得先走完第一、二阶段。 外汇与贵金属自动化高风险,回测通过不代表实盘能复现;把这堆代码当研究素材而非印钞机,自己开 MT5 把 Stage1 跑通再谈下一步。