开发多币种 EA 交易 (第 10 部分):从字符串创建对象·进阶篇
🏭

开发多币种 EA 交易 (第 10 部分):从字符串创建对象·进阶篇

(2/3)· 当数据库里只躺着一串 class CSimpleVolumesStrategy(...) 文本,你怎么把它变回能跑的EA组件?

偏理论 第 2/3 篇

不少多币种EA开发者习惯把策略参数写死在代码里,每次换品种就得重新编译。数据库里存好的策略字符串如果无法直接变成对象,回测结果就只是一堆死数据,没法在下一阶段复用。

「用工厂方法接管可变类名的对象生成」

在 MT5 的 EA 架构里,如果初始化字符串里写死的类名永远不变,直接用 new 把参数塞进构造函数最省事。但一旦我们要在运行时按配置切换不同交易策略(比如今天跑 CVirtualAdvisor,明天换 CSimpleVolumesStrategy),new 就失效了——编译器在编译期就必须知道类,而你拿到的是字符串。 这时候把创建逻辑收口到一个静态方法里更干净。下面这段工厂只认初始化字符串,先拆出类名,再按类名分支 new 出对应对象,返回统一的 CFactorable* 基类指针。类名不匹配时返回 NULL 并打印错误,避免程序带着野指针往下跑。 两个宏是给日常调用减负的。NEW(Params) 就是一行调用工厂的缩写;CREATE(Class, Object, Params) 只能写在主对象构造函数里,它会先声明指针置 NULL,主对象有效时才去工厂拿对象,并用 dynamic_cast 做类型校验。转换失败指针留 NULL,主对象被标无效并中止构造——这样嵌套对象建不出来时不会静默带病运行。 实测中,若初始化串写成 "CSimpleVolumesStrategy(period=10)" 而工厂没登记这个类,Create() 会走最后的错误分支输出 'Constructor not found',返回 NULL,调用方 IsValid() 立刻为假。外汇与贵金属策略加载涉及高杠杆风险,这类对象创建失败应直接阻断 EA 初始化而非继续交易。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Object factory class                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualFactory {
class="kw">public:
  class=class="str">"cmt">// Create an object from the initialization class="type">class="kw">string
  class="kw">static CFactorable* Create(class="type">class="kw">string p_params) {
    class=class="str">"cmt">// Read the object class name
    class="type">class="kw">string className = CFactorable::ReadClassName(p_params);
    
    class=class="str">"cmt">// Pointer to the object being created
    CFactorable* object = NULL;
    class=class="str">"cmt">// Call the corresponding constructor  depending on the class name
    if(className == "CVirtualAdvisor") {
      object = new CVirtualAdvisor(p_params);
    } else if(className == "CVirtualStrategyGroup") {
      object = new CVirtualStrategyGroup(p_params);
    } else if(className == "CSimpleVolumesStrategy") {
      object = new CSimpleVolumesStrategy(p_params);
    }
    class=class="str">"cmt">// If the object is not created or is created in the invalid state, report an error
    if(!object) {
      PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\nclass %s(%s)",
                  className, p_params);
    } else if(!object.IsValid()) {
      PrintFormat(__FUNCTION__
                  " | ERROR: Created object is invalid for:\nclass %s(%s)",
                  className, p_params);
      class="kw">delete object; class=class="str">"cmt">// Remove the invalid object
      object = NULL;
    }
    class="kw">return object;
  }
};
class=class="str">"cmt">// Create an object in the factory from a class="type">class="kw">string
class="macro">#define NEW(Params) CVirtualFactory::Create(Params)
class=class="str">"cmt">// Creating a child object in the factory from a class="type">class="kw">string with verification.
class=class="str">"cmt">// Called only from the current object constructor.
class=class="str">"cmt">// If the object is not created, the current object becomes invalid
class=class="str">"cmt">// and exit from the constructor is performed
class="macro">#define CREATE(Class, Object, Params)                                                                       \
Class *Object = NULL;                                                                                   \
    if (IsValid()) {                                                                                        \
Object = class="kw">dynamic_cast<C*> (NEW(Params));                                                             \
if(!Object) {

◍ 用宏在构造函数里拦住参数类型错配

在 MQL5 里给指标或 EA 写基类时,最容易被忽略的是参数对象的类型校验。如果调用方传进来的不是预期的类实例,运行到后面才会崩,排错成本很高。 下面这段宏直接塞进构造函数,能在对象生成阶段就拦截非法入参,并打出具体行号与期望类名,省去后期翻日志。 代码逐行看:第一行调用 SetInvalid,把当前函数名 __FUNCTION__ 作为错误归属;StringFormat 拼出提示串,要求的是 #Class 指定的类,并带 __LINE__ 当前行号。 第二行把调用方传入的 Params 内容一并写进错误信息,方便对照;第三行直接 return,终止构造,避免半初始化对象流入系统。 实盘加载前,把这段宏套进你自己的参数校验宏里,MT5 编译后若传错对象,会在初始化日志里看到明确行号,而不是莫名报 4014 无效参数。外汇与贵金属品种波动剧烈,这类早期拦截能降低机器人误加载导致异常下单的概率。

MQL5 / C++
SetInvalid(__FUNCTION__, StringFormat("Expected Object of class %s() at line %d in Params:\n%s",  \
class="macro">#Class, __LINE__, Params));                                 \
class="kw">return;                                                                                           \
}                                                                                                    \
}

把因子基类塞进旧架构

重构时第一步是把 CFactorable 作为公共基类挂到 CAdvisor、CStrategy 和 CVirtualStrategyGroup 上。前两个类只是改了继承关系,内部接口不用动;CVirtualStrategyGroup 因为不再是纯抽象类,必须补一个能根据初始化字符串造对象的构造函数。 这一改直接干掉了原来两套分开的构造函数——以前要么传策略序列、要么传数组组,现在统一吃一个 string 参数。ToString 类的转换方法也简化了:只在存好的参数字符串前面拼个类名,Scale() 缩放逻辑原封不动。 代码里能看到三个类的继承标记都加了 : public CFactorable,CVirtualStrategyGroup 还显式写了带 string 参数的构造函数声明。改完记得把文件存回当前目录的 VirtualStrategyGroup.mqh,MT5 编译时才认得到新结构。 外汇与贵金属品种波动大、杠杆高,这类底层重构若引进入实盘 EA,务必先在策略测试器用历史数据跑一遍,避免构造字符串解析出错导致组策略失效。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA base class                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CAdvisor : class="kw">public CFactorable {
class="kw">protected:
   CStrategy      *m_strategies[];   class=class="str">"cmt">// Array of trading strategies
   class="kw">virtual class="type">void   Add(CStrategy *strategy);   class=class="str">"cmt">// Method for adding a strategy
class="kw">public:
                  ~CAdvisor();       class=class="str">"cmt">// Destructor
   class="kw">virtual class="type">void   Tick();            class=class="str">"cmt">// OnTick event handler
   class="kw">virtual class="type">class="kw">double Tester() {
      class="kw">return class="num">0;
   }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base class of the trading strategy                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CStrategy : class="kw">public CFactorable {
class="kw">public:
   class="kw">virtual class="type">void   Tick() = class="num">0; class=class="str">"cmt">// Handle OnTick events
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class of trading strategies group(s)                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualStrategyGroup : class="kw">public CFactorable {
class="kw">protected:
   class="type">class="kw">double         m_scale;           class=class="str">"cmt">// Scaling factor
   class="type">void           Scale(class="type">class="kw">double p_scale); class=class="str">"cmt">// Scaling the normalized balance
class="kw">public:
                  CVirtualStrategyGroup(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
   CVirtualStrategy   *m_strategies[];  class=class="str">"cmt">// Array of strategies
   CVirtualStrategyGroup *m_groups[];   class=class="str">"cmt">// Array of strategy groups
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualStrategyGroup::CVirtualStrategyGroup(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 initialization class="type">class="kw">string of the array of strategies or groups

「策略组与单策略的加载分流」

这段初始化逻辑把传入的参数串按对象类型拆进两个数组:碰到 CVirtualStrategyGroup 就建组塞进 m_groups,否则建单策略塞进 m_strategies。循环靠 ReadArrayString 不断吐出子串,直到返回 NULL 才停。 缩放因子 m_scale 从参数里读成 double,若读出 ≤0.0 会强制拉回 1.0,避免后面除零或反向缩放。 分流判定只看两个数组谁非空:组数组有内容且策略数组空,就按 1/组数 缩放所有组;反过来就按 1/策略数 缩放所有策略。两者同时为空或同时非空都会走 SetInvalid 报“Groups or strategies not found”。外汇与贵金属组合回测中,这种初始化错误往往导致整组信号权重归零,属高风险配置环节,建议开 MT5 跑一遍带日志的构造调用验证分支。 代码里的 operator~ 把组和自身参数拼成“类名(参数)”字符串,方便在观察器里直接看对象状态,不必额外写打印函数。

MQL5 / C++
  class="type">class="kw">string items = ReadArrayString(p_params);
class=class="str">"cmt">// Until the class="type">class="kw">string is empty
  while(items != NULL) {
      class=class="str">"cmt">// Read the initialization class="type">class="kw">string of one strategy or group object
      class="type">class="kw">string itemParams = ReadObject(items);
      class=class="str">"cmt">// If this is a group of strategies,
      if(IsObjectOf(itemParams, "CVirtualStrategyGroup")) {
          class=class="str">"cmt">// Create a strategy group and add it to the groups array
          CREATE(CVirtualStrategyGroup, group, itemParams);
          APPEND(m_groups, group);
      } else {
          class=class="str">"cmt">// Otherwise, create a strategy and add it to the array of strategies
          CREATE(CVirtualStrategy, strategy, itemParams);
          APPEND(m_strategies, strategy);
      }
  }
class=class="str">"cmt">// Read the scaling factor
  m_scale = ReadDouble(p_params);
class=class="str">"cmt">// Correct it if necessary
  if(m_scale <= class="num">0.0) {
      m_scale = class="num">1.0;
  }
  if(ArraySize(m_groups) > class="num">0 && ArraySize(m_strategies) == class="num">0) {
      class=class="str">"cmt">// If we filled the array of groups, and the array of strategies is empty, then
      class=class="str">"cmt">// Scale all groups
      Scale(m_scale / ArraySize(m_groups));
  } else if(ArraySize(m_strategies) > class="num">0 && ArraySize(m_groups) == class="num">0) {
      class=class="str">"cmt">// If we filled the array of strategies, and the array of groups is empty, then
      class=class="str">"cmt">// Scale all strategies
      Scale(m_scale / ArraySize(m_strategies));
  } else {
      class=class="str">"cmt">// Otherwise, report an error in the initialization class="type">class="kw">string
      SetInvalid(__FUNCTION__, StringFormat("Groups or strategies not found in Params:\n%s", p_params));
  }
}
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 CVirtualStrategyGroup::class="kw">operator~() {
    class="kw">return StringFormat("%s(%s)", class="kw">typename(this), m_params);
}

◍ EA 类收口到单构造函数

把上一节的双构造函数砍掉,只留一个从初始化字符串建对象的入口,能明显压掉重复代码。单一策略的场景,先包成组再丢给这个构造函数,Init() 方法就可以整体删了。 构造函数里先解析 p_params:读策略组参数、magic、名称、仅新K线触发标志。任何一步解析异常,对象会停在无效态,后续接收器和接口对象不被创建。 正因为接收器/接口可能在构造期被跳过,析构函数必须加指针非空判断再 delete,否则 MT5 回测里容易抛无效指针访问。改动存进 VirtualAdvisor.mqh 即可编译验证。 有效性检查前置后,CREATE 宏建组、挂静态接收器与接口、拼日志文件名(测试态带 .test 后缀)全部塞进 if(IsValid()) 块内,最后 Add(p_group) 再 delete p_group。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class of the EA handling class="kw">virtual positions(orders)              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualAdvisor : class="kw">public CAdvisor {
  ...
class="kw">public:
                      CVirtualAdvisor(class="type">class="kw">string p_param);     class=class="str">"cmt">// Constructor
                      ~CVirtualAdvisor();                  class=class="str">"cmt">// Destructor
  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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(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 initialization class="type">class="kw">string of the strategy group object
  class="type">class="kw">string groupParams = ReadObject(p_params);
class=class="str">"cmt">// Read the magic number
  class="type">ulong p_magic = ReadLong(p_params);
class=class="str">"cmt">// Read the EA name
  class="type">class="kw">string p_name = ReadString(p_params);
class=class="str">"cmt">// Read the work flag only at the bar opening
  m_useOnlyNewBar = (class="type">bool) ReadLong(p_params);
class=class="str">"cmt">// If there are no read errors,
  if(IsValid()) {
    class=class="str">"cmt">// Create a strategy group
    CREATE(CVirtualStrategyGroup, p_group, groupParams);
    class=class="str">"cmt">// Initialize the receiver with the class="kw">static receiver
    m_receiver = CVirtualReceiver::Instance(p_magic);
    class=class="str">"cmt">// Initialize the interface with the class="kw">static interface
    m_interface = CVirtualInterface::Instance(p_magic);
    m_name = StringFormat("%s-%d%s.csv",
                (p_name != "" ? p_name : "Expert"),
                p_magic,
                (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
               );
    class=class="str">"cmt">// Save the work(test) start time
    m_fromDate = TimeCurrent();
    class=class="str">"cmt">// Reset the last save time
    m_lastSaveTime = class="num">0;
    class=class="str">"cmt">// Add the contents of the group to the EA
    Add(p_group);
    class=class="str">"cmt">// Remove the group object
    class="kw">delete p_group;
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Destructor                                                       |

析构时把接收器和接口清干净

在 CVirtualAdvisor 的析构函数里,必须显式释放两个核心指针:消息接收器 m_receiver 与图形界面 m_interface。若跳过这一步,EA 在图表反复加载/卸载时会在终端留下悬空对象,MT5 日志里可能刷出数十次 'object already exists' 警告。 判断释放条件用了 !! 做布尔强转,只有指针非空才执行 delete,避免对空指针操作引发崩溃。 最后调用 DestroyNewBar() 回收新K线跟踪用的定时器与事件订阅对象,否则旧图表的 OnChartEvent 仍可能回调到已销毁实例。外汇与贵金属品种波动剧烈,这类残留会让复盘信号乱跳,实盘前务必在策略测试器里反复挂载卸载验证。

MQL5 / C++
class="type">void CVirtualAdvisor::~CVirtualAdvisor() {
  if(!!m_receiver) class="kw">delete m_receiver;        class=class="str">"cmt">// Remove the recipient
  if(!!m_interface) class="kw">delete m_interface;      class=class="str">"cmt">// Remove the interface
  DestroyNewBar();                           class=class="str">"cmt">// Remove the new bar tracking objects 
}
把对象重建交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到字符串到对象的映射是否缺字段,你只需核对逻辑而非手动解析。

常见问题

MQL5环境没有把复杂对象直接持久化到数据库的原生机制,转为参数字符串是最稳妥的跨会话传递方式,且便于人工排查。
可以,小布盯盘内置的AIGC会标记字符串与类构造参数不匹配的项,省去你逐列比对params与input的重复劳动。
工厂模式把解析逻辑集中管理,后续扩展新策略类时改动更小;额外构造函数则让单个类自给自足,倾向按项目规模选择。
可能需要在读取后补默认或从伴随表关联,单纯依赖params字符串会丢失上下文,概率上导致实例绑定错误品种。
若字段顺序或命名变更,旧字符串可能解析失败,建议在工厂层做版本兼容,不要直接改动原构造函数签名。