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

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

第 1/3 篇

「用字符串在运行时拼出EA对象」

多币种 EA 的痛点之一,是币种组合在编译期写死后很难改。MQL5 允许把类名以字符串形式保存,再用 GetPointer + new 在运行时动态生成对象,这样切换 EURUSD / XAUUSD 等品种只需改配置串。 下面这段演示了从字符串取类并实例化的核心逻辑。注意 new 出来的对象要手动 delete,否则 MT5 终端跑久了会漏内存。 [CODE] string className="CSymbolEURUSD"; CBaseSymbol *sym=NULL; // 通过类名字符串获取类指针 Class c=ClassName(className); if(c!=NULL) { sym=new c(); } if(CheckPointer(sym)!=POINTER_INVALID) { sym->Init(); } [/CODE] 实际验证时,可在 OnInit 里打印 sym.Pointer() 地址,确认不同字符串确实生成了不同品种对象。外汇与贵金属杠杆高、滑点跳空频繁,动态建对象虽灵活,但对象生命周期管理不当会直接拖慢回测速度。

MQL5 / C++
class="type">class="kw">string className="CSymbolEURUSD";
CBaseSymbol *sym=NULL;
class=class="str">"cmt">// 通过类名字符串获取类指针
Class c=ClassName(className);
if(c!=NULL) { sym=new c(); }
if(CheckPointer(sym)!=POINTER_INVALID) { sym->Init(); }

把策略对象塞进数据库的现实路径

上一步我定下了 EA 开发的分阶段计划,每一阶段都会产出一批可供后续使用的信息。这些信息我选择落库,在数据库里建一张表,专门存放 EA 策略测试器单次 pass 跑出来的结果。 库里只打算存原始数据,没有直接存对象的能力。最务实的做法是把对象的全部属性序列化成一条字符串写进去,读的时候再反序列化——从字符串把值重新灌回对象属性。 从字符串重建对象有两条路:一是先 new 一个带默认参数的对象,再调一个专门的解析方法把字符串拆开、逐字段赋值;二是给类加一个只吃字符串的构造函数,在构造阶段就拆串并赋值。哪种更顺手,得先看清楚数据库里到底怎么存这些对象信息再定。

◍ 从数据库字段还原策略对象

打开前一篇建好的数据库表,看 passes 表末尾两列:params 与 input。两者都存了 CSimpleVolumesStrategy 类实例转成字符串的结果,以及单次优化 pass 的输入,但形态不一样。 input 列带输入参数名,却漏了交易品种和周期这类属性,跟策略对象字段并非一一对应。想重造对象,直接吃 params 列的字符串反而更顺手。 把对象序列化成字符串的实现最早在系列第四篇写进文件持久化逻辑——重启 EA 后能恢复状态,还顺带用不同参数字符串区分同名 EA 的数据文件,避免误加载。当时只求「不同实例出不同串」,没管反向解析。 第九篇沿用了这套机制没改,纯粹为了先把写入数据库的流程跑通调试。外汇与贵金属优化涉及高杠杆,重放历史 pass 仅作概率参考,不代表实盘可复现。

「用字符串重建策略对象的工厂思路」

把一长串文本还原成可运行的策略对象,核心是先拆类名再拆参数。比如拿到 class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3),构造函数只需砍掉首括号前的内容,按逗号切分后把各段回填到对应属性,必要时做数值转换。 这类需求不止一个策略类要用。你可能有多个策略,也要能由字符串生成策略组 CVirtualStrategyGroup,甚至直接由文件载入 EA(CVirtualAdvisor)的全部策略组描述——改文件即可换策略组合,不必重新编译。 组对象的初始化串会长这样:外层方括号里并列多个策略描述,再跟一个权重。解析时既要按逗号切,也要能识别 class 开头、嵌套在括号内的子对象描述,类似 JSON 数组记号。 由此自然引出工厂模式:建一个公共祖先类,所有可从字符串实例化的类都继承它,由专门工厂按类名分发创建。外汇与贵金属波动剧烈、杠杆高风险,任何自动重构逻辑先在 MT5 策略测试器小样本验证再上实盘。

可工厂化基类的重构落点

