MеtaTrader5 图表上的水平示意图·进阶篇
📊

MеtaTrader5 图表上的水平示意图·进阶篇

(2/3)· 面对几十根水平柱状线,手动管理名称与坐标只会越改越乱,本文给出可复用的对象数组方案

实战向 第 2/3 篇
很多人写分布类指标时,直接循环创建一堆OBJ_RECTANGLE却懒得统一命名,结果图表上对象越堆越多,想删都删不干净。更糟的是把时间坐标写死,窗口平移后示意图直接错位。水平示意图看似简单,对象一多就成了事故源。

◍ 历史示意图只算一次,当前柱交给定时器

这类指标把活儿拆成两段:一段是把除当前柱以外的示意图一次性算完画好,之后终端自己管,不用再动;另一段是盯当前周期包含当下时间的那根柱,定期重算重画,通常塞进 OnTimer() 里跑。 历史部分放在 OnCalculate() 的首次触发里做。从第一根柱循环到 MaxHDcount,每根在 TargetPeriod 时间帧上算一遍,算通了就发命令让管理器把新数据绘成示意图。循环结束调一次 ChartRedraw(),再置一个 bCreateHis 标志,告诉系统这部分活儿干完了。 当前柱的维护逻辑很直白:在 SourcePeriod 等新蜡烛→算数→把数据丢给当前周期的示意图去增绘。原作者觉得太浅没给伪代码,但节奏就是「等新K线来了才动」,别在每 tick 里空转。 下面这段是 OnCalculate 里历史绘制的骨架,注意 prev_calculated==0 或 rates_total 增长超过 1 才进历史分支,否则只走已建标志的跳过逻辑。

MQL5 / C++
<span class="keyword">class="type">int</span> <span class="functions">OnCalculate</span>(...)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="keyword">if</span> (prev_calculated == <span class="number">class="num">0</span> || rates_total &gt; prev_calculated + <span class="number">class="num">1</span>) {
&nbsp;&nbsp; }<span class="keyword">else</span> {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!bCreateHis) 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> br = <span class="number">class="num">1</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">while</span> (br &lt; MaxHDcount) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(Calculate <span class="keyword">for</span> bar "br") 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
                  sdata.bRemovePrev = class="kw">false;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">Print</span>("Send data to the new Diagramm");
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ChartRedraw</span>();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bCreateHis = <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp; }&nbsp;&nbsp;
&nbsp;&nbsp; <span class="keyword">class="kw">return</span>(rates_total);
&nbsp;&nbsp;}&nbsp;&nbsp;

「用 CHDiags 把示意图属性收口到一处」

画水平示意图时,如果每根线都各自存一份绑定方式、颜色、线宽,内存和逻辑都会乱。CHDiags 管理器自己不画图形,只持有一组统一属性,以及指向各个 CHDiagDraw 对象的指针数组 m_pHdArray[],下属示意图共享同一套外观参数。 管理器里 m_position、m_style、m_dir 决定绑定与样式,m_iHorSize 是水平最大尺寸,m_cColor 和 m_iWidth 控制颜色线宽;m_id 给多管理器场景做唯一标识,m_chart 默认 0 即当前图表,m_imCount 限制图表上示意图总数。m_BaseName 最关键——所有对象名都基于它拼接,避免重名创建失败。 m_dtVis 是个实用开关:构造函数里用 bool 控制“仅在终端可见蜡烛上显示”,默认 true,位于该时间戳左侧的示意图不创建,能减轻终端负载。下面的 AddHDiag 方法先拦掉过期时间,再查是否超 m_imCount,最后扩容数组并 new 出对象,返回索引或 -1。 除了增删示意图,CHDiags 还提供 RemoveDiag 按名彻底删除、RemoveContext 只清图形基元保留空壳、SetData 用结构填数据绘图,以及 Align 在 CHARTEVENT_CHART_CHANGE 里把绑边示意图拉回原位。 别让 base 名撞车 m_BaseName 是静态 const,多个管理器实例若用同一 base 前缀,对象名会冲突导致创建返回 NULL。开 MT5 写个测试 EA,故意让两个 CHDiags 共用 base 名,跑 AddHDiag 看哪次返回 -1,比读文档直观。

MQL5 / C++
class="kw">private: 
                HD_POSITION m_position; 
                HD_STYLE    m_style; 
                HD_DIRECT   m_dir; 
                class="type">int         m_iHorSize; 
                class="type">class="kw">color       m_cColor; 
                class="type">int         m_iWidth; 
                class="type">int         m_id;
                class="type">int         m_imCount; 
                class="type">long        m_chart; 
                class="type">class="kw">datetime    m_dtVis; 
  class="kw">static class="kw">const class="type">class="kw">string       m_BaseName; 
                CHDiagDraw* m_pHdArray[];
