使用单独模块构建智能交易系统·进阶篇
(2/3)· 当尾随止损、新闻过滤、时间表散落代码各处,改一个模块就能让整只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) == 0 | PositionGetString(POSITION_SYMBOL) == m_symbol) { |
|---|---|
| if (m_lmagic < 0 | PositionGetInteger(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 < 0 | PositionGetInteger(POSITION_MAGIC) == lmagic) |
|---|---|
| }//if (StringLen(symbol) == 0 | PositionGetString(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 策略测试器里少踩重连丢状态的坑。
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。
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 跑前先用历史数据验证覆盖逻辑。
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 对照编译:
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 里改条件。
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: