开发多币种 EA 交易 (第 10 部分):从字符串创建对象·进阶篇
(2/3)· 当数据库里只躺着一串 class CSimpleVolumesStrategy(...) 文本,你怎么把它变回能跑的EA组件?
不少多币种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 初始化而非继续交易。
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 无效参数。外汇与贵金属品种波动剧烈,这类早期拦截能降低机器人误加载导致异常下单的概率。
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,务必先在策略测试器用历史数据跑一遍,避免构造字符串解析出错导致组策略失效。
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~ 把组和自身参数拼成“类名(参数)”字符串,方便在观察器里直接看对象状态,不必额外写打印函数。
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。
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 仍可能回调到已销毁实例。外汇与贵金属品种波动剧烈,这类残留会让复盘信号乱跳,实盘前务必在策略测试器里反复挂载卸载验证。
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 }