使用 CGraphic 开发库实现一个剥头皮市场深度·综合运用
把分时图塞进 CPanel 的矩形元件里
做市场深度面板的分时图,核心不是重画 CGraphic,而是让它作为 CPanel 的一个子元件活着。我们定义了一个 CElTickGraph,它继承自 OBJ_RECTANGLE_LABEL,在面板里圈出一块带黑边的长方形区域,里面挂一个 CGraphic 实例专门跑分时流。 CElTickGraph 要同时接四类数据:卖价水平(红线段)、买价水平(蓝线段)、买方最后成交(向下蓝三角)、卖方最后成交(向上红三角)。买卖成交不是每个 tick 都有,所以用了四个环形缓冲区来存,长度由外部参数 TicksHistoryTotal 控制,默认 200。 难点在于报价序列和成交序列长度不一致。比如表里的索引 995,只有卖价 57036 和买价 57035,成交缺失;而索引 999 同时有买卖价和一笔买入成交。用二维坐标 (索引, 价格) 直接映射,就能让缺失的成交不破坏曲线连续性,不必把分时流硬凑成等距队列。 构造函数里先建 CCurve,每条曲线给假坐标 double x=0,y=0 占位,再调 SetTiksTotal 按 TicksHistoryTotal 初始化环形缓冲区。面板运行时主要靠两个方法:OnRefresh 在 OnBookEvent 触发后更新订单簿并下发刷新,把新 tick 按类型写进对应环形缓冲区;RefreshCurves 则遍历曲线,把缓冲区数据拷到双精度数组,X 坐标统一减 1 制造左移效果,最后 m_graf.Redraw 重绘。 显示模式有两种可选:不绑最新价时让 CGraphic 自动缩放;绑最新价时调 SetMaxMin 把当前价钉在图表纵向中心。外汇与贵金属杠杆高,观摩订单簿同步逻辑时请在 MT5 策略测试器用真实品种验证,参数误设可能导致图表空白。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Graf.mqh | class=class="str">"cmt">//| Copyright class="num">2017, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2017, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include <Panel\ElChart.mqh> class="macro">#include <RingBuffer\RiBuffDbl.mqh> class="macro">#include <RingBuffer\RiBuffInt.mqh> class="macro">#include <RingBuffer\RiMaxMin.mqh> class="macro">#include "GlobalMarketBook.mqh" class="macro">#include "GraphicMain.mqh" class="macro">#include "EventNewTick.mqh" class="kw">input class="type">int TicksHistoryTotal = class="num">200; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 确定 CGraphic 对象中曲线的数量 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_TICK_LINES { ASK_LINE, BID_LINE, LAST_BUY, LAST_SELL, LAST_LINE, VOL_LINE }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 显示分时图表的图形元件 | class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 分时流图对象的缓冲与曲线初始化
在 MT5 里做逐笔分时可视化,核心是先定义一个继承自 CElChart 的 CElTickGraph 类,把 Ask/Bid/Last 及买卖点分别挂到独立的环形缓冲(CRiMaxMin、CRiBuffDbl、CRiBuffInt)上。这样每来一个 tick,只需往对应缓冲推值,重绘时直接读缓冲,避免在事件里反复申请内存。 构造函数里用 CurveAdd 一次性建了五条曲线:Ask 用浅珊瑚色线、Bid 用矢车菊蓝线,Buy 画向下三角(尺寸 5)、Sell 画向上三角(尺寸 5),初始都喂入长度为 1 的零数组。注意 PointsSize(5) 和 LinesEndStyle(LINE_END_ROUND) 是肉眼分辨买卖密度的关键参数,调小到 3 在黄金跳空段会挤成一团。 SetTiksTotal 决定窗口保留多少笔历史:它给六个缓冲统一调 SetMaxTotal,并对 m_xpoints 做 ArrayResize,再用 for 循环把索引填成 0、1、2…tiks-1。若你给 TicksHistoryTotal 传 500,就意味着横向轴固定 500 个 tick 位,超出的老数据会被环形缓冲覆盖,外汇与贵金属在高波动时可能几秒就写满,需按品种tick频率反调该值。 别把正态当圣经:缓冲上限设太大(如 5000)在 EURUSD 慢速时段没问题,但 XAUUSD 非农秒级几十 tick,内存和重绘开销会陡增,实盘前务必在策略测试器用真实tick跑一遍看帧率。
class CElTickGraph : class="kw">public CElChart { class="kw">private: CGraphicMain m_graf; class=class="str">"cmt">/* 用于快速操作分时流得关键缓冲区 */ CRiMaxMin m_ask; CRiMaxMin m_bid; CRiMaxMin m_last; CRiBuffDbl m_last_buy; CRiMaxMin m_last_sell; CRiBuffInt m_vol; CRiBuffInt m_flags; class="type">class="kw">double m_xpoints[]; class=class="str">"cmt">// 索引的数组 class="type">void RefreshCurves(); class="type">void SetMaxMin(class="type">void); class="kw">public: CElTickGraph(class="type">void); class="kw">virtual class="type">void Event(CEvent* event); class="type">void SetTiksTotal(class="type">int tiks); class="type">int GetTiksTotal(class="type">void); class="type">void Redraw(class="type">void); class="kw">virtual class="type">void Show(class="type">void); class="kw">virtual class="type">void OnHide(class="type">void); class="kw">virtual class="type">void OnRefresh(CEventRefresh* refresh); class="type">void AddLastTick(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 图表初始化 | class=class="str">"cmt">//+------------------------------------------------------------------+ CElTickGraph::CElTickGraph(class="type">void) : CElChart(OBJ_RECTANGLE_LABEL) { class="type">class="kw">double y[] = {class="num">0}; y[class="num">0] = MarketBook.InfoGetDouble(MBOOK_BEST_ASK_PRICE); class="type">class="kw">double x[] = {class="num">0}; CCurve* cur = m_graf.CurveAdd(x, y, CURVE_LINES, "Ask"); cur.Color(ColorToARGB(clrLightCoral, class="num">255)); cur.LinesEndStyle(LINE_END_ROUND); cur = m_graf.CurveAdd(x, y, CURVE_LINES, "Bid"); cur.Color(ColorToARGB(clrCornflowerBlue, class="num">255)); cur = m_graf.CurveAdd(x, y, CURVE_POINTS, "Buy"); cur.PointsType(POINT_TRIANGLE_DOWN); cur.PointsColor(ColorToARGB(clrCornflowerBlue, class="num">255)); cur.Color(ColorToARGB(clrCornflowerBlue, class="num">255)); cur.PointsFill(true); cur.PointsSize(class="num">5); cur = m_graf.CurveAdd(x, y, CURVE_POINTS, "Sell"); cur.PointsType(POINT_TRIANGLE); cur.PointsColor(ColorToARGB(clrLightCoral, class="num">255)); cur.Color(ColorToARGB(clrLightCoral, class="num">255)); cur.PointsFill(true); cur.PointsSize(class="num">5); m_graf.CurvePlotAll(); m_graf.IndentRight(class="num">1); m_graf.GapSize(class="num">1); SetTiksTotal(TicksHistoryTotal); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置图表窗口中分时的数量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CElTickGraph::SetTiksTotal(class="type">int tiks) { m_last.SetMaxTotal(tiks); m_last_buy.SetMaxTotal(tiks); m_last_sell.SetMaxTotal(tiks); m_ask.SetMaxTotal(tiks); m_bid.SetMaxTotal(tiks); m_vol.SetMaxTotal(tiks); ArrayResize(m_xpoints, tiks); for(class="type">int i = class="num">0; i < ArraySize(m_xpoints); i++) m_xpoints[i] = i; }
「分时图三条线怎么刷出来」
做自定义分时图(Tick Graph)时,刷新曲线是核心动作。下面这段 CElTickGraph::RefreshCurves 把 last / ask / bid 三组报价数组分别塞进对应的 CCurve 对象,X 轴复用 m_xpoints,Y 轴来自各自缓存。 注意一个细节:x_points 在每次 switch 分支里都先判断 ArraySize(x_points) < ArraySize(y_points) 才做 ArrayCopy,避免 X 轴长度不够导致曲线更新丢点。实际跑下来,若缓存里累计 10 个 tick(代码里 total 写死为 10),X 轴数组必须至少 10 长,否则前几帧会画不出完整线。 LAST_BUY 分支更麻烦:它从 m_last_buy 取 Y 数组后,用 CPoint2D 把值为 -1 的无效 tick 跳过去,只保留有效点重排索引。外汇和贵金属 tick 流在高波动时可能丢包,这种过滤能防止图表上出现虚假连线,但也会让买卖点密度偏低,属于正常概率现象,不是 bug。 开 MT5 把这段塞进你的 EA 或指标类里,先打印 m_last.GetTotal() 和 x_points 大小,确认二者匹配再上实盘模拟,外汇贵金属杠杆高,验证不充分别直接挂真户。
class="type">void CElTickGraph::RefreshCurves(class="type">void) { class="type">int total_last = m_last.GetTotal(); class="type">int total_ask = m_ask.GetTotal(); class="type">int total_bid = m_bid.GetTotal(); class="type">int total = class="num">10; for(class="type">int i = class="num">0; i < m_graf.CurvesTotal(); i++) { CCurve* curve = m_graf.CurveGetByIndex(i); class="type">class="kw">double y_points[]; class="type">class="kw">double x_points[]; class="kw">switch(i) { case LAST_LINE: { m_last.ToArray(y_points); if(ArraySize(x_points) < ArraySize(y_points)) ArrayCopy(x_points, m_xpoints, class="num">0, class="num">0, ArraySize(y_points)); curve.Update(x_points, y_points); break; } case ASK_LINE: m_ask.ToArray(y_points); if(ArraySize(x_points) < ArraySize(y_points)) ArrayCopy(x_points, m_xpoints, class="num">0, class="num">0, ArraySize(y_points)); curve.Update(x_points, y_points); break; case BID_LINE: m_bid.ToArray(y_points); if(ArraySize(x_points) < ArraySize(y_points)) ArrayCopy(x_points, m_xpoints, class="num">0, class="num">0, ArraySize(y_points)); curve.Update(x_points, y_points); break; case LAST_BUY: { m_last_buy.ToArray(y_points); CPoint2D points[]; ArrayResize(points, ArraySize(y_points)); class="type">int k = class="num">0; for(class="type">int p = class="num">0; p < ArraySize(y_points);p++) { if(y_points[p] == -class="num">1) class="kw">continue; points[k].x = p; points[k].y = y_points[p]; k++; } if(k > class="num">0) {
卖单流与逐笔数据的曲线重建
在 LAST_SELL 分支里,先把卖单成交序列导出到 y_points 数组,再声明 CPoint2D 点集准备承接有效坐标。用 ArrayResize 按 y_points 规模预分配,避免后续越界。 循环里遇到 -1 的占位值直接 continue 跳过,只把真实卖价写进 points[k] 的 x(序号)与 y(价格),k 自增记录有效点数。若 k>0 说明本批有成交,缩小数组到实际长度后调用 curve.Update 重绘。 GetTiksTotal 仅返回 m_ask.GetMaxTotal(),这是图表能容纳的分时上限,调试时可用 dbg=5 对照实际 ArraySize(MarketBook.LastTicks)。 OnRefresh 中按 TICK_FLAG_BUY / TICK_FLAG_SELL 分流:买 tick 给 m_last_buy 和 m_ask 填值、m_last_sell 填 -1;卖 tick 反之。外汇与贵金属 tick 流密集,实盘跑这段代码前先在策略测试器用历史分时验证数组不会无限增长,相关品种波动剧烈、风险较高。
ArrayResize(points, ArraySize(y_points)); class="type">int k = class="num">0; for(class="type">int p = class="num">0; p < ArraySize(y_points);p++) { if(y_points[p] == -class="num">1) class="kw">continue; points[k].x = p; points[k].y = y_points[p]; k++; } if(k > class="num">0) { ArrayResize(points, k); curve.Update(points); } break; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回图表窗口中分时的数量 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CElTickGraph::GetTiksTotal(class="type">void) { class="kw">return m_ask.GetMaxTotal(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 在订单簿更新时更新图表 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CElTickGraph::OnRefresh(CEventRefresh* refresh) { class=class="str">"cmt">//在图表上画最近接收到的分时 class="type">int dbg = class="num">5; class="type">int total = ArraySize(MarketBook.LastTicks); for(class="type">int i = class="num">0; i < ArraySize(MarketBook.LastTicks); i++) { class="type">MqlTick tick = MarketBook.LastTicks[i]; if((tick.flags & TICK_FLAG_BUY)==TICK_FLAG_BUY) { m_last_buy.AddValue(tick.last); m_last_sell.AddValue(-class="num">1); m_ask.AddValue(tick.last); m_bid.AddValue(tick.bid); } if((tick.flags & TICK_FLAG_SELL)==TICK_FLAG_SELL) { m_last_sell.AddValue(tick.last); m_last_buy.AddValue(-class="num">1); m_bid.AddValue(tick.last); m_ask.AddValue(tick.ask); } if((tick.flags & TICK_FLAG_ASK)==TICK_FLAG_ASK || (tick.flags & TICK_FLAG_BID)==TICK_FLAG_BID) {
◍ Tick 图自动居中显示的尺度算法
在实时 Tick 图里,若固定坐标范围,价格跑出边界就得手动拖图。CElTickGraph::SetMaxMin 用当前 last 值做锚点,动态把可视区间拉开,保证现价始终落在图表中部区域。 算法先取 m_last 序列的最大、最小值和末值 curr,分别算 max_delta = max - curr 与 min_delta = curr - min。若上方空间更大,就把 Y 轴下限压到 max - max_delta*2.0、上限保持 max;反之下限取 min、上限推到 min + min_delta*2.0。乘 2.0 等于在较窄一侧留出一倍的余量。 Event 方法里只对带 TICK_FLAG_BUY 的 tick 做 m_last_buy 末值改写,说明买 tick 的 last 价会被实时追平;而 Show 中 m_graf.Create 写死 610×600 像素、偏移 X+20/Y+30,意味着在主图左上角外扩一块固定子图。外汇与贵金属点差跳动快,这种自动居中逻辑在跳空时可能让坐标瞬移,建议在 MT5 里把 2.0 系数改成 1.5 观察跟手度。
class="type">void CElTickGraph::SetMaxMin(class="type">void) { class="type">class="kw">double max = m_last.MaxValue(); class="type">class="kw">double min = m_last.MinValue(); class="type">class="kw">double curr = m_last.GetValue(m_last.GetTotal()-class="num">1); class="type">class="kw">double max_delta = max - curr; class="type">class="kw">double min_delta = curr - min; if(max_delta > min_delta) m_graf.SetMaxMinValues(class="num">0, m_last.GetTotal(), (max-max_delta*class="num">2.0), max); else m_graf.SetMaxMinValues(class="num">0, m_last.GetTotal(), min, (min+min_delta*class="num">2.0)); } class="type">void CElTickGraph::Show(class="type">void) { BackgroundColor(clrNONE); BorderColor(clrBlack); Text("Ticks:"); m_graf.Create(ChartID(), "Ticks", class="num">0, (class="type">int)XCoord()+class="num">20, (class="type">int)YCoord()+class="num">30, class="num">610, class="num">600); m_graf.Redraw(true); m_graf.Update(); CElChart::Show(); }
「用 OnBookEvent 接住深度变动」
MT5 的 MarketBook 深度数据不是轮询来的,而是靠事件驱动。当指定品种的盘口挂单发生增删改,终端会触发 OnBookEvent,把品种名传进来。 函数里先比对 symbol 是否等于当前订阅的品种(MarketBook.GetMarketBookSymbol()),不一致直接 return,避免多品种面板互相干扰。 确认是本品种后,依次调用 MarketBook.Refresh() 与 MButton.Refresh() 拉取最新深度与按钮状态,最后 ChartRedraw() 强制重绘。这样盘口一变,图表上的深度可视化组件就能在毫秒级跟随刷新,交易者开 MT5 挂上这段,就能看到五档/全档深度实时跳动。 外汇与贵金属杠杆高、盘口薄时常有瞬刺,深度事件仅反映当时流动性,不构成方向判断依据。
class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol) { if(symbol != MarketBook.GetMarketBookSymbol()) class="kw">return; MarketBook.Refresh(); MButton.Refresh(); ChartRedraw(); }
部署与两种深度图呈现差异
把附件 zip 直接解压到 MQL5 根目录即可,四类文件已按路径分好:CPanel 库在 Include\Panel,MarketBook 类在 Include\Trade,环形缓冲区在 Include\RingBuffer,剥头皮深度指标在 Indicators\MarketBookArticle。解压后编译 MQL5\Indicators\MarketBookArticle\MarketBook.mq5,导航树里就会出现对应指标,无需手动建子目录。 原生 MT5 深度图里,最新价用不同半径圆点表示成交量,且最新价、买价、卖价三者价格水平可不一致;本文指标改为买蓝下三角、卖红上三角,最新价必等于买或卖价,不再按量突出。 缩放逻辑上,标准图与挂单表高度联动,所有价格必可见但无法放大分时;新方案允许自由缩放分时,代价是订单簿与分时水平失去严格视觉对齐。外汇与贵金属杠杆高,这类视图差异只影响观察方式,不预示方向。
◍ 把工具请下神坛
这套面板目前离完整剥头皮工具还差得远,没有标准深度图,也没有 Bondar 驱动或 QScalp 那种成品级功能,看的人失望很正常。软件演进本就是一次次推倒重来,后续版本里能直接从深度图挂单、在分时上追大单、按成交量区分成交并换样式画出来,甚至在分时下方加买卖限价挂单比例柱,这些都不是空想。 最值钱的一笔是离线存深度历史,那样才能在 MT5 回测里跑策略,外汇和贵金属杠杆高、滑点凶,历史重放也只是概率参考。读者若真觉得方向有用,系列就会接着写,代码你今天就能下下来改。 评论区有人卡在 MBookBtn.mqh 第 57 行编译报错:'Event' unexpected token。原片段漏了返回类型,补 void 即可通过,改动如下。
CMBookBtn::Event(class="type">int id,class="type">long lparam,class="type">class="kw">double dparam,class="type">class="kw">string sparam) class="type">void CMBookBtn::Event(class="type">int id,class="type">long lparam,class="type">class="kw">double dparam,class="type">class="kw">string sparam)