开发多模块智能交易系统·进阶篇
🧩

开发多模块智能交易系统·进阶篇

(2/3)·单体EA改一处就得全量重编,模块拆开后能单独替换哪块就动哪块

案例拆解 第 2/3 篇

接上篇,我们继续深挖把交易机器人拆成独立模块的具体做法。不少交易者把策略写进单个文件,改个出场逻辑就要重编译整个EA,还只能由原作者动代码。把功能切成可替换的砖块,后续迭代才不用每次都推倒重来。

EA 初始化与逐笔追踪止损的骨架

这段逻辑来自一个模块化 EA 的初始化尾段与 OnTick 前半部分,核心是把指标句柄有效性、最小交易量以及两种动态模块(手数、追踪止损)挂到主循环上。 初始化里先判断 m7 指标句柄是否不等于 INVALID_HANDLE,若成立就把 on_Breakeven 置真,说明保本逻辑模块已就绪;随后用 SymbolInfoDouble 取 SYMBOL_VOLUME_MIN 写入 min_lot,最后返回 INIT_SUCCEEDED。这一步决定了后续下单不会低于经纪商允许的最小合约。 OnTick 开头抓取 AccountInfoDouble(ACCOUNT_EQUITY) 得到净值,接着模块 1 在 on_lot 开启时从 handle_m1 拷贝最新 1 根缓冲到 buffer_m1,把 lot 设为 buffer_m1[0]——也就是用外部指标实时决定下单手数。 模块 4 是追踪止损:仅当 on_Trail 开、当前品种有持仓且浮盈大于 0 才继续。它从 handle_m4 取缓冲值 TR,对 BUY 单计算 price_SL = 现价 - TR*_Point,若高于原 SL 就调用 trade.PositionModify 上移止损;SELL 单对称处理。外汇与贵金属杠杆高,这类自动挪损可能在快速回撤中频繁触发,建议在 MT5 策略测试器用点差 2~3 倍真实值回测。 别把 TR 当固定点数 TR 来自指标缓冲而非写死数值,不同品种周期里 TR 可能从 15 点跳到 200 点,直接套用会让止损距离失控,务必先 Print 出 TR 观察分布再实盘。

MQL5 / C++
if(handle_m7!=INVALID_HANDLE)
     on_Breakeven=true;
   }
class=class="str">"cmt">//--- 交易操作允许的最小交易量
  min_lot=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
  class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 智能系统逐笔报价函数                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
  class="type">class="kw">double Equity=AccountInfoDouble(ACCOUNT_EQUITY);
class=class="str">"cmt">//--- 模块 class="num">1
  if(on_lot)
    { class=class="str">"cmt">// 如果有额外模块
      class="type">class="kw">double buffer_m1[];
      ArraySetAsSeries(buffer_m1,true);
      if(CopyBuffer(handle_m1,class="num">0,class="num">0,class="num">1,buffer_m1)<class="num">0) class="kw">return;
      lot=buffer_m1[class="num">0];
    }