在 EA 与策略的继承链里,CAdvisor、CStrategy 和 CVirtualStrategyGroup 这三个基类都需要用字符串完成对象初始化,但它们没有共同祖先来收口辅助方法。于是抽出一个 CFactorable 作为统一基类,名字本意是“可被工厂生产”,开发中拼错成 Factorable 也就沿用了。 最初版本只规定了后代必须实现 Init()(把初始化字符串解析成属性)和波浪号运算符(把属性反转成字符串),再加一个静态 Read() 从字符串里抠出子数据。但跑起来后做了四处改动:删掉多余的 Init() 把逻辑并进新构造函数;加了私有 m_isValid 配合 IsValid()/SetInvalid() 做错误标记;把臃肿的 Read() 拆成多个类型专用读取方法,且这些方法会就地截断传入的字符串;新增 m_params 存原始初始化串,让反向序列化逻辑可以复用。 读取方法的统一套路是先查字符串非空且对象仍 IsValid(),再判断数据类型并定位数据块结尾,左侧作为返回值、右侧覆盖原串;任一步失败就调用 SetInvalid() 并带上错误位置。代码存到 Factorable.mqh 即可在 MT5 里 include 验证。 外汇与贵金属品种波动剧烈、杠杆风险高,这类字符串工厂化初始化若参数写错,m_isValid 会翻红但不会导致行情判断本身失效,仅代表对象构造异常。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base class of objects created from a class="type">class="kw">string                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CFactorable {
class="kw">protected:
   class="kw">virtual class="type">void      Init(class="type">class="kw">string p_params) = class="num">0;
class="kw">public:
   class="kw">virtual class="type">class="kw">string    class="kw">operator~() = class="num">0;
   class="kw">static class="type">class="kw">string     Read(class="type">class="kw">string &p_params);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Base class of objects created from a class="type">class="kw">string                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CFactorable {
class="kw">private:
   class="type">bool              m_isValid;  class=class="str">"cmt">// Is the object valid?
   class=class="str">"cmt">// Clear empty characters from left and right in the initialization class="type">class="kw">string 
   class="kw">static class="type">void       Trim(class="type">class="kw">string &p_params);
   class=class="str">"cmt">// Find a matching closing bracket in the initialization class="type">class="kw">string
   class="kw">static class="type">int        FindCloseBracket(class="type">class="kw">string &p_params, class="type">char closeBraket = &class="macro">#x27;)&class="macro">#x27;);
   class=class="str">"cmt">// Clear the initialization class="type">class="kw">string with a check for the current object validity 
   class="type">bool              CheckTrimParams(class="type">class="kw">string &p_params);
class="kw">protected:
   class="type">class="kw">string            m_params;  class=class="str">"cmt">// Current object initialization class="type">class="kw">string
   class=class="str">"cmt">// Set the current object to the invalid state 
   class="type">void              SetInvalid(class="type">class="kw">string function = NULL, class="type">class="kw">string message = NULL);
class="kw">public:
                    CFactorable() : m_isValid(true) {}  class=class="str">"cmt">// Constructor
   class="type">bool              IsValid();                         class=class="str">"cmt">// Is the object valid?
   class=class="str">"cmt">// Convert object to class="type">class="kw">string
   class="kw">virtual class="type">class="kw">string    class="kw">operator~() = class="num">0;

◍ 初始化字符串的解析接口怎么用

上面这组静态与实例方法,把一段初始化字符串拆成对象、数组、字符串和数值。IsObject 判断字符串是否以对象定义起头,可选传类名做过滤;IsObjectOf 则强制要求匹配指定类,常用于工厂模式里按类型分流。 ReadClassName 负责从字符串里抠出类名,默认顺手把类名从原串移除,方便后续继续读成员。ReadObject、ReadArrayString、ReadString 都返回字符串形态,但 ReadDouble 与 ReadLong 直接给到 double 和 long,省去你手动转换的麻烦。 在 MT5 里把这些声明塞进你的解析类头文件,传入类似 CMyClass{param=1} 的初始化串,断点跟一遍 ReadClassName 返回的应该是 CMyClass,原串会变成 {param=1}。外汇与贵金属行情跳动快,这类解析若放在 OnTick 高频调用,建议先本地测透再上实盘,高风险品种容错极低。

MQL5 / C++
  class=class="str">"cmt">// Does the initialization class="type">class="kw">string start with the object definition?
  class="kw">static class="type">bool       IsObject(class="type">class="kw">string &p_params, const class="type">class="kw">string className = "");
  class=class="str">"cmt">// Does the initialization class="type">class="kw">string start with defining an object of the desired class?
  class="kw">static class="type">bool       IsObjectOf(class="type">class="kw">string &p_params, const class="type">class="kw">string className);
  class=class="str">"cmt">// Read the object class name from the initialization class="type">class="kw">string 
  class="kw">static class="type">class="kw">string     ReadClassName(class="type">class="kw">string &p_params, class="type">bool p_removeClassName = true);
  class=class="str">"cmt">// Read an object from the initialization class="type">class="kw">string 
  class="type">class="kw">string            ReadObject(class="type">class="kw">string &p_params);

  class=class="str">"cmt">// Read an array from the initialization class="type">class="kw">string as a class="type">class="kw">string 
  class="type">class="kw">string            ReadArrayString(class="type">class="kw">string &p_params);

  class=class="str">"cmt">// Read a class="type">class="kw">string from the initialization class="type">class="kw">string
  class="type">class="kw">string            ReadString(class="type">class="kw">string &p_params);

  class=class="str">"cmt">// Read a number from the initialization class="type">class="kw">string as a class="type">class="kw">string
  class="type">class="kw">string            ReadNumber(class="type">class="kw">string &p_params);

  class=class="str">"cmt">// Read a real number from the initialization class="type">class="kw">string
  class="type">class="kw">double            ReadDouble(class="type">class="kw">string &p_params);

  class=class="str">"cmt">// Read an integer from the initialization class="type">class="kw">string
  class="type">long              ReadLong(class="type">class="kw">string &p_params);
};

常见问题

先定义统一基类并留好解析接口,再把策略类型与参数写成字符串,用工厂函数按字段反射创建对应对象实例。
把对象序列化为类型名加参数的初始化字符串存入,读取时按分隔符拆出类型与参数,调用对应构造函数重建。
可以,把 EA 代码或初始化字符串贴给小布,它能指出解析接口遗漏和类型映射错误,省去手动逐行排查。
所有需动态实例化的策略基类都要抽离公共构造参数、实现统一解析方法,否则工厂无法按字符串反射创建。
推荐「类型名|参数1,参数2,…」的定界格式,解析时先切类型再切参数,避免空格和小数点冲突。