使用单独模块构建智能交易系统(基础篇)
「把EA拆成独立模块来写」
在 MT5 里直接把整个策略塞进一个 mq5 文件,后期改逻辑会非常痛苦。把智能交易系统按功能切成独立模块(信号、风控、下单),是 2019 年之后社区里逐渐被接受的做法,一篇阅读量 6105 的 EA 架构文就明确建议用单独模块构建。 模块化的直接好处是:信号模块只负责算入场条件,下单模块只管发单和撤单,互不污染。你在 MT5 里新建 include 目录放 .mqh,主 EA 用 #include 调用,编译一次就能验证接口是否对得上。 外汇和贵金属杠杆高、跳空频繁,模块边界不清时一个数组越界就可能让实盘 EA 直接崩。先把结构拆干净,再谈参数优化,概率上更稳。
◍ 把时间表逻辑拆出来单独养
写 EA、指标或脚本时,真正跟交易逻辑无关的杂活代码特别多,最典型的就是智能交易系统的运行时间表——按日、按周或按月开关。这类代码如果每次都重写,既费事又容易埋 bug。 把这些通用片段抽成一个独立模块,留一个简单接口给交易逻辑和其他组件调用,以后新项目直接复用,基本不用大改。外汇和贵金属杠杆高,这类时间表若写错,可能在极端波动时段误触发下单,风险不小。 本文面向刚接触 MQL5 复用的初学者,尝试把这套模块思路落到 EA 搭建上,顺便看一个开发过程中自然冒出来的有趣可能。
EA 模块化的老套路与痛点
想搞清楚一套 EA 到底由什么拼成,先看程序员日常怎么干。尾随止损、交易时段表、新闻过滤器这类功能,在不同 EA 里逻辑高度雷同,没人愿意每次重写——通常是写一次模块,往后往新 EA 里插就行。 但早期拼法很乱。原文里那张关系图只画了 3 个模块加 OnStart、OnTick 两个响应程序,就已经够绕;真实复杂 EA 内部绑定只会更糟。要删一个模块或加一个,都极费劲,初调与排错也不轻松。 根子在于没用系统方法管绑定。后来约定了一条简单顺序:所有模块在 OnInit 创建,EA 逻辑只塞进 OnTick,模块只跟 OnTick 交换信息,OnDeinit 里销毁。这样单独模块热插拔、调试都轻快,OnTick 也好维护。 这算不上严格‘观察者’范式,但结构清晰直观多了。外汇与贵金属杠杆高、滑点跳空频繁,这种模块化若没隔离好,实盘里一个模块异常就可能拖垮整策略,务必在 MT5 策略测试器里分步验证。
「用极简模块 EA 验证前一根看跌烛条信号」
想验证「前一根烛条看跌就做空」这套逻辑,不需要写复杂的智能交易系统,一个演示型 EA 就够了。核心思路是:若上一根 K 线收阴,就在 OnTick 里检查当前是否已有同魔幻数持仓,没有则调用交易模块的 Sell() 开空单。 EA 采用模块化拆分。CTradeMod 负责下单,公开字段 dBaseLot、dProfit、dStop、lMagic 分别管手数、止盈距离、止损距离和魔幻数;CParam 则集中存放止损位、冻结位等随品种变化的参数,比如用 SymbolInfoInteger 取 SYMBOL_TRADE_STOPS_LEVEL 再乘 Point() 得到真实止损门槛。把这类易变参数收进单独模块,能避免全局变量散落。 真实项目里更稳妥的做法是给模块加取值器(getter)而非直接暴露字段,示例为省代码才开放字段。另外 CParam 可特批给所有模块访问,但本例开仓检查全在 OnTick 做,两者独立运行也无妨——多数 EA 的模块正是经 OnTick 调度且互不感知。 输入块给了具体数字:MaxSpread=100 点差上限、dlot=0.01、profit=50 点、stop=50 点、Magic=123456。OnInit 里把这些映射到 trade 对象,若检查不通过直接 INIT_FAILED 终止,比运行时反复判错干净。外汇与贵金属杠杆高,这类实验 EA 仅用于逻辑验证,实盘前须重测点差与止损位约束。 别把开放字段当标准写法 示例里 CTradeMod 和 CParam 都直接 public 字段,仅为缩短代码。真要做可维护的 EA,用 double GetStop() 这类取值器封装,后面接别的响应程序时才不会改一处崩一片。
class CTradeMod { class="kw">public: class="type">class="kw">double dBaseLot; class="type">class="kw">double dProfit; class="type">class="kw">double dStop; class="type">long lMagic; class="type">void Sell(); class="type">void Buy(); }; class="type">void CTradeMod::Sell() { CTrade Trade; Trade.SetExpertMagicNumber(lMagic); class="type">class="kw">double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); Trade.Sell(dBaseLot,NULL,class="num">0,ask + dStop,ask - dProfit); } class="type">void CTradeMod::Buy() { CTrade Trade; Trade.SetExpertMagicNumber(lMagic); class="type">class="kw">double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); Trade.Buy(dBaseLot,NULL,class="num">0,bid - dStop,bid + dProfit); } class CParam { class="kw">public: class="type">class="kw">double dStopLevel; class="type">class="kw">double dFreezeLevel; CParam() { new_day(); }class=class="str">"cmt">//EA_PARAM() class="type">void new_day() { dStopLevel = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL) * Point(); dFreezeLevel = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL) * Point(); }class=class="str">"cmt">//class="type">void new_day() }; ... class="kw">input class="type">int MaxSpread = class="num">100; ... OnTick() { if(ask - bid > MaxSpread * Point() ) class="kw">return; .... } class="kw">input class="type">class="kw">double dlot = class="num">0.01; class="kw">input class="type">int profit = class="num">50; class="kw">input class="type">int stop = class="num">50; class="kw">input class="type">long Magic = class="num">123456; CParam par; CTradeMod trade; class="type">int OnInit() { trade.dBaseLot = dlot; trade.dProfit = profit * _Point; trade.dStop = stop * _Point; trade.lMagic = Magic; class="kw">return (INIT_SUCCEEDED); } class="type">void OnDeinit(const class="type">int reason) { } class="type">void OnTick() { class="type">int total = PositionsTotal(); class="type">class="kw">ulong ticket, tsell = class="num">0; class="type">ENUM_POSITION_TYPE type; class="type">class="kw">double l, p; for (class="type">int i = total - class="num">1; i >= class="num">0; i--) { if ( (ticket = PositionGetTicket(i)) != class="num">0) {
◍ 空仓时靠前一根阴线触发卖单
这段逻辑跑在持仓遍历之后:如果当前图表品种、当前魔术码下没有卖单(tsell 仍为 0),就回头看前一根 K 线。用 iOpen(NULL, PERIOD_CURRENT, 1) 取 1 号柱开盘价、iClose 取收盘价,若收低于开(c < o),视为阴线,直接 trade.Sell() 进场。 外汇与贵金属杠杆高,这种“无持仓+前根阴线”的裸触发在震荡市可能连续挨巴掌,只适合拿到 MT5 策略测试器里按历史数据验证胜率与最大回撤。 下面把核心片段拆开:外层 if 确认同品种同魔术码持仓为空,内部取前柱 O/C 做比较,成立才下单,没有止损止盈附加——参数层面你至少得补上 volume 与 slippage 控制。
if (PositionGetString(POSITION_SYMBOL) == _Symbol) { if (PositionGetInteger(POSITION_MAGIC) == Magic) { type = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); l = PositionGetDouble(POSITION_VOLUME); p = PositionGetDouble(POSITION_PRICE_OPEN); class="kw">switch(type) { case POSITION_TYPE_SELL: tsell = ticket; break; }class=class="str">"cmt">//class="kw">switch(type) }class=class="str">"cmt">//if (PositionGetInteger(POSITION_MAGIC) == lmagic) }class=class="str">"cmt">//if (PositionGetString(POSITION_SYMBOL) == symbol) }class=class="str">"cmt">//if ( (ticket = PositionGetTicket(i)) != class="num">0) }class=class="str">"cmt">//for (class="type">int i = total - class="num">1; i >= class="num">0; i--) if (tsell == class="num">0) { class="type">class="kw">double o = iOpen(NULL,PERIOD_CURRENT,class="num">1); class="type">class="kw">double c = iClose(NULL,PERIOD_CURRENT,class="num">1); if (c < o) { trade.Sell(); } }