使用单独模块构建智能交易系统(基础篇)
📘

使用单独模块构建智能交易系统(基础篇)

第 1/3 篇

「把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() 这类取值器封装,后面接别的响应程序时才不会改一处崩一片。

MQL5 / C++
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 控制。

MQL5 / C++
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();
      }
  }

常见问题

后期改交易时段要动整段代码,容易误删下单逻辑;建议把时间表单独写成一个函数或文件,改时间不影响主策略。
空仓时读取上一根K线收盘与开盘,若收盘低于开盘则视为阴线,直接挂卖单;先跑通这个再叠加过滤条件。
可以,把EA代码贴给小布,它会指出哪些逻辑该独立成模块、时间判断是否耦合在主循环里,并给重构建议。
早期写法常把全局变量散落各模块,依赖顺序执行;拆了文件但没隔离状态,改一处崩一片,不如极简模块逐步验证。
会,单根阴线只是弱势信号,趋势中可能被连续阳线打断;可加均线过滤或仅在特定时段启用,外汇贵金属高风险需先回测。