开发多模块智能交易系统(基础篇)
📘

开发多模块智能交易系统(基础篇)

第 1/3 篇

◍ 从零搭一个多模块 EA 的骨架

在 MT5 里写智能交易系统,最忌讳把所有逻辑塞进一个文件。把订单管理、信号生成、风控拆成独立模块,后期改策略只动一处,回测也更容易定位问题。 Sergey Pavlov 在 2018 年 7 月 9 日发布的这篇基础框架帖,至今在站内有 4864 次查看、18 条讨论,说明多模块拆分的思路在 MQL5 社区是被反复验证过的起点。 外汇与贵金属杠杆交易高风险,模块再清晰也不能消除滑点和黑天鹅,实盘前务必用策略测试器跑至少三个月 tick 数据。

为什么把 EA 拆成模块更省心

做 MT5 智能交易系统,常见路子就三种:模块化、面向对象、结构化。这篇只聊模块化——核心就一句话:把程序切成独立模块,各自管一件事。 它的底层逻辑是“切分与规则”:模块之间界限清楚,换掉某个模块时,系统其余部分不用动。帕纳斯强调“信息隐藏”,模块把算法细节包起来,将来能用另一个模块原地替换;科恩把它定义成“执行特定功能的独立程序单元”;Tseytin 更直白,说模块就是拼程序的砖。 反例是单体程序:所有逻辑揉在一个文件里。想改个出场规则,往往得原作者或熟 MQL5 的人动手;编译后的 ex5 还只有版权方改得了。自己或外聘开发改关键函数,模块化结构明显更顺手,MT5 里打开源码你就能直接定位到对应模块文件。

「把EA拆成能独立替换的零件」

模块化编程的本质,是把一个交易系统的任务切成多个子任务,每个子任务写成单独编译的文件。在 MT5 里,这类文件可以是智能交易系统、指标或脚本;它们之间靠明确的数据接口通信,而不是互相纠缠内部状态。 判断一个模块合不合格,看四条:只认输入给它的源数据、不依赖被调用的历史、干完活把控制权交还父模块、模块间数据交换尽可能少。满足这几条,逻辑结构的任一部分都能单独改而不波及别处。 MQL5 里最实用的搭配,是主模块用智能交易系统扛交易管理,信号计算、条件过滤写成自定义指标挂在外围。指标算完丢进缓冲区,EA 按需取用或忽略。这样换市场状态,只动外部指标模块就行。 我更倾向把核心下单和风控锁在主模块里,外部模块只做适配不同行情用。功能边界该由用的人定,不是写代码时拍脑袋定死。外汇和贵金属杠杆高、滑点跳空频繁,模块间若耦合太紧,一次重编译就可能把活仓逻辑带崩,拆分时务必留好隔离。

◍ 把单文件EA拆成可插拔的主控骨架

主模块是整套交易机器人的调度中心,本身必须带交易执行能力,否则策略再漂亮也只是空壳。一个实用的切法是从自包含EA改起:比如原版用 iBands 通道、固定手数、在上下轨逆势开仓,不依赖任何外部文件,先保证它能独立跑起来。 要转成模块化项目,核心只做三件事:声明后续要挂的外部模块(指标或功能件)、补齐集成所需的接口函数、给外部开发者留文档生成开关。以资金管理为例,外部模块得把‘手数’回传主模块,持仓跟踪模块则要回传‘距现价的点数’,数据结构对齐才能通信。 改造后这套骨架最多可挂 7 个外部模块:1号给手数,2号给开仓价算的止损点,3号给止盈点,4号给现价算的尾随止损点,5号给信号值,6号给信号过滤值,7号给盈亏平衡止损点。即便指定目录里缺了某些模块文件,机器人也会用默认参数继续跑,缺件不会让系统崩。 下面的代码片段是生成器产出的主模块头部,能看到 bands 周期默认 80、偏差 3.0、固定手数 0.01,以及 7 组 on_xxx / handle_mx 的模块开关与句柄声明。你打开 MT5 新建 EA 把这段贴进头部,就能直接看到模块化框架的变量布局。

