开发多币种 EA 交易 (第 13 部分):自动化第二阶段 分组选择·进阶篇
用字符串拼出虚拟策略组的初始化参数
在 MT5 里做多策略回测或虚拟下单时,往往不写死对象,而是用 StringFormat 把参数拼成类初始化串,再交给工厂函数实例化。下面这段就是先把单个策略实例的参数收进 strategyParams,再套一层 CVirtualStrategyGroup 的壳。 组初始化串模板里用了 %s 承接策略参数、%f 承接 scale_ 缩放系数,注意方括号和缩进只是可读性包装,运行时按字符串解析。若 scale_ 设 1.0,组与策略手数比例就是 1:1;调大它可能让虚拟订单批量放大,外汇与贵金属品种波动剧烈,放大前先在策略测试器里用小资金验证。 风险管理器 CVirtualRiskManager 这里暂时全填 0(0,0,0,0,0,0),代表不限制仓位与回撤,仅作骨架演示;实盘逻辑里这六个浮点/整型位通常对应模式、阈值、超时等。最后 CVirtualAdvisor 把组串、风控串和几个整型开关拼成 EA 初始化串,丢给虚拟顾问统一调度。 别把全 0 风控当可用配置 上面 riskManagerParams 的六个 0 只是占位,直接拿去跑黄金或欧美大概率会因无风控在极端行情亏穿。至少把回撤上限和单组订单数填真实值,再上 MT5 测试器跑 3 个月以上 tick 数据。
class="type">class="kw">string groupParams = StringFormat( "class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],%f\n" " )", strategyParams, scale_ ); class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for the risk manager class="type">class="kw">string riskManagerParams = StringFormat( "class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%d,%.2f" " )", class="num">0,class="num">0,class="num">0,class="num">0,class="num">0,class="num">0 ); class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for an EA with a group of a single strategy and the risk manager class="type">class="kw">string expertParams = StringFormat( "class CVirtualAdvisor(\n" " %s,\n" " %s,\n" " %d,%s,%d\n" ")", groupParams,
「把参数塞进虚拟持仓引擎的初始化」
在 MT5 的 EA 初始化函数里,这段调用把风控参数、幻数(magic_)和标识名 "SimpleVolumesSingle" 一起丢给专家参数构造器,并打印出结构化参数串。注意第三个布尔量 true 通常代表允许虚拟持仓(不占真实账户仓位),外汇与贵金属品种开虚拟单同样有滑点漂移风险,回测和实盘可能不一致。 随后用 NEW(expertParams) 生成处理虚拟持仓的 expert 对象;若指针为空直接返回 INIT_FAILED,否则返回 INIT_SUCCEEDED。把这段贴进 OnInit 尾部,编译后从 Experts 日志能看到 Expert Params 的完整输出,确认参数没被截断。
riskManagerParams,
magic_, "SimpleVolumesSingle", true
);
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);
}◍ 给第二阶段优化 EA 接上任务库
第二阶段的核心动作,是把第一阶段的产出收敛成一组可复选的单个策略实例。做法是以 OptGroupExpert.mq5 为基底改出 SimpleVolumesStage2.mq5,让 EA 在优化前先从主库抽数、写进独立的任务库,再按指定的索引组去选实例。 任务库只放两张字段:id_pass(第一阶段 tester pass ID)和 params(该 pass 的 EA 初始化串)。如果表已存在就先删再建,保证每次第二阶段重跑都拿到干净的新候选集。 从主库拉数的 SQL 会 join passes / tasks / jobs / stages 四张表,硬性过滤条件是 stage 名等于 "First"、job ID 等于传入的 idParentJob_。质量门槛设了三道可选线:规范化利润 > 2500、成交笔数 > 20、夏普比率 > 2;这三者是为兼顾候选量和策略质量,参数可按你自己的第一阶段分布去调。 实例数从 8 扩到 16,所以输入参数新增 i9_ 到 i12_(原文片段里 i12_ 实际对应索引 11,复制代码时要核对别串行),count_ 默认值同步改成 16。索引先塞进 CHashSet 去重,集合大小若小于数组长度就报输入错误、不启动。 LoadParams() 也重写了:不再全文件读入,而是按传入的索引列表拼 SQL 只取任务库里那几行,再把初始化串用逗号拼好返回。OnTesterInit() 里调 CreateTaskDB() 建库;单独测试跑时没有库就当场重建,避免空指针。外汇与贵金属市场杠杆高、滑点跳空频繁,这类多实例组合在历史回测里表现好,实盘仍可能显著回撤。
class="macro">#define PARAMS_FILE "database892.stage2.sqlite" class="macro">#class="kw">property tester_file PARAMS_FILE class="kw">input group "::: Selection for the group" class="kw">input class="type">class="kw">string fileName_ = "database892.sqlite"; class=class="str">"cmt">// - File with the main database class="kw">input class="type">int idParentJob_ = class="num">1; class=class="str">"cmt">// - Parent job ID class="kw">input class="type">int count_ = class="num">16; class=class="str">"cmt">// - Number of strategies in the group(class="num">1 .. class="num">16) class="kw">input class="type">int i1_ = class="num">1; class=class="str">"cmt">// - Strategy index #class="num">1 class="kw">input class="type">int i2_ = class="num">2; class=class="str">"cmt">// - Strategy index #class="num">2 class="kw">input class="type">int i3_ = class="num">3; class=class="str">"cmt">// - Strategy index #class="num">3 class="kw">input class="type">int i4_ = class="num">4; class=class="str">"cmt">// - Strategy index #class="num">4 class="kw">input class="type">int i5_ = class="num">5; class=class="str">"cmt">// - Strategy index #class="num">5 class="kw">input class="type">int i6_ = class="num">6; class=class="str">"cmt">// - Strategy index #class="num">6 class="kw">input class="type">int i7_ = class="num">7; class=class="str">"cmt">// - Strategy index #class="num">7 class="kw">input class="type">int i8_ = class="num">8; class=class="str">"cmt">// - Strategy index #class="num">8 class="kw">input class="type">int i9_ = class="num">9; class=class="str">"cmt">// - Strategy index #class="num">9 class="kw">input class="type">int i10_ = class="num">10; class=class="str">"cmt">// - Strategy index #class="num">10 class="kw">input class="type">int i12_ = class="num">11; class=class="str">"cmt">// - Strategy index #class="num">11
分阶段优化里的参数筛选与落库
在多阶段 EA 优化框架里,先把策略索引 12 到 16 用 input 暴露出来,方便在 MT5 参数页直接切换不同子策略的编号,而不必改代码重编译。 CreateTaskDB 做的是给「单独阶段任务」建一张干净的参数库:先连本地参数文件、删掉旧 passes 表再建新表,字段只有自增 id 和 params 文本,结构极简。 随后它回连主数据库,用一条 JOIN 四张表(passes / tasks / jobs / stages)的 SQL 把上一阶段叫 'First' 且父任务 id 匹配的结果捞出来。过滤条件很硬:custom_ontester 大于 2500、成交笔数 trades 大于 20、夏普比率 sharpe_ratio 大于 2,才进下一阶段。 这套写法意味着你跑完第一阶段优化后,只有约满足上述阈值的参数组合会被喂给后续阶段,能明显压住组合爆炸。外汇与贵金属品种波动大、滑点不确定,这类阈值只是历史回测筛选,实盘表现可能明显偏离。
class="kw">input class="type">int i11_ = class="num">12; class=class="str">"cmt">// - Strategy index #class="num">12 class="kw">input class="type">int i13_ = class="num">13; class=class="str">"cmt">// - Strategy index #class="num">13 class="kw">input class="type">int i14_ = class="num">14; class=class="str">"cmt">// - Strategy index #class="num">14 class="kw">input class="type">int i15_ = class="num">15; class=class="str">"cmt">// - Strategy index #class="num">15 class="kw">input class="type">int i16_ = class="num">16; class=class="str">"cmt">// - Strategy index #class="num">16 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Creating a database for a separate stage task | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateTaskDB(const class="type">class="kw">string fileName, const class="type">int idParentJob) { class=class="str">"cmt">// Create a new database for the current optimization task DB::Connect(PARAMS_FILE, class="num">0); DB::Execute("DROP TABLE IF EXISTS passes;"); DB::Execute("CREATE TABLE passes(id_pass INTEGER PRIMARY KEY AUTOINCREMENT, params TEXT);"); DB::Close(); class=class="str">"cmt">// Connect to the main database DB::Connect(fileName); class=class="str">"cmt">// Request to obtain the required information from the main database class="type">class="kw">string query = StringFormat( "SELECT DISTINCT p.params" " FROM passes p" " JOIN" " tasks t ON p.id_task = t.id_task" " JOIN" " jobs j ON t.id_job = j.id_job" " JOIN" " stages s ON j.id_stage = s.id_stage" " WHERE(s.name=&class="macro">#x27;First&class="macro">#x27; AND " " j.id_job = %d AND" " p.custom_ontester > class="num">2500 AND " " trades > class="num">20 AND " " p.sharpe_ratio > class="num">2)" " ORDER BY s.id_stage ASC," " j.id_job ASC," " p.custom_ontester DESC;", idParentJob); class=class="str">"cmt">// Execute the request class="type">int request = DatabasePrepare(DB::Id(), query);
「把优化任务拆进独立数据库的实现细节」
在 MT5 的优化器里做分布式任务切分,核心是把每一组策略参数落地到单独的 SQLite 数据库,而不是全堆在主库。下面这段逻辑先判断 SQL 请求句柄是否合法,无效就打印函数名、查询语句和错误码后断开连接直接返回,避免脏数据继续写入。 [CODE]if(request == INVALID_HANDLE) { PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError()); DB::Close(); return; } // Structure for query results struct Row { string params; } row; // Array for requests to insert data into a new database string queries[]; // Fill the request array: we will only save the initialization strings while(DatabaseReadBind(request, row)) { APPEND(queries, StringFormat("INSERT INTO passes VALUES(NULL, '%s');", row.params)); } // Reconnect to the new database and fill it DB::Connect(PARAMS_FILE, 0); DB::ExecuteTransaction(queries); // Reconnect to the main database DB::Connect(fileName); DB::Close(); } //+------------------------------------------------------------------+
| // | Initialization before optimization |
|---|
//+------------------------------------------------------------------+ int OnTesterInit(void) { // Create a database for a separate stage task CreateTaskDB(fileName_, idParentJob_); // Get the number of strategy parameter sets int totalParams = GetParamsTotal(); // If nothing is loaded, report an error if(totalParams == 0) { PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n" "Check that it exists in data folder or in common data folder.", fileName_); return(INIT_FAILED); } // Set scale_ to 1 ParameterSetRange("scale_", false, 1, 1, 1, 2); // Set the ranges of change for the parameters of the set index iteration for(int i = 1; i <= 16; i++) { if(i <= count_) { ParameterSetRange("i" + (string) i + "_", true, 0, 1, 1, totalParams); } else { // Disable the enumeration for extra indices ParameterSetRange("i" + (string) i + "_", false, 0, 1, 1, totalParams); } } return CVirtualAdvisor::TesterInit(idTask_); } //+------------------------------------------------------------------+
| // | Number of strategy parameter sets in the task database |
|---|
//+------------------------------------------------------------------+
int GetParamsTotal() {
int paramsTotal = 0;
// If the task database is open,
if(DB::Connect(PARAMS_FILE, 0)) {
// Create a request to get the number of passes for this task
string query = "SELECT COUNT(*) FROM passes p";
int request = DatabasePrepare(DB::Id(), query);[/CODE]
逐行拆解几个关键点:struct Row 只存了 params 字符串,说明落库时只保留初始化参数本身;while(DatabaseReadBind(...)) 把主库读出的每一行绑定到 row,再用 APPEND 拼成 INSERT 语句塞进 queries 数组,最后靠 DB::ExecuteTransaction 一次性写新库,比逐条执行更省 IO。
OnTesterInit 里 ParameterSetRange("scale_", false, 1, 1, 1, 2) 把 scale_ 锁死为单值,而循环到 i<=16 时,只有 i<=count_ 的索引才开放枚举范围 0~totalParams,其余强制 disable——这意味着你最多并行调度 16 个参数维度,超出的部分不会参与优化器遍历。
GetParamsTotal 用 SELECT COUNT(*) FROM passes p 查任务库里的参数组数,若文件缺失或连不上,OnTesterInit 会返回 INIT_FAILED 并提示去 data folder 或 common data folder 核查。外汇与贵金属品种做这类批量优化时杠杆和滑点敏感,回测结果和实际跑盘可能有偏差,属于高风险验证,建议先拿 1~2 组参数在 MT5 策略测试器里手动跑通再扩量。
if(request == INVALID_HANDLE) { PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError()); DB::Close(); class="kw">return; } class=class="str">"cmt">// Structure for query results class="kw">struct Row { class="type">class="kw">string params; } row; class=class="str">"cmt">// Array for requests to insert data into a new database class="type">class="kw">string queries[]; class=class="str">"cmt">// Fill the request array: we will only save the initialization strings class="kw">while(DatabaseReadBind(request, row)) { APPEND(queries, StringFormat("INSERT INTO passes VALUES(NULL, &class="macro">#x27;%s&class="macro">#x27;);", row.params)); } class=class="str">"cmt">// Reconnect to the new database and fill it DB::Connect(PARAMS_FILE, class="num">0); DB::ExecuteTransaction(queries); class=class="str">"cmt">// Reconnect to the main database DB::Connect(fileName); DB::Close(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Initialization before optimization | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnTesterInit(class="type">void) { class=class="str">"cmt">// Create a database for a separate stage task CreateTaskDB(fileName_, idParentJob_); class=class="str">"cmt">// Get the number of strategy parameter sets class="type">int totalParams = GetParamsTotal(); class=class="str">"cmt">// If nothing is loaded, report an error if(totalParams == class="num">0) { 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.", fileName_); class="kw">return(INIT_FAILED); } class=class="str">"cmt">// Set scale_ to class="num">1 ParameterSetRange("scale_", false, class="num">1, class="num">1, class="num">1, class="num">2); class=class="str">"cmt">// Set the ranges of change for the parameters of the set index iteration for(class="type">int i = class="num">1; i <= class="num">16; i++) { if(i <= count_) { ParameterSetRange("i" + (class="type">class="kw">string) i + "_", true, class="num">0, class="num">1, class="num">1, totalParams); } else { class=class="str">"cmt">// Disable the enumeration for extra indices ParameterSetRange("i" + (class="type">class="kw">string) i + "_", false, class="num">0, class="num">1, class="num">1, totalParams); } } class="kw">return CVirtualAdvisor::TesterInit(idTask_); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Number of strategy parameter sets in the task database | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int GetParamsTotal() { class="type">int paramsTotal = class="num">0; class=class="str">"cmt">// If the task database is open, if(DB::Connect(PARAMS_FILE, class="num">0)) { class=class="str">"cmt">// Create a request to get the number of passes for this task class="type">class="kw">string query = "SELECT COUNT(*) FROM passes p"; class="type">int request = DatabasePrepare(DB::Id(), query);
◍ 从本地库按索引抽取参数集
把优化后的多组参数落库后,真正在 EA 里调用时只需按输入索引把对应行读出来拼成逗号串。下面这段 LoadParams 就是干这个:先拿总组数,再拼 IN 子句,最后逐行 bind 拼接。 别把索引拼串想简单了 原文在 FOREACH 拼完 indexes 后硬加了一个 "0",原因是循环里每个索引后都带了逗号,若不补一个不存在的 id,最后会多出一个逗号导致 SQL 语法错。这个坑在 MT5 的 DatabasePrepare 阶段就会报 INVALID_HANDLE,而不是执行期才炸。 读结果用 while 而不是 if GetParamsTotal 里用 DatabaseReadBind 读单行 total 用 if 足够;但 LoadParams 面对多组参数必须用 while 循环把每行 params 累加。若错写成 if,只会拿到第一组,EA 实际加载的也就一组,回测和实盘信号可能明显偏离优化预期。外汇与贵金属杠杆品种下,这种参数缺失会放大滑点与隔夜风险。 直接在 MT5 里建个 PARAMS_FILE 的 sqlite,跑一遍 LoadParams(Indexes[]) 看返回的 params 字符串组数是否等于输入索引数,就能验证这套读取链路通不通。
class="type">class="kw">string LoadParams(class="type">int &indexes[]) { class="type">class="kw">string params = NULL; class=class="str">"cmt">// Get the number of sets class="type">int totalParams = GetParamsTotal(); class=class="str">"cmt">// If they exist, then if(totalParams > class="num">0) { if(DB::Connect(PARAMS_FILE, class="num">0)) { class=class="str">"cmt">// Form a class="type">class="kw">string from the indices of the comma-separated sets taken from the EA inputs class=class="str">"cmt">// for further substitution into the SQL query class="type">class="kw">string strIndexes = ""; FOREACH(indexes, strIndexes += IntegerToString(indexes[i]) + ","); strIndexes += "class="num">0"; class=class="str">"cmt">// Add a non-existent index so as not to remove the last comma class=class="str">"cmt">// Form a request to obtain sets of parameters with the required indices class="type">class="kw">string query = StringFormat("SELECT params FROM passes p WHERE id_pass IN(%s)", strIndexes); class="type">int request = DatabasePrepare(DB::Id(), query); if(request != INVALID_HANDLE) { class=class="str">"cmt">// Data structure for query results class="kw">struct Row { class="type">class="kw">string params; } row; class=class="str">"cmt">// Read the query results and join them with a comma class="kw">while(DatabaseReadBind(request, row)) { params += row.params + ","; } } else { PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError()); } DB::Close(); } } class="kw">return params; }