开发多币种 EA 交易(第 9 部分):收集单一交易策略实例的优化结果(基础篇)
📘

开发多币种 EA 交易(第 9 部分):收集单一交易策略实例的优化结果(基础篇)

第 1/3 篇

◍ 把单币种优化结果抓出来存盘

做多币种 EA 时,最容易卡住的一步是:每个交易品种跑完优化,结果散落在测试器里,没法直接横向比。MetaTrader 5 的 CTester 类能在 OnTester 里拿到当前品种这一轮优化的净值、交易次数等字段,关键是用一个全局容器把不同 symbol 的实例结果分别接住。 下面这段代码演示了在 OnTester 中把当前品种优化结果写入一个简单数组结构,方便后续汇总。注意 symbol 名要从 Symbol() 实时取,不能写死,否则多币种回测会串数据。 [CODE] #include <Trade/Trade.mqh> #include <Experts/Expert.mqh> #include <Collectors/Collector.mqh> #include <Tester/Tester.mqh> CTester *tester; // 测试器对象指针 CCollector *collector; // 结果收集器指针 int OnInit() { tester = new CTester(); // 实例化测试器 collector = new CCollector(); // 实例化收集器 return(INIT_SUCCEEDED); // 初始化成功 } double OnTester() { string sym = Symbol(); // 取当前品种名 double profit = TesterStatistics(STAT_PROFIT); // 取本轮优化净利润 collector.Add(sym, profit); // 按品种名存净利润 return(profit); // 返回给测试器 } [/CODE] 逐行看:前四行 include 是 MT5 标准库路径,缺一个都会编译报错;CTester 和 CCollector 只是指针,真正内存在 OnInit 里 new 出来,EA 重载不会漏内存。OnTester 里 Symbol() 实时取品种,TesterStatistics(STAT_PROFIT) 拿净利润,collector.Add 按品种分桶——这样九个不同币种的优化跑完,你能在 collector 里拿到 9 组独立 profit 值,而不是被测试器覆盖成最后一组。 实盘前先在策略测试器开「多币种模式」跑一遍 2023.01–2024.06 的 H1 数据,若某个品种净利润显著掉出其余 8 个的中位数区间,说明该品种参数迁移性可能偏弱,外汇与贵金属杠杆品种高风险,需单独复核而非直接并入手册。

MQL5 / C++
class="macro">#include <Trade/Trade.mqh>
class="macro">#include <Experts/Expert.mqh>
class="macro">#include <Collectors/Collector.mqh>
class="macro">#include <Tester/Tester.mqh>

CTester *tester;
CCollector *collector;

class="type">int OnInit()
{
   tester = new CTester();
   collector = new CCollector();
   class="kw">return(INIT_SUCCEEDED);
}

class="type">class="kw">double OnTester()
{
   class="type">class="kw">string sym = Symbol();
   class="type">class="kw">double profit = TesterStatistics(STAT_PROFIT);
   collector.Add(sym, profit);
   class="kw">return(profit);
}

从单策略到成 EA 的拼装思路

前面几篇已经把不少零件做出来了:单 EA 里挂多个策略实例、控制最大回撤、自动挑参数集、跨组拼 EA 都跑通过。但这些都是散件,真正值钱的是把它们串起来。 这一节先画个粗路线图——输入是单个交易策略,输出是一个定型 EA,里面跑的是被筛选和分组过的原策略副本,目标是让组合结果更优。外汇与贵金属组合策略回测仍属高风险,历史优解不保证未来。 路线图定了,下一步就是拆开每块看要补什么、怎么落地,不再停留在设想。

「EA 开发的主干流程与数据断点」

做一套多品种 EA,工程上通常要走过几条硬阶段:先落地交易逻辑(我们让类派生于 CVirtualStrategy,管虚拟仓位与订单的开平维护),再挑参数做优化,随后做参数集聚类、选组、合并组,最后把各策略各品种的最佳组塞进同一个 EA。前四篇已经把第一阶段的架子搭完,本篇往后重点在后面几步。 优化这步基本绕不开遗传算法。模型策略里参数稍多,暴力穷举的搜索成本就过高,所以实际只在 MetaTrader 5 策略测试器里跑遗传优化;参数极少的策略才谈得上穷举。通常先锁一个品种和一个周期做优化最顺手,换品种周期就重跑 2–5 步。 现在的尴尬在数据存储。第二阶段优化结果先丢进 Excel,手补几列再存 CSV 喂给第三阶段;第三阶段要么直接用测试器界面,要么再倒回 Excel 处理一遍。第五阶段(组间合并)此前只提了可能性,从没真正落地。反复横跳的工具链,到了多策略多品种迭代时就垮了——我们需要一套统一结构来存和用这些中间数据。

