通用智能交易系统:事件模型和交易策略原型(第二章)·进阶篇
(2/3)· 还在为每个 tick、每根新柱分别写 On* 处理函数?集中事件模型让 EA 逻辑不再碎片化
「从日志看交易事件的触发次序」
在 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 开盘瞬间有没有重复初始化的问题。外汇与贵金属品种同样存在该现象,且点差跳变会放大事件乱序,属高风险观察项。
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 标具体品种。策略方法拿到引用后直接读这三样做判断,不依赖触发源。 下面这段是结构体原文,逐行对应上面说的三件事:
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 再决定副本数。
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 里高频跑。
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; }
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。
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]; }
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 后,多单并行管理直接可用。 外汇与贵金属杠杆高、滑点跳空频繁,抽象层省了移植成本,却省不掉穿仓风险,参数和品种切换前建议在策略测试器跑一轮多品种回测。
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);