通用智能交易系统:事件模型和交易策略原型(第二章)·进阶篇
⚙️

通用智能交易系统:事件模型和交易策略原型(第二章)·进阶篇

(2/3)· 还在为每个 tick、每根新柱分别写 On* 处理函数?集中事件模型让 EA 逻辑不再碎片化

含代码示例实战向 第 2/3 篇
不少 EA 作者把 NewTick、Timer、TradeTransaction 散落在各自 On 函数里,结果订单状态和多品种事件互相打架。事件不归口,回测和实盘的行为偏差往往就从这开始。

「从日志看交易事件的触发次序」

在 MT5 的 Expert Advisor 运行中,事件日志能直接暴露策略类的内部调用链。上面这段真实日志来自 Si-12.15 的 M1 图表,时间锚点在 2015.11.30 14:13:39.981 到 14:13:40.015 这 34 毫秒内。 同一毫秒 14:13:39.981 下,CExpert 连续打出了 BAR_OPEN 与 TICK 两类事件:InitBuy、InitSell、SupportBuy、SupportSell 均被触发,说明 K 线开盘瞬间策略同时重置了双向挂单与防护逻辑,而 TICK 事件则在同一时刻补齐了即时报价下的支撑判定。 到了 14:13:40.015,仅隔约 34 毫秒,InitBuy 又收到 MARKET_EVENT_TICK——此时 BAR_OPEN 已不再出现,表明新 tick 到达时策略只做即时买入初始化,不再重复处理 bar 级事件。这种微秒级交错提醒我们:M1 周期下事件驱动的并发密度极高,回测与实盘日志的对齐必须精确到毫秒,否则可能误判信号丢失。 开 MT5 加载任意 EA 并打开「Experts」标签,把上面这类时间戳贴到表格里排一下,就能看出你自己的策略在 bar 开盘瞬间有没有重复初始化的问题。外汇与贵金属品种同样存在该现象,且点差跳变会放大事件乱序,属高风险观察项。

MQL5 / C++
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">40.015 ExampExp2(Si-class="num">12.15,M1) CExpert::InitBuy EventID: MARKET_EVENT_TICK
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">39.981 ExampExp2(Si-class="num">12.15,M1) CExpert::SupportSell EventID: MARKET_EVENT_BAR_OPEN
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">39.981 ExampExp2(Si-class="num">12.15,M1) CExpert::SupportBuy EventID: MARKET_EVENT_BAR_OPEN
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">39.981 ExampExp2(Si-class="num">12.15,M1) CExpert::InitSell EventID: MARKET_EVENT_BAR_OPEN
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">39.981 ExampExp2(Si-class="num">12.15,M1) CExpert::InitBuy EventID: MARKET_EVENT_BAR_OPEN
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">39.981 ExampExp2(Si-class="num">12.15,M1) CExpert::SupportSell EventID: MARKET_EVENT_TICK
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">39.981 ExampExp2(Si-class="num">12.15,M1) CExpert::SupportBuy EventID: MARKET_EVENT_TICK
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">39.981 ExampExp2(Si-class="num">12.15,M1) CExpert::InitSell EventID: MARKET_EVENT_TICK
class="num">2015.11.class="num">30 class="num">14:class="num">13:class="num">39.981 ExampExp2(Si-class="num">12.15,M1) CExpert::InitBuy EventID: MARKET_EVENT_TICK

用统一结构体接住多品种事件

做多货币对分析时,原生 OnTick 只在 EA 所在图表来报价才触发,盯别的品种就得另想办法。MQL5 里能用的入口有三个:OnTick 接本品种报价、OnBookEvent 接订单簿变动(需用 MarketBookAdd 订阅,但盘口刷新极密、资源吃紧)、OnTimer 按设定间隔轮询。实际工程里,多数系统只在 OnTimer 里扫所需品种,不必常开盘口监听。 把这三路信号收口到一个中间层(事件处理器 EventProcessor)是干净做法:它统一分析多个品种的价格变化,再向外抛结构化的 MarketEvent。至于事件最早是从 OnTick、OnTimer 还是 OnBookEvent 进来的,策略层不用管,只要知道『某品种有新情况』即可。这样同一个处理器能喂给列表里多个策略实例,各自决定跟或不跟。 MarketEvent 结构体就三字段,足够描述一次事件:type 标事件类别,period 标图表周期(NewTick/Timer 用不上的时候恒定填 PERIOD_CURRENT),symbol 标具体品种。策略方法拿到引用后直接读这三样做判断,不依赖触发源。 下面这段是结构体原文,逐行对应上面说的三件事:

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|结构体定义事件类型,事件发生的图表对象及时间框架(用于BarOpen事件)        |
class=class="str">"cmt">//|                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct MarketEvent
  {
   ENUM_MARKET_EVENT_TYPE type;       class=class="str">"cmt">// 事件类型
   ENUM_TIMEFRAMES       period;     class=class="str">"cmt">// 事件发生所在图表的时间框架(仅用于MARKET_EVENT_BAR_OPEN)
   class="type">class="kw">string                symbol;     class=class="str">"cmt">// 事件发生所在的货币对名称
  };