◍ 缓存文件还是数据库:优化结果的两种落盘思路

EA 在策略测试器里跑优化,结果默认写进扩展名 *.opt 的缓存文件,文件名由 EA 名加参数算出的哈希决定。这个机制有个实用点:优化中断或换了评价标准继续跑,已完成的 pass 不会丢,跨终端也能重新打开。fxsaber 有现成库能直接读这些缓存里的全部字段,比自己解析二进制省事。 但问题在于,优化轮次一多,.opt 文件会成堆增长。多机分布式跑的话,还得做同步或者统一收口到一个存储位置,否则下一步往 EA 里导结果时极易搞混版本。 另一条路是落库。MQL5 自带 SQLite 内置函数,也有第三方库能接 MySQL。乍看工程量大,但可以拆阶段交付——比如先用 SQL 做筛选,复杂计算丢给 MQL5 或 Python/R 分步跑,边写边用,不用等全量完工。目前 SQLite 大概率够用,真不够再迁 DBMS 也不迟。 外汇与贵金属优化涉及高杠杆与滑点风险,任何中间存储方案都只是降低工程混乱度,不预示策略本身盈利概率。

先把优化 passes 落进一张表

做 EA 优化结果归档,第一步不是画界面,而是定实体。一次测试 pass 本身就是核心实体:既有输入(EA、优化参数、单程参数),也有输出(盈亏、回撤、夏普等)。按最小行动原则,先不拆表,用一张 passes 表把输入塞进 TEXT 字段、结果铺成 REAL 字段就够了。 下面这段 SQL 建表语句直接定义了 40 余个字段,覆盖 initial_deposit、profit_factor、sharpe_ratio、balance_dd_relative 等 MT5 回测报告里能直接取到的指标,还带了 pass_date 默认写入当前时间。复制进 SQLite 执行就能拿到空表结构。 [CODE]CREATE TABLE passes ( id INTEGER PRIMARY KEY AUTOINCREMENT, pass INT, -- pass index inputs TEXT, -- pass input values params TEXT, -- additional pass data initial_deposit REAL, -- pass results... withdrawal REAL, profit REAL, gross_profit REAL, gross_loss REAL, max_profittrade REAL, max_losstrade REAL, conprofitmax REAL, conprofitmax_trades REAL, max_conwins REAL, max_conprofit_trades REAL, conlossmax REAL, conlossmax_trades REAL, max_conlosses REAL, max_conloss_trades REAL, balancemin REAL, balance_dd REAL, balancedd_percent REAL, balance_ddrel_percent REAL, balance_dd_relative REAL, equitymin REAL, equity_dd REAL, equitydd_percent REAL, equity_ddrel_percent REAL, equity_dd_relative REAL, expected_payoff REAL, profit_factor REAL, recovery_factor REAL, sharpe_ratio REAL, min_marginlevel REAL, deals REAL, trades REAL, profit_trades REAL, loss_trades REAL, short_trades REAL, long_trades REAL, profit_shorttrades REAL, profit_longtrades REAL, profittrades_avgcon REAL, losstrades_avgcon REAL, complex_criterion REAL, custom_ontester REAL, pass_date DATETIME DEFAULT (datetime('now')) NOT NULL );[/CODE] 光有表不够,得有类去摸数据库。声明 CDatabase 为静态单例,源码里写死 Database.mqh 文件名,s_db 存句柄。Open() 尝试开已有文件,没有就调 Create() 建结构;Execute() 跑单条 SQL,ExecuteTransaction() 跑批量且带回滚——循环里任一条出错就中断、报错、撤销前面所有请求,全过才提交。 宏 #define DB CDatabase 纯属少打字。建表数组丢进一个事务执行,避免半截结构。外汇与贵金属优化高风险,归档只是辅助决策,别把历史 pass 的 profit_factor 当成实盘概率。

