使用单独模块构建智能交易系统·进阶篇
🧩

使用单独模块构建智能交易系统·进阶篇

(2/3)· 当尾随止损、新闻过滤、时间表散落代码各处,改一个模块就能让整只EA崩盘

实战向 第 2/3 篇
把尾随停止、新闻过滤、时间表直接写进 OnTick 不同位置,是多数 EA 难维护的根因。模块间随意互调,删一个组件就要翻遍全文改绑定。这种结构在策略变复杂后,调试一次能耗掉你半个周末。

◍ 把持仓遍历从 OnTick 里拆出去

EA 在 OnTick() 里直接遍历全部持仓,是很多 MQL5 新手写的第一个性能坑。尤其当你用网格或均摊,循环会膨胀到几十行,还逼着 EA 去维护一套虚拟价位。更现实的问题是:暂停重连后,EA 得重新识别已下订单、统计总持仓和最大回撤,这些活不该和信号判断揉在一起。 做法是将持仓扫描独立成一个模块。最简形态只需两个字段——运行魔幻数字 m_lmagic 和品种名 m_symbol,在 OnInit() 里赋值,CreateSnap() 负责把符合过滤条件的持仓捞出来。下面这段是原文给出的 CSnap 骨架与扫描逻辑: [CODE] class CSnap { public: void CSnap() { m_lmagic = -1; m_symbol = Symbol(); } virtual void ~CSnap() {} bool CreateSnap(); long m_lmagic; string m_symbol; }; bool CSnap::CreateSnap() { int total = PositionsTotal(); ulong ticket; ENUM_POSITION_TYPE type; double l, p; for (int i = total - 1; i >= 0; i--) { if ( (ticket = PositionGetTicket(i)) != 0) {

if (StringLen(m_symbol) == 0PositionGetString(POSITION_SYMBOL) == m_symbol) {
if (m_lmagic < 0PositionGetInteger(POSITION_MAGIC) == m_lmagic) {

type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); l = PositionGetDouble(POSITION_VOLUME); p = PositionGetDouble(POSITION_PRICE_OPEN); switch(type) { case POSITION_TYPE_BUY: // ??????????????????????????????????????? break; case POSITION_TYPE_SELL: // ??????????????????????????????????????? break; }//switch(type)

}//if (lmagic < 0PositionGetInteger(POSITION_MAGIC) == lmagic)
}//if (StringLen(symbol) == 0PositionGetString(POSITION_SYMBOL) == symbol)

}//if ( (ticket = PositionGetTicket(i)) != 0) }//for (int i = total - 1; i >= 0; i--) return true; } CStrategy* m_strategy; [/CODE] 逐行拆解:第 6–9 行构造函数默认魔幻数字 -1(不过滤)、品种取当前图 Symbol();CreateSnap() 第 18 行用 PositionsTotal() 拿持仓数,第 21 行倒序遍历避免删除元素错位;第 23–24 行先按品种、再按魔幻数字过滤;第 26–28 行取类型、手数、开仓价;switch 里两个 case 留了问号注释,原文没填——你若要计算加权均价,就在这里把 l、p 往 m_strategy 指向的对象里塞。 如果只要 EA 知道“有单没单”,CSnap 自己存字段就够了;要分别处理每笔参数,就得在类里挂一个 CStrategy* 指针,把数据转给后续对象做决策。基类 CModule 提供 SetStrategy(...) 后,任何主动计算模块都能接同一条策略线,不需要的模块不调用即可,代价仅是少量冗余代码。外汇与贵金属杠杆高,这类模块拆分纯属工程纪律,不改善任何胜率,但能让你在 MT5 策略测试器里少踩重连丢状态的坑。

MQL5 / C++
class CSnap {
class="kw">public:
    class="type">void CSnap() {
        m_lmagic = -class="num">1;
        m_symbol = Symbol();
    }
    class="kw">virtual class="type">void ~CSnap() {}
    class="type">bool CreateSnap();
    class="type">long m_lmagic;
    class="type">class="kw">string m_symbol;
};
class="type">bool CSnap::CreateSnap() {
    class="type">int total = PositionsTotal();
    class="type">class="kw">ulong ticket;
    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) {
            if (StringLen(m_symbol) == class="num">0 || PositionGetString(POSITION_SYMBOL) == m_symbol) {
                if (m_lmagic < class="num">0 || PositionGetInteger(POSITION_MAGIC) == m_lmagic) {
                    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_BUY:
class=class="str">"cmt">// ???????????????????????????????????????
                            break;
                        case POSITION_TYPE_SELL:
class=class="str">"cmt">// ???????????????????????????????????????
                            break;
                    }class=class="str">"cmt">//class="kw">switch(type)
                }class=class="str">"cmt">//if (lmagic < class="num">0 || PositionGetInteger(POSITION_MAGIC) == lmagic)
            }class=class="str">"cmt">//if (StringLen(symbol) == class="num">0 || 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--)
    class="kw">return true;
}
CStrategy* m_strategy;

把策略指针塞进模块前的那道安检

在 MT5 的 EA 架构里,模块(CModule)通常通过持有 CStrategy* 指针来调用具体交易逻辑。但指针一旦无效,后续调用 OnBuyFind / OnSellFind 会直接让 EA 在实时外汇或贵金属行情中断线崩掉。 SetStrategy 的第一行就用 CheckPointer(strategy) == POINTER_INVALID 做拦截,返回 false 拒绝挂载。这一步不是优雅写法,是实盘必须的防线——外汇和贵金属杠杆高,无效指针引发的异常可能在一根 M1 棒内吞掉止损逻辑。 挂载成功后,模块内部用 switch(type) 分流:POSITION_TYPE_BUY 走 OnBuyFind,POSITION_TYPE_SELL 走 OnSellFind。回测中若策略类未正确实例化,SetStrategy 返回 false 的概率在初始化阶段接近 100%,日志里看不到任何 OnXxxFind 触发就是信号。 开 MT5 自建一个 CModule 派生类,把下面代码原样贴进头文件,编译后故意传 NULL 进 SetStrategy,验证返回值是否为 false。

MQL5 / C++
class="type">bool SetStrategy(CStrategy* strategy) {
   if(CheckPointer(strategy) == POINTER_INVALID) class="kw">return false;
   m_strategy = strategy;
   class="kw">return true;
}
class=class="str">"cmt">//class="type">bool SetStrategy(CStrategy* strategy)
   class="kw">switch(type) {
      case POSITION_TYPE_BUY:
         m_strategy.OnBuyFind(ticket, p, l);
         break;
      case POSITION_TYPE_SELL:
         m_strategy.OnSellFind(ticket, p, l);
         break;
   }class=class="str">"cmt">//class="kw">switch(type
class CModule {
   class="kw">public:
      CModule() {m_strategy = NULL;}
      ~CModule() {}
      class="kw">virtual class="type">bool SetStrategy(CStrategy* strategy) {
         if(CheckPointer(strategy) == POINTER_INVALID) class="kw">return false;
         m_strategy = strategy;
         class="kw">return true;
      }class=class="str">"cmt">//class="type">bool SetStrategy(CStrategy* strategy)
   class="kw">protected:
      CStrategy* m_strategy;
};
class="kw">public:
   const class="type">class="kw">string GetName() const {class="kw">return m_sModName;}
class="kw">protected:
      class="type">class="kw">string m_sModName;

「把决策逻辑留给派生类」

EA 里的入场、离场、改仓和部分平仓决策,不能写成可复用的固定模块。一旦把决策对象模块化,所有 EA 都会长成同一个样子,失去针对具体行情独立判断的意义。 基类 CStrategy 只定义接口不实现逻辑,两个方法 OnBuyFind 和 OnSellFind 被声明为纯虚函数(=0),编译期强制派生类覆盖。这样每个 EA 继承后写自己的买卖触发条件,框架本身保持稳定。 在 CModule.mqh 里用 #include 把 CStrategy.mqh 挂进来,EA 骨架就齐了。接下来要做的只是往派生类填策略,而不是动框架。外汇与贵金属杠杆高,回测通过的派生类在实盘仍可能因滑点失效,上 MT5 跑前先用历史数据验证覆盖逻辑。

MQL5 / C++
class CStrategy  {
class="kw">public:
   class="kw">virtual    class="type">void      OnBuyFind(class="type">class="kw">ulong ticket, class="type">class="kw">double price, class="type">class="kw">double lot) = class="num">0;
   class="kw">virtual    class="type">void      OnSellFind(class="type">class="kw">ulong ticket, class="type">class="kw">double price, class="type">class="kw">double lot) = class="num">0;      
};class=class="str">"cmt">// class CStrategy
class="macro">#include "CStrategy.mqh"

◍ 给策略类补齐决策与初始化接口

把 CSnap 和具体 EA 逻辑解耦的关键,是让 CSnap 只认 CStrategy 的虚方法,而不关心背后是谁在决策。于是 CStrategy 必须暴露一组强制派生的接口:Name() 返回策略名,CreateSnap() 触发快照,MayAndEnter() 与 MayAndContinue() 分别处理首仓和加仓信号,MayAndClose() 负责清仓。 这些虚函数全部纯虚(=0),意味着任何实盘策略都得自己实现,编译器不会放过未覆盖的子类。对于外汇与贵金属自动化,这种约束能避免挂错品种时静默失效,但杠杆交易仍属高风险,信号误判可能快速放大亏损。 光有决策方法还不够,CStrategy 还要持有各模块指针,却不该直接暴露公有字段在 OnInit() 里赋值。这里引入 Initialize(CInitializeStruct* pInit) 做统一注入,入参是个空壳类 CInitializeStruct,专门留给后续继承扩展。这样 OnTick() 只需调 CStrategy 成员,完全不用感知底层模块。 下面这段是接口声明与初始化基类的原样代码,可直接贴进 CStrategy.mqh 对照编译:

MQL5 / C++
  class="kw">virtual  class="type">class="kw">string  Name() const     = class="num">0;
  class="kw">virtual  class="type">void    CreateSnap()      = class="num">0;  
  class="kw">virtual  class="type">bool    MayAndEnter()     = class="num">0;  
  class="kw">virtual  class="type">bool    MayAndContinue() = class="num">0;        
  class="kw">virtual  class="type">void    MayAndClose()     = class="num">0;
class="kw">virtual  class="type">bool    Initialize(CInitializeStruct* pInit) = class="num">0;
class CInitializeStruct {};

前一根看跌就空一手的EA骨架

把策略落进代码,逻辑可以压到极简:只要前一根烛条收阴,就用固定点数的止盈止损开一单空头;只要账上还有这张空单没平,就不允许再开新仓。外汇与贵金属杠杆高,这类单信号策略在震荡市可能连续触发假突破,实盘前务必在 MT5 策略测试器跑历史数据。 CRealStrat1 从 CStrategy 派生,核心只重写了少数几个虚函数。CreateSnap() 先把记录空单票据的 m_tb 清零,再调 CSnap 模块的快照;若快照发现本 EA 开的空单,就会回调 OnSellFind(),把票据写回 m_tb。MayAndEnter() 读到 m_tb 非零即放弃下单,从而天然实现「一笔未平不开二笔」。 Initialize() 是另一个关键点:基类不知道子类要挂哪些模块,于是传一个空的 CInitializeStruct,子类再强转成自带的 CInit1 取指针。OnInit 里把各模块对象建好传进去,之后 OnTick 只需调一个统一接口,复杂的对象网就被收口成一行调用。 下面这段就是派生类的主体声明,逐行看能直接照抄到自己的 EA 里改条件。

MQL5 / C++
class CRealStrat1 : class="kw">public CStrategy  {
class="kw">public:
  class="kw">static  class="type">class="kw">string  m_name;
                      CRealStrat1(){};
                      ~CRealStrat1(){};
  class="kw">virtual  class="type">class="kw">string  Name() const {class="kw">return m_name;}
  class="kw">virtual  class="type">bool    Initialize(CInitializeStruct* pInit) {
                      m_pparam = ((CInit1* )pInit).m_pparam;
                      m_psnap = ((CInit1* )pInit).m_psnap;
                      m_ptrade = ((CInit1* )pInit).m_ptrade;
                      m_psnap.SetStrategy(GetPointer(this));
                      class="kw">return true;
          }class=class="str">"cmt">//Initialize(EA_InitializeStruct* pInit)
  class="kw">virtual  class="type">void    CreateSnap() {
                      m_tb = class="num">0;
                      m_psnap.CreateSnap();
          }  
  class="kw">virtual  class="type">bool    MayAndEnter();
  class="kw">virtual  class="type">bool    MayAndContinue() {class="kw">return false;}        
  class="kw">virtual  class="type">void    MayAndClose()     {}
  class="kw">virtual  class="type">bool    Stop()            {class="kw">return false;}  
  class="kw">virtual  class="type">void    OnBuyFind(class="type">class="kw">ulong ticket, class="type">class="kw">double price, class="type">class="kw">double lot) {}
  class="kw">virtual  class="type">void    OnBuySFind(class="type">class="kw">ulong ticket, class="type">class="kw">double price, class="type">class="kw">double lot) {}  
  class="kw">virtual  class="type">void    OnBuyLFind(class="type">class="kw">ulong ticket, class="type">class="kw">double price, class="type">class="kw">double lot) {}
  class="kw">virtual  class="type">void    OnSellFind(class="type">class="kw">ulong ticket, class="type">class="kw">double price, class="type">class="kw">double lot) {tb = ticket;}  
  class="kw">virtual  class="type">void    OnSellSFind(class="type">class="kw">ulong ticket, class="type">class="kw">double price, class="type">class="kw">double lot) {}  
  class="kw">virtual  class="type">void    OnSellLFind(class="type">class="kw">ulong ticket, class="type">class="kw">double price, class="type">class="kw">double lot) {}        
class="kw">private:
把模块接线图交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 模块绑定健康度提示,你专注策略逻辑而非排错。

常见问题

功能分散在 OnTick 多处会导致绑定关系隐式存在,改动一处可能破坏其他模块调用,模块化后只在统一接口交换信息,断连更轻松。
本文结构限制模块仅与 OnTick 通信、在 OnInit 创建,比经典观察者更收敛,降低内部耦合但牺牲部分灵活广播能力。
可以,小布内置的 AIGC 看板能呈现品种页上 EA 模块接线状态,帮助快速定位未正常初始化的组件。
集中创建让资源分配和销毁路径清晰,OnDeinit 配对删除即可,避免运行期动态生成带来的竞态和泄漏概率。
先确认新模块是否只通过 OnTick 与核心交互,禁止跨模块直连,逐步接入并用单独用例验证,能显著降低故障概率。