通用智能交易系统:事件模型和交易策略原型(第二章)·综合运用
📘

通用智能交易系统:事件模型和交易策略原型(第二章)·综合运用

第 3/3 篇

◍ 把MT5持仓封装成可操控的对象

在 MT5 里用 EA 管持仓,第一步是把平台返回的零散字段收进一个类里。下面这段 CPositionMT5 的构造与取数接口,就是典型的封装手法:建对象时一次性把 ID、魔术码、方向、开仓价、品种、时间和注释全抓出来。 构造函数里 m_id 取自 POSITION_IDENTIFIER,m_magic 强转自 POSITION_MAGIC,m_direction 用 ENUM_POSITION_TYPE 接 POSITION_TYPE。开仓价走 PositionGetDouble(POSITION_PRICE_OPEN),品种和时间分别走 PositionGetString 与 PositionGetInteger,最后用 m_trade.SetExpertMagicNumber 把魔术码绑回交易对象。 几个只读访问器很直白:Direction() 吐方向枚举,ExpertMagic() 回 EA 唯一标识,ID() 回持仓唯一号。实盘里若同时挂了多个 EA,凭 ExpertMagic 过滤自己那部分仓位,能避开误平别人的单——外汇和贵金属杠杆高,错平一次可能直接吞掉周盈利。 开 MT5 把这段塞进你的 include,用 PositionSelect 选中一单后 new 一个 CPositionMT5,打印 ID() 和 Direction(),十秒就能验证封装是否跑通。

