使用单独模块构建智能交易系统·综合运用
🧩

使用单独模块构建智能交易系统·综合运用

(3/3)·从混乱绑定的实战坑到清晰模块交互,一篇看清独立组件如何拼出易维护智能交易系统

案例拆解 第 3/3 篇
把尾随停止、新闻过滤写成散落各处的代码,改一个逻辑就要翻遍整个EA。用独立模块在OnInit创建、OnTick交互,断连调试能省掉大半重复劳动。

「用上一根K线收阴直接空的逻辑骨架」

这段 MQL5 片段给出了一个极简策略类的成员结构和触发入口:只要上一根当前周期 K 线收盘低于开盘,就走 Sell(),不依赖任何指标过滤。 进场函数里先判断 m_tb 是否为 0,非零说明已有挂单或持仓标记,直接 return false 放弃本次信号,避免重复触发。接着取 PERIOD_CURRENT 下第 1 根(已收盘)K 线的开盘价 o 与收盘价 c,c < o 即阴线才允许卖出。 初始化子类 CInit1 用 CheckPointer 校验 pparam 与 psnap 指针有效性,任一为 POINTER_INVALID 就返回 false,通过后把三个模块指针挂到实例上。外汇与贵金属杠杆高,这种裸 K 反手在震荡市可能频繁止损,上 MT5 用策略测试器跑 EURUSD 15M 能看到信号密度。

MQL5 / C++
 CParam*              m_pparam;
   CSnap*              m_psnap;  
   CTradeMod*          m_ptrade;  
   class="type">class="kw">ulong               m_tb;              
};
class="kw">static class="type">class="kw">string CRealStrat1::m_name = "Real Strategy class="num">1";
class="type">bool CRealStrat1::MayAndEnter() {
   if (tb != class="num">0) class="kw">return false;  
   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) {
      m_ptrade.Sell();
      class="kw">return true;
   }  
   class="kw">return false;
}
class CInit1: class="kw">public CInitializeStruct {
class="kw">public:
   class="type">bool Initialize(CParam* pparam, CSnap* psnap, CTradeMod* ptrade) {
   if (CheckPointer(pparam) == POINTER_INVALID ||
       CheckPointer(psnap)  == POINTER_INVALID) class="kw">return false;  
      m_pparam = pparam;
      m_psnap  = psnap;
      m_ptrade = ptrade;
      class="kw">return true;
   }
   CParam* m_pparam;
   CSnap*  m_psnap;
   CTradeMod* m_ptrade;
};

◍ 初始化与报价响应的骨架怎么搭

EA 的生命周期里,OnInit() 只做一件事:new 一个 CRealStrat1 实例,并用 CInit1 对象把参数、快照、交易接口三个全局模块挂上去。挂失败就返回 INIT_FAILED,MT5 会在日志里直接报初始化错误,EA 不会跑。 OnDeinit() 里用 CheckPointer 判一下指针有效性再 delete,避免重复释放或野指针。这个写法在切换周期、重载 EA 时不会抛 'object already deleted' 异常。 OnTick() 被压到极简:IsNewCandle() 为真才刷新快照并跑策略判断。实测在 M1 上每根 K 线只进一次逻辑,CPU 占用比每 tick 都算低一个数量级,回测时也少很多无意义计算。 这种结构留了个口子——你想让 EA 等用户点按钮再交易,直接在 OnTick 里加个全局开关判断就行,不用动策略类内部。外汇和贵金属杠杆高,这类开关能帮你避开开盘瞬间的滑点乱单,但仍是概率性防护,不保证不亏损。

MQL5 / C++
CParam     par;
CSnap     snap;
CTradeMod  trade;
CStrategy* pS1;
class="type">int OnInit()
  {
   ...
   pS1 = new CRealStrat1();
   CInit1 ci;
   if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) class="kw">return (INIT_FAILED);
   pS1.Initialize(GetPointer(ci));
   class="kw">return (INIT_SUCCEEDED);
   }

class="type">void OnDeinit(const class="type">int reason)
  {
    if (CheckPointer(pS1) != POINTER_INVALID) class="kw">delete pS1;
   }
class="type">void OnTick()
  {
    if (IsNewCandle() ){
      pS1.CreateSnap();
      pS1.MayAndEnter();
      }
   }

在一个EA里热插拔多套策略

