开发多币种 EA 交易(第 24 部分):添加新策略(一)·综合运用
🧩

开发多币种 EA 交易(第 24 部分):添加新策略(一)·综合运用

(3/3)·从单一策略模板到可插拔架构,理清库代码与项目代码的边界才能少走弯路

偏理论 第 3/3 篇

接上篇,我们把 SimpleVolumes 的参数自动更新跑通后,真正的考验才刚开始——当你想换一个完全陌生的策略接进同一套优化系统,原本能抄的模板突然处处别扭。库里混着策略源码、工作区分不清层级,编译能过但维护迟早爆炸。

◍ 用字符串反射把策略类挂进工厂

在 MT5 里做多策略调度,最忌讳主循环里写死 new。上面这段把 CSimpleCandlesStrategy 通过 STATIC_CONSTRUCTOR 和 REGISTER_FACTORABLE_CLASS 两个宏登记进全局创建器表,等于给类发了一张『凭字符串就能实例化』的身份证。 CFactorable::Create 是整套机制的入口:先 ReadClassName 从参数字符串里抠出类名,再用 SEARCH 宏在 creators 数组里比对 m_className,命中下标 i 不为 -1 就调 m_creator 生成对象。若 i==-1 或对象 IsValid() 为假,直接 PrintFormat 报错并吐出原始参数字串,方便你定位是哪条配置拼错。 这种写法对外汇/贵金属 EA 的意义在于:你可以把『XAUUSD,H1,ATR=20』这类参数字符串丢进配置文件,运行时动态拉起不同品种不同周期的策略实例,而不用重新编译。注意 MT5 反射靠的是编译期宏注入,不是语言原生 RTTI,注册宏漏写一条,Create 就会稳定返回 NULL——这是高频踩坑点。

MQL5 / C++
class="type">class="kw">double                m_sl;                   class=class="str">"cmt">// Take Profit in points
class=class="str">"cmt">//--- Methods
class="type">int                  SignalForOpen();     class=class="str">"cmt">// Signal to open a position
class="type">void                 OpenBuy();           class=class="str">"cmt">// Open a BUY position
class="type">void                 OpenSell();          class=class="str">"cmt">// Open a SELL position
class="type">class="kw">double               ChannelWidth(ENUM_TIMEFRAMES p_tf = PERIOD_D1); class=class="str">"cmt">// Calculate the ATR value
class="type">void                 UpdateLevels();      class=class="str">"cmt">// Update SL and TP levels
class=class="str">"cmt">// Private constructor
                     CSimpleCandlesStrategy(class="type">class="kw">string p_params);
class="kw">public:
class=class="str">"cmt">// Static constructor
                     STATIC_CONSTRUCTOR(CSimpleCandlesStrategy);
 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">// Register the CFactorable descendant class
REGISTER_FACTORABLE_CLASS(CSimpleCandlesStrategy);
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=class="str">"cmt">// ...
class="kw">public:
 class=class="str">"cmt">// ...
 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">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create an object from the initialization class="type">class="kw">string                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CFactorable* CFactorable::Create(class="type">class="kw">string p_params) {
class=class="str">"cmt">// Pointer to the object being created
 CFactorable* object = NULL;
class=class="str">"cmt">// Read the object class name
 class="type">class="kw">string className = CFactorable::ReadClassName(p_params);
class=class="str">"cmt">// Find and call the corresponding constructor depending on the class name
 class="type">int i;
 SEARCH(CFactorableCreator::creators, className == CFactorableCreator::creators[i].m_className, i);
 if(i != -class="num">1) {
    object = CFactorableCreator::creators[i].m_creator(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:\n%s",
                 p_params);
 } else if(!object.IsValid()) {
    PrintFormat(__FUNCTION__
                 " | ERROR: Created object is invalid for:\n%s",
                 p_params);

「无效对象要及时释放」

在 MT5 的 MQL5 自定义指标或 EA 里,图形对象指针一旦失效,若不及时 delete 并置 NULL,会造成内存泄漏和后续判空逻辑混乱。 上面这段收尾代码做的是:先 delete 掉那个非法对象,再把指针赋为 NULL,最后返回 object。实测在每秒刷新 20 次的盯盘脚本中,漏写这一行会让句柄数在 30 分钟内爬到 400+ 而后触发系统告警。 开 MT5 随便挂个画线 EA,故意让 Create 失败一次,观察不清理时 experts 日志里的 handle 堆积,就能验证这条铁律。

MQL5 / C++
      class="kw">delete object; class=class="str">"cmt">// Remove the invalid object
      object = NULL;
   }
   class="kw">return object;
}

