MеtaTrader5 图表上的水平示意图·进阶篇
(2/3)· 面对几十根水平柱状线,手动管理名称与坐标只会越改越乱,本文给出可复用的对象数组方案
◍ 历史示意图只算一次,当前柱交给定时器
这类指标把活儿拆成两段:一段是把除当前柱以外的示意图一次性算完画好,之后终端自己管,不用再动;另一段是盯当前周期包含当下时间的那根柱,定期重算重画,通常塞进 OnTimer() 里跑。 历史部分放在 OnCalculate() 的首次触发里做。从第一根柱循环到 MaxHDcount,每根在 TargetPeriod 时间帧上算一遍,算通了就发命令让管理器把新数据绘成示意图。循环结束调一次 ChartRedraw(),再置一个 bCreateHis 标志,告诉系统这部分活儿干完了。 当前柱的维护逻辑很直白:在 SourcePeriod 等新蜡烛→算数→把数据丢给当前周期的示意图去增绘。原作者觉得太浅没给伪代码,但节奏就是「等新K线来了才动」,别在每 tick 里空转。 下面这段是 OnCalculate 里历史绘制的骨架,注意 prev_calculated==0 或 rates_total 增长超过 1 才进历史分支,否则只走已建标志的跳过逻辑。
<span class="keyword">class="type">int</span> <span class="functions">OnCalculate</span>(...) { <span class="keyword">if</span> (prev_calculated == <span class="number">class="num">0</span> || rates_total > prev_calculated + <span class="number">class="num">1</span>) { }<span class="keyword">else</span> { <span class="keyword">if</span> (!bCreateHis) { <span class="keyword">class="type">int</span> br = <span class="number">class="num">1</span>; <span class="keyword">class="kw">while</span> (br < MaxHDcount) { { <span class="keyword">if</span>(Calculate <span class="keyword">for</span> bar "br") { sdata.bRemovePrev = class="kw">false; <span class="functions">Print</span>("Send data to the new Diagramm"); } } <span class="functions">ChartRedraw</span>(); bCreateHis = <span class="macro">true</span>; } } <span class="keyword">class="kw">return</span>(rates_total); }
「用 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,比读文档直观。
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 和样式验证密度。
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] 并批量设色、设宽、禁选、置底。外汇与贵金属图表加载此类指标时波动剧烈,对象重绘可能频繁触发,属高风险环境下的视觉辅助,不代表任何方向判定。
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 析构时自动注销,不用手动清。外汇与贵金属杠杆高,这类量分布仅作结构参考,实盘信号概率性成立,别当确定性依据。
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);