MQL5 酷客宝典: 实现您自己的市场深度·进阶篇
(2/3)·标准 DOM 只够看不够用,本文把事件模型、封装类与面板绘制一次打通
DOM 快照怎么从事件里抠出来
在 MT5 里抓深度-of-market 数据,入口是 OnBookEvent(const string &symbol) 这个系统回调。每次某交易品种盘口变动,终端就会触发它,你只能拿到 symbol 名,真正的买卖挂单数组得从自己维护的 BookOnSi.MarketBook[] 里读。 下面这段是最小可用骨架:回调里直接取 MarketBook[0] 塞进 MqlBookInfo 结构,随后用 InfoGetDouble(MBOOK_BEST_ASK_PRICE) 拿最优卖价。注意 MarketBook[0] 未必是卖单开头,只是数组首元素,索引语义要靠后面枚举厘清。
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol) { class=class="str">"cmt">//--- MqlBookInfo info = BookOnSi.MarketBook[class="num">0]; } class="type">class="kw">double best_ask = BookOnSi.InfoGetDouble(MBOOK_BEST_ASK_PRICE);
class="type">void CMarketBook::SetBestAskAndBidIndex(class="type">void) { if(!FindBestBid()) { class="type">int bookSize = ArraySize(MarketBook); for(class="type">int i = class="num">0; i < bookSize; i++) { if((MarketBook[i].type == BOOK_TYPE_BUY) || (MarketBook[i].type == BOOK_TYPE_BUY_MARKET)) { m_best_ask_index = i-class="num">1; FindBestBid(); class="kw">break; } } } }
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol) { class=class="str">"cmt">//--- MqlBookInfo info = BookOnSi.MarketBook[class="num">0]; } class="type">class="kw">double best_ask = BookOnSi.InfoGetDouble(MBOOK_BEST_ASK_PRICE); enum ENUM_MBOOK_INFO_INTEGER { MBOOK_BEST_ASK_INDEX, class=class="str">"cmt">// 最佳卖出价索引 MBOOK_BEST_BID_INDEX, class=class="str">"cmt">// 最佳买入价索引 MBOOK_LAST_ASK_INDEX, class=class="str">"cmt">// 最坏卖出价索引 MBOOK_LAST_BID_INDEX, class=class="str">"cmt">// 最坏买入价索引 MBOOK_DEPTH_ASK, class=class="str">"cmt">// 卖出级别数量 MBOOK_DEPTH_BID, class=class="str">"cmt">// 买入级别数量 MBOOK_DEPTH_TOTAL class=class="str">"cmt">// DOM 级别总数 }; enum ENUM_MBOOK_INFO_DOUBLE { MBOOK_BEST_ASK_PRICE, class=class="str">"cmt">// 最佳卖出价 MBOOK_BEST_BID_PRICE, class=class="str">"cmt">// 最佳买入价 MBOOK_LAST_ASK_PRICE, class=class="str">"cmt">// 最坏卖出价 MBOOK_LAST_BID_PRICE, class=class="str">"cmt">// 最坏买入价 MBOOK_AVERAGE_SPREAD class=class="str">"cmt">// 卖出价与买入价之间的平均点差 }; class="type">void CMarketBook::SetBestAskAndBidIndex(class="type">void) { if(!FindBestBid()) { class="type">int bookSize = ArraySize(MarketBook); for(class="type">int i = class="num">0; i < bookSize; i++) { if((MarketBook[i].type == BOOK_TYPE_BUY) || (MarketBook[i].type == BOOK_TYPE_BUY_MARKET)) { m_best_ask_index = i-class="num">1; FindBestBid(); class="kw">break; } } } }
◍ 从挂单簿切片算量价偏离
深度簿里 best ask 之后紧邻的那一档未必就是 best bid,代码先校验 m_best_ask_index 指向的必须是 BOOK_TYPE_SELL 或 BOOK_TYPE_SELL_MARKET,否则直接返回 false 认为结构无效。 确认卖侧后,用 bestBid = m_best_ask_index+1 去取下一档,再判断它是否属于 BOOK_TYPE_BUY / BOOK_TYPE_BUY_MARKET;命中才把 m_best_bid_index 记下并返回 true,这一步保证了 ask/bid 配对顺序不被乱序簿打乱。 GetDeviationByVol 做的是另一件事:给定 vol 手数,沿 ask 或 bid 方向把挂单 volume 逐档乘 price 累加,直到 vol 被吃光。若循环结束 volume_exe 仍大于 0,说明簿上流动性不够吞下该量,函数返回 -1.0 表示偏离无限。 正常走完时 avrg_price 除以 vol 得到平均成交价,这个数值和现价的差就是量加权偏离。外汇与贵金属盘口薄时常抽空,返回 -1.0 的概率不低,实盘调用前应先判返回值再决定策略。
(MarketBook[m_best_ask_index].type == BOOK_TYPE_SELL ||
MarketBook[m_best_ask_index].type == BOOK_TYPE_SELL_MARKET);
if(!isBestAsk)class="kw">return false;
class="type">int bestBid = m_best_ask_index+class="num">1;
class="type">bool isBestBid = bestBid >= class="num">0 && bestBid < m_depth_total &&
(MarketBook[bestBid].type == BOOK_TYPE_BUY ||
MarketBook[bestBid].type == BOOK_TYPE_BUY_MARKET);
if(isBestBid)
{
m_best_bid_index = bestBid;
class="kw">return true;
}
class="kw">return false;
}
class="type">int bestBid = m_best_ask_index+class="num">1;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| MarketBook 的方向 |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MBOOK_SIDE
{
MBOOK_ASK, class=class="str">"cmt">// 卖出方
MBOOK_BID class=class="str">"cmt">// 买入方
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取交易量的偏离值。返回 -class="num">1.0 如果偏离是无限 (流动性不足) |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CMarketBook::GetDeviationByVol(class="type">long vol, ENUM_MBOOK_SIDE side)
{
class="type">int best_ask = InfoGetInteger(MBOOK_BEST_ASK_INDEX);
class="type">int last_ask = InfoGetInteger(MBOOK_LAST_ASK_INDEX);
class="type">int best_bid = InfoGetInteger(MBOOK_BEST_BID_INDEX);
class="type">int last_bid = InfoGetInteger(MBOOK_LAST_BID_INDEX);
class="type">class="kw">double avrg_price = class="num">0.0;
class="type">long volume_exe = vol;
if(side == MBOOK_ASK)
{
for(class="type">int i = best_ask; i >= last_ask; i--)
{
class="type">long currVol = MarketBook[i].volume < volume_exe ?
MarketBook[i].volume : volume_exe ;
avrg_price += currVol * MarketBook[i].price;
volume_exe -= MarketBook[i].volume;
if(volume_exe <= class="num">0)class="kw">break;
}
}
else
{
for(class="type">int i = best_bid; i <= last_bid; i++)
{
class="type">long currVol = MarketBook[i].volume < volume_exe ?
MarketBook[i].volume : volume_exe ;
avrg_price += currVol * MarketBook[i].price;
volume_exe -= MarketBook[i].volume;
if(volume_exe <= class="num">0)class="kw">break;
}
}
if(volume_exe > class="num">0)
class="kw">return -class="num">1.0;
avrg_price/= (class="type">class="kw">double)vol;「从深度快照里拆出偏离值」
上面那段返回逻辑干的事很直接:给定挂单方向,算出平均价与最优买卖价的差。side 为 MBOOK_ASK 时,deviation = avrg_price - MarketBook[best_ask].price;反之用最优买价减平均价,结果就是单边的价格偏离距离。 真正要把深度跑起来,得先挂上 CMarketBook 类。用 #include <Trade\MarketBook.mqh> 引头文件,再以 CMarketBook Book(Symbol()) 绑定当前品种,行情刷新靠 Book.Refresh() 拉取。 PrintMbookInfo() 里几个整型字段是盯盘第一手数据:MBOOK_DEPTH_TOTAL 是深度总档位,MBOOK_DEPTH_ASK / MBOOK_DEPTH_BID 分开看买卖侧厚度,MBOOK_BEST_ASK_INDEX 和 MBOOK_BEST_BID_INDEX 定位最优价在数组里的下标。双精度里 MBOOK_BEST_ASK_PRICE、MBOOK_BEST_BID_PRICE、MBOOK_LAST_ASK_PRICE 分别对应最优卖、最优买和末档卖价。 外汇与贵金属的盘口深度在新闻市容易瞬间抽薄,用这套接口读到的档位数量可能为 0,实盘验证前先在模拟盘跑 PrintMbookInfo 看品种实际供给。
class="type">class="kw">double deviation = class="num">0.0; if(side == MBOOK_ASK) deviation = avrg_price - MarketBook[best_ask].price; else deviation = MarketBook[best_bid].price - avrg_price; class="kw">return deviation; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestMarketBook.mq5 | 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">#class="kw">property version "class="num">1.00" class="macro">#include <Trade\MarketBook.mqh> class=class="str">"cmt">// 包含 CMarketBook 类 CMarketBook Book(Symbol()); class=class="str">"cmt">// 以当前金融工具初始化类 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { PrintMbookInfo(); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序开始函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 打印 MarketBook 信息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PrintMbookInfo() { Book.Refresh(); class=class="str">"cmt">// 更新市场深度状态。 class=class="str">"cmt">/* 获取基础统计 */ class="type">int total=Book.InfoGetInteger(MBOOK_DEPTH_TOTAL); class="type">int total_ask = Book.InfoGetInteger(MBOOK_DEPTH_ASK); class="type">int total_bid = Book.InfoGetInteger(MBOOK_DEPTH_BID); class="type">int best_ask = Book.InfoGetInteger(MBOOK_BEST_ASK_INDEX); class="type">int best_bid = Book.InfoGetInteger(MBOOK_BEST_BID_INDEX); printf("市场深度总数: "+(class="type">class="kw">string)total); printf("卖出价位数量: "+(class="type">class="kw">string)total_ask); printf("买入价位数量: "+(class="type">class="kw">string)total_bid); printf("最佳卖出价索引: "+(class="type">class="kw">string)best_ask); printf("最佳买入价索引: "+(class="type">class="kw">string)best_bid); class="type">class="kw">double best_ask_price = Book.InfoGetDouble(MBOOK_BEST_ASK_PRICE); class="type">class="kw">double best_bid_price = Book.InfoGetDouble(MBOOK_BEST_BID_PRICE); class="type">class="kw">double last_ask = Book.InfoGetDouble(MBOOK_LAST_ASK_PRICE);
从市场深度回读里抓报价边界
在 MT5 的 Market Book 回调里,除了拿最优买卖价,还能顺手把边界价格和平均点差读出来。下面这段把最后成交买价、平均点差以及四个极端报价打印到日志,方便你核对盘口厚度。 double last_bid = Book.InfoGetDouble(MBOOK_LAST_BID_PRICE); double avrg_spread = Book.InfoGetDouble(MBOOK_AVERAGE_SPREAD); printf("最佳卖出价: " + DoubleToString(best_ask_price, Digits())); printf("最佳买入价: " + DoubleToString(best_bid_price, Digits())); printf("最坏卖出价: " + DoubleToString(last_ask, Digits())); printf("最坏买入价: " + DoubleToString(last_bid, Digits())); printf("平均点差: " + DoubleToString(avrg_spread, Digits())); } 逐行看:前两行用 InfoGetDouble 取末次买价和平均点差;随后五个 printf 分别输出最优卖、最优买、最差卖、最差买和平均点差,Digits() 控制小数位跟品种精度对齐。 实际在 OFZ2-9.15 的 D1 测试里,2015.06.16 17:13:23 那一刻输出显示:最优买 9840、最优卖 9910、最差买 9831、最差卖 9999,平均点差 70。盘口共 9 档,卖价 7 档、买价 2 档,买卖厚度明显不对称,这类倾斜在贵金属跳空前后也常出现,外汇和贵金属杠杆高,盘口薄时滑点概率会放大。 把这段代码塞进你的 BookEvent 回调,跑一天就知道自己常做的品种盘口边界长什么样,不用凭感觉估滑点。
class="type">class="kw">double last_bid = Book.InfoGetDouble(MBOOK_LAST_BID_PRICE); class="type">class="kw">double avrg_spread = Book.InfoGetDouble(MBOOK_AVERAGE_SPREAD); printf("最佳卖出价: " + DoubleToString(best_ask_price, Digits())); printf("最佳买入价: " + DoubleToString(best_bid_price, Digits())); printf("最坏卖出价: " + DoubleToString(last_ask, Digits())); printf("最坏买入价: " + DoubleToString(last_bid, Digits())); printf("平均点差: " + DoubleToString(avrg_spread, Digits())); }
◍ 把市场深度做成一个可隐藏的图表面板
在 MT5 里做 DOM 面板,基础选指标而不是 EA,原因很直接:一个图表只能挂一个 EA,但指标可以无限叠加。若用 EA 做面板,同一图表就没法再跑交易 EA,实战中极不方便。面板默认放在图表左上角,避开右上角交易 EA 的图标区域,用同一个箭头按钮切换显示和隐藏。 指标代码里 OnCalculate 留空即可,因为面板不消费价格序列,indicator_plots 设 0。真正驱动它的是 OnBookEvent,启动时用 MarketBookAdd 订阅当前图表品种,才能收到深度变动。 初版只用一个 CBookPanel 类,里面先放箭头和 'MarketBook' 标签。所有元素都从 CNode 基类派生,基类自带 Show / Hide 和唯一实例名,方便用标准函数建图形对象。此时点击箭头只会弹出灰色画布,还没填数据。 接着在 OnChartEvent 里接管点击,CNode 扩展出 CArrayObj 容器装子节点。单元格用 CBookCeil 类,建对象时指定类型:买限价蓝、卖限价粉,文本用 OBJ_TEXT_LABEL,底色用 OBJ_RECTANGLE_LABEL。CreateCeils 按 DOM 索引动态生成单元,编译后就能看到带价位和量的初版表格。 直接在单元格 Refresh 里算直方条最省事,但 40 档深度每档都全量迭代,一次刷新就 800 次循环,太浪费。改法是把最大量计算和买/卖总量统计塞进 CMarketBook 内的 CBookCalculation 子类,用 m_calculate 标志做延迟计算——只有 Reset 被调用才重算,两次刷新间最多迭代一次。 优化后把直方条宽度计算挪到面板层,CBookLine 子类按 best_bid 索引乘 15 像素再加 19 像素常量画分割线。莫斯科交易所还推送实时限价单数和交易量,通过 SymbolInfoInteger / Double 取,再包进 CMarketBook 的 InfoGet 系列,集中访问更顺手。外汇和贵金属波动剧烈、杠杆高风险大,这类 DOM 面板仅作盘口观察,不构成方向判断。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MarketBook.mq5 | 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."
「把深度报价挂上图表事件链」
想在 MT5 主图直接看市场深度,核心不是画指标线,而是把 MarketBook 事件接进图表生命周期。下面这段指标骨架用 indicator_plots 0 声明自己不画任何缓冲线,纯粹当事件容器用。 OnInit 里只做一件事:MarketBookAdd(Symbol()) 把当前品种订阅进深度簿。返回 INIT_SUCCEEDED 后,只要盘口有变动,终端就会触发 OnBookEvent,这里转手调 Panel.Refresh() 重绘面板。 OnChartEvent 负责把点击、拖拽等 GUI 交互透传给 Panel.Event(),随后 ChartRedraw() 强制刷新。OnCalculate 故意返回 rates_total 而不做价格计算——它在本例只是保活接口,真实渲染全在面板类里。 外汇与贵金属盘口薄时常抽空,深度数据延迟或断流都可能发生,挂此面板前先确认券商开了 MarketBook 权限,否则 OnBookEvent 不会触发。
class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_plots class="num">0 class="macro">#include <Trade\MarketBook.mqh> class="macro">#include "MBookPanel.mqh" CBookPanel Panel; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 指标缓存区映射 MarketBookAdd(Symbol()); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MarketBook 变化事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol) { Panel.Refresh(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 图表事件 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(class="kw">const class="type">int id, class=class="str">"cmt">// 事件表示符 class="kw">const class="type">long& lparam, class=class="str">"cmt">// 长整形事件参数 class="kw">const class="type">class="kw">double& dparam, class=class="str">"cmt">// 双精度形事件参数 class="kw">const class="type">class="kw">string& sparam) class=class="str">"cmt">// 字符串形事件参数 { Panel.Event(id,lparam,dparam,sparam); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { class=class="str">"cmt">//--- class=class="str">"cmt">//--- 返回 prev_calculated 值用于下次调用 class="kw">return(rates_total); } class=class="str">"cmt">//+------------------------------------------------------------------+