市场深度抽象请求类:从零搭建 DOM 订单对象模型(基础篇)
(1/3)· 很多交易者盯着买一卖一却看不懂 DOM 全貌,本篇用基类+衍生类把订单拆明白
「给市场深度建一个抽象请求类」
在 DoEasy 函数库的价格系列里,市场深度(DOM)不再只是终端里那张买卖挂单表,而是可以被对象化请求的数据源。2021 年 4 月 5 日发布的这一版实现,把 DOM 访问抽象成一个请求类,衍生类再去具体描述某一类订单对象,这样上层逻辑不必关心底层是即时深度还是聚合深度。
抽象订单对象类只定义接口与必要属性,例如价格、成交量、方向;衍生类负责把真实的市场深度快照映射进来。这种写法让后续扩展(比如只抓最优五档、或过滤掉小于某手数的挂单)变成改一个衍生类的事,而不是动核心循环。
打开 MT5 自建脚本,引用该抽象类后,你能直接拿到 DOM 每档的 price 与 volume 做高频统计——外汇与贵金属的 DOM 跳动快、滑点大,实盘验证前请先在策略测试器用历史 tick 跑一遍,高风险品种尤其要控仓。
把 DOM 拆成可订阅的对象模型
做市场深度(DOM)操控,本质和之前写的函数库类没两样,区别在于我们要先建一套 DOM 特征数据模型:在 OnBookEvent() 触发时,用 MarketBookGet() 把当前品种的挂单拉进来。每一次 DOM 变动,处理程序会为所有订阅了该品种的事件各发一次通知。 DOM 的类结构可以分成四层:单笔订单对象(描述某一笔挂单)、某品种单次快照的模型对象(一次 OnBookEvent 里并发拿到的全部订单集合)、由每次快照构成的单一品种时间序列、以及跨品种 DOM 时间序列的集合类。今天先落地第一层——订单对象类,并验证在当前品种上订阅后能否正常取到数据。 MqlBookInfo 结构给了订单的原始字段:类型来自 ENUM_BOOK_TYPE、价格、交易量、以及更高精度的交易量。枚举里实际只有四种单子——卖单、市价卖单、买单、市价买单。为了快速把供给和需求分开,我们在原有属性上补一个方向标记(买/卖),这样一次快照进来就能立刻切两半。 基类做抽象订单,再派生四个带类型规则的子类,这套构造思路在库开发最前面两篇就定过调了。动手前得先在 Data.mqh 里补 DOM 相关的消息索引,并在 Symbol.mqh 中加上订阅/取消订阅 DOM 失败的错误文本;TickSeries.mqh 里则删掉原先为了调试贴在图表上的注释,把上次报价时间直接写进 m_last_time,少一层无谓开销。 下面这段是消息枚举里新增的 DOM 订阅与错误项,以及即时报价序列类的部分消息声明,注意高亮两行是本次加的订阅错误反馈:
MSG_SYM_EVENT_SYMBOL_ADD, class=class="str">"cmt">// Added symbol to Market Watch window MSG_SYM_EVENT_SYMBOL_DEL, class=class="str">"cmt">// Symbol removed from Market Watch window MSG_SYM_EVENT_SYMBOL_SORT, class=class="str">"cmt">// Changed location of symbols in Market Watch window MSG_SYM_SYMBOLS_MODE_CURRENT, class=class="str">"cmt">// Work with current symbol only MSG_SYM_SYMBOLS_MODE_DEFINES, class=class="str">"cmt">// Work with predefined symbol list MSG_SYM_SYMBOLS_MODE_MARKET_WATCH, class=class="str">"cmt">// Work with Market Watch window symbols MSG_SYM_SYMBOLS_MODE_ALL, class=class="str">"cmt">// Work with full list of all available symbols MSG_SYM_SYMBOLS_BOOK_ADD, class=class="str">"cmt">// Subscribed to Depth of Market MSG_SYM_SYMBOLS_BOOK_DEL, class=class="str">"cmt">// Unsubscribed from Depth of Market MSG_SYM_SYMBOLS_MODE_BOOK, class=class="str">"cmt">// Subscription to Depth of Market MSG_SYM_SYMBOLS_ERR_BOOK_ADD, class=class="str">"cmt">// Error subscribing to DOM MSG_SYM_SYMBOLS_ERR_BOOK_DEL, class=class="str">"cmt">// Error unsubscribing from DOM class=class="str">"cmt">//--- CAccount class=class="str">"cmt">//--- CTickSeries MSG_TICKSERIES_TEXT_TICKSERIES, class=class="str">"cmt">// Tick series MSG_TICKSERIES_ERR_GET_TICK_DATA, class=class="str">"cmt">// Failed to get tick data MSG_TICKSERIES_FAILED_CREATE_TICK_DATA_OBJ, class=class="str">"cmt">// Failed to create tick data object MSG_TICKSERIES_FAILED_ADD_TO_LIST, class=class="str">"cmt">// Failed to add tick data object to list MSG_TICKSERIES_TEXT_IS_NOT_USE, class=class="str">"cmt">// Tick series not used. Set the flag using SetAvailable() MSG_TICKSERIES_REQUIRED_HISTORY_DAYS, class=class="str">"cmt">// Requested number of days class=class="str">"cmt">//--- CMarketBookOrd
◍ 深度映射DOM字段与订阅事件
在 MQL5 的行情深度(DOM)相关封装类里,挂单方向的字段标签必须和终端内部枚举一一对齐。下面这组映射把俄文原串和英文输出绑定,覆盖了「Order in Depth of Market / Volume / Volume Real / Buy side / Sell side」以及限价单、市价单的买卖类别,共 9 个 DOM 订单字段。 订阅状态机也同样用字符串表管理:从「Added symbol to Market Watch」到「Subscribed to Depth of Market」,再到背景高亮的「错误订阅/退订」空串回执,一共 11 条市场观察与 DOM 事件描述。实盘跑 EA 时若 BookAdd() 返回 false,优先排查最后两条错误回执是否触发。 外汇与贵金属的 DOM 数据受流动性断层影响明显,订阅失败概率在重大数据发布前后会显著抬升,属正常现象而非代码缺陷。
MSG_MBOOK_ORD_TEXT_MBOOK_ORD, class=class="str">"cmt">// Order in DOM MSG_MBOOK_ORD_VOLUME, class=class="str">"cmt">// Volume MSG_MBOOK_ORD_VOLUME_REAL, class=class="str">"cmt">// Extended accuracy volume MSG_MBOOK_ORD_STATUS_BUY, class=class="str">"cmt">// Buy side MSG_MBOOK_ORD_STATUS_SELL, class=class="str">"cmt">// Sell side MSG_MBOOK_ORD_TYPE_SELL, class=class="str">"cmt">// Sell order MSG_MBOOK_ORD_TYPE_BUY, class=class="str">"cmt">// Buy order MSG_MBOOK_ORD_TYPE_SELL_MARKET, class=class="str">"cmt">// Sell order by Market MSG_MBOOK_ORD_TYPE_BUY_MARKET, class=class="str">"cmt">// Buy order by Market }; class=class="str">"cmt">//+------------------------------------------------------------------+ {"В окно \"Обзор рынка\" добавлен символ","Added symbol to \"Market Watch\" window"}, {"Из окна \"Обзор рынка\" удалён символ","Removed from \"Market Watch\" window"}, {"Изменено расположение символов в окне \"Обзор рынка\"","Changed arrangement of symbols in \"Market Watch\" window"}, {"Работа только с текущим символом","Work only with the current symbol"}, {"Работа с предопределённым списком символов","Work with predefined list of symbols"}, {"Работа с символами из окна \"Обзор рынка\"","Working with symbols from \"Market Watch\" window"}, {"Работа с полным списком всех доступных символов","Work with full list of all available symbols"}, {"Осуществлена подписка на стакан цен ","Subscribed to Depth of Market"}, {"Осуществлена отписка от стакан цен ","Unsubscribed from Depth of Market"}, {"Подписка на стакан цен","Subscription to Depth of Market"}, {"Ошибка при подписке на стакан цен",""}, {"Ошибка при отписке от стакан цен",""}, class=class="str">"cmt">//--- CAccount class=class="str">"cmt">//--- CMarketBookOrd {"Заявка в стакане цен","Order in Depth of Market"}, {"Объем","Volume"}, {"Объем c повышенной точностью","Volume Real"}, {"Сторона Buy","Buy side"}, {"Сторона Sell","Sell side"}, {"Заявка на продажу","Sell order"}, {"Заявка на покупку","Buy order"}, {"Заявка на продажу по рыночной цене","Sell order at market price"}, {"Заявка на покупку по рыночной цене","Buy order at market price"}, }; class=class="str">"cmt">//+---------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Subscribe to the Depth of Market | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSymbol::BookAdd(class="type">void) {
「深度簿订阅与tick刷新的底层开关」
在 MT5 里抓盘口深度,第一步是向终端注册符号的市场深度簿。MQL5 用 MarketBookAdd() 订阅、MarketBookRelease() 退订,而 MQL4 环境直接走 false/true 短路,因为老平台根本没有 DOM 接口。 订阅结果会写回对象的两个地方:m_book_subscribed 标志位和 m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]。成功就打印添加日志,失败则通过 GetLastError() 把具体错误码带出来,方便你排查品种是否支持深度簿(部分外汇经纪商对 XAUUSD 可能不推盘口)。 退订时若标志位本来就是关的,函数直接返回 true,避免重复释放。真正调用 MarketBookRelease() 后,无论成败都会同步刷新状态位——这点很关键,否则对象内部会以为还挂着簿,后续刷新逻辑可能空转。 tick 系列刷新靠 CopyTicksRange(),从 m_last_time+1 毫秒拉到最新。参数 COPY_TICKS_ALL 表示全类型 tick 都收,total>0 才进循环建对象;如果某秒没新 tick,total=0,列表原样不动,不会报错但也不会补数据。外汇和贵金属波动快,这种增量拉取能压低内存,但高频场景下漏 tick 的概率会随经纪商延迟上升,属正常风险。
this.m_book_subscribed=(class="macro">#ifdef __MQL5__ ::MarketBookAdd(this.m_name) class="macro">#else false class="macro">#endif); this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed; if(this.m_book_subscribed) ::Print(CMessage::Text(MSG_SYM_SYMBOLS_BOOK_ADD)+" "+this.m_name); else ::Print(CMessage::Text(MSG_SYM_SYMBOLS_ERR_BOOK_ADD)+": "+CMessage::Text(::GetLastError())); class="kw">return this.m_book_subscribed; } class="type">bool CSymbol::BookClose(class="type">void) { if(!this.m_book_subscribed) class="kw">return true; class="type">bool res=( class="macro">#ifdef __MQL5__ ::MarketBookRelease(this.m_name) class="macro">#else true class="macro">#endif ); if(res) { this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed=false; ::Print(CMessage::Text(MSG_SYM_SYMBOLS_BOOK_DEL)+" "+this.m_name); } else { this.m_long_prop[SYMBOL_PROP_BOOKDEPTH_STATE]=this.m_book_subscribed=true; ::Print(CMessage::Text(MSG_SYM_SYMBOLS_ERR_BOOK_DEL)+": "+CMessage::Text(::GetLastError())); } class="kw">return res; } class="type">void CTickSeries::Refresh(class="type">void) { class="type">MqlTick ticks_array[]; if(IsNewTick()) { class="type">int err=ERR_SUCCESS; class="type">int total=::CopyTicksRange(this.Symbol(),ticks_array,COPY_TICKS_ALL,this.m_last_time+class="num">1,class="num">0); if(total>class="num">0) {
增量刷新与容量裁剪的落地写法
在自定义 tick 序列类里,Refresh() 只做一件事:从上一帧记录的 m_last_time+1 毫秒开始,把新到达的 tick 增量拉进内存。CopyTicksRange 的第四个参数传 this.m_last_time+1、第五个参数传 0,意味着只取该时间点之后的全部 tick 类型(COPY_TICKS_ALL),不重复消费已经处理过的行情。 循环里每取到一个 MqlTick,就调 CreateNewTickObj 生成一个 CDataTick 对象塞进链表;若某次返回 NULL 直接 break,避免半截数据污染序列。同时把 end_time 记成 ticks_array 最后一笔的 time_msc,作为下一轮刷新的起点——AUDUSD 这类活跃品种在伦敦-纽约重叠时段每秒可能推十几笔 tick,不滚动这个时间窗就会重复拷贝整段历史。 内存不是无限用的。代码里设了 TICKSERIES_MAX_DATA_TOTAL 作为上限,一旦 DataTotal() 超出,就从链表头部按超出量 m_list_ticks.Total()-TICKSERIES_MAX_DATA_TOTAL 循环 Delete(j) 裁掉最老的 tick 对象。外汇与贵金属tick流受流动性与滑点影响波动剧烈,实盘跑这段代码前建议在策略测试器用真实tick数据确认上限值不会让近期价格行为样本被过早丢弃。
for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- Create the tick object and add it to the list CDataTick *tick_obj=this.CreateNewTickObj(ticks_array[i]); if(tick_obj==NULL) class="kw">break; class=class="str">"cmt">//--- Write the last tick time for subsequent copying of newly arrived ticks class="type">long end_time=ticks_array[::ArraySize(ticks_array)-class="num">1].time_msc; if(this.Symbol()=="AUDUSD") Comment(DFUN,this.Symbol(),", copied=",total,", m_last_time=",TimeMSCtoString(m_last_time),", end_time=",TimeMSCtoString(end_time),", total=",DataTotal()); this.m_last_time=end_time; } class=class="str">"cmt">//--- If the number of ticks in the list exceeds the class="kw">default maximum number, class=class="str">"cmt">//--- remove the calculated number of tick objects from the end of the list if(this.DataTotal()>TICKSERIES_MAX_DATA_TOTAL) { class="type">int total_del=m_list_ticks.Total()-TICKSERIES_MAX_DATA_TOTAL; for(class="type">int j=class="num">0;j<total_del;j++) this.m_list_ticks.Delete(j); } } class="type">void CTickSeries::Refresh(class="type">void) { class="type">MqlTick ticks_array[]; if(IsNewTick()) { class=class="str">"cmt">//--- Copy ticks from m_last_time time+class="num">1 ms to the end of history class="type">int err=ERR_SUCCESS; class="type">int total=::CopyTicksRange(this.Symbol(),ticks_array,COPY_TICKS_ALL,this.m_last_time+class="num">1,class="num">0); class=class="str">"cmt">//--- If the ticks have been copied, create new tick data objects and add them to the list in the loop by their number if(total>class="num">0) { for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- Create the tick object and add it to the list CDataTick *tick_obj=this.CreateNewTickObj(ticks_array[i]); if(tick_obj==NULL)
◍ 用最后 tick 时间截断旧数据防溢出
在 tick 接收循环的末尾,先记下本次取到的最新 tick 时间戳,供下一轮只拷贝新到达的 tick。代码用 ticks_array[ArraySize(ticks_array)-1].time_msc 取数组末位,毫秒级时间写入 m_last_time,避免每轮全量重拉。
当链表里的 tick 对象总数超过 TICKSERIES_MAX_DATA_TOTAL 这个上限时,要从头部删掉多出来的部分。先算 total_del = 当前总数 - 上限,再从索引 0 开始循环 Delete(j),把最旧的 tick 逐条扔掉。
在 MT5 里把 TICKSERIES_MAX_DATA_TOTAL 设成比如 100000,跑一晚上高频品种,若发现内存曲线被压平,就说明这段裁剪逻辑生效了。外汇与贵金属 tick 流速受事件影响波动极大,链表无上限会拖垮 EA,实盘前务必在策略测试器用真实tick验证裁剪阈值。
class="kw">break; class=class="str">"cmt">//--- Write the last tick time for subsequent copying of newly arrived ticks this.m_last_time=ticks_array[::ArraySize(ticks_array)-class="num">1].time_msc; } class=class="str">"cmt">//--- If the number of ticks in the list exceeds the class="kw">default maximum number, class=class="str">"cmt">//--- remove the calculated number of tick objects from the end of the list if(this.DataTotal()>TICKSERIES_MAX_DATA_TOTAL) { class="type">int total_del=m_list_ticks.Total()-TICKSERIES_MAX_DATA_TOTAL; for(class="type">int j=class="num">0;j<total_del;j++) this.m_list_ticks.Delete(j); } } } } class=class="str">"cmt">//+------------------------------------------------------------------+