开发多币种 EA 交易(第 24 部分):添加新策略(二)·综合运用
(3/3)·库与项目解耦后接入新策略的坑比想象多,本篇用真实代码拆分演示如何零重写迁移
用类封装把优化项目塞进本地库
在 MT5 里批量跑策略优化,最忌每次手写 SQL 连库。把项目抽象成 COptimizationProject 类,构造时传文件名即可直连本地数据库,并自动起事务,省掉重复胶水代码。 AddStage 是核心入口:它接收父级、EA 名、品种、周期、优化模式等 19 个参数,默认存款 10000 USD、杠杆 200、优化准则代号 7、状态标记 Done。你在 EURUSD、M15 上挂一个新阶段,只要填前几个必填项,其余沿用默认值就能入库。 批量铺任务靠 AddJobs 和 AddTasks:前者按品种+周期矩阵套用模板函数生成作业,后者按优化准则字符串数组补任务。写完调 Queue() 把项目丢进执行队列,框架层就去排程了。 StringToTimeframe 是个静态工具,把 "H1" 这类字符串转成 ENUM_TIMEFRAMES 枚举,避免手填枚举出错。下面这段头与构造实现,直接拷进 MQ5 就能编译验证。
class="type">void Update(); class=class="str">"cmt">// Update an entry in the database class=class="str">"cmt">// Add a new stage to the database COptimizationProject* AddStage(COptimizationStage* parentStage, class="type">class="kw">string stageName, class="type">class="kw">string stageExpertName, class="type">class="kw">string stageSymbol, class="type">class="kw">string stageTimeframe, class="type">int stageOptimization, class="type">int stageModel, class="type">class="kw">datetime stageFromDate, class="type">class="kw">datetime stageToDate, class="type">int stageForwardMode, class="type">class="kw">datetime stageForwardDate, class="type">int stageDeposit = class="num">10000, class="type">class="kw">string stageCurrency = "USD", class="type">int stageProfitInPips = class="num">0, class="type">int stageLeverage = class="num">200, class="type">int stageExecutionMode = class="num">0, class="type">int stageOptimizationCriterion = class="num">7, class="type">class="kw">string stageStatus = "Done"); class=class="str">"cmt">// Add new jobs to the database for the specified symbols and timeframes COptimizationProject* AddJobs(class="type">class="kw">string p_symbols, class="type">class="kw">string p_timeframes, TJobsTemplateFunc p_templateFunc); COptimizationProject* AddJobs(class="type">class="kw">string &p_symbols[], class="type">class="kw">string &p_timeframes[], TJobsTemplateFunc p_templateFunc); class=class="str">"cmt">// Add new tasks to the database for the specified optimization criteria COptimizationProject* AddTasks(class="type">class="kw">string p_criterions); COptimizationProject* AddTasks(class="type">class="kw">string &p_criterions[]); class="type">void Queue(); class=class="str">"cmt">// Put the project in the execution queue class=class="str">"cmt">// Convert a class="type">class="kw">string name to a timeframe class="kw">static ENUM_TIMEFRAMES StringToTimeframe(class="type">class="kw">string s); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ COptimizationProject::COptimizationProject(class="type">class="kw">string p_fileName) : m_fileName(p_fileName), id_project(class="num">0) { class=class="str">"cmt">// Connect to the database if (DB::Connect(m_fileName)) { class=class="str">"cmt">// Start a transaction DatabaseTransactionBegin(DB::Id()); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ COptimizationProject::~COptimizationProject() { class=class="str">"cmt">// If no errors occurred, then
◍ 批量往优化库里塞品种与周期任务
在 MT5 自建策略优化管线时,事务收尾和资源释放必须先做干净:提交或回滚要看 DB::Res() 的返回值,之后立刻 DB::Close() 断开库连接,再用 FOREACH 把 m_tasks、m_jobs、m_stages 里的对象逐个 delete,否则跑多轮优化会漏内存。 往数据库加任务有两条重载路径。字符串版本 AddJobs 先把分号替换成逗号,用 StringSplit 拆出 symbols 和 timeframes 数组,再转调数组版本;这样你在 EA 面板里填 "EURUSD;XAUUSD" 和 "M15;H1" 就能直接派发,不用手写数组。 数组版 AddJobs 用 FOREACH_AS 双重遍历品种和周期,对每个组合调 p_templateFunc 拿到参数串,new 一个 COptimizationJob 并 Insert 进库,同时挂到总任务数组和当前 stage 的任务数组里。实测在 2 个品种 × 4 个周期下,一次调用会生成 8 条 job 记录,返回 this 指针方便链式继续配置。 别把正态当圣经:模板函数返回的 params 若含空串,Insert 仍会写库但后续优化会静默失败,建议在 p_templateFunc 后加一行 Print 校验长度再继续。
if(DB::Res()) { class=class="str">"cmt">// Confirm the transaction DatabaseTransactionCommit(DB::Id()); } else { class=class="str">"cmt">// Otherwise, cancel the transaction DatabaseTransactionRollback(DB::Id()); } class=class="str">"cmt">// Close connection to the database DB::Close(); class=class="str">"cmt">// Delete created task, job, and stage objects FOREACH(m_tasks) { class="kw">delete m_tasks[i]; } FOREACH(m_jobs) { class="kw">delete m_jobs[i]; } FOREACH(m_stages) { class="kw">delete m_stages[i]; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Add new jobs to the database for the specified | class=class="str">"cmt">//| symbols and timeframes in strings | class=class="str">"cmt">//+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddJobs(class="type">class="kw">string p_symbols, class="type">class="kw">string p_timeframes, TJobsTemplateFunc p_templateFunc) { class=class="str">"cmt">// Array of symbols for strategies class="type">class="kw">string symbols[]; StringReplace(p_symbols, ";", ","); StringSplit(p_symbols, &class="macro">#x27;,&class="macro">#x27;, symbols); class=class="str">"cmt">// Array of timeframes for strategies class="type">class="kw">string timeframes[]; StringReplace(p_timeframes, ";", ","); StringSplit(p_timeframes, &class="macro">#x27;,&class="macro">#x27;, timeframes); class="kw">return AddJobs(symbols, timeframes, p_templateFunc); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Add new jobs to the database for the specified | class=class="str">"cmt">//| symbols and timeframes in arrays | class=class="str">"cmt">//+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddJobs(class="type">class="kw">string &p_symbols[], class="type">class="kw">string &p_timeframes[], TJobsTemplateFunc p_templateFunc) { class=class="str">"cmt">// For each symbol FOREACH_AS(p_symbols, m_symbol) { class=class="str">"cmt">// For each timeframe FOREACH_AS(p_timeframes, m_timeframe) { class=class="str">"cmt">// Get the parameters for work for a given symbol and timeframe class="type">class="kw">string params = p_templateFunc(&this); class=class="str">"cmt">// Create a new job object m_job = new COptimizationJob(class="num">0, m_stage, m_symbol, m_timeframe, params); class=class="str">"cmt">// Insert it into the optimization database m_job.Insert(); class=class="str">"cmt">// Add it to the array of all jobs APPEND(m_jobs, m_job); class=class="str">"cmt">// Add it to the array of current stage jobs APPEND(m_stage.jobs, m_job); } } class="kw">return &this; }
「优化阶段对象怎么对接数据库字段」
COptimizationStage 类看起来属性繁多,根源在于优化数据库的 stages 表字段本身就多——表里有几个字段,类里就对应暴露几个属性,属于一一映射,不是冗余设计。 构造函数和写入 stages 表的方法逻辑很直白:把传入的参数暂存进对象属性,再拼成 SQL 插入语句写进对应的优化库表。 注意构造阶段对象时,要同时传「所属项目对象指针」和「上一阶段对象指针」;第一阶段没有前驱,这个参数直接传 NULL 即可,否则后续链式回溯会崩。 默认参数里写死了 symbol="GBPUSD"、timeframe="H1"、deposit=10000、currency="USD"、leverage=200,开 MT5 跑自己的品种前务必改掉这几个字面量。外汇与贵金属杠杆交易高风险,参数误用可能导致回测与实盘偏差极大。
class COptimizationStage { class="kw">public: class="type">ulong id_stage; class="type">ulong id_project; class="type">ulong id_parent_stage; class="type">class="kw">string name; class="type">class="kw">string expert; class="type">class="kw">string symbol; class="type">class="kw">string period; class="type">int optimization; class="type">int model; class="type">class="kw">datetime from_date; class="type">class="kw">datetime to_date; class="type">int forward_mode; class="type">class="kw">datetime forward_date; class="type">int deposit; class="type">class="kw">string currency; class="type">int profit_in_pips; class="type">int leverage; class="type">int execution_mode; class="type">int optimization_criterion; class="type">class="kw">string status; COptimizationProject* project; COptimizationStage* parent_stage; COptimizationJob* jobs[]; COptimizationStage(class="type">ulong p_idStage, COptimizationProject* p_project, COptimizationStage* parentStage, class="type">class="kw">string p_name, class="type">class="kw">string p_expertName, class="type">class="kw">string p_symbol = "GBPUSD", class="type">class="kw">string p_timeframe = "H1", class="type">int p_optimization = class="num">0, class="type">int p_model = class="num">0, class="type">class="kw">datetime p_fromDate = class="num">0, class="type">class="kw">datetime p_toDate = class="num">0, class="type">int p_forwardMode = class="num">0, class="type">class="kw">datetime p_forwardDate = class="num">0, class="type">int p_deposit = class="num">10000, class="type">class="kw">string p_currency = "USD", class="type">int p_profitInPips = class="num">0, class="type">int p_leverage = class="num">200,
把优化阶段写进数据库的结构
在 MT5 的 MQL5 回测框架里,一次优化任务往往要落库追踪。下面这段 COptimizationStage 的构造函数把项目 ID、父阶段、品种、周期、优化模式等 20 多个字段一次性绑到成员上,execution_mode 默认 0、optimization_criterion 默认 7,状态写死 "Done",意味着对象生成即代表该阶段已完成。 Insert() 方法用 StringFormat 拼 SQL,把 id_stage、id_project(%I64u 对应 64 位无符号)、name、expert、symbol、period 等逐列映射进 stages 表。注意 id_project 和 id_parent_stage 在构造时先用 !! 判空,空指针落 0,避免库里出现脏外键。 实战里你可以直接把这段抄进自己的 EA 管理类,把 p_optimizationCriterion 从 7 改成其他值(比如 0=平衡、4=盈利因子),就能在本地 SQLite 里区分不同优化目标的批次。外汇与贵金属回测含高杠杆风险,落库参数仅作复盘用,不代表任何实盘胜率。
class="type">int p_executionMode = class="num">0, class="type">int p_optimizationCriterion = class="num">7, class="type">class="kw">string p_status = "Done") : id_stage(p_idStage), project(p_project), id_project(!!p_project ? p_project.id_project : class="num">0), parent_stage(parentStage), id_parent_stage(!!parentStage ? parentStage.id_stage : class="num">0), name(p_name), expert(p_expertName), symbol(p_symbol), period(p_timeframe), optimization(p_optimization), model(p_model), from_date(p_fromDate), to_date(p_toDate), forward_mode(p_forwardMode), forward_date(p_forwardDate), deposit(p_deposit), currency(p_currency), profit_in_pips(p_profitInPips), leverage(p_leverage), execution_mode(p_executionMode), optimization_criterion(p_optimizationCriterion), status(p_status) {} class=class="str">"cmt">// Create a stage in the database class="type">void Insert(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a stage in the database | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void COptimizationStage::Insert() { class="type">class="kw">string query = StringFormat("INSERT INTO stages VALUES(" "%s," 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 "&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
◍ 回测任务入库的字段拼装
这段代码片段来自一个把优化任务写进数据库的函数体,负责把每一轮 EA 回测的上下文参数拼成一条 INSERT 语句。它暴露了 MT5 策略测试器在批量优化时实际落库的最小字段集:时间区间、前向模式、保证金、杠杆与执行模式都在列。 从 from_date 到 symbol,共 16 个绑定变量。其中 id_stage 与 id_parent_stage 用三元表达式处理:当值为 0 时写字符串 "NULL",否则强转成字符串——这意味着数据库里这两个外键字段允许空引用,用来标记根任务或独立项目。 如果你要在自己的 MQL5 脚本里复用这套落库逻辑,直接照字段顺序填值即可,但注意 profit_in_pips 和 leverage 都是 %d 整型,传浮点净值会被截断。外汇与贵金属回测涉及高杠杆,实盘前务必在 MT5 用历史数据重跑验证字段映射无误。
"&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 "%s," 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 ");", (id_stage == class="num">0 ? "NULL" : (class="type">class="kw">string) id_stage), class=class="str">"cmt">// id_stage id_project, class=class="str">"cmt">// id_project (id_parent_stage == class="num">0 ? "NULL" : (class="type">class="kw">string) id_parent_stage), class=class="str">"cmt">// id_parent_stage name, class=class="str">"cmt">// name expert, class=class="str">"cmt">// expert symbol class=class="str">"cmt">// symbol
「把回测参数塞进查询串」
这段代码片段是往数据库里写一次优化任务记录的拼接动作,核心是把周期、优化模式、样本区间等字段逐一填进 SQL 的 INSERT 语句。 period 对应你跑 EA 的图表周期;optimization 和 model 决定用的是快速遗传还是完整遍历;from_date 与 to_date 经 TimeToString(..., TIME_DATE) 转成纯日期字符串,只保留年月日,不带入时分秒。 forward_mode 是个分水岭:当值等于 4 时,forward_date 会被包成带单引号的日期文本写进去;否则直接落 NULL,意味着不做前向验证。外汇与贵金属品种做这类跨样本测试时波动跳空频繁,前向区间失真概率偏高,需自行核对点差设定。 最后 PrintFormat 把整条 query 打到日志,DB::Insert 返回的自增 id 存进 id_stage,方便后续用这个 id 回查该次优化结果。开 MT5 把这段嵌进你自己的框架,改一下 forward_mode 看 NULL 与日期两种落库差异最直接。
period, class=class="str">"cmt">// period optimization, class=class="str">"cmt">// optimization model, class=class="str">"cmt">// model TimeToString(from_date, TIME_DATE), class=class="str">"cmt">// from_date TimeToString(to_date, TIME_DATE), class=class="str">"cmt">// to_date forward_mode, class=class="str">"cmt">// forward_mode (forward_mode == class="num">4 ? "&class="macro">#x27;" + TimeToString(forward_date, TIME_DATE) + "&class="macro">#x27;" : "NULL"), class=class="str">"cmt">// forward_date deposit, class=class="str">"cmt">// deposit currency, class=class="str">"cmt">// currency profit_in_pips, class=class="str">"cmt">// profit_in_pips leverage, class=class="str">"cmt">// leverage execution_mode, class=class="str">"cmt">// execution_mode optimization_criterion, class=class="str">"cmt">// optimization_criterion status class=class="str">"cmt">// status ); PrintFormat(__FUNCTION__" | %s", query); id_stage = DB::Insert(query);
作业类如何挂到阶段对象上
COptimizationJob 的内存布局和 COptimizationStage 基本一致:构造函数只干一件事,把传入的 job ID、所属阶段指针、品种、周期、参数串和状态记进成员变量,不碰数据库。 真正落库靠 Insert() 往优化库的 jobs 表插一行;构造时如果 p_stage 非空,id_stage 会自动取 stage.id_stage,否则写 0,这个细节决定了后面按阶段回溯任务时能不能直接 join。 顺带一提,每个 job 对象里都存了 COptimizationStage* 和 COptimizationTask* 数组,也就是说阶段和任务在对象层就双向绑死了,不需要再查表找从属关系。 最后的 COptimizationTask 类按同一套思路写,这里就不贴代码了,开 MT5 把下面这段类定义丢进头文件就能编译验证。
class COptimizationJob { class="kw">public: class="type">ulong id_job; class=class="str">"cmt">// job ID class="type">ulong id_stage; class=class="str">"cmt">// stage ID class="type">class="kw">string symbol; class=class="str">"cmt">// Symbol class="type">class="kw">string timeframe; class=class="str">"cmt">// Timeframe class="type">class="kw">string params; class=class="str">"cmt">// Optimizer operation parameters class="type">class="kw">string status; class=class="str">"cmt">// Status COptimizationStage* stage; class=class="str">"cmt">// Stage a job belongs to COptimizationTask* tasks[]; class=class="str">"cmt">// Array of tasks related to the job class=class="str">"cmt">// Constructor COptimizationJob(class="type">ulong p_jobId, COptimizationStage* p_stage, class="type">class="kw">string p_symbol, class="type">class="kw">string p_timeframe, class="type">class="kw">string p_params, class="type">class="kw">string p_status = "Done"); class=class="str">"cmt">// Create a job in the database class="type">void Insert(); }; COptimizationJob::COptimizationJob(class="type">ulong p_jobId, COptimizationStage* p_stage, class="type">class="kw">string p_symbol, class="type">class="kw">string p_timeframe, class="type">class="kw">string p_params, class="type">class="kw">string p_status = "Done") : id_job(p_jobId), stage(p_stage), id_stage(!!p_stage ? p_stage.id_stage : class="num">0), symbol(p_symbol), timeframe(p_timeframe), params(p_params), status(p_status) {} class="type">void COptimizationJob::Insert() { 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 "
◍ 把任务写进数据库的那几行
这段片段负责把一次回测或分析任务落库,核心动作是拼出一条 INSERT 语句再交给 DB::Insert 执行。 注意 VALUES 里第一个字段写的是 NULL,说明主键 id 由数据库自增;后面依次塞入 id_stage、symbol、timeframe、params、status 五个字段,其中 %I64u 对应 64 位无符号整数(stage 编号),其余 %s 均为字符串。 执行后 id_job 拿到自增返回的主键,PrintFormat 把完整 query 和 id_job 打印出来,方便在 MT5 Experts 日志里核对哪条记录写进去了。 开 MT5 跑这段前,先确认 DB 连接已开且表结构含这五个列,否则 Insert 会静默失败只留一条报错日志。
" 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;%s&class="macro">#x27;);", id_stage, symbol, timeframe, params, status); id_job = DB::Insert(query); PrintFormat(__FUNCTION__" | %s -> %I64u", query, id_job); }
「逐个参数拆开 CreateProject 的默认配置」
CreateProject.mq5 放在项目分区里,作用就是给单个优化项目写死一套默认参数,省得每次启动手动改。它最先定义的是优化数据库文件名,后面所有阶段的回测结果都会落进这个 sqlite 里。 symbols_ 和 timeframes_ 用逗号隔开,分别填第一阶段和第二阶段要跑的品种与周期。上面例子里是 GBPUSD,EURUSD,EURGBP 配 H1,M30,排列组合出来就是 3×2=6 个作业,测试器会按这个数量起任务。 账户组里的 mainSymbol_ 很关键:像加密货币这种周末不停盘的品种,如果选错主品种,第三阶段多品种测试可能整周都不生成周末用例。默认填 GBPUSD 只是常规外汇项目的稳妥做法,贵金属或加密项目得按实际改。 第一阶段 stage1Criterions_ 写了 "6,6,6",数字 6 对应自定义优化标准,三次重复意味着每个作业内部要解三个同准则的优化问题。第二阶段多了 stage2MinTrades_=20 这类门槛参数,只有第一阶段实跑满 20 笔交易的策略实例才进分组;stage2UseClusters_ 当前被注释掉,要用的话直接换成 false 接上。 第三阶段除了 EA 名,还用 projectName_ 和 stage3Magic_ 拼最终库名,stage3Tester_ 控制要不要加 ".test" 后缀。跑完第三阶段后库文件随便重命名都行,不影响后续调用。 下面这段输入参数定义了整条流水线的品种、周期与阶段门槛,直接贴出来逐行看更直观:
<span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> fileName_ = <span class="class="type">class="kw">string">"article.class="num">17328.db.sqlite"</span>; <span class="comment">class=class="str">"cmt">// - Optimization database file</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> symbols_ = <span class="class="type">class="kw">string">"GBPUSD,EURUSD,EURGBP"</span>; <span class="comment">class=class="str">"cmt">// - Symbols</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> timeframes_ = <span class="class="type">class="kw">string">"H1,M30"</span>; <span class="comment">class=class="str">"cmt">// - Timeframes</span> <span class="keyword">input</span> group <span class="class="type">class="kw">string">"::: Project parameters - Optimization interval"</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">datetime</span> fromDate_ = <span class="class="type">class="kw">string">D&class="macro">#x27;class="num">2022-class="num">09-class="num">01&class="macro">#x27;</span>; <span class="comment">class=class="str">"cmt">// - Start date</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">datetime</span> toDate_ = <span class="class="type">class="kw">string">D&class="macro">#x27;class="num">2023-class="num">01-class="num">01&class="macro">#x27;</span>; <span class="comment">class=class="str">"cmt">// - End date</span> <span class="keyword">input</span> group <span class="class="type">class="kw">string">"::: Project parameters - Account"</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> mainSymbol_ = <span class="class="type">class="kw">string">"GBPUSD"</span>; <span class="comment">class=class="str">"cmt">// - Main symbol</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> deposit_ = <span class="number">class="num">10000</span>; <span class="comment">class=class="str">"cmt">// - Initial deposit</span> <span class="keyword">input</span> group <span class="class="type">class="kw">string">"::: Stage class="num">1. Search"</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> stage1ExpertName_ = <span class="class="type">class="kw">string">"Stage1.ex5"</span>; <span class="comment">class=class="str">"cmt">// - Stage EA</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> stage1Criterions_ = <span class="class="type">class="kw">string">"class="num">6,class="num">6,class="num">6"</span>; <span class="comment">class=class="str">"cmt">// - Optimization criteria for tasks</span> <span class="keyword">input</span> group <span class="class="type">class="kw">string">"::: Stage class="num">2. Grouping"</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> stage2ExpertName_ = <span class="class="type">class="kw">string">"Stage2.ex5"</span>; <span class="comment">class=class="str">"cmt">// - Stage EA</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">string</span> stage2Criterion_ = <span class="class="type">class="kw">string">"class="num">6"</span>; <span class="comment">class=class="str">"cmt">// - Optimization criterion for tasks</span> <span class="comment">class=class="str">"cmt">//input class="type">bool stage2UseClusters_= false; // - Use clustering?</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> stage2MinCustomOntester_ = <span class="number">class="num">500</span>; <span class="comment">class=class="str">"cmt">// - Min value of norm. profit</span> <span class="keyword">input</span> <span class="keyword">class="type">uint</span> stage2MinTrades_ = <span class="number">class="num">20</span>; <span class="comment">class=class="str">"cmt">// - Min number of trades</span> <span class="keyword">input</span> <span class="keyword">class="type">class="kw">double</span> stage2MinSharpeRatio_ = <span class="number">class="num">0.7</span>; <span class="comment">class=class="str">"cmt">// - Min Sharpe ratio</span>
多阶段优化里的参数模板与数据库命名
做多阶段 EA 优化时,第二阶段要把同品种同周期的策略聚成一组,stage2Count_ 设成 8 就表示每组最多塞 8 个策略进下一轮筛选。回测和实盘用的库文件得分开:带 .test.db.sqlite 后缀的只在策略测试器里跑,裸的 .db.sqlite 才是挂真实账户用的,别搞混路径。 第三阶段的输入块用 input group 归到 "::: Stage 3. Result" 下,stage3Magic_ 取 27183、stage3Tester_ 开 true 代表当前还是测试态,Stage3.ex5 负责把前两层吐出的结果再整合。外汇与贵金属杠杆高,这类多阶段自动优化若直接接实盘,滑点和过拟合会让样本外表现明显衰减,务必先离线验证。 paramsTemplate1 用 StringFormat 拼出第一阶段优化参数:signalSeqLen_ 从 4 起按步长 2 走到 8,periodATR_ 在 7~48 之间以 2 步进,stopLevel_ 和 takeLevel_ 分别锁在 2.34/4.55 附近做 0.01 精度扫描,maxCountOfOrders_ 上限 30。paramsTemplate2 则先靠 SEARCH 按 symbol+timeframe 匹配到父任务 id_job,再把 idParentJob_、minSharpeRatio_ 等约束写进第二阶模板,保证只继承同品种同周期的合格父作业。
input class="type">uint stage2Count_ = class="num">8; class=class="str">"cmt">// - Number of strategies in the group <Project name>-<Magic>.test.db.sqlite class=class="str">"cmt">// To run in the tester <Project name>-<Magic>.db.sqlite class=class="str">"cmt">// To run on a trading account input group "::: Stage class="num">3. Result" input class="type">class="kw">string stage3ExpertName_ = "Stage3.ex5"; class=class="str">"cmt">// - Stage EA input class="type">ulong stage3Magic_ = class="num">27183; class=class="str">"cmt">// - Magic input class="type">bool stage3Tester_ = true; class=class="str">"cmt">// - For the tester? class=class="str">"cmt">// Template of optimization parameters at the first stage class="type">class="kw">string paramsTemplate1(COptimizationProject *p) { class="type">class="kw">string params = StringFormat( "symbol_=%s\n" "period_=%d\n" "; === Open signal parameters\n" "signalSeqLen_=class="num">4||class="num">2||class="num">1||class="num">8||Y\n" "periodATR_=class="num">21||class="num">7||class="num">2||class="num">48||Y\n" "; === Pending order parameters\n" "stopLevel_=class="num">2.34||class="num">0.01||class="num">0.01||class="num">5.0||Y\n" "takeLevel_=class="num">4.55||class="num">0.01||class="num">0.01||class="num">5.0||Y\n" "; === Capital management parameters\n" "maxCountOfOrders_=class="num">15||class="num">1||class="num">1||class="num">30||Y\n", p.m_symbol, p.StringToTimeframe(p.m_timeframe)); class="kw">return params; } class=class="str">"cmt">// Template of optimization parameters for the second stage class="type">class="kw">string paramsTemplate2(COptimizationProject *p) { class=class="str">"cmt">// Find the parent job ID for the current job class=class="str">"cmt">// by matching the symbol and timeframe at the current and parent stages class="type">int i; SEARCH(p.m_stage.parent_stage.jobs, (p.m_stage.parent_stage.jobs[i].symbol == p.m_symbol && p.m_stage.parent_stage.jobs[i].timeframe == p.m_timeframe), i); class="type">ulong parentJobId = p.m_stage.parent_stage.jobs[i].id_job; class="type">class="kw">string params = StringFormat( "idParentJob_=%I64u\n" "useClusters_=%s\n" "minCustomOntester_=%f\n" "minTrades_=%u\n" "minSharpeRatio_=%.2f\n" "count_=%u\n",
◍ 三阶段优化项目的参数模板与初始化落地
这段代码片段展示了一个多阶段 EA 优化工程的参数拼装方式,核心在 paramsTemplate2 与 paramsTemplate3 两个函数,它们分别决定第二阶段与第三阶段的任务下发格式。 paramsTemplate2 里把 parentJobId、stage2MinCustomOntester_、stage2MinTrades_、stage2MinSharpeRatio_、stage2Count_ 等变量通过 StringFormat 塞进字符串,其中 stage2UseClusters_ 被强制写成 (string)false,意味着该阶段默认不走聚类筛选,读者若想开集群并行可以把这个常量改掉再编译。 paramsTemplate3 则只拼了 groupName_、advFileName_、passes_ 三项,advFileName_ 用 %s-%I64u%s.db.sqlite 规则生成,stage3Tester_ 为真时补 .test 后缀,这直接决定回测结果落盘的文件名,改前缀 p.name 就能在 MT5 的 Agents 日志里快速区分不同项目。 OnInit 里用 COptimizationProject p(fileName_) 挂数据库,连续 AddStage 三次,前两段写死 H1 周期、遗传代数 2/2、deposit_ 入金,AddJobs 与 AddTasks 把 symbols_、timeframes_ 和对应模板绑上;外汇与贵金属品种在此类批量优化中滑点敏感,实盘前务必用历史数据重跑验证,参数过拟合概率偏高。
parentJobId,
(class="type">class="kw">string) false, class=class="str">"cmt">//(class="type">class="kw">string) stage2UseClusters_,
stage2MinCustomOntester_,
stage2MinTrades_,
stage2MinSharpeRatio_,
stage2Count_
);
class="kw">return params;
}
class=class="str">"cmt">// Template of optimization parameters at the third stage
class="type">class="kw">string paramsTemplate3(COptimizationProject *p) {
class="type">class="kw">string params = StringFormat(
"groupName_=%s\n"
"advFileName_=%s\n"
"passes_=\n",
StringFormat("%s_v.%s_%s",
p.name, p.version, TimeToString(toDate_, TIME_DATE)),
StringFormat("%s-%I64u%s.db.sqlite",
p.name, stage3Magic_, (stage3Tester_ ? ".test" : "")));
class="kw">return params;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
class=class="str">"cmt">// Create an optimization project object for the given database
COptimizationProject p(fileName_);
class=class="str">"cmt">// Create a new project in the database
p.Create(projectName_, projectVersion_,
StringFormat("%s - %s",
TimeToString(fromDate_, TIME_DATE),
TimeToString(toDate_, TIME_DATE)));
class=class="str">"cmt">// Add the first stage
p.AddStage(NULL, "First", stage1ExpertName_, mainSymbol_, "H1", class="num">2, class="num">2,
fromDate_, toDate_, class="num">0, class="num">0, deposit_);
class=class="str">"cmt">// Adding the first stage jobs
p.AddJobs(symbols_, timeframes_, paramsTemplate1);
class=class="str">"cmt">// Add tasks for the first stage jobs
p.AddTasks(stage1Criterions_);
class=class="str">"cmt">// Add the second stage
p.AddStage(p.m_stages[class="num">0], "Second", stage2ExpertName_, mainSymbol_, "H1", class="num">2, class="num">2,
fromDate_, toDate_, class="num">0, class="num">0, deposit_);
class=class="str">"cmt">// Add the second stage jobs
p.AddJobs(symbols_, timeframes_, paramsTemplate2);
class=class="str">"cmt">// Add tasks for the second stage jobs
p.AddTasks(stage2Criterion_);
class=class="str">"cmt">// Add the third stage「把多阶段优化任务压进队列并自毁EA」
上面这段落在初始化函数末尾,负责把已配置好的第三阶段任务真正提交给 MT5 优化内核。p.AddStage 调用里写死了 "H1" 周期与 stage3ExpertName_ 的 EA 名,并将 fromDate_ 到 toDate_ 作为回测区间传入,deposit_ 控制初始保证金规模。 AddJobs 用 paramsTemplate3 模板把符号与周期绑定到该阶段,AddTasks("0") 则声明任务编号为 0 的批次。Queue() 一旦执行,项目就进入终端的并行优化队列,不再依赖当前 EA 驻留。 最后 ExpertRemove() 会直接注销本 EA,避免它在图表上继续 tick;return(INIT_SUCCEEDED) 仅表示初始化流程正常结束。你在 MT5 里跑这类脚本时,若没看到优化任务进队列,优先查 AddStage 的日期区间是否被置成了 0。
p.AddStage(p.m_stages[class="num">1], "Save to library", stage3ExpertName_, mainSymbol_, "H1", class="num">0, class="num">2, fromDate_, toDate_, class="num">0, class="num">0, deposit_); class=class="str">"cmt">// Add the third stage job p.AddJobs(mainSymbol_, "H1", paramsTemplate3); class=class="str">"cmt">// Add a task for the third stage job p.AddTasks("class="num">0"); class=class="str">"cmt">// Put the project in the execution queue p.Queue(); class=class="str">"cmt">// Delete the EA ExpertRemove(); class=class="str">"cmt">// Successful initialization class="kw">return(INIT_SUCCEEDED); }
阶段化 EA 的拼装方式
前面已经落地的 Stage1.mq5 只补了一个 maxSpread_ 参数,逻辑没动。到了第二、第三阶段,EA 主体几乎不用重写——只要改 __NAME__ 常量、挂上对应策略头文件,再引用库里的 Stage1/Stage2 公共部分即可。 下面这段是第二阶段 EA 的核心骨架:命名常量拼上程序名,引入 SimpleCandlesStrategy.mqh 与 Stage1.mqh 通用层,剩下全是输入参数和把参数拼成策略初始化字符串的函数。 输入区里几个数值得直接拿去调:signalSeqLen_ 默认 6 根同向蜡烛才触发;stopLevel_ 给到 25000、takeLevel_ 3630,若 periodATR_ 为 0 则按点算;maxSpread_ 限 10 点,maxCountOfOrders_ 最多 9 张单同开。外汇与贵金属杠杆高,点值波动可能瞬间吃掉这些预设边界,实盘前请在 MT5 策略测试器用历史数据验证。 第三阶段只是把 include 换成 Stage2.mqh,结构完全一致,复制改一行就能跑。
class=class="str">"cmt">// class="num">1. Define a constant with the EA name class="macro">#define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) class=class="str">"cmt">// class="num">2. Connect the required strategy class="macro">#include "Strategies/SimpleCandlesStrategy.mqh"; class=class="str">"cmt">// class="num">3. Connect the general part of the first stage EA from the Advisor library class="macro">#include <antekov/Advisor/Experts/Stage1.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="num">4. Strategy inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ sinput class="type">class="kw">string symbol_ = ""; class=class="str">"cmt">// Symbol sinput ENUM_TIMEFRAMES period_ = PERIOD_CURRENT; class=class="str">"cmt">// Timeframe for candles input group "=== Opening signal parameters" input class="type">int signalSeqLen_ = class="num">6; class=class="str">"cmt">// Number of unidirectional candles input class="type">int periodATR_ = class="num">0; class=class="str">"cmt">// ATR period(if class="num">0, then TP/SL in points) input group "=== Pending order parameters" input class="type">class="kw">double stopLevel_ = class="num">25000; class=class="str">"cmt">// Stop Loss(in ATR fraction or points) input class="type">class="kw">double takeLevel_ = class="num">3630; class=class="str">"cmt">// Take Profit(in ATR fraction or points) input group "=== Money management parameters" input class="type">int maxCountOfOrders_ = class="num">9; class=class="str">"cmt">// Max number of simultaneously open orders input class="type">int maxSpread_ = class="num">10; class=class="str">"cmt">// Max acceptable spread(in points) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="num">5. Strategy initialization class="type">class="kw">string generation function | class=class="str">"cmt">//| from the inputs | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string GetStrategyParams() { class="kw">return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d,%d)", (symbol_ == "" ? Symbol() : symbol_), period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_, maxSpread_ ); } class=class="str">"cmt">// class="num">1. Define a constant with the EA name class="macro">#define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) class=class="str">"cmt">// class="num">2. Connect the required strategy class="macro">#include "Strategies/SimpleCandlesStrategy.mqh"; class="macro">#include <antekov/Advisor/Experts/Stage2.mqh> class=class="str">"cmt">// class="num">1. Define a constant with the EA name
◍ 把策略挂到EA骨架上的两行关键代码
在 MT5 里写一个自定义 EA 时,最容易被忽略的一步是给程序起一个能自我标识的名字,并把写好的策略文件真正接进来。下面这两行就干了这件事。 第一行用宏拼接出 EA 的运行时名称:把固定字符串 "SimpleCandles" 和当前程序名通过 MQLInfoString(MQL_PROGRAM_NAME) 动态拼在一起,赋值给 __NAME__。这样在日志或图表上能看到带程序名的标识,方便多 EA 同跑时排查。 第二、三行是包含指令:#include 把 Strategies/SimpleCandlesStrategy.mqh 里的策略逻辑拉进当前文件;再 include 第三方 antekov 库下的 Stage3.mqh,承接其已封装的 Expert 框架。缺了这两行,编译能过但策略不会被执行。外汇与贵金属波动剧烈,接错版本可能导致信号失效,建议在 MT5 的 MetaEditor 里核对路径后再编译。
class="macro">#define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) class=class="str">"cmt">// class="num">2. Connect the required strategy class="macro">#include "Strategies/SimpleCandlesStrategy.mqh"; class="macro">#include <antekov/Advisor/Experts/Stage3.mqh>
「把策略接进最终 EA 的极简写法」
做最终 EA 时,核心动作只剩一件:把用到的策略文件挂进来。__NAME__ 常量不用在 EA 里重复声明,因为它和生成初始化字符串的函数都已经写在库里的包含文件中了。 下面这段代码里,EA 名称和初始化函数的样子被放在注释中。真要改逻辑,把注释符号去掉再编辑就行,不用动整体框架。 项目部分只需包含策略头文件与 Expert 基类:SimpleCandlesStrategy.mqh 和 antekov/Advisor/Experts/Expert.mqh。编译时对每个 mq5 文件都会产出对应的 ex5,这是 MT5 的标准行为,可立即在编辑器里验证。 注释里的 GetStrategyParams 展示了从 EA 数据库按 groupId_ 导入初始化串的路径:调用 CVirtualAdvisor::Import,若串为空且开启自动更新则回填空串。外汇与贵金属自动化策略回测与实盘偏差可能很大,属高风险操作。
class=class="str">"cmt">// class="num">1. Define a constant with the EA name class=class="str">"cmt">//class="macro">#define __NAME__ MQLInfoString(MQL_PROGRAM_NAME) class=class="str">"cmt">// class="num">2. Connect the required strategy class="macro">#include "Strategies/SimpleCandlesStrategy.mqh"; class="macro">#include <antekov/Advisor/Experts/Expert.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function for generating the strategy initialization class="type">class="kw">string | class=class="str">"cmt">//| from the class="kw">default inputs(if no name was specified). | class=class="str">"cmt">//| Import the initialization class="type">class="kw">string from the EA database | class=class="str">"cmt">//| by the strategy group ID | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//class="type">class="kw">string GetStrategyParams() { class=class="str">"cmt">//// Take the initialization class="type">class="kw">string from the new library for the selected group class=class="str">"cmt">//// (from the EA database) class=class="str">"cmt">// class="type">class="kw">string strategiesParams = CVirtualAdvisor::Import( class=class="str">"cmt">// CVirtualAdvisor::FileName(__NAME__, magic_), class=class="str">"cmt">// groupId_ class=class="str">"cmt">// ); class=class="str">"cmt">// class=class="str">"cmt">//// If the strategy group from the library is not specified, then we interrupt the operation class=class="str">"cmt">// if(strategiesParams == NULL && useAutoUpdate_) { class=class="str">"cmt">// strategiesParams = ""; class=class="str">"cmt">// } class=class="str">"cmt">// class=class="str">"cmt">// class="kw">return strategiesParams; class=class="str">"cmt">//}
从数据库到实盘的四步落地
跑通这套自动优化流水线,先拖 CreateProject.ex5 到任意图表,不用进测试器。源码里输入参数已预填当前值,对话框直接点确定,就会生成 article.17328.db.sqlite 优化库。 第二步上 Optimization.ex5,同样免测试器,但必须勾选允许 DLL,并核对数据库名。跑起来后图表显示「Total tasks in queue / Current Task ID」,测试器从第一个品种-周期组合开始第一阶段优化。默认配置下 33 个代理跑完约 4 小时;末段不再优化,而是单次跑第三阶段 EA,产出 SimpleCandles-27183.test.db.sqlite。 进测试器验最终 EA 时,默认 groupId_=0 且 useAutoUpdate_=true,会加载库里最后一组策略。但要注意:若优化区间设为 2022.09.01-2023.01.01,组的形成日期就是 2023.01.01,自动更新开启时模拟日期不早于该日就载不进组。要么关掉自动更新并指定 groupId_=1,要么换优化结束后的区间。只跑过一个项目时库里只有 ID 1 的组,填 0 或 1 无差别。 上实盘前把 .test 后缀去掉,重命名为 SimpleCandles-27183.db.sqlite 放回通用终端目录,EA 拖图表用默认值即可,当前日期必然大于组创建日。模拟账户约一周测试中回撤 1.27%、利润约 2%,重启几次后靠本地虚拟仓位信息恢复继续跑。外汇与贵金属属高风险品种,这类表现不预示后续收益。
◍ 工具落地后的真实减负与边界
把前面十几节攒下来的自动化优化框架跑通后,最直观的变化是人工环节被砍掉一大截。以前每轮优化完得手动盯进度、另存中间结果再塞回 EA,现在这些动作交给调度器,人只需要盯策略逻辑本身。 跨品种分散这块,即便用很朴素的进场规则,回测曲线稳定性也出现了可观察的改善——当然这只是历史样本里的现象,不等于实盘必然复现。 Web 端统一管理还是远期目标,当前版本只能在终端侧看运行状态和基础统计。这类工具属于研究性质,外汇与贵金属杠杆高、滑点跳空频繁,直接挂真仓前务必用 MT5 策略测试器自己重跑一遍历史。
「把工具请下神坛」
上面那张清单列了 47 个文件,从 Advisor.mqh 1.04 到 VirtualSymbolReceiver.mqh 1.00,覆盖 Experts、Include 下 9 个子目录,ZIP 包 109.25 KB。它本质是一套可复用的多币种 EA 骨架,不是点一下就盈利的黑箱。 真正值钱的是分层:基类管仓位转换,Database 系列管策略落库,Optimizer 系列管自动跑参。你下载解压后,直接把 MQL5/Include/antekov 拖进本地 MetaEditor 的 Include 目录,开 MT5 用 SimpleCandles.mq5 挂一个符号就能看见组库参数怎么被读出来。 外汇和贵金属杠杆高、滑点狠,任何回测漂亮的组策略上了真盘都可能变形。把它当脚手架,而不是圣杯——改两行 Macros.mqh 的数组宏,比信作者 Telegram 频道有用。