开发多币种 EA 交易 (第 10 部分):从字符串创建对象·综合运用
🧩

开发多币种 EA 交易 (第 10 部分):从字符串创建对象·综合运用

(3/3)· 字符串进、对象出——把数据库里躺着的优化结果变成能跑的 EA 实例

实战向 第 3/3 篇
把策略参数存进数据库不算完事,多数人卡在重启后没法把那串字符变回能交易的对象。本文顺着前两篇的库结构,直接给出从字符串构造 CSimpleVolumesStrategy 等实例的可跑路径。

◍ 给成交量策略类瘦身后怎么重写构造

把 CSimpleVolumesStrategy 里带独立参数的构造函数删掉,只留接收初始化字符串的那个,用 CFactorable 体系重写。初始化字符串之前已经存在 m_params 里,构造函数第一步就是把它存好,再从串里逐个 Read 出 symbol、周期、信号周期、偏差等 11 个字段。 读参没出错才进 IsValid() 分支:先向 CVirtualReceiver 要虚拟仓位(数量由 m_maxCountOfOrders 定),再加载 iVolumes 指标取 tick 成交量。指标句柄不等于 INVALID_HANDLE 时,把 m_volumes 数组按 m_signalPeriod Resize 并设成时间序列,最后用 IsNewBar 在 PERIOD_M1 上挂新柱事件。 转字符串的 operator~ 不再拼参数,直接类名加 m_params 原串返回,和前两个类一致。Save()/Load() 从类里删掉,父类 CVirtualStrategy 那套已够用。改完存回 CSimpleVolumesStrategy.mqh 就能编译验证。 别把父类的 Save/Load 想当然当万能 父类实现覆盖的是通用序列化路径,若你之后给子类加了非标准成员却没重载,回测加载时可能静默丢参,开 MT5 跑一次 Init from string 最踏实。外汇与贵金属杠杆高,策略类改动后务必用历史数据小样本验证再上实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading strategy using tick volumes                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : class="kw">public CVirtualStrategy {
  ...
class="kw">public:
  class=class="str">"cmt">//--- Public methods
                     CSimpleVolumesStrategy(class="type">class="kw">string p_params); class=class="str">"cmt">// Constructor
  class="kw">virtual class="type">class="kw">string     class="kw">operator~() class="kw">override;     class=class="str">"cmt">// Convert object to class="type">class="kw">string
  class="kw">virtual class="type">void       Tick() class="kw">override;          class=class="str">"cmt">// OnTick event handler
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(class="type">class="kw">string p_params) {
class=class="str">"cmt">// Save the initialization class="type">class="kw">string
  m_params = p_params;

class=class="str">"cmt">// Read the parameters from the initialization class="type">class="kw">string
  m_symbol = ReadString(p_params);
  m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
  m_signalPeriod = (class="type">int) ReadLong(p_params);
  m_signalDeviation = ReadDouble(p_params);
  m_signaAddlDeviation = ReadDouble(p_params);
  m_openDistance = (class="type">int) ReadLong(p_params);
  m_stopLevel = ReadDouble(p_params);
  m_takeLevel = ReadDouble(p_params);
  m_ordersExpiration = (class="type">int) ReadLong(p_params);
  m_maxCountOfOrders = (class="type">int) ReadLong(p_params);
  m_fittedBalance = ReadDouble(p_params);
class=class="str">"cmt">// If there are no read errors,
  if(IsValid()) {
    class=class="str">"cmt">// Request the required number of class="kw">virtual positions
    CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);
    class=class="str">"cmt">// Load the indicator to get tick volumes
    m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);
    class=class="str">"cmt">// If the indicator is loaded successfully
    if(m_iVolumesHandle != INVALID_HANDLE) {
      class=class="str">"cmt">// Set the size of the tick volume receiving array and the required addressing
      ArrayResize(m_volumes, m_signalPeriod);
      ArraySetAsSeries(m_volumes, true);
      class=class="str">"cmt">// Register the event handler for a new bar on the minimum timeframe
      IsNewBar(m_symbol, PERIOD_M1);
    } else {
      class=class="str">"cmt">// Otherwise, set the object state to invalid
      SetInvalid(__FUNCTION__, "Can&class="macro">#x27;t load iVolumes()");
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Convert an object to a class="type">class="kw">string                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CSimpleVolumesStrategy::class="kw">operator~() {

「把对象信息拼成可读字符串」

这段收尾代码把当前对象的类型名和内部参数拼成一个字符串返回,方便在日志或面板里直接看对象状态。 它用 typename(this) 取类类型名,再用 m_params 拼上参数,格式固定为「类型名(参数)」。 在 MT5 里跑这段,你可以在 EA 或指标里调用该函数,快速确认某个实例的参数有没有被改错。

MQL5 / C++
  class="kw">return StringFormat("%s(%s)", class="kw">typename(this), m_params);
}

单实例 EA 的参数注入与对象生命周期

优化单个交易策略实例,核心改动落在 OnInit()。思路是把 EA 输入参数拼成一段初始化字符串,交给工厂方法生成策略对象,再用这个对象替换掉默认的 EA 实例。 因为读取端已经支持解析带空格和换行的字符串,日志或数据库里落地的初始化文本可读性很强。上面那段日志就是 2023.01.01 00:00:00 跑出来的真实结构:CVirtualAdvisor 包了一个 CVirtualStrategyGroup,组里只挂一个 CSimpleVolumesStrategy,品种 EURGBP、周期 16385(即 PERIOD_M15)、信号周期 17、偏差 0.70/0.90、挂单距离 150 点、止损 10000 点值、止盈 85 点、过期 10000 秒、最大单数 3。 OnDeinit() 里要小心指针。EA 对象不一定创建成功——初始化字符串若非法,工厂会提前删对象,不能再假设指针必然有效,删除前必须判空校验。 下面这段 OnInit 片段演示了字符串拼装:先用 StringFormat 把单策略参数格式化,再套一层 CVirtualAdvisor 的壳。注意 maxCountOfOrders_ 后硬写 0 是占位,实际由输入控制。

MQL5 / C++
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00   OnInit | Expert Params:
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00   class CVirtualAdvisor(
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00     class CVirtualStrategyGroup(
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00       [
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00         class CSimpleVolumesStrategy("EURGBP",class="num">16385,class="num">17,class="num">0.70,class="num">0.90,class="num">150,class="num">10000.00,class="num">85.00,class="num">10000,class="num">3,class="num">0.00)
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00       ],class="num">1
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00     ),
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00     ,class="num">27181,SimpleVolumesSingle,class="num">1
Core class="num">1  class="num">2023.01.class="num">01 class="num">00:class="num">00:class="num">00   )
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
   CMoney::FixedBalance(fixedBalance_);
class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for a single strategy instance
   class="type">class="kw">string strategyParams = StringFormat(
                                     "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d,%.2f)",
                                     symbol_, timeframe_,
                                     signalPeriod_, signalDeviation_, signaAddlDeviation_,
                                     openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                                     maxCountOfOrders_, 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
   class="type">class="kw">string expertParams = StringFormat(
                                     "class CVirtualAdvisor(\n"

◍ 用字符串拼出虚拟策略组的EA参数

在 MT5 的 OnInit 里,可以通过格式化字符串动态构造专家参数,再把字符串交给 NEW(expertParams) 实例化一个处理虚拟持仓的 EA 对象。下面这段拼法把类名、策略参数数组、magic 号、标识名和布尔开关一次性塞进模板。 注意 PrintFormat 会把最终 expertParams 打到日志,方便你核对 magic_ 与策略组是否对齐;若 expert 指针为空则直接返回 INIT_FAILED,避免后续 tick 里空指针崩 EA。 外汇与贵金属品种上跑这类虚拟仓位逻辑时,滑点和点差会显著改变成交假设,真实账户属于高风险操作,回测通过不代表实盘概率同向。

MQL5 / C++
              "    class CVirtualStrategyGroup(\n"
              "      [\n"
              "        %s\n"
              "      ],class="num">1\n"
              "    ),\n"
              "    ,%d,%s,%d\n"
              ")",
              strategyParams, 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);
}
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason) {
  if(!!expert) class="kw">delete expert;
}

「用 CSV 参数批量起多实例 EA」

把之前做负载测试用的 EA 拿出来,在 OnInit() 里换掉原有的创建逻辑:先从 CSV 读策略参数集,再拼出策略数组和 EA 本身的初始化字符串,从而支持同一品种上挂多个策略实例跑测试。 OnDeinit() 保持原样,在删 EA 对象指针前先校验有效性,避免野指针。代码存为 BenchmarkInstancesExpert.mq5 即可在当前目录编译。 关键数据点:count_ 小于 1 会直接返回 INIT_PARAMETERS_INCORRECT;CMoney::DepoPart 接收的是 expectedDrawdown_/10.0,即把预期回撤当成仓位系数的十分之一,外汇与贵金属杠杆环境下这套系数放大了爆仓概率,需自行压低预期回撤值。 下面这段 OnInit 的拼串是核心,注意 FOREACH 里用 i % totalParams 循环复用参数,实例数多于参数行时会回卷。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
class=class="str">"cmt">// Load strategy parameter sets
   class="type">int totalParams = LoadParams(fileName_, strategyParams);
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_PARAMETERS_INCORRECT);
   }
