使用 CGraphic 开发库实现一个剥头皮市场深度·进阶篇
📊

使用 CGraphic 开发库实现一个剥头皮市场深度·进阶篇

(2/3)· 两年间 MQL5 图形能力大改,旧版订单簿已不够看,本文带你整合 CPanel 与 CGraphic 做出带分时图的市场深度

案例拆解 第 2/3 篇
很多人还在用两年前的裸订单簿盯盘,却没注意到 MetaQuotes 已经放出了 CGraphic 和分时历史访问接口。把分时成交流叠到市场深度上,剥头皮时对盘口惯性判断会直接上一个台阶。

用对象事件把按钮挂进图表面板

在 MT5 里做自定义面板,核心是把图表对象事件转成内部事件对象再分发。下面这段把 CHARTEVENT_OBJECT_CLICK 拦截后,按事件 id 生成对应的 CEventChartObjClick,交给 Fon.Event() 处理,之后立刻 delete 掉,避免堆内存泄漏。

MQL5 / C++
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;
  }
}
初始化时 Fon 是一个 OBJ_RECTANGLE_LABEL 容器,宽 250、高 100,坐标定在 (200,200),白底黑边。Label 是 OBJ_BUTTON 子类 CEnjoy 的实例,宽 150 放在 (250,240),通过 Fon.AddElement(&Label) 挂进容器,再 Fon.Show() 和 ChartRedraw() 一次性画出。 CEnjoy 重写了 OnClick:第一次点把文字改成 "Enjoy!",再点切回 "MetaQuotes 语言"。你打开 MT5 把这段塞进 EA 的 OnInit,就能在图表上看到一个可切换文字的按钮,外汇和贵金属图表上跑这套同样有效,但这类界面脚本不改交易逻辑,实盘仍需警惕滑点与断连高风险。

MQL5 / C++
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 只解决触发问题,进出场必须另写风控。

MQL5 / C++
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,订单簿事件进来就能拿到同步后的状态。

MQL5 / C++
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,实盘前请在模拟环境跑通再上真仓。

MQL5 / C++
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 版本上画布不跟随重绘。

MQL5 / C++
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 回测环境验证。

MQL5 / C++
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);
交给小布盯盘看盘口
这些盘口与分时的叠加诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时市场深度与成交节奏,你只需专注进出场决策。

常见问题

CPanel 是从原市场深度图形面板中抽取出的独立轻量库,包含可复用的基础元件类,放在 Include 文件夹,方便其他面板项目直接调用。
小布盯盘内置了对应的盘口与分时可视化模块,逻辑等同本文整合方案,无需自己编译 EA 即可在品种页看到带分时的市场深度。
非常规品种名或存在交易面板时,默认按钮容易盖住图表活动元件,人工设定偏移能避免重叠导致误触或遮挡。
它把近期成交流按时间轴铺开,配合买卖挂单厚度,可能帮助识别扫单与回填节奏,但外汇贵金属杠杆高,仍需以概率视角看待信号。