通用智能交易系统:事件模型和交易策略原型(第二章)·综合运用
◍ 把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(),十秒就能验证封装是否跑通。
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 失败分支不会在跳空时误改单。
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),调用方要自己区分“零仓”和“选择失败”,否则容易把无单当空仓刷单。外汇与贵金属杠杆高,自动平仓逻辑上线前务必在策略测试器用历史数据跑一遍。
{
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 事件再走支持与初始化流程。
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 跳变。
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),可让多策略同账户互不干扰。外汇与贵金属杠杆高,错用魔术码可能误平他人仓位,实盘前务必用策略测试器跑一遍。
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。
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">//| 如果给定的符号和时间框架中存在"......",则返回 true。 class=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 派生类,编译后开两个不同周期的同品种图表观察日志,只在匹配的那张图上才会看到后续回调被执行。
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; }