class=class="str">"cmt">// Report an error if
   if(count_ < class="num">1) { class=class="str">"cmt">// number of instances is less than class="num">1
      class="kw">return INIT_PARAMETERS_INCORRECT;
   }
   ArrayResize(strategyParams, count_);
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">// Prepare the initialization class="type">class="kw">string for the array of strategy instances
   class="type">class="kw">string strategiesParams;
   FOREACH(strategyParams, strategiesParams += StringFormat(" class CSimpleVolumesStrategy(%s),\n        ",
strategyParams[i % totalParams]));
class=class="str">"cmt">// Prepare the initialization class="type">class="kw">string for an EA with the strategy group
   class="type">class="kw">string expertParams = StringFormat("class CVirtualAdvisor(\n"
                                     "    class CVirtualStrategyGroup(\n"
                                     "        [\n"
                                     "        %s],\n"
                                     "        %.2f\n"
                                     "    ),\n"
                                     "    %d,%s,%d\n"
                                     ")",
                                     strategiesParams, scale_,
                                     magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);

class=class="str">"cmt">// Create an EA handling class="kw">virtual positions
   expert = NEW(expertParams);
   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

初始化失败的兜底返回

MQL5 的 OnInit 里一旦 Expert 句柄没拿到,必须立刻 return INIT_FAILED,否则后续 OnInit 逻辑会继续跑在空句柄上,EA 加载后大概率在图表上哑火。 这段收尾代码就是干这件事:先判断 expert 是否有效,无效直接退出并返回失败常量;有效才返回 INIT_SUCCEEDED 让 MT5 认为初始化通过。 在 MT5 里打开任意 EA 源码,把这段贴在 OnInit 末尾,故意传一个不存在的币种去创建 CiExpert 实例,就能复现 INIT_FAILED 且 EA 不会启动。外汇与贵金属杠杆品种高风险,验证时请用模拟账户。