class=class="str">"cmt">//--- 模块 class="num">4
  if(on_Trail)
    if(PositionSelect(_Symbol))
      if(PositionGetDouble(POSITION_PROFIT)>class="num">0)
        { class=class="str">"cmt">// 如果有额外模块
          class="type">class="kw">double buffer_m4[];
          ArraySetAsSeries(buffer_m4,true);
          if(CopyBuffer(handle_m4,class="num">0,class="num">0,class="num">1,buffer_m4)<class="num">0) class="kw">return;
          class="type">class="kw">double TR=buffer_m4[class="num">0];
          if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
            if(PositionGetDouble(POSITION_PRICE_CURRENT)-TR*_Point>PositionGetDouble(POSITION_PRICE_OPEN))
              {
                class="type">class="kw">double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)-TR*_Point;
                if(price_SL>PositionGetDouble(POSITION_SL))
                  trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),class="num">0);
              }
          if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
            if(PositionGetDouble(POSITION_PRICE_CURRENT)+TR*_Point<PositionGetDouble(POSITION_PRICE_OPEN))
              {

◍ 用 TR 与缓冲带把止损拉到成本线之上

跟踪止损段先取当前价加 TR 倍点值作为候选止损位:price_SL = PositionGetDouble(POSITION_PRICE_CURRENT) + TR*_Point。只有当它比原 SL 更优、或原 SL 为空时,才调用 PositionModify 把止损推过去,避免每 tick 无谓发单。 保本模块(on_Breakeven)依赖一个外部指标句柄 handle_m7,CopyBuffer 只取最新 1 根数值放进 TRB。若持仓盈利且现价越过「开仓价 ± TRB*_Point」这条缓冲带,就把止损挪到现价反向 5 个点处——买仓用减 5 点、卖仓用加 5 点。 注意那行 POSITION_SL==NULL 的判断:MT5 里未设止损返回 0 而非严格 NULL,实盘里更稳妥的写法是显式比对 0.0。外汇与贵金属杠杆高,这类自动改 SL 逻辑若在点差扩大时触发,可能把止损顶到不利位置,建议先在策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍。 下面这段是模块 7 的核心改写逻辑,逐行拆一下关键意图:

MQL5 / C++
class="type">class="kw">double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)+TR*_Point;
if(price_SL<PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL)==NULL)
   trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),class="num">0);
class=class="str">"cmt">//--- 模块 class="num">7
if(on_Breakeven)
 if(PositionSelect(_Symbol))
  if(PositionGetDouble(POSITION_PROFIT)>class="num">0)
   { class=class="str">"cmt">// 如果有额外模块
     class="type">class="kw">double buffer_m7[];
     ArraySetAsSeries(buffer_m7,true);
     if(CopyBuffer(handle_m7,class="num">0,class="num">0,class="num">1,buffer_m7)<class="num">0) class="kw">return;
     class="type">class="kw">double TRB=buffer_m7[class="num">0];
     if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
      if(PositionGetDouble(POSITION_PRICE_CURRENT)-TRB*_Point>PositionGetDouble(POSITION_PRICE_OPEN))
       {
         class="type">class="kw">double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)-class="num">5*_Point;
         if(price_SL>PositionGetDouble(POSITION_SL))
           trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),PositionGetDouble(POSITION_TP));
       }
     if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
      if(PositionGetDouble(POSITION_PRICE_CURRENT)+TRB*_Point<PositionGetDouble(POSITION_PRICE_OPEN))
       {
         class="type">class="kw">double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)+class="num">5*_Point;
         if(price_SL<PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL)==NULL)
           trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),PositionGetDouble(POSITION_TP));

「开仓前的保证金闸门」

这段逻辑卡在信号触发之后、真正下单之前,核心就一件事:用 OrderCalcMargin 先算占用的保证金,再跟 Equity 比大小,只有 margin<Equity 才放行。外汇和贵金属杠杆高,净值回撤时硬开仓可能直接触发强平,这层判断能挡掉一部分自杀式下单。 无持仓时走新仓分支:Buy 信号下先 SymbolInfoTick 抓 ask,Sell 信号抓 bid,手数统一 NormalizeDouble(lot,2) 保留两位小数。若当前已持空单且 Buy 信号来了,先 PositionClose 反手,再按同样保证金校验开多,注释写的是「逆向」仓。 注意代码里 min_lot 兜底:lot<min_lot 时强制拉回最小手数,避免经纪商拒绝。实际跑 MT5 时把 Equity 换成 AccountInfoDouble(ACCOUNT_EQUITY) 更稳,原片段里的 Equity 应是全局变量,复制前确认已赋值。

MQL5 / C++
   if(lot<min_lot) lot=min_lot;