class="type">int CHDiags::AddHDiag(class="type">class="kw">datetime dtCreatedIn)
class="type">int CHDiags::AddHDiag(class="type">class="kw">datetime dtCreatedIn) {
  if(dtCreatedIn < m_dtVis ) class="kw">return (-class="num">1);
  class="type">int iSize = ArraySize(m_pHdArray);
  if (iSize >= m_imCount) class="kw">return (-class="num">1);
  if (ArrayResize(m_pHdArray,iSize+class="num">1) == -class="num">1) class="kw">return (-class="num">1);
  m_pHdArray[iSize] = new CHDiagDraw(GetPointer(this) );
  if (m_pHdArray[iSize] == NULL) {
    class="kw">return (-class="num">1);
  }
  class="kw">return (iSize);
}class=class="str">"cmt">//AddHDiag()
  class="type">bool        RemoveDiag(class="kw">const class="type">class="kw">string& dname);
  class="type">void        RemoveContext(class="type">int index, class="type">bool bRemovePrev);
  class="type">int         SetData(class="kw">const HDDATA& hddata, class="type">int index); 
  class="type">void        Align();

CHDiagDraw 类怎么把逐笔量画成图元

在 MT5 里做自定义水平示意图,核心类可以从 CObject 派生,命名为 CHDiagDraw。构造函数里保存管理器指针到 m_pProp,同时定好该示意图的唯一名称,后续所有图元都靠这个命名空间避免撞车。 Type() 方法直接映射管理器里的样式枚举:HD_RECTANGLE 返回 OBJ_RECTANGLE,HD_LINE 返回 OBJ_TREND,其余返回 0。这意味着示意图类型和应用图形基元类型是严格绑定的,改样式不用动绘制逻辑。 真正干活的是 SetData,由管理器调用,传入 HDDATA 结构。结构里 pcur[] 是价格绑定数组,vcur[] 是同长度的逐笔报价交易量数组,prsize 决定画几个基元,dtLastTime 优先于管理器参数做时间锚点,bRemovePrev 为 true 会清掉旧图元重画、为 false 则“遗忘”旧的只叠新的。 SetData 内部先按 prmax 和 GetHDHorSize 算缩放 dZoom,再用 MathCeil(vcur[i]/dZoom) 把交易量四舍五入成蜡烛数。循环里根据 HD_LEFT / HD_RIGHT / HD_CNDLE 三种位置模式算出每个基元的结束蜡烛索引与时间,最后用 GetUnicObjNameByPart 生成唯一名并创建。方法返回 prsize 即示意图级数。 别把正态当圣经:dZoom 因为图表以蜡烛为整数,强制 MathCeil 会带来向上偏差,小交易量级别可能画出比实际偏宽的柱。 用户从不直接调 CHDiagDraw 的方法,全部走水平示意图管理器。完整类、结构、枚举代码在 HDiagsE.mqh,开 MT5 把该文件挂到 EA 里就能改 prsize 和样式验证密度。

MQL5 / C++
class="type">int CHDiagDraw::Type() class="kw">const
  {
   class="kw">switch (m_pProp.GetHDStyle() ) {
     case HD_RECTANGLE:
       class="kw">return (OBJ_RECTANGLE);
     case HD_LINE:
       class="kw">return (OBJ_TREND);
   }
   class="kw">return (class="num">0);
  }