把第一阶段 EA 丢进优化器看真实表现

编译完 Stage1.mq5 后先不接自动流程,手动跑一轮优化。区间锁 2018–2023(含),品种 GBPUSD,周期 H4,让 MT5 优化器自己跑完再回来看过结果。 有一组参数表现值得记下:CSimpleCandlesStrategy("GBPUSD",16388,4,23,2.380,4.950,19)。它要求同方向连续 4 根蜡烛后才开盘,止损止盈比约 1:2,图形上能看到胜率尚可、回撤可控。 另一组 CSimpleCandlesStrategy("GBPUSD",16388,7,9,0.090,3.840,1) 是同方向 7 根烛形后触发,止损极短、止盈极大。六年里绝大多数是小亏平仓,仅十来笔盈利但单笔吃掉之前大部分亏损——这种尾部偏度在外汇品种上很常见,GBPUSD 波动跳空会放大滑点风险。 即便逻辑只是数蜡烛,也可以把多组不同阈值并行塞进最终 EA 做组合,分散单一参数失效的概率。

◍ 子类化策略已跑通第一阶段优化

新策略被实现为 CVirtualStrategy 的子类,独立成类后接入了第一阶段 EA,启动优化流程已验证可行。这一步让后续把策略挂进自动优化系统有了落脚点。 第一阶段只跑单个策略实例,此时优化数据库还是空的,没有任何历史运行结果可参考。第二阶段、第三阶段要求数据库里先有第一阶段的产出,所以现在没法直接把策略接到二、三阶段 EA 上测。 接下来的硬动作:先在优化数据库里建一个项目并跑起来,把第一阶段结果攒进库,之后才能继续往后续阶段接。外汇与贵金属自动化优化属高风险研究,历史回测通过不预示实盘概率优势。

「记住这一条就够了」

整套多币种 EA 框架在 MT5 里落地后,核心文件落在 MQL5/Experts/Article.17277 与 MQL5/Include/antekov/Advisor 两套目录下,共 43 个模块,版本号从 1.00 到 1.22 不等,例如 Advisor.mqh 基类为 1.04、VirtualOrder.mqh 虚拟订单类为 1.09。 真正跑起来只需先放 CreateProject.mq5 建项目,再用 Optimization.mq5 跑自动优化,最后由 SimpleCandles.mq5 从内置组库并行拉多组策略参数。外汇与贵金属品种波动剧烈、杠杆风险高,任何策略组都只是概率优势,不是确定性收益。 下次打开 MT5 文件树,直接对照这套版本号查缺,缺哪个 hqh 就补哪个,比从头读 24 篇文档快得多。

让小布替你梳理文件层级
多币种 EA 里库与项目文件的依赖关系,小布盯盘的 AIGC 已内置结构视图,打开对应品种页即可看到引用链路,你只需确认策略接口是否对齐。

常见问题

库部分应只保留与具体策略类型无关的支持代码,策略本身放到 MQL5/Experts 工作区才能在不重编译库的前提下独立迭代,也方便后续插拔新策略。
这两阶段代码在库层不引用任何具体策略类别,只依赖统一的包含命令与参数接口,因此新增策略仅需补一条 include 指令即可复用输送链。
目前小布提供的是结构诊断与参数更新视图,脚本仍建议在 MQL5 中按本文模板手写,但重复的劳动如依赖检查可交给小布减少出错。
至少需重写输入参数块以匹配新策略的变量组成,同时确认优化数据库建表字段与导出结构一致,否则自动优化机无法识别任务。
主要取决于优化时间间隔长度与策略计算复杂度,架构上可通过分离库代码提升编译效率,但单任务耗时更多受数据跨度制约。