class=class="str">"cmt">//---
   if(on_trade)
      {
       sSignal signal=Buy_or_Sell();
       class=class="str">"cmt">//--- 所需以及可用保证金的值
       class="type">class="kw">double margin,free_margin=AccountInfoDouble(ACCOUNT_MARGIN_FREE);
       class=class="str">"cmt">//--- 买入
       if(signal.Buy)
         {
          if(!PositionSelect(_Symbol))
            {
             SymbolInfoTick(_Symbol,last_tick);
             if(OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,NormalizeDouble(lot,class="num">2),last_tick.ask,margin))
               if(margin<Equity)
                  trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,NormalizeDouble(lot,class="num">2),last_tick.ask,class="num">0,class="num">0,"买入: 新仓");
            }
          else
            {
             if(PositionGetDouble(POSITION_PROFIT)<class="num">0) class="kw">return;
             if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
               {
                trade.PositionClose(_Symbol);
                SymbolInfoTick(_Symbol,last_tick);
                if(OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,NormalizeDouble(lot,class="num">2),last_tick.ask,margin))
                  if(margin<Equity)
                     trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,NormalizeDouble(lot,class="num">2),last_tick.ask,class="num">0,class="num">0,"买入: 逆向");
               }
            }
         }
       class=class="str">"cmt">//--- 卖出
       if(signal.Sell)
         {
          if(!PositionSelect(_Symbol))
            {
             SymbolInfoTick(_Symbol,last_tick);
             if(OrderCalcMargin(ORDER_TYPE_SELL,_Symbol,NormalizeDouble(lot,class="num">2),last_tick.bid,margin))
               if(margin<Equity)
                  trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,NormalizeDouble(lot,class="num">2),last_tick.bid,class="num">0,class="num">0,"卖出: 新仓");
            }

盈利持仓的反向切换与挂损挂盈

这段逻辑只处理已持仓且当前不亏损的情况:先过滤掉浮亏单,仅当持仓盈利时才进入反向操作分支。若原持仓为买入,则平掉多单并立刻抓取最新 tick,用 OrderCalcMargin 估算卖单保证金,只有当估算保证金小于账户净值 Equity 时才以市价卖开,注释标为「卖出: 逆向」。 OnTrade 里的扩展模块依赖两个外部指标句柄:模块2通过 CopyBuffer 取缓冲首值作为 SL 点数,模块3取首值作为 TP 点数,任一拷贝失败直接 return。需注意 CopyBuffer(handle,0,0,1,buf) 只拉1根,若指标未就绪会返回 -1,实盘里可能漏改。 修改持仓时只针对 SL 为 0 的仓位:买入单的 TP 设为开仓价加 TP*_Point,SL 设为开仓价减 SL*_Point。外汇与贵金属杠杆高,这种自动改仓逻辑在滑点行情中可能把止损设到远离市价处,开 MT5 用策略测试器跑 EURUSD 15M 能直接看 PositionGetDouble(POSITION_SL) 是否被填。