MQL5 / C++
class=class="str">"cmt">//****** project(module expert): test_module_exp.mq5
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                    程序代码由模块化项目生成器生成 |
class=class="str">"cmt">//|        版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008) |
class=class="str">"cmt">//|          [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008)"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property link      "class="num">1.00"
class="macro">#class="kw">property link      "多模块智能系统示例: 项目 TEST 主模块。"
class="macro">#class="kw">property link      "该项目使用 class="num">7 个外部模块。"
class=class="str">"cmt">//---
class="macro">#include <Trade\Trade.mqh>
class=class="str">"cmt">//---
class="type">MqlTick   last_tick;
CTrade    trade;
class=class="str">"cmt">//---
class="kw">input class="type">int                e_bands_period=class="num">80;            class=class="str">"cmt">// 移动均线周期
class="type">int                      e_bands_shift=class="num">0;             class=class="str">"cmt">// 位移
class="kw">input class="type">class="kw">double             e_deviation=class="num">3.0;             class=class="str">"cmt">// 标准偏差
class="kw">input ENUM_APPLIED_PRICE e_applied_price=PRICE_CLOSE; class=class="str">"cmt">// 价格类型
class="kw">input class="type">bool               on_module=class="kw">false;             class=class="str">"cmt">// 是否使用插件
class=class="str">"cmt">//---
class="type">class="kw">double lot=class="num">0.01;        class=class="str">"cmt">// 固定手数
class="type">class="kw">double min_lot=class="num">0.01;    class=class="str">"cmt">// 最低允许手数
class="type">bool   on_trade=class="kw">false;  class=class="str">"cmt">// 交易功能标志
class=class="str">"cmt">//--- 用于存储 iBands 指标句柄的变量
class="type">int    handle_Bands;
class=class="str">"cmt">//--- 模块 class="num">1
class="type">bool   on_lot=class="kw">false;
class="type">int    handle_m1;
class=class="str">"cmt">//--- 模块 class="num">2
class="type">bool   on_SL=class="kw">false;
class="type">int    handle_m2;
class=class="str">"cmt">//--- 模块 class="num">3
class="type">bool   on_TP=class="kw">false;
class="type">int    handle_m3;
class=class="str">"cmt">//--- 模块 class="num">4
class="type">bool   on_Trail=class="kw">false;
class="type">int    handle_m4;
class=class="str">"cmt">//--- 模块 class="num">5
class="type">bool   on_signals=class="kw">false;
class="type">int    handle_m5;
class=class="str">"cmt">//--- 模块 class="num">6
class="type">bool   on_Filter=class="kw">false;

信号结构体与多模块合成逻辑

EA 里把买卖判定收口到一个 sSignal 结构,只含 Buy / Sell 两个布尔位,后续主循环直接读这两个字段,避免散落各处的标志位互相打架。 信号生成函数 Buy_or_Sell() 先置 {false,false},随后按开关逐级叠模块。模块5在 on_signals 为真时取 handle_m5 的第0根缓冲:值小于 -1 置 Sell,大于 1 置 Buy;若 CopyBuffer 返回小于 0 直接原样返回空信号。 模块6受 on_Filter 控制,它不止过滤还顺手改 lot——把 buffer_m6[0] 赋给 lot,且当缓冲小于 1 时砍掉 Buy、大于 -1 时砍掉 Sell。这意味着过滤模块的输出既管方向也管仓位,接 EA 时要确认 buffer_m6 的量纲,否则 lot 可能被写成负数或极小值。 函数后半段拉通布林带三缓冲(中轨0、上轨1、下轨2)与近期 Low/High 序列,全部 ArraySetAsSeries(true) 后 Copy 最新1根。外汇与贵金属杠杆高,这类多模块合成在极端滑点下可能错过首根信号,建议开 MT5 用策略测试器单步看缓冲值。