class="type">int        SetData(class="kw">const HDDATA& hddata);
class="kw">struct HDDATA
{
   class="type">class="kw">double   pcur[];
   class="type">class="kw">double   prmax;
   class="type">class="kw">double   prmin;   
   class="type">int      prsize;
   class="type">class="kw">double   vcur[];
   class="type">class="kw">datetime dtLastTime;
class="type">bool     bRemovePrev;
};
class="type">int CHDiagDraw::SetData(class="kw">const HDDATA &hddata)
  {
   RemoveContext(hddata.bRemovePrev);
   if(hddata.prmax == class="num">0.0 || hddata.prsize == class="num">0) class="kw">return (class="num">0);
   class="type">class="kw">double dZoom=NormalizeDouble(hddata.prmax/m_pProp.GetHDHorSize(),Digits());
   if(dZoom==class="num">0.0) dZoom=class="num">1;
   ArrayResize(m_hItem,hddata.prsize);
   m_hItemCount=hddata.prsize;
   class="type">int iTo,t;
   class="type">class="kw">datetime dtTo;
   class="type">class="kw">string n;
   class="type">class="kw">double dl=hddata.pcur[class="num">0],dh=class="num">0;

   GetBorders(hddata);
   for(class="type">int i=class="num">0; i<hddata.prsize; i++)
     {
      if (hddata.vcur[i] == class="num">0) class="kw">continue;
      t=(class="type">int)MathCeil(hddata.vcur[i]/dZoom);
      class="kw">switch(m_pProp.GetHDPosition())
        {
         case HD_LEFT:
         case HD_RIGHT:
           iTo=m_iFrom+m_pProp.GetHDPosition()*t;
           dtTo=m_pProp.GetBarTime(iTo);
           class="kw">break;
         case HD_CNDLE:
           iTo   = m_iFrom + m_pProp.GetHDDirect() * t;
           dtTo  = m_pProp.GetBarTime(iTo);
           class="kw">break;
         class="kw">default:
           class="kw">return (-class="num">1);
        }class=class="str">"cmt">//class="kw">switch (m_pProp.m_position)
      n=CHDiags::GetUnicObjNameByPart(m_pProp.GetChartID(),m_hname,m_iNameBase);
      m_iNameBase++;
      class="type">bool b=class="kw">false;
      class="kw">switch(m_pProp.GetHDStyle())
        {

◍ 水平线还是矩形块,由样式分支决定

这段绘制逻辑在循环里按 m_style 分流:遇到 HD_LINE 就调 CHDiags::ObjectCreateRay 画一条贯穿左右边界的水平射线,价格取 hddata.pcur[i];遇到 HD_RECTANGLE 则用 OBJ_RECTANGLE 拉出一个区间块,上下边由相邻两个 pcur 取中值拼出。 矩形分支里 dl 与 dh 的赋值值得盯一下:dl 只在首次或数值跳变时接 dh,而 dh 对最后一根用 pcur[i]、其余用 pcur[i] 与 pcur[i+1] 的均值,意味着块高随样本滑动。 对象创建失败会 Print 错误并返回 -1,成功才把句柄存进 m_hItem[i] 并批量设色、设宽、禁选、置底。外汇与贵金属图表加载此类指标时波动剧烈,对象重绘可能频繁触发,属高风险环境下的视觉辅助,不代表任何方向判定。

MQL5 / C++
      case HD_LINE:
            b=CHDiags::ObjectCreateRay(m_pProp.GetChartID(),n,dtTo,hddata.pcur[i],m_dtFrom,hddata.pcur[i]);
            class="kw">break;
      case HD_RECTANGLE:
            if(dl!=hddata.pcur[i]) dl=dh;
            dh=(i == hddata.prsize-class="num">1) ? hddata.pcur[i] :(hddata.pcur[i]+hddata.pcur[i+class="num">1])/class="num">2;
            b = ObjectCreate(m_pProp.GetChartID(),n,OBJ_RECTANGLE,class="num">0,dtTo,dl,m_dtFrom,dh);
            class="kw">break;
      }class=class="str">"cmt">//class="kw">switch(m_pProp.m_style)
      if(!b)
      {
       Print("ERROR class="kw">while creating graphic item: ",n);
       class="kw">return (-class="num">1);
         } else {
       m_hItem[i]=n;
       ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_COLOR, m_pProp.GetHDColor() );
       ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_WIDTH, m_pProp.GeHDWidth() );
       ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_SELECTABLE, class="kw">false);
       ObjectSetInteger(m_pProp.GetChartID(), n, OBJPROP_BACK, true);
      }class=class="str">"cmt">//if (!ObjectCreateRay(n, dtTo, hddata.pcur[i], m_dtFrom, hddata.pcur[i]) )    
   }class=class="str">"cmt">// for (class="type">int i = class="num">0; i < l; i++)      
  class="kw">return (hddata.prsize);
  }class=class="str">"cmt">//class="type">int CHDiagDraw::SetData(class="kw">const HDDATA& hddata)

「把逐笔量塞进示意图的返回链路」