MQL5 / C++
else
  {
   if(PositionGetDouble(POSITION_PROFIT)<class="num">0) class="kw">return;
   if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
     {
      trade.PositionClose(_Symbol);
      SymbolInfoTick(_Symbol,last_tick);
      if(OrderCalcMargin(ORDER_TYPE_SELL,_Symbol,NormalizeDouble(lot,class="num">2),last_tick.bid,margin))
        if(margin<Equity)
          trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,NormalizeDouble(lot,class="num">2),last_tick.bid,class="num">0,class="num">0,"卖出: 逆向");
     }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 交易函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTrade()
  {
   if(on_SL && on_TP) class=class="str">"cmt">// 如果有额外模块
     {
      class=class="str">"cmt">//--- 模块 class="num">2
      class="type">class="kw">double buffer_m2[];
      ArraySetAsSeries(buffer_m2,true);
      if(CopyBuffer(handle_m2,class="num">0,class="num">0,class="num">1,buffer_m2)<class="num">0) class="kw">return;
      class="type">class="kw">double SL=buffer_m2[class="num">0];
      class=class="str">"cmt">//--- 模块 class="num">3
      class="type">class="kw">double buffer_m3[];
      ArraySetAsSeries(buffer_m3,true);
      if(CopyBuffer(handle_m3,class="num">0,class="num">0,class="num">1,buffer_m3)<class="num">0) class="kw">return;
      class="type">class="kw">double TP=buffer_m3[class="num">0];
      class=class="str">"cmt">//--- 持仓修改
      if(PositionSelect(_Symbol))
        if(PositionGetDouble(POSITION_SL)==class="num">0)
          {
           class=class="str">"cmt">//--- 买入
           if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY)
             {
              class="type">class="kw">double priceTP=PositionGetDouble(POSITION_PRICE_OPEN)+TP*_Point;
              class="type">class="kw">double priceSL=PositionGetDouble(POSITION_PRICE_OPEN)-SL*_Point;

◍ 按持仓方向回算止损止盈并改单

在遍历持仓的循环里,先判断当前持仓是买入还是卖出,再据开仓价反向推算 TP 与 SL。买入仓位以开仓价加 TP*_Point 作为止盈、减 SL*_Point 作为止损;卖出仓位则反过来,开仓价减 TP*_Point 为止盈、加 SL*_Point 为止损。 推算出的价格必须过 NormalizeDouble(...,Digits()) 对齐品种报价精度,否则 Modify 会因小数位不符被拒。最后调用 trade.PositionModify(_Symbol, SL, TP) 写回同标的的止损止盈。 这段代码只处理已存在持仓的移损移盈,不涉及开仓。外汇与贵金属杠杆高,改单触发后价格可能跳空,实际成交价与设价有偏差属正常概率。

MQL5 / C++
      trade.PositionModify(_Symbol,NormalizeDouble(priceSL,Digits()),NormalizeDouble(priceTP,Digits()));
      }
      class=class="str">"cmt">//--- 卖出
      if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
        {
         class="type">class="kw">double priceTP=PositionGetDouble(POSITION_PRICE_OPEN)-TP*_Point;
         class="type">class="kw">double priceSL=PositionGetDouble(POSITION_PRICE_OPEN)+SL*_Point;
         trade.PositionModify(_Symbol,NormalizeDouble(priceSL,Digits()),NormalizeDouble(priceTP,Digits()));
        }
     }
  }
}

「把 EA 拆成可替换的功能块」

做多模块智能系统,起点不是写代码,而是把 EA 里能独立出来的那段逻辑先想清楚:哪些函数换了算法就能明显改变交易行为,就把它们圈成封闭片段,后续再编译成模块。 大多数 MT5 智能交易系统跑的都是同一套骨架:资金(风险)管理、持仓跟踪(止损止盈)、尾随停止、信号生成、信号过滤。这五个环节每个都能用不同算法实现,模块化的价值正在于此——同一套信号逻辑,换一个风控模块就可能变成另一类系统。 本文给的是最朴素的实现,重点不在把某个函数写得多花哨,而是让你习惯「功能块可插拔」这种写法。打开 MT5 的 MetaEditor,新建一个 .mqh 把持仓跟踪先抽出来,比直接堆在主 EA 里更容易后期改算法。外汇与贵金属杠杆高,模块随便换参数也可能放大回撤,改完务必用策略测试器跑一遍历史。

把重复劳动交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到模块调用状态和信号分离情况,你专注决策就好。

常见问题

它要求模块隐藏内部算法细节,只暴露接口,日后可用同接口的另一模块直接替换,不影响上层调用。
小布盯盘可识别主模块与指标型子模块的输出,在品种页呈现各模块信号,但EA本身仍需你在终端部署。
弱链接让模块只依赖传入参数、不依赖调用历史,单独改一个模块不会引发连锁bug,符合IPO原则。
管理交易的主模块用EA,计算类功能常用指标实现,数据放缓冲区供EA按需取用,避免硬编码耦合。
前者强调模块是独立执行特定功能的单元,后者把模块视作组成程序的砖块,角度不同但都指向解耦。