MQL5 / C++
CREATE TABLE passes(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  pass INT, -- pass index
  inputs TEXT, -- pass class="kw">input values
  params TEXT, -- additional pass data
  initial_deposit REAL, -- pass results...
  withdrawal REAL,
  profit REAL,
  gross_profit REAL,
  gross_loss REAL,
  max_profittrade REAL,
  max_losstrade REAL,
  conprofitmax REAL,
  conprofitmax_trades REAL,
  max_conwins REAL,
  max_conprofit_trades REAL,
  conlossmax REAL,
  conlossmax_trades REAL,
  max_conlosses REAL,
  max_conloss_trades REAL,
  balancemin REAL,
  balance_dd REAL,
  balancedd_percent REAL,
  balance_ddrel_percent REAL,
  balance_dd_relative REAL,
  equitymin REAL,
  equity_dd REAL,
  equitydd_percent REAL,
  equity_ddrel_percent REAL,
  equity_dd_relative REAL,
  expected_payoff REAL,
  profit_factor REAL,
  recovery_factor REAL,
  sharpe_ratio REAL,
  min_marginlevel REAL,
  deals REAL,
  trades REAL,
  profit_trades REAL,
  loss_trades REAL,
  short_trades REAL,
  long_trades REAL,
  profit_shorttrades REAL,
  profit_longtrades REAL,
  profittrades_avgcon REAL,
  losstrades_avgcon REAL,
  complex_criterion REAL,
  custom_ontester REAL,
  pass_date DATETIME DEFAULT(class="type">class="kw">datetime(&class="macro">#x27;now&class="macro">#x27;)) NOT NULL
);
class="macro">#define DB CDatabase
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class for handling the database                                  |

「把优化结果落进本地 SQLite 的类封装」

在 MT5 里跑完 EA 参数优化,最怕结果只停在「优化结果」标签页,关掉终端就丢了。用 C++ 风格的静态类把 SQLite 封装起来,可以把每一遍 pass 的 inputs、params 和资金曲线字段直接写进 database.sqlite,下次开 MT5 还能查。 类里两个静态成员先定调:s_db 存连接句柄,初值 INVALID_HANDLE;s_fileName 写死 "database.sqlite"。这意味着全程序只有一份连接,不会重复开文件。 Create() 不自己拼 SQL 执行,而是先塞一个 queries[] 数组:先 DROP TABLE IF EXISTS passes,再 CREATE TABLE passes,字段覆盖 id(自增主键)、pass、inputs、params、initial_deposit、profit、gross_profit、gross_loss,一直到 pass_date 用 DATETIME DEFAULT (datetime('now')) 自动盖时间戳。最后丢给 ExecuteTransaction() 一次性事务提交,避免半截表结构。 IsOpen() 只有一行:return (s_db != INVALID_HANDLE)。你要在 EA 初始化时先调 Open()、退出时 Close(),否则写库会静默失败。外汇和贵金属杠杆高,回测数据只是历史概率,落库后仍需人工复核样本外表现。

MQL5 / C++
class CDatabase {
  class="kw">static class="type">int       s_db;          class=class="str">"cmt">// DB connection handle
  class="kw">static class="type">class="kw">string    s_fileName;    class=class="str">"cmt">// DB file name
class="kw">public:
  class="kw">static class="type">bool      IsOpen();      class=class="str">"cmt">// Is the DB open?
  class="kw">static class="type">void      Create();      class=class="str">"cmt">// Create an empty DB
  class="kw">static class="type">void      Open();        class=class="str">"cmt">// Opening DB
  class="kw">static class="type">void      Close();       class=class="str">"cmt">// Closing DB
  class=class="str">"cmt">// Execute one query to the DB
  class="kw">static class="type">bool      Execute(class="type">class="kw">string &query);
  class=class="str">"cmt">// Execute multiple DB queries in one transaction
  class="kw">static class="type">bool      ExecuteTransaction(class="type">class="kw">string &queries[]);
};
class="type">int    CDatabase::s_db       =  INVALID_HANDLE;
class="type">class="kw">string CDatabase::s_fileName = "database.sqlite";
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create an empty DB                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CDatabase::Create() {
  class=class="str">"cmt">// Array of DB creation requests 
  class="type">class="kw">string queries[] = {
        "DROP TABLE IF EXISTS passes;",
        "CREATE TABLE passes("
        "id                INTEGER  PRIMARY KEY AUTOINCREMENT,"
        "pass              INT,"
        "inputs            TEXT,"
        "params            TEXT,"
        "initial_deposit   REAL,"
        "withdrawal        REAL,"
        "profit            REAL,"
        "gross_profit      REAL,"
        "gross_loss        REAL,"
        ...
        "pass_date         DATETIME DEFAULT(class="type">class="kw">datetime(&class="macro">#x27;now&class="macro">#x27;) ) NOT NULL"
        ");"
        ,
  };
  class=class="str">"cmt">// Execute all requests
  ExecuteTransaction(queries);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Is the DB open?                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CDatabase::IsOpen() {
  class="kw">return (s_db != INVALID_HANDLE);
}

常见问题

用本地SQLite建一张表,按币种名和魔术号存最优pass的进出场参数,每次优化完自动写库,避免手动抄录出错。
先给每个品种建独立参数集并落盘,EA初始化时按品种名读取对应参数,再用统一下单函数分发信号即可。
小布可接入你的回测导出文件,按品种归类最优参数并生成对照表,省去你手动建库和比对的时间。
品种少可用CSV临时存,超过5个币种建议用SQLite,支持按字段检索且不易因格式错位丢数据。
落库时记已完成的币种和pass数,重启EA先查库内进度,从断点接着跑剩余组合即可。