◍ 用检测器类抓多品种新报价与新柱

在多货币对策略里,EA 不能只盯图表当前品种。把新报价和新柱形的到达抽象成内部模型,外层逻辑才不必关心底层时序。这类模型挂在 CStrategy 内部,交易者写 EA 时不会直接碰它们,但调度效率全靠它们。 CTickDetector 是第一个要理解的类。它的核心只有 IsNewTick:被监控品种一旦出现新报价就返回 true,监控目标用 Symbol 方法写入。类从 CObject 派生,因此能直接塞进 CArrayObj 容器——这意味着你可以建 10 个 CTickDetector 副本,每个盯一个货币对,轮询数组就能在毫秒级定位到是哪个品种动了。 新柱形同理交给 CBarDetector,主要方法 IsNewBar 在品种走出新柱时返回 true。下面这段是 CTickDetector 的骨架代码,注意它没用定时器,纯靠每次调用比对 MqlTick 结构判断增量。 别把 10 个副本当免费午餐:外汇与贵金属市场高波动时段报价频率可能破百 tick/秒,轮询数组过长会吃掉主循环时间,EA 在 VPS 上可能掉帧。实盘前用 MT5 策略测试器跑一晚多品种回放,看 OnTick 耗时是否超 5 ms 再决定副本数。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 NewTickDetector.mqh |
class=class="str">"cmt">//|                                     Copyright class="num">2015, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                                    [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 新报价检测器                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTickDetector : class="kw">public CObject
  {
class="kw">private:
   class="type">class="kw">string            m_symbol;        class=class="str">"cmt">// 追踪该货币的新报价
   class="type">MqlTick           m_last_tick;     class=class="str">"cmt">// 最近一次报价
class="kw">public:
                     CTickDetector(class="type">void);
                     CTickDetector(class="type">class="kw">string symbol);
   class="type">class="kw">string            Symbol(class="type">void);
   class="type">void              Symbol(class="type">class="kw">string symbol);
   class="type">bool              IsNewTick(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数默认使用当前时间框架
class=class="str">"cmt">//| 和货币对进行设置
class=class="str">"cmt">//+------------------------------------------------------------------+
CTickDetector::CTickDetector(class="type">void)
  {
   m_symbol=_Symbol;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用预设的货币对和时间框架创建一个对象
class=class="str">"cmt">//+------------------------------------------------------------------+
CTickDetector::CTickDetector(class="type">class="kw">string symbol)
  {
   m_symbol=symbol;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置需要追踪新报价的货币对名称
class=class="str">"cmt">//|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTickDetector::Symbol(class="type">class="kw">string symbol)
  {
   m_symbol=symbol;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「从报价追踪切到新K线判定」

上一节的 CTickDetector 只解决“哪个品种刚来了新 tick”的问题,但做价格行为分析时往往要等一根 K 线收完再动作,所以紧接着要有一个柱形检测器。下面这段把两个类的核心方法摊开看。 CTickDetector::Symbol() 直接返回内部缓存的 m_symbol,也就是最近一次触发新报价的品种名;IsNewTick() 则每次调用 SymbolInfoTick 取实时 tick,只要 last 价格或 time 时间戳和上次记录不同就判定为新 tick,并更新 m_last_tick 后返回 true。逻辑极轻量,适合塞进 OnTick 里高频跑。

MQL5 / C++
class="type">class="kw">string CTickDetector::Symbol(class="type">void)
  {
   class="kw">return m_symbol;
  }
class="type">bool CTickDetector::IsNewTick(class="type">void)
  {
   class="type">MqlTick tick;
   SymbolInfoTick(m_symbol,tick);
   if(tick.last!=m_last_tick.last ||
      tick.time!=m_last_tick.time)
     {
      m_last_tick=tick;
      class="kw">return true;
     }
   class="kw">return class="kw">false;
  }
代码逐行拆解:第1行定义返回品种名的公开方法;第3行把私有成员 m_symbol 交出去。第6行声明 MqlTick 结构体变量准备接数据;第7行用 SymbolInfoTick 向 MT5 内核要当前 tick;第8–9行比对 last 与 time 任一变动即视为新 tick;第11行覆盖旧记录;第12行返回真;否则第14行返回假。 后面的 CBarDetector 类沿用相似思路,私有成员多了 m_timeframe 与 m_last_time,靠对比柱形起始时间 datetime 来判断新柱。外汇与贵金属杠杆高、滑点突变频繁,这类检测在实盘里可能漏掉跨帧跳空,建议你在 MT5 用 EURUSD 的 M1 跑一遍 IsNewBar 打印时间,确认和本地时钟偏差。

MQL5 / C++
class="type">class="kw">string CTickDetector::Symbol(class="type">void)
  {
   class="kw">return m_symbol;
  }
class="type">bool CTickDetector::IsNewTick(class="type">void)
  {
   class="type">MqlTick tick;
   SymbolInfoTick(m_symbol,tick);
   if(tick.last!=m_last_tick.last ||
      tick.time!=m_last_tick.time)
     {
      m_last_tick=tick;
      class="kw">return true;
     }
   class="kw">return class="kw">false;
  }

柱体检测器类的构造与判新逻辑

在 MT5 里做跨周期、跨品种的新柱监听,先得把追踪对象固化下来。下面这个类用两个构造函数把默认上下文和自定义上下文分开:无参版本直接吃当前图表环境,带参版本让你显式指定 symbol 与 timeframe。

MQL5 / C++
class=class="str">"cmt">//| 构造函数默认使用当前时间框架
class=class="str">"cmt">//| 和货币对进行设置
class=class="str">"cmt">//+------------------------------------------------------------------+
CBarDetector::CBarDetector(class="type">void)
  {
   m_symbol=_Symbol;
   m_timeframe=Period();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用预设的货币对和时间框架创建一个对象
class=class="str">"cmt">//+------------------------------------------------------------------+
CBarDetector::CBarDetector(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf)
  {
   m_symbol=symbol;
   m_timeframe=tf;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置你想追踪的新柱形出现的时间框架
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CBarDetector::Timeframe(ENUM_TIMEFRAMES tf)
  {
   m_timeframe=tf;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回你所追踪的新柱形出现
class=class="str">"cmt">//| 的时间框架
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CBarDetector::Timeframe(class="type">void)
  {
   class="kw">return m_timeframe;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置想要追踪的货币对名称
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CBarDetector::Symbol(class="type">class="kw">string symbol)
  {
   m_symbol=symbol;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回新柱形出现的货币对名称
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CBarDetector::Symbol(class="type">void)
  {
   class="kw">return m_symbol;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 如果给定货币对和时间框架下出现新的报价,
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];
  }
逐行拆解:CBarDetector::CBarDetector(void) 是无参构造,m_symbol 接 _Symbol(当前图表品种),m_timeframe 接 Period()(当前图表周期)。带参构造 CBarDetector(string,ENUM_TIMEFRAMES) 把外部传入的 symbol 与 tf 写进成员变量,便于盯 EURUSD 的 M15 而脚本挂在 XAUUSD 的 H1 上跑。 Timeframe() 与 Symbol() 各有读写两个重载:写版本改 m_timeframe / m_symbol,读版本回传当前值。注意这些只是存参,不触发任何行情拉取。 IsNewBar() 才是核心:先用 CopyTime 取最新 1 根 K 线的开盘时间,若返回值小于 1(没取到数据)直接 false;再比 time[0] 与 m_last_time,相等说明还是老柱,不等就更新 m_last_time 并返回 true。外汇与贵金属波动剧烈,跨周期监听存在滑点与时延,新柱判定仅作信号参考,实盘须自行评估高风险。

MQL5 / C++
class=class="str">"cmt">//| 构造函数默认使用当前时间框架
class=class="str">"cmt">//| 和货币对进行设置
class=class="str">"cmt">//+------------------------------------------------------------------+
CBarDetector::CBarDetector(class="type">void)
  {
   m_symbol=_Symbol;
   m_timeframe=Period();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用预设的货币对和时间框架创建一个对象
class=class="str">"cmt">//+------------------------------------------------------------------+
CBarDetector::CBarDetector(class="type">class="kw">string symbol,ENUM_TIMEFRAMES tf)
  {
   m_symbol=symbol;
   m_timeframe=tf;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置你想追踪的新柱形出现的时间框架
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CBarDetector::Timeframe(ENUM_TIMEFRAMES tf)
  {
   m_timeframe=tf;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回你所追踪的新柱形出现
class=class="str">"cmt">//| 的时间框架
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CBarDetector::Timeframe(class="type">void)
  {
   class="kw">return m_timeframe;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 设置想要追踪的货币对名称
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CBarDetector::Symbol(class="type">class="kw">string symbol)
  {
   m_symbol=symbol;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回新柱形出现的货币对名称
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CBarDetector::Symbol(class="type">void)
  {
   class="kw">return m_symbol;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 如果给定货币对和时间框架下出现新的报价,
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];
  }

◍ 用封装类把头寸管理从平台里抽出来

做跨平台 EA 的核心障碍从来不是指标计算,而是头寸管理函数。MT4 和 MT5 的 MQL 本质是一套语言配了两套函数集,差异主要集中在 PositionSelect / PositionGet 这类持仓操作上。 如果把头寸读写全部塞进一个抽象类里,EA 主体就只调用统一接口,不碰任何平台专属函数。理论上,换一套底层类(一套调 MT4、一套调 MT5)就能让同一份策略源码跑在两个终端,不用改逻辑。 这个类把当前未平持仓的属性(方向、开仓价、magic、品种、开仓时间)包成成员变量,对外只暴露读取方法和平仓、改止盈止损的操作。交易引擎逐个遍历持仓传入策略,EA 就能独立管每一单——MT5 虽只允许一个同向持仓,但把这层抽象迁到 MT4 后,多单并行管理直接可用。 外汇与贵金属杠杆高、滑点跳空频繁,抽象层省了移植成本,却省不掉穿仓风险,参数和品种切换前建议在策略测试器跑一轮多品种回测。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 PositionMT5.mqh |
class=class="str">"cmt">//|                                                      Copyright class="num">2015, Vasiliy Sokolov. |
class=class="str">"cmt">//|                                                                [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#include <Object.mqh>
class="macro">#include "Logs.mqh"
class="macro">#include <Trade\Trade.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 典型策略的持仓头寸类                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CPositionMT5 : class="kw">public CObject
  {
class="kw">private:
   class="type">ulong               m_id;                    class=class="str">"cmt">// 持仓头寸唯一标识
   class="type">uint                m_magic;                class=class="str">"cmt">// 头寸所属EA的唯一标识
   class="type">ENUM_POSITION_TYPE m_direction;             class=class="str">"cmt">// 方向
   class="type">class="kw">double              m_entry_price;          class=class="str">"cmt">// 开仓价
   class="type">class="kw">string              m_symbol;               class=class="str">"cmt">// 持仓头寸的品种
   class="type">class="kw">datetime            m_time_open;             class=class="str">"cmt">// 开仓时间
   class="type">class="kw">string              m_entry_comment;        class=class="str">"cmt">// 注释
   class="type">bool                m_is_closed;            class=class="str">"cmt">// 如果头寸被平仓则 True
   CLog*               Log;                     class=class="str">"cmt">// 日志
   CTrade              m_trade;                class=class="str">"cmt">// 交易模型
class="kw">public:
                       CPositionMT5(class="type">void);
   class="type">uint                ExpertMagic(class="type">void);
   class="type">ulong               ID(class="type">void);
   class="type">ENUM_POSITION_TYPE Direction(class="type">void);
   class="type">class="kw">double              EntryPrice(class="type">void);
让小布替你跑这套事件巡检
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多品种事件冲突与柱形检测延迟的提示,把重复劳动交给小布,你专注决策。

常见问题

前者把行情、成交、计时等事件抽象成统一枚举,由 CStrategy 集中分发;后者是终端按对象触发的独立入口,难以跨品种聚合。
可以,小布的品种页已内置对多对象事件与柱形检测的巡检,不需要你自己接 MT5 回调就能看到潜在冲突。
一般含事件类型、来源品种句柄、时间戳与附加数据指针;具体字段依 CStrategy 定义,重点是用来源标识隔离跨品种噪声。
报价推演在实盘更接近 tick 真实到达,但回测若用不规则数据可能漏柱;时间轴法简单却可能在跳空时误判,倾向按品种流动性选。