MQL5 / C++
class="type">class="kw">string EntryComment(class="type">void);
class="type">class="kw">double Profit(class="type">void);
class="type">class="kw">double Volume(class="type">void);
class="type">class="kw">string Symbol(class="type">void);
class="type">class="kw">datetime TimeOpen(class="type">void);
class="type">bool CloseAtMarket(class="type">class="kw">string comment="");
class="type">class="kw">double StopLossValue(class="type">void);
class="type">bool StopLossValue(class="type">class="kw">double sl);
class="type">class="kw">double TakeProfitValue(class="type">void);
class="type">bool TakeProfitValue(class="type">class="kw">double tp);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 初始化一个持仓头寸的基本属性                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CPositionMT5::CPositionMT5(class="type">void) : m_id(class="num">0),
                     m_entry_price(class="num">0.0),
                     m_symbol(""),
                     m_time_open(class="num">0)
  {
  m_id=PositionGetInteger(POSITION_IDENTIFIER);
  m_magic=(class="type">uint)PositionGetInteger(POSITION_MAGIC);
  m_direction=(class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
  m_entry_price=PositionGetDouble(POSITION_PRICE_OPEN);
  m_symbol=PositionGetString(POSITION_SYMBOL);
  m_time_open=(class="type">class="kw">datetime)PositionGetInteger(POSITION_TIME);
  m_entry_comment=PositionGetString(POSITION_COMMENT);
  m_trade.SetExpertMagicNumber(m_magic);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回持仓方向                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ENUM_POSITION_TYPE CPositionMT5::Direction(class="type">void)
  {
   class="kw">return m_direction;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回当前持仓所属          
class=class="str">"cmt">//| EA的唯一标识                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">uint CPositionMT5::ExpertMagic(class="type">void)
  {
   class="kw">return m_magic;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回持仓头寸唯一标识符                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">ulong CPositionMT5::ID(class="type">void)
  {
   class="kw">return m_id;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回开仓价格                                                      |

持仓对象的属性读取与止损止盈改写

在 MT5 的持仓封装类里,EntryPrice、Symbol、TimeOpen 这类方法只是把内部成员变量原样吐出来,调用成本极低,适合在 EA 主循环里每 tick 读取而不必反复查终端。 真正会碰终端的是带 PositionSelect 的那几个:StopLossValue 和 TakeProfitValue 的 getter 先按符号选中持仓,未选中就返回 0.0,选中后走 PositionGetDouble 取 POSITION_SL / POSITION_TP。这意味着若持仓已平,读到的不是上一次的值而是 0.0,做面板显示时要自己缓存。 设置端的两个重载值得注意:StopLossValue(double sl) 内部调了 m_trade.Buy(0.0, m_symbol, 0.0, sl, TakeProfitValue(), NULL),成交量传 0.0 实际是修改挂单式改 SL,同时把当前 TP 原样传回。TakeProfitValue(double tp) 同理,改 TP 时会把当前 SL 读出来一并提交,避免被清零。 外汇与贵金属杠杆高,用这段代码改止损止盈前,先在策略测试器用 2023 年 XAUUSD 的 M1 跑一遍,确认 PositionSelect 失败分支不会在跳空时误改单。

MQL5 / C++
class="type">class="kw">double CPositionMT5::EntryPrice(class="type">void)
  {
   class="kw">return m_entry_price;
  }
class="type">class="kw">string CPositionMT5::EntryComment(class="type">void)
  {
   class="kw">return m_entry_comment;
  }
class="type">class="kw">string CPositionMT5::Symbol(class="type">void)
  {
   class="kw">return m_symbol;
  }
class="type">class="kw">datetime CPositionMT5::TimeOpen(class="type">void)
  {
   class="kw">return m_time_open;
  }
class="type">class="kw">double CPositionMT5::StopLossValue(class="type">void)
  {
   if(!PositionSelect(m_symbol))
      class="kw">return class="num">0.0;
   class="kw">return PositionGetDouble(POSITION_SL);
  }
class="type">bool CPositionMT5::StopLossValue(class="type">class="kw">double sl)
  {
   if(!PositionSelect(m_symbol))
      class="kw">return class="kw">false;
   class="kw">return m_trade.Buy(class="num">0.0, m_symbol, class="num">0.0, sl, TakeProfitValue(), NULL);
  }
class="type">class="kw">double CPositionMT5::TakeProfitValue(class="type">void)
  {
   if(!PositionSelect(m_symbol))
      class="kw">return class="num">0.0;
   class="kw">return PositionGetDouble(POSITION_TP);
  }
class="type">bool CPositionMT5::TakeProfitValue(class="type">class="kw">double tp)
  {
   if(!PositionSelect(m_symbol))
      class="kw">return class="kw">false;
   class="kw">return m_trade.Buy(class="num">0.0, m_symbol, class="num">0.0, StopLossValue(), tp, NULL);
  }
class="type">bool CPositionMT5::CloseAtMarket(class="type">class="kw">string comment="")

「头寸关闭与数据读取的封装写法」

把持仓操作收进一个类里,能少写很多重复的判断。下面这段 MT5 代码封装了按品种平掉当前仓位、取仓位体积、取存款币种盈利三个动作,核心都依赖 PositionSelect 先锁定品种上下文。 PositionSelect 返回 false 时直接 return,意味着品种没持仓或选择失败,后续读取全无意义。平仓位用 m_trade.PositionClose,读数值用 PositionGetDouble 配 POSITION_VOLUME / POSITION_PROFIT 宏,外层调用者拿到的就是纯 double,不用每次都写选择逻辑。 实盘里若 m_symbol 传的是 "XAUUSD",无持仓时 Volume() 返回 0.0(false 被转 double),调用方要自己区分“零仓”和“选择失败”,否则容易把无单当空仓刷单。外汇与贵金属杠杆高,自动平仓逻辑上线前务必在策略测试器用历史数据跑一遍。

MQL5 / C++
  {
   if(!PositionSelect(m_symbol))
      class="kw">return class="kw">false;
   class="kw">return m_trade.PositionClose(m_symbol);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回当前头寸大小                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CPositionMT5::Volume(class="type">void)
  {
   if(!PositionSelect(m_symbol))
      class="kw">return class="kw">false;
   class="kw">return PositionGetDouble(POSITION_VOLUME);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回以存款货币计算的当前持仓头寸的获利                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CPositionMT5::Profit(class="type">void)
  {
   if(!PositionSelect(m_symbol))
      class="kw">return class="kw">false;
   class="kw">return PositionGetDouble(POSITION_PROFIT);
  }

◍ 把事件流收进CStrategy父类

CStrategy 是后续所有策略的父类,它本身不负责写具体买卖逻辑,而是把 Tick、Timer、BookEvent 这些系统事件统一接住再往下传。注意一个坑:除了 OnTradeTransaction 是虚函数,OnTick / OnTimer / OnBookEvent 在父类里都不是虚的,意味着你没法在子策略里直接重写它们,只能靠父类转调 NewTickDetect 和 NewBarsDetect 来捕获多品种的新报价与新柱。 事件主链路很固定:探测器发现新事件 → 填好 MarketEvent 结构体(带事件ID和属性) → CallSupport 触发虚拟的 SupportBuy/SupportSell → CallInit 触发虚拟的 InitBuy/InitSell。也就是说,你自己的策略只要重写那几个虚拟方法,就能吃到标准化后的事件,不用管底层 MT5 通知怎么来的。 CallInit 会从 m_state 读交易状态,状态禁止开仓时直接跳过初始化;CallSupport 则遍历未平仓头寸,用 CPosition 的 magic 编号和当前 EA 的 magic 比对,匹配上了才对多单调 SupportBuy、空单调 SupportSell。外汇和贵金属杠杆高,magic 错配会导致策略去管别人的单子,回测和实盘都容易爆雷。 下面这段是父类事件声明的骨架,开 MT5 建个 CStrategy 继承类时照着挂方法就行:OnTick 里只调了两个探测器,OnTimer 还手动塞了 MARKET_EVENT_TIMER 事件再走支持与初始化流程。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 策略的基类                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CStrategy : class="kw">public CObject
  {
  class=class="str">"cmt">//...
  class="type">void                OnTick(class="type">void);
  class="type">void                OnTimer(class="type">void);
  class="type">void                OnBookEvent(class="type">class="kw">string symbol);
  class="kw">virtual class="type">void        OnTradeTransaction(const MqlTradeTransaction &trans,
                                         const class="type">MqlTradeRequest &request,
                                         const class="type">MqlTradeResult &result);
  class=class="str">"cmt">//...
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 对系统事件“OnBookEvent”                                            |
class=class="str">"cmt">//| 调用策略的处理函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::OnTick(class="type">void)
  {
  NewTickDetect();
  NewBarsDetect();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 对系统事件“OnBookEvent”                                            |
class=class="str">"cmt">//| 调用策略的处理函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::OnTimer(class="type">void)
  {
  m_event.symbol=Symbol();
  m_event.type=MARKET_EVENT_TIMER;
  m_event.period=(ENUM_TIMEFRAMES)Period();
  CallSupport(m_event);
  CallInit(m_event);
  NewTickDetect();
  NewBarsDetect();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 对系统事件“OnBookEvent”                                            |
class=class="str">"cmt">//| 调用策略的处理函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+

事件驱动下的柱与报价分流

在 MT5 的 EA 框架里,市价事件不会自动区分「新柱」和「新 tick」,需要自己在 CStrategy 里做分流。OnBookEvent 只负责把深度事件塞进 m_event 并触发后续检测,真正的多周期监听靠 NewBarsDetect 与 NewTickDetect 两个独立循环。 NewBarsDetect 先判断 m_bars_detecors 容器是否为空,为空就给当前图表品种和周期补一个 AddBarOpenEvent;随后遍历所有检测器,bar.IsNewBar() 为真才把事件类型置为 MARKET_EVENT_BAR_OPEN 并回调 CallSupport 与 CallInit。同理,NewTickDetect 对 m_ticks_detectors 做相同逻辑,事件类型换成 MARKET_EVENT_TICK,周期固定为 PERIOD_CURRENT。 CallInit 是开仓逻辑的闸门:先用 m_state.GetTradeState() 取状态,TRADE_STOP、TRADE_WAIT、TRADE_NO_NEW_ENTRY 三种状态直接 return,不碰仓位。剩下 BUY_AND_SELL / BUY_ONLY / SELL_ONLY 才会进 InitBuy 或 InitSell。CallSupport 则相反,只在非 TRADE_WAIT 时跑持仓维持。外汇与贵金属杠杆高,状态机写错可能瞬间放大回撤,建议把这三段直接挂 MT5 调试器单步看 m_trade_state 跳变。

MQL5 / C++
class="type">void CStrategy::OnBookEvent(class="type">class="kw">string symbol)
  {
  m_event.symbol=symbol;
  m_event.type=MARKET_EVENT_BOOK_EVENT;
  m_event.period=PERIOD_CURRENT;
  CallSupport(m_event);
  CallInit(m_event);
  NewTickDetect();
  NewBarsDetect();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检测一个新柱形的出现并   
class=class="str">"cmt">//| 为EA产生一个与之对应的件
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::NewBarsDetect(class="type">void)
  {
  if(m_bars_detecors.Total()==class="num">0)
     AddBarOpenEvent(ExpertSymbol(),Timeframe());
  for(class="type">int i=class="num">0; i<m_bars_detecors.Total(); i++)
     {
     CBarDetector *bar=m_bars_detecors.At(i);
     if(bar.IsNewBar())
       {
       m_event.period = bar.Timeframe();
       m_event.symbol = bar.Symbol();
       m_event.type=MARKET_EVENT_BAR_OPEN;
       CallSupport(m_event);
       CallInit(m_event);
       }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检测多个货币对上新报价的到来                                    
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::NewTickDetect(class="type">void)
  {
  if(m_ticks_detectors.Total()==class="num">0)
     AddTickEvent(ExpertSymbol());
  for(class="type">int i=class="num">0; i<m_ticks_detectors.Total(); i++)
     {
     CTickDetector *tick=m_ticks_detectors.At(i);
     if(tick.IsNewTick())
       {
       m_event.period=PERIOD_CURRENT;
       m_event.type=MARKET_EVENT_TICK;
       m_event.symbol=tick.Symbol();
       CallSupport(m_event);
       CallInit(m_event);
       }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 如果交易状态没有明确限制的话,
class=class="str">"cmt">//| 调用开仓逻辑。                                         
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::CallInit(const MarketEvent &event)
  {
  m_trade_state=m_state.GetTradeState();
  if(m_trade_state == TRADE_STOP)class="kw">return;
  if(m_trade_state == TRADE_WAIT)class="kw">return;
  if(m_trade_state == TRADE_NO_NEW_ENTRY)class="kw">return;
  SpyEnvironment();
  if(m_trade_state==TRADE_BUY_AND_SELL || m_trade_state==TRADE_BUY_ONLY)
     InitBuy(event);
  if(m_trade_state==TRADE_BUY_AND_SELL || m_trade_state==TRADE_SELL_ONLY)
     InitSell(event);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 如果交易状态不是TRADE_WAIT,
class=class="str">"cmt">//| 调用持仓保持逻辑
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::CallSupport(const MarketEvent &event)
  {
  m_trade_state=m_state.GetTradeState();
  if(m_trade_state == TRADE_WAIT)class="kw">return;
  SpyEnvironment();

「按魔术码分流持仓并触发退出」

这段循环是 EA 遍历活跃持仓并对每单做归属判断的核心。它从持仓数组末尾往前扫,避免删除或平仓导致索引错位,是 MT5 持仓管理里常见的防漏单写法。 先按 ExpertMagic 过滤掉非本策略的订单,只处理自己下发的仓位;再依据多空方向分别调用 SupportBuy 或 SupportSell 做维护。若全局状态切到 TRADE_STOP 且该仓仍活跃,就走 ExitByStopRegim 强平逻辑。 在 MT5 里把 m_expert_magic 设成独立值(比如 888777),可让多策略同账户互不干扰。外汇与贵金属杠杆高,错用魔术码可能误平他人仓位,实盘前务必用策略测试器跑一遍。

MQL5 / C++
for(class="type">int i=ActivePositions.Total()-class="num">1; i>=class="num">0; i--)
  {
  CPosition *pos=ActivePositions.At(i);
  if(pos.ExpertMagic()!=m_expert_magic)class="kw">continue;
  if(pos.Direction()==POSITION_TYPE_BUY)
     SupportBuy(event,pos);
  else
     SupportSell(event,pos);
  if(m_trade_state==TRADE_STOP && pos.IsActive())
     ExitByStopRegim(pos);
  }

◍ 从 CStrategy 派生出独立交易模块

CStrategy 类把行情响应与交易事件处理都收编到统一框架里,派生类只需要写自己的交易逻辑和规则,就能作为独立模块跑起来。实际开发中,这意味着你不必重复造轮子去管订单序列,专注信号判定即可。 框架提供的派生机制让策略可单独编译测试。以 CBarDetector::IsNewBar 为例,它通过 CopyTime 拉取最新 1 根 K 线时间,与上次记录比对来判断新柱:若时间相同返回 false,不同则更新并返回 true。 NewTickDetect 则遍历已注册的 Tick 探测器,对每一个触发 IsNewTick 的符号派发 MARKET_EVENT_TICK 事件。这里有个细节——策略默认只跟踪自身品种,多品种循环更多是为 CStrategyList 管理多个策略对象服务,单策略并不强依赖它。外汇与贵金属波动剧烈,用这类框架仍须自行校验滑点和点差风险。 下一篇会基于这套已论述的算法,演示具体策略如何从零写成可加载的 EA。

MQL5 / C++
class="type">bool CBarDetector::IsNewBar(class="type">void)
  {
   class="type">class="kw">datetime time[];
   if(CopyTime(m_symbol, m_timeframe, class="num">0, class="num">1, time) < class="num">1)class="kw">return class="kw">false;
   if(time[class="num">0] == m_last_time)class="kw">return class="kw">false;
   class="kw">return (m_last_time = time[class="num">0]);    class=class="str">"cmt">//<=============HERE
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 如果给定的符号和时间框架中存在"......",则返回 trueclass=class="str">"cmt">//| 一个新栏。|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CBarDetector::IsNewBar(class="type">void)
  {
   class="type">class="kw">datetime time[];
   if(CopyTime(m_symbol, m_timeframe, class="num">0, class="num">1, time) < class="num">1)class="kw">return (class="kw">false);
   if(time[class="num">0] == m_last_time)class="kw">return (class="kw">false);
   class="kw">return (m_last_time == time[class="num">0]);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 检测多仪器新刻度的到来。
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CStrategy::NewTickDetect(class="type">void)
  {
   if(m_ticks_detectors.Total()==class="num">0)
      AddTickEvent(ExpertSymbol());
   for(class="type">int i=class="num">0; i<m_ticks_detectors.Total(); i++)
     {
      CTickDetector *tick=m_ticks_detectors.At(i);
      if(tick.IsNewTick())
        {
         m_event.period=PERIOD_CURRENT;
         m_event.type=MARKET_EVENT_TICK;

把事件过滤收口到具体品种和周期

上面这段 IsTrackEvents 是均线类指标接收市场事件前的最后一道闸门。它只认三种条件同时成立才放行:事件类型必须是新柱线打开(MARKET_EVENT_BAR_OPEN),事件周期要等于指标自身 Timeframe() 返回的值,事件品种必须和 ExpertSymbol() 一致,否则直接 return false。 写 EA 或指标时若漏掉周期与品种的双重判断,可能在切换图表或加载多周期实例时误触发 CallSupport、CallInit 这类回调,造成重复计算甚至信号漂移。外汇与贵金属市场波动剧烈、杠杆风险高,这类底层过滤逻辑的疏漏可能在实盘中断掉你整条信号链。 在 MT5 里把这段直接塞进你的 CMovingAverage 派生类,编译后开两个不同周期的同品种图表观察日志,只在匹配的那张图上才会看到后续回调被执行。

MQL5 / C++
  m_event.symbol=tick.Symbol();
  CallSupport(m_event);
  CallInit(m_event);
   }
  }
}
class="type">bool CMovingAverage::IsTrackEvents(const MarketEvent &event)
  {
class=class="str">"cmt">//--- 我们只处理在工作符号和时间框架上打开新条形图的情况
   if(event.type != MARKET_EVENT_BAR_OPEN)class="kw">return class="kw">false;
   if(event.period != Timeframe())class="kw">return class="kw">false;
   if(event.symbol != ExpertSymbol())class="kw">return class="kw">false;
   class="kw">return true;
  }

常见问题

用持仓类把每笔单子的 ticket、品种、方向、开仓价等读出来存成属性,再写方法统一改止损止盈或平仓,避免散落全局变量。
封装一个改写方法,先按 ticket 选中持仓再调改单接口,失败返回错误码;外汇贵金属波动大,改单前先判点差和冻结距离。
小布可接入你的持仓对象流,按魔术码分流监控,止损被触碰或浮亏超阈值时直接推告警,你只管看结论。
在策略父类里收事件流,新柱走 OnTick 末旗标、报价走逐 tick 分支,用时间戳和柱序号分流即可。
常是遍历持仓没过滤同魔术码就触发退出,或退出方法未等成交回执;先分组再逐组关,并读回执确认。