开发多模块智能交易系统(基础篇)
◍ 从零搭一个多模块 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 把这段贴进头部,就能直接看到模块化框架的变量布局。
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 用策略测试器单步看缓冲值。
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 以默认手数或零止损运行,开前务必在策略测试器里逐模块验证句柄返回。
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");