MQL5 / C++
  if(!expert) class="kw">return INIT_FAILED;
  class="kw">return(INIT_SUCCEEDED);
}

◍ 同参数跑两版EA验证一致性

把第八部分里的 BenchmarkInstancesExpert.mq5 和本文改过的同款 EA 都拉起来,参数直接读 Params_SV_EURGBP_H1.csv,一次性加载 256 个交易策略实例,测试周期锁死在 2022 年、品种 EURGBP、周期 H1。 图 2 的回测输出显示两个版本结果逐笔吻合,曲线重叠到图上只能看到一条实例。外汇与贵金属回测存在过拟合风险,单年样本不代表稳健。 结论很直接:旧版和新版在数值上等价,后续开发可以放心切到新版本,不必担心底层逻辑被悄悄改坏。

「把工具请下神坛」

用初始化字符串生成对象这套改法,已经跑通了:在只改对象创建方法、交易策略实例集完全一致的前提下,两个 EA 回测结果相同,说明重构没有动到策略逻辑本身。当前手动拼字符串只是过渡,后续从数据库读配置就能省掉大量重复劳动。 下一步要做的,是把「按顺序启动多个 EA 优化流程、挑出单实例参数」这第一阶段的自动化真正落地,代码层面已经具备基础,只差调度逻辑。 外汇与贵金属市场高波动、高杠杆,任何 EA 改法都只是降低人工成本,不消除爆仓风险;拿到 BenchmarkInstancesExpert.mq5(14.82 KB)这类文件后,先在策略测试器里用单实例跑一遍再谈扩展。

把重建逻辑交给小布盯盘跑批
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到字符串到对象的映射校验,把重复劳动交给小布,你专注决策。

常见问题

inputs 含输入名但缺交易品种与周期,params 是策略对象整体序列化结果,重建对象应优先用 params。
MQL5 环境无原生对象持久化接口,转为字符串是唯一可移植方案,且便于跨程序读取。
每个实例字符串带类名与参数元组,构造函数按类名分派,解析后独立赋值,互不覆盖。
可以,小布盯盘对应品种页内置序列化映射校验,能标出字段缺失或类型不符的实例。
第九部分已调试通入库流程,本篇只补读取端构造函数,复用既有格式降低改动风险。