MQL5 / C++
class="type">int    handle_m6;
class=class="str">"cmt">//--- 模块 class="num">7
class="type">bool   on_Breakeven=class="kw">false;
class="type">int    handle_m7;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 交易信号结构                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct sSignal
  {
   class="type">bool               Buy;    class=class="str">"cmt">// 买入信号
   class="type">bool               Sell;   class=class="str">"cmt">// 卖出信号
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 交易信号生成器                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
sSignal Buy_or_Sell()
  {
   sSignal res={class="kw">false,class="kw">false};
class=class="str">"cmt">//--- 模块 class="num">5
   if(on_signals)
     { class=class="str">"cmt">// 如果有额外模块
      class="type">class="kw">double buffer_m5[];
      ArraySetAsSeries(buffer_m5,true);
      if(CopyBuffer(handle_m5,class="num">0,class="num">0,class="num">1,buffer_m5)<class="num">0) class="kw">return(res);
      if(buffer_m5[class="num">0]<-class="num">1) res.Sell=true;
      if(buffer_m5[class="num">0]>class="num">1) res.Buy=true;
     }
class=class="str">"cmt">//--- 模块 class="num">6
   if(on_Filter)
     { class=class="str">"cmt">// 如果有额外模块
      class="type">class="kw">double buffer_m6[];
      ArraySetAsSeries(buffer_m6,true);
      if(CopyBuffer(handle_m6,class="num">0,class="num">0,class="num">1,buffer_m6)<class="num">0) class="kw">return(res);
      lot=buffer_m6[class="num">0];
      if(buffer_m6[class="num">0]<class="num">1) res.Buy=class="kw">false;
      if(buffer_m6[class="num">0]>-class="num">1) res.Sell=class="kw">false;
     }
class=class="str">"cmt">//---
class=class="str">"cmt">//--- 指标缓存区
   class="type">class="kw">double          UpperBuffer[];
   class="type">class="kw">double          LowerBuffer[];
   class="type">class="kw">double          MiddleBuffer[];
   ArraySetAsSeries(MiddleBuffer,true); CopyBuffer(handle_Bands,class="num">0,class="num">0,class="num">1,MiddleBuffer);
   ArraySetAsSeries(UpperBuffer,true);   CopyBuffer(handle_Bands,class="num">1,class="num">0,class="num">1,UpperBuffer);
   ArraySetAsSeries(LowerBuffer,true);   CopyBuffer(handle_Bands,class="num">2,class="num">0,class="num">1,LowerBuffer);
class=class="str">"cmt">//--- 时间序列
   class="type">class="kw">double L[];
   class="type">class="kw">double H[];
   ArraySetAsSeries(L,true); CopyLow(_Symbol,_Period,class="num">0,class="num">1,L);

「初始化时挂载外部模块的逻辑」

EA 在 OnInit 里先建布林带句柄,周期与图表一致,偏差和均价方式由外部参数 e_deviation、e_applied_price 决定;句柄无效直接返回 INIT_FAILED,有效才把 on_trade 置真,说明主交易开关依赖指标可用。 若 on_module 开启,代码会依次用 iCustom 去 Market 目录找 7 个外部模块:test_module_MM、SL、TP、Trail、signals、Filter、Breakeven,各自拿到句柄非 INVALID_HANDLE 时,把对应的 on_lot / on_SL / on_TP / on_Trail / on_signals / on_Filter / on_Break 标记打开。这种写法意味着仓位、止损、止盈、追踪、信号、过滤、保本逻辑全部可插拔,实盘前你能在 MT5 的 Market 文件夹里缺哪个模块就关哪个开关。 外汇与贵金属杠杆高,模块缺失若未手动核对开关,可能让 EA 以默认手数或零止损运行,开前务必在策略测试器里逐模块验证句柄返回。

MQL5 / C++
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建指标句柄
   handle_Bands=iBands(_Symbol,_Period,e_bands_period,e_bands_shift,e_deviation,e_applied_price);
   if(handle_Bands==INVALID_HANDLE)
      class="kw">return(INIT_FAILED);
   else
      on_trade=true;
   if(on_module)
     {
      class=class="str">"cmt">//--- 模块 class="num">1
      class=class="str">"cmt">//--- 检查: 是否有外部模块?
      handle_m1=iCustom(NULL,class="num">0,"Market\\test_module_MM");
      if(handle_m1!=INVALID_HANDLE)
         on_lot=true;
      class=class="str">"cmt">//--- 模块 class="num">2
      class=class="str">"cmt">//--- 检查: 是否有外部模块?
      handle_m2=iCustom(NULL,class="num">0,"Market\\test_module_SL");
      if(handle_m2!=INVALID_HANDLE)
         on_SL=true;
      class=class="str">"cmt">//--- 模块 class="num">3
      class=class="str">"cmt">//--- 检查: 是否有外部模块?
      handle_m3=iCustom(NULL,class="num">0,"Market\\test_module_TP");
      if(handle_m3!=INVALID_HANDLE)
         on_TP=true;
      class=class="str">"cmt">//--- 模块 class="num">4
      class=class="str">"cmt">//--- 检查: 是否有外部模块?
      handle_m4=iCustom(NULL,class="num">0,"Market\\test_module_Trail");
      if(handle_m4!=INVALID_HANDLE)
         on_Trail=true;
      class=class="str">"cmt">//--- 模块 class="num">5
      class=class="str">"cmt">//--- 检查: 是否有外部模块?
      handle_m5=iCustom(NULL,class="num">0,"Market\\test_module_signals");
      if(handle_m5!=INVALID_HANDLE)
         on_signals=true;
      class=class="str">"cmt">//--- 模块 class="num">6
      class=class="str">"cmt">//--- 检查: 是否有外部模块?
      handle_m6=iCustom(NULL,class="num">0,"Market\\test_module_Filter");
      if(handle_m6!=INVALID_HANDLE)
         on_Filter=true;
      class=class="str">"cmt">//--- 模块 class="num">7
      class=class="str">"cmt">//--- 检查: 是否有外部模块?
      handle_m7=iCustom(NULL,class="num">0,"Market\\test_module_Breakeven");

常见问题

先写主控文件只管调度,把信号、风控、下单拆成独立函数或文件,后期替换模块不用动主逻辑。
单文件改一处容易崩全局,模块化后能单独换信号源或止损逻辑,调试和回测都更快。
小布可读取你的模块结构,标出耦合点和重复代码,并提示哪些信号模块可替换为内置AIGC策略。
用统一结构体存各模块输出的方向、强度、有效期,主控按权重或阈值合成最终下单信号。
在初始化函数里用配置开关或外部参数加载对应模块指针,运行时按标识调用,避免硬编码。