在全局作用域里先挂三个东西:管理器指针 pHd、两个整型索引 iCurr / iCurr0,以及 HDDATA 结构 sdata。前两者负责定位已注册的示意图,sdata 则是往示意图里搬数据的载体,价格区间和量分布都临时存在这里。 OnInit 里根据输入参数直接 new 出 CHDiags 管理器,样式、对齐方式、最大示意图数 MaxHDcount 一并写死。若返回 NULL 或 AddHDiag 拿到 -1,说明资源没建起来,直接 INIT_FAILED 退出,避免在 OnCalculate 里空指针崩终端。 PrepareForBar 是真正干活的:它先用 iTime 拿到目标周期 bar 的起止时间,再用 iHigh / iLow 取该根 K 的边界,除以 hdPoint 并向上取整转成整数级。prsize 在极差上 +10 级留余量,随后 ArrayResize 把 pcur、vcur 两个数组撑开等填数。 填数逻辑基于一个近似:把 MqlRates 的 tick_volume 视作从 low 到 high 均匀铺开,再按未来示意图的价格级边界归桶。这样形成的是目标周期内逐笔量在价格轴上的分布数组,实际大小在收尾才定,也就顺带定了图形基元数量。 调用方有责任把 sdata 字段填对,尤其是 bRemovePrev 这类控制开关。填完调 SetData 推给管理器,必要时用 Align 贴左或贴右。所有示意图在 CHDiags 析构时自动注销,不用手动清。外汇与贵金属杠杆高,这类量分布仅作结构参考,实盘信号概率性成立,别当确定性依据。

MQL5 / C++
CHDiags      *pHd;
class="type">int         iCurr, iCurr0;
HDDATA      sdata;
  pHd        = new CHDiags(HD_CNDLE, hdStyle, HD_LEFTRIGHT, hdHorSize, hdColor, hdWidth, class="num">0, MaxHDcount);
  if(pHd      == NULL) class="kw">return (INIT_FAILED);
  iCurr      = pHd.AddHDiag(TimeCurrent() );
  if(iCurr  == -class="num">1) class="kw">return (INIT_FAILED);
  iCurr0     = pHd.AddHDiag(TimeCurrent() );
  if(iCurr0  == -class="num">1) class="kw">return (INIT_FAILED);
      {
        class="type">int br=class="num">1;
        class="kw">while(br<MaxHDcount)
         {
          if(PrepareForBar(br++,sdata))
           {
             sdata.bRemovePrev = class="kw">false;
             if(iCurr!=-class="num">1)
              {
                Print(br-class="num">1," diag level: ",pHd.SetData(sdata,iCurr));
              }
           }
         }
        ChartRedraw();
        bCreateHis=true;
      }
class="type">bool PrepareForBar(class="type">int bar, HDDATA& hdta) {
  hdta.prmax  = hdta.prmin  = hdta.prsize  = class="num">0;
  class="type">int iSCount;
  class="type">class="kw">datetime dtStart, dtEnd;
  dtEnd = (bar == class="num">0)? TimeCurrent() : iTime(Symbol(), TargetPeriod, bar - class="num">1);
  hdta.dtLastTime = dtStart = iTime(Symbol(), TargetPeriod, bar);

  hdta.prmax = iHigh(Symbol(), TargetPeriod, bar);
  if(hdta.prmax == class="num">0) class="kw">return (class="kw">false);
  hdta.prmax      = (class="type">int)MathCeil(NormalizeDouble(hdta.prmax, hdDigit) / hdPoint );

  hdta.prmin = iLow(Symbol(), TargetPeriod, bar);
  if(hdta.prmin == class="num">0) class="kw">return (class="kw">false);
  hdta.prmin      = (class="type">int)MathCeil(NormalizeDouble(hdta.prmin, hdDigit) / hdPoint );
  iSCount = CopyRates(Symbol(), SourcePeriod, dtStart, dtEnd, source);
  if (iSCount < class="num">1) class="kw">return (class="kw">false);

  hdta.prsize = (class="type">int)hdta.prmax - (class="type">int)hdta.prmin + class="num">10;

  ArrayResize(hdta.pcur,  hdta.prsize);
  ArrayResize(hdta.vcur,  hdta.prsize);
把对象生命周期交给小布盯盘
这些示意图的创建、缩放与销毁逻辑,小布盯盘的AIGC诊断模块已内置样例,打开对应品种页就能直接套用,你只管调绑定参数。

常见问题

水平示意图的柱状条沿时间轴排布、价格轴分固定步幅区间;垂直直方图则相反,时间维度堆在价格轴上,二者基元锚点计算逻辑不同。
可以,小布盯盘会对当前品种页的图形对象做命名前缀扫描,列出未释放的示意图组,方便一键清理避免占用资源。
当示意图覆盖区间超出可视范围、起点在屏幕外时,绑左边缘可随窗口平移保持相对位置,避免时间坐标失效导致整体漂移。
用透明度或错开极小的价格偏移绘制,并在管理类里维护三个独立数组,分别控制可见性,不要混进同一个对象池。