把日程表或策略写死在原有类里,后期调试和维护会迅速失控。更干净的做法是把模块做成可替换对象:为同一逻辑准备“测试版”和“发布版”,再用一个管理器控制加载哪一个。 沿这套思路能走得更远——直接动态替换 EA 的内核策略。比如派生自 CStrategy 的 CRealStrat1 负责趋势,再挂一个横盘策略类,甚至能在亚洲时段切出第三套逻辑。一个 EA 内跑多个智能系统并按条件切换,在 MT5 上并非难事。 具体落地四步:写决策类继承 CStrategy(如 CRealStrat1);写初始化类继承 CInitializeStruct(如 CInit1);把编译结果连进项目;输入参数加一个变量决定启动加载哪套策略。演示里第二套策略逻辑极简——前一根看涨就开固定止盈止损多单,且旧仓未平不开新仓。 切换本身不复杂:建新策略对象、删旧对象、把指针赋给变量,此后 OnTick() 自然走新逻辑。下面这段代码就是输入枚举与运行时切换器的骨架,外汇与贵金属波动剧烈、杠杆风险高,切换策略前务必在策略测试器跑历史校验。 代码里 enum CSTRAT 定义两个策略ID,input 变量 strt 决定初始加载;OnInit 调用 SwitchStrategy 失败则 INIT_FAILED。OnChartEvent 捕获面板点击事件后调 SwitchStrategy((CSTRAT)EDlg.GetStratID())。SwitchStrategy 函数先比对 str_curr,相同直接返回;否则按枚举 new 对应策略并 Initialize,删掉旧指针 pS1 再接管。这样热插拔就闭环了。

MQL5 / C++
enum CSTRAT {
  strategy_1 = class="num">1,
  strategy_2 = class="num">2
};
class="kw">input CSTRAT strt  = strategy_1;
CSTRAT str_curr = -class="num">1;
class="type">int OnInit()
  {  
  ...
  if (!SwitchStrategy(strt) ) class="kw">return (INIT_FAILED);
  ...       
  class="kw">return (INIT_SUCCEEDED);
  }

class="type">void OnChartEvent(const class="type">int id,
                  const class="type">long &lparam,
                  const class="type">class="kw">double &dparam,
                  const class="type">class="kw">string &sparam)
  {
    if (id == CHARTEVENT_OBJECT_CLICK && StringCompare(...) == class="num">0 ) {
      SwitchStrategy((CSTRAT)EDlg.GetStratID() );
    } 
  } 
class="type">bool SwitchStrategy(CSTRAT sr) {
   if (str_curr == sr) class="kw">return true;
   CStrategy* s = NULL;
   class="kw">switch(sr) {
     case strategy_1:
       {
         CInit1 ci;
         if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) class="kw">return false;
         s = new CRealStrat1();
         s.Initialize(GetPointer(ci));  
       }
       break;
     case strategy_2:
       {
         CInit2 ci;
         if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) class="kw">return false;
         s = new CRealStrat2();
         s.Initialize(GetPointer(ci));                
       } 
       break;
   }
   if (CheckPointer(pS1) != POINTER_INVALID) class="kw">delete pS1;
   pS1 = s;   
   str_curr = sr;
   class="kw">return true;
}
CStrategy* pS1;

「范式落地的参考源」

智能交易系统的搭建已经用到了观察者(Observer)与外观(Facade)这类标准设计范式,结构上的解耦和接口收敛基本跑通。 想要把这套范式吃透,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著的《设计范式:可复用面向对象软件的基础》是最直接的底层参考,里面不只讲这两个,还覆盖了更多可套进 MT5 EA 的范式。 有空建议翻一遍原书,对照自己 EA 的模块划分,看哪些类关系能用范式重构成更低耦合的形态。

◍ 把工具请下神坛

这套 EA 与模块压缩包(Ea&Modules.zip,7.74 KB)只是把 MVC 思路落进 MQL5 的载体,不是什么圣杯。有读者在 2019 年 11 月反馈,模块化写法让刚接触 MQL 的新手第一次看清 EA 该有的结构;另有人用 MQL_Easy 做跨平台通用代码,作者回了一句:丢进 MT4 编译试试,错误不会多,代码本身够简单。 真要验证,直接开 MT5 加载压缩包里的模块,看 OnTick 里调度逻辑是否如你所想;外汇和贵金属杠杆高、滑点狠,跑通和赚到是两回事,概率上更该把它当脚手架而不是印钞机。 工具归工具,写自己的 EA 才是正路,下载完就关掉网页去改参数吧。

把模块关系交给小布盯盘梳理
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到EA组件调用热区,你专注策略而非绑定的故障排查。

常见问题

复杂绑定让添加或剔除模块都牵一发而动全身,初始调试和故障排除成本陡增,集中到OnTick交互更直观。
统一初始化让生命周期清晰,配合OnDeinit删除,模块连接断连和修改的代价显著降低。
不必,本文结构只是借其思路让EA更直观,核心限制模块间及与响应程序按序通信即可。
可以,小布盯盘的品种页能呈现组件调用情况,帮你快速定位哪块模块在OnTick占用异常。
关于独立模块创建完整讨论见《使用单独模块构建智能交易系统·基础篇》与《使用单独模块构建智能交易系统·实战篇》。