使用 CGraphic 开发库实现一个剥头皮市场深度·进阶篇
(2/3)· 两年间 MQL5 图形能力大改,旧版订单簿已不够看,本文带你整合 CPanel 与 CGraphic 做出带分时图的市场深度
用对象事件把按钮挂进图表面板
在 MT5 里做自定义面板,核心是把图表对象事件转成内部事件对象再分发。下面这段把 CHARTEVENT_OBJECT_CLICK 拦截后,按事件 id 生成对应的 CEventChartObjClick,交给 Fon.Event() 处理,之后立刻 delete 掉,避免堆内存泄漏。
class="kw">const class="type">class="kw">string& sparam class=class="str">"cmt">// 字符串类型的事件参数 ) { CEvent* event = NULL; class="kw">switch(id) { case CHARTEVENT_OBJECT_CLICK: event = new CEventChartObjClick(sparam); break; } if(event != NULL) { Fon.Event(event); class="kw">delete event; } }
class="kw">const class="type">class="kw">string& sparam class=class="str">"cmt">// 字符串类型的事件参数 ) { CEvent* event = NULL; class="kw">switch(id) { case CHARTEVENT_OBJECT_CLICK: event = new CEventChartObjClick(sparam); break; } if(event != NULL) { Fon.Event(event); class="kw">delete event; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MarketBookArticlePanel.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 <Panel\ElChart.mqh> class="macro">#include <Panel\Node.mqh> CElChart Fon(OBJ_RECTANGLE_LABEL); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CEnjoy : class="kw">public CElChart { class="kw">protected: class="kw">virtual class="type">void OnClick(class="type">void); class="kw">public: CEnjoy(class="type">void); }; CEnjoy::CEnjoy(class="type">void) : CElChart(OBJ_BUTTON) { } class="type">void CEnjoy::OnClick(class="type">void) { if(Text() != "Enjoy!") Text("Enjoy!"); else Text("MetaQuotes 语言"); } CEnjoy Label; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Fon.Height(class="num">100); Fon.Width(class="num">250); Fon.YCoord(class="num">200); Fon.XCoord(class="num">200); Fon.BackgroundColor(clrWhite); Fon.BorderType(BORDER_FLAT); Fon.BorderColor(clrBlack); Label.Text("Meta Quotes 语言"); Label.BorderType(BORDER_FLAT); Label.BorderColor(clrBlack); Label.Width(class="num">150); Label.YCoord(class="num">240); Label.XCoord(class="num">250); class=class="str">"cmt">//Label.Show(); Fon.AddElement(&Label); Fon.Show(); ChartRedraw(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
「图表点击事件如何转成交易信号」
在 MT5 的 EA 里,图表交互事件靠 OnChartEvent 回调捕获。上面这段把对象点击(CHARTEVENT_OBJECT_CLICK)封装成 CEventChartObjClick 抛给 Fon.Event 处理,是典型的「图形对象→事件对象→调度器」链路。 代码逐行看:函数头四个参数分别是事件 ID、长整型、双精度、字符串参数;switch 只接了对象点击一种,new 出事件对象后 break;判空通过后交给 Fon.Event 并 delete,最后 ChartRedraw 重绘。OnTick 目前是空壳,说明订单逻辑还没接进来,只处理界面事件。 你在 MT5 里建个按钮或水平线,点一下能触发重绘就证明链路通了。外汇和贵金属波动大、杠杆高,这类交互 EA 只解决触发问题,进出场必须另写风控。
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">// 字符串类型的事件参数 ) { CEvent* event = NULL; class="kw">switch(id) { case CHARTEVENT_OBJECT_CLICK: event = new CEventChartObjClick(sparam); break; } if(event != NULL) { Fon.Event(event); class="kw">delete event; } ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA交易订单分时函数 |//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用 CopyTicks 把分时和订单簿钉在一起
订单簿在动荡行情里每秒可能翻好几次,终端只会在 OnBookEvent 里通知你哪个品种变了。之前写的 CMarketBook 类靠 Refresh() 抓当前簿口状态,但 MT5 新版本的市场深度旁边还挂了实时分时图,光有簿口不够,还得把分时流也对上。 MQL5 抓分时有三条路:SymbolInfoTick 拿最后一个分时的参数、EA 在 OnTick / 指标在 OnCalculate 里处理新到达、CopyTicks 和 CopyTicksRange 拉历史。前两种在分时成包到达时会丢数据——比如一笔买 9 个黄金合约可能拆成 3 次以上成交,MetaTrader 5 会收到一包分时,而 OnCalculate 里的 SymbolInfoTick 只返回包里最后一个,前面的全没影。 CopyTicks 不返回“最近 N 个”,而是给指定时刻之后的全部分时,得自己缓存上次数组再做差。难点在于单个分时不能直接比:图 5 里两组红框分时时间、量、方向、价格完全一致,比不出新来。只能比“一组”——若当前数组连续 20 个和之前数组的 20 个相同,就认定这 20 个之后的都是新的。CompareTicks 实际请求最近 20 秒所有分时,用这个滑动匹配法算出增量。 同步框架可以这样想:极短间隔调两次 CopyTicks,返回数组可能有重叠序列(如图 6 中序号 6–14 与另一数组 1–8 重合),差值就是新分时数,无论两数组长度差多少、有无重合都算得准。算完把新分时塞进 CMarketBook 的公有 LastTiks 字段,订单簿每次刷新时刻就自带“这次更新前到现在”的分时列表,簿口和分时流才算真正锁死。后面图形库会用到这份保证。 下面这段指标骨架演示了怎么挂 MarketBook:OnInit 里 SetMarketBookSymbol 绑定品种,OnBookEvent 里先过滤品种再 Refresh,订单簿事件进来就能拿到同步后的状态。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MarketBook.mq5 | 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">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_buffers class="num">0 class="macro">#class="kw">property indicator_plots class="num">0 class="macro">#include <MarketBook.mqh> CMarketBook MarketBook.mqh class="type">class="kw">double fake_buffer[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { MarketBook.SetMarketBookSymbol(Symbol()); class=class="str">"cmt">// 把当前图表品种绑给订单簿对象 class=class="str">"cmt">//--- 指标缓冲区映射 SetIndexBuffer(class="num">0,fake_buffer,INDICATOR_CALCULATIONS); 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) { if(symbol != MarketBook.GetMarketBookSymbol()) class="kw">return; MarketBook.Refresh(); class=class="str">"cmt">// 订单簿变动时刷新,内部已同步分时流
用 tick 比对捞出新分时的实战写法
做深度盘口监控时,最怕重复处理旧 tick。下面这段逻辑核心是把当前接收到的 MqlTick 数组和上一次缓存的做逆向比对,连续匹配上 20 个就判定已对齐历史,剩下没对上的就是新增分时。 CompareTiks 里先用 CopyTicks 拉取最近 20 秒、最多 1000 条全量 tick(t_begin 用 TimeCurrent() 减 20 秒乘 1000 转毫秒)。若本地 m_ticks 还是空,直接整组拷进去当底仓;否则从尾部往前逐条比,limit_comp 写死 20 作为对齐门槛,命中超门槛就 break,效率比全量扫一遍高不少。 对齐完成后,代码用 n_t 记录新 tick 数量,把 n_tiks 整体覆盖进 m_ticks,再按索引截取尾部 n_t 条塞进 LastTicks 供外部读。OnBookEvent 只认自己订阅的品种,触发后调 Refresh 并打印收到的新分时条数——你在 MT5 里挂上这段,控制台就能实时看见「已经收到了 N 个新的分时信息」。 外汇与贵金属 tick 流在高波动时会瞬时拥挤,这种比对法只能降低重复处理概率,不保证零漏tick,实盘前请在模拟环境跑通再上真仓。
ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 比较两个分时集合并寻找新的分时 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMarketBook::CompareTiks(class="type">void) { class="type">MqlTick n_tiks[]; class="type">class="kw">ulong t_begin = (TimeCurrent()-(class="num">1*class="num">20))*class="num">1000; class=class="str">"cmt">// 从20秒之前开始 class="type">int total = CopyTicks(m_symbol, n_tiks, COPY_TICKS_ALL, t_begin, class="num">1000); if(total<class="num">1) { printf("接收分时失败"); class="kw">return; } if(ArraySize(m_ticks) == class="num">0) { ArrayCopy(m_ticks, n_tiks, class="num">0, class="num">0, WHOLE_ARRAY); class="kw">return; } class="type">int k = ArraySize(m_ticks)-class="num">1; class="type">int n_t = class="num">0; class="type">int limit_comp = class="num">20; class="type">int comp_sucess = class="num">0; class=class="str">"cmt">//遍历从上次开始所有接收到的交易 for(class="type">int i = ArraySize(n_tiks)-class="num">1; i >= class="num">0 && k >= class="num">0; i--) { if(!CompareTiks(n_tiks[i], m_ticks[k])) { n_t = ArraySize(n_tiks) - i; k = ArraySize(m_ticks)-class="num">1; comp_sucess = class="num">0; } else { comp_sucess += class="num">1; if(comp_sucess >= limit_comp) break; k--; } } class=class="str">"cmt">//记住接收到的分时 ArrayResize(m_ticks, total); ArrayCopy(m_ticks, n_tiks, class="num">0, class="num">0, WHOLE_ARRAY); class=class="str">"cmt">//计算新分时起始的索引并把它们复制到缓冲区来访问 ArrayResize(LastTicks, n_t); if(n_t > class="num">0) { class="type">int index = ArraySize(n_tiks)-n_t; ArrayCopy(LastTicks, m_ticks, class="num">0, index, n_t); } } 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) { if(symbol != MarketBook.GetMarketBookSymbol()) class="kw">return; MarketBook.Refresh(); class="type">class="kw">string new_tiks = (class="type">class="kw">string)ArraySize(MarketBook.LastTicks); printf("已经收到了 " + new_tiks + " 个新的分时信息"); }
「用 CGraphic 画出第一张报价图」
CGraphic 本质是 CCurve 对象的容器,每条曲线由 X、Y 坐标点构成,按设定类型渲染成线、柱或点。做市场深度面板时,只需用到其中一小部分:两条线分别标卖价与买价水平,再用独立点标最新成交流向。 跑一段最简 EA 就能验证二维线性图:x 数组填 1 到 10,y 数组填 {1,2,3,2,4,3,5,6,4,3},CurveAdd 以 CURVE_LINES 接入后,Create 在图表坐标 (50,60) 起、宽 510 高 300 开画布,Redraw(true) 即出图。把 CURVE_LINES 换成 CURVE_POINTS 并设点型为圆,同一组数据就变成散点图,适合突出离散成交。 曲线对象由 CurveAdd 返回指针,后续类型、点型都靠这个引用改。所有线加完,必须调 Create + Redraw 才会真正绘制。我们做深度项目时,把这串流程封进从 CElChart 派生的 CElTickGraph 类,数据按 tick 节奏特制。 别在 OnInit 之外漏掉 Graph.Update(),否则部分 MT5 版本上画布不跟随重绘。
class="macro">#include <Graphics\Graphic.mqh> CGraphic Graph; class="type">int OnInit() { class="type">class="kw">double x[] = {class="num">1,class="num">2,class="num">3,class="num">4,class="num">5,class="num">6,class="num">7,class="num">8,class="num">9,class="num">10}; class="type">class="kw">double y[] = {class="num">1,class="num">2,class="num">3,class="num">2,class="num">4,class="num">3,class="num">5,class="num">6,class="num">4,class="num">3}; CCurve* cur = Graph.CurveAdd(x, y, CURVE_LINES, "Line"); Graph.CurvePlotAll(); Graph.Create(ChartID(), "Ticks", class="num">0, (class="type">int)class="num">50, (class="type">int)class="num">60, class="num">510, class="num">300); Graph.Redraw(true); Graph.Update(); class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() { }
◍ 把散点图塞进自定义窗口
上面的片段演示了如何用 CCurve 在独立图形对象里画一组离散点。x 是 1 到 10 的等距序列,y 取 {1,2,3,2,4,3,5,6,4,3},这种非单调起伏恰好能看出局部回撤结构。 CURVE_POINTS 模式只画点不连线,配合 POINT_CIRCLE 把每个样本标成圆点,比默认方块更适合肉眼分辨密集成交区。Graph.Create() 里 (int)50,(int)60,510,300 分别是相对图表的左上偏移与宽高,实测在 1920 宽屏下窗口右留白约 1360 像素,不会挡主图 K 线。 最后 Redraw(true) 加 Update() 强制重绘,INIT_SUCCEEDED 返回代表初始化链路没断。外汇与贵金属波动大,这类Overlay图形仅作辅助观测,信号失效概率不低,请自行在 MT5 回测环境验证。
class="type">class="kw">double y[] = {class="num">1,class="num">2,class="num">3,class="num">2,class="num">4,class="num">3,class="num">5,class="num">6,class="num">4,class="num">3}; CCurve* cur = Graph.CurveAdd(x, y, CURVE_POINTS, "Points"); cur.PointsType(POINT_CIRCLE); Graph.CurvePlotAll(); Graph.Create(ChartID(), "Ticks", class="num">0, (class="type">int)class="num">50, (class="type">int)class="num">60, class="num">510, class="num">300); Graph.Redraw(true); Graph.Update(); class="kw">return(INIT_SUCCEEDED); } class="type">class="kw">double x[] = {class="num">1,class="num">2,class="num">3,class="num">4,class="num">5,class="num">6,class="num">7,class="num">8,class="num">9,class="num">10}; class="type">class="kw">double y[] = {class="num">1,class="num">2,class="num">3,class="num">2,class="num">4,class="num">3,class="num">5,class="num">6,class="num">4,class="num">3}; CCurve* cur = Graph.CurveAdd(x, y, CURVE_POINTS, "Points"); cur.PointsType(POINT_CIRCLE);