直方图形式的统计分布, 无需指标缓冲区和数组(基础篇)
用直方图画出无缓冲区的分布
在 MT5 里做统计分布展示,不一定非得占用指标缓冲区和额外数组。借助直方图(histogram)形态,可以直接把频数分布画到副图上,省掉维护数组的麻烦。 Sergey Pavlov 在 2017-01-27 发布的示例被查看 5 749 次、收藏 9 次,说明这种轻量画法在实盘辅助分析里确有需求。打开 MT5 按作者名搜该示例,能直接看到直方图怎么绕过常规缓冲区挂载。 对做价格行为统计的交易者,这路子适合快速验证某周期内的收盘价差分布,贵金属与外汇波动大、杠杆高,结论只作概率参考,别当成方向证据。
◍ 把图形内存当统计缓冲区用
直方图的本质,是按样本落入各区间的频率来呈现一组数据的分布形态。传统做法大多依赖指标缓冲区或数组做计数、排序和抽样,MT5 代码库里能翻出不下十种这类例程,但计算开销和代码复杂度都不低。 我们换一条路:直接挪用图形对象的“隐藏内存”。绘制自定义直方图时本就要建图形对象,而它们的属性存储区可以顺手当成轻量统计容器,免去了显式数组管理与重算。 这套思路只解决一件事——用最低成本把市场特征的分布画出来。本文不展开直方图释义,也不谈它在实际交易里的优势,只看怎么在 MT5 里把分布可视化和基础特征跑通。外汇与贵金属波动剧烈,任何分布特征都只是概率描述,不能直接当方向依据。
「用图形对象属性当隐藏内存」
直方图本质是频率柱线图:一根轴排变量值,另一根轴排出现频次,柱高即该区间内的样本数。本文采用垂直直方图,价格按升序落在纵轴,频率落在横轴,买卖价分别贴在当前柱线右侧与左侧,且每个直方图间隔锁定为该品种最小点数(tick size),新报价到来即累加对应列频次。 难点在于题设禁止指标缓冲区、数组与结构。动态数组虽能应对未知价格区间,但被条件排除;同时还需解决重算与重绘时的数据检索排序。MQL5 的图形对象自带一组“隐藏”属性,每个对象创建时即捆绑若干可写变量,本意存对象参数,实际可挪作他用且不损功能——这就是现成的“图形内存”。 把变量写进某个图形对象的属性,等效于在终端里开了块持久存储。普通全局变量自末次访问起仅存活 4 周便自动清除,而图形内存跟着对象存活,对象不删它就一直在,对外汇与贵金属这类高波动、需长周期统计的品种做盘内分布累计更稳。
把价格频率塞进图形对象属性里
在 MT5 里创建一个图形对象时,必须给 OBJPROP_NAME 赋一个独有字符串。这个名称支持内嵌格式化基本类型(整型、布尔、浮点、颜色、时间),所以它能直接携带供给价、采购价这类主数据,而不必另开数组。 相比之下 OBJPROP_TEXT 更自由,是个可读写的文本字段。我们这套做法把频率存进 OBJPROP_TEXT,把价格塞进 OBJPROP_NAME,坐标则交给 OBJPROP_PRICE 与 OBJPROP_TIME,全程不碰指标缓冲区。 有人会问:既然对象本身已有价格坐标属性,为何还要在名字里重复写价格?实测若用 OBJPROP_PRICE 做比对,每当新报价进来就得遍历全部对象、取属性、做 double 比较——浮点直接判等本就不可靠,还会把算法拖慢一个量级。 更干脆的办法是:利用“同名对象不会被重复创建、属性不清零”的机制,直接往目标名字写频率。首次进样本的价格,其 OBJPROP_TEXT 为空,我们写入带星号前缀的 "*1";后续同价再来,就剥掉星号做整数累加。这样免去了存在性检查与克隆开销。 向右偏移直方图也靠同一套:新柱生成后图表左移,我们给时间坐标加一个 hsize 偏移量,把直方图钉在当前柱右侧,避免被推到视野外。
class="type">void DrawHistogram(class="type">bool draw, class=class="str">"cmt">// 向左侧或右侧绘制直方图 class="type">class="kw">string h_name, class=class="str">"cmt">// 对象名称的独有前缀 class="type">class="kw">double price, class=class="str">"cmt">// 价格 (分析参数) class="type">class="kw">datetime time, class=class="str">"cmt">// 将直方图绑定到当前柱线 class="type">int span, class=class="str">"cmt">// 已分析参数的数位容量 class="type">int swin=class="num">0) class=class="str">"cmt">// 直方图窗口 { class="type">class="kw">double y=NormalizeDouble(price,span); class="type">class="kw">string pfx=DoubleToString(y,span); class=class="str">"cmt">// 如果 draw=true, 向右侧绘制直方图 if(draw) { class="type">class="kw">string name="+ "+h_name+pfx; class=class="str">"cmt">// 对象名称: 前缀+价格 ObjectCreate(class="num">0,name,OBJ_TREND,swin,time,y); class=class="str">"cmt">// 创建对象 ObjectSetInteger(class="num">0,name,OBJPROP_COLOR,color_R_active); class=class="str">"cmt">// 设置对象颜色 ObjSet; class=class="str">"cmt">// 宏代码缩写 if(StringFind(ObjectGetString(class="num">0,name,OBJPROP_TEXT),"*",class="num">0)<class="num">0) {class=class="str">"cmt">// 如果结果价格首次进入样本 ObjectSetString(class="num">0,name,OBJPROP_TEXT,"*class="num">1"); class=class="str">"cmt">// 价格频率为 class="num">1 ObjectSetInteger(class="num">0,name,OBJPROP_TIME,class="num">1,time+hsize); class=class="str">"cmt">// 定义时间坐标 } else {class=class="str">"cmt">// 如果结果价格并非首次进入样本 class="type">class="kw">string str=ObjectGetString(class="num">0,name,OBJPROP_TEXT); class=class="str">"cmt">// 获取属性值 class="type">class="kw">string strint=StringSubstr(str,class="num">1); class=class="str">"cmt">// 获得子字符串 class="type">long n=StringToInteger(strint); class=class="str">"cmt">// 获取频率用于进一步计算 } } }
◍ 直方图随新K线平移的底层逻辑
这段逻辑处理的是指标对象在右侧(draw=true)与左侧(draw=false)两种模式下,如何用趋势线对象模拟直方图柱的累加与定位。右侧每次命中就在文本前加「*」并让 n 自增 1,时间坐标按 time+hsize*n 向右推;左侧对称地用 time-hsize*n 向左排,首次出现写「*1」,之后抽取子串转整型再 +1。 新柱线抵达由 time[0]>prevTimeBar 判定,一旦成立就遍历全图对象(ObjectsTotal 取总数倒序循环),只筛首字符等于前缀 R 的条目。外汇与贵金属市场跳空频繁,hsize 若取错周期量级,左侧直方图可能叠到不可见区域,属高风险调试点。 下面拆右侧递增核心三行: n++; // 数值递增 1 ObjectSetString(0,name,OBJPROP_TEXT,"*"+(string)n); // 新数值写入到属性 ObjectSetInteger(0,name,OBJPROP_TIME,1,time+hsize*n);//定义时间坐标 左侧首次与迭代分支同理,只是坐标减 hsize*n,且用 StringFind(...,"*",0)<0 区分首建与累加。跑 MT5 时把 R 前缀和 hsize 打印出来,能直接看到对象是否按预期平移。
n++; class=class="str">"cmt">// 数值递增 class="num">1 ObjectSetString(class="num">0,name,OBJPROP_TEXT,"*"+(class="type">class="kw">string)n); class=class="str">"cmt">// 新数值写入到属性 ObjectSetInteger(class="num">0,name,OBJPROP_TIME,class="num">1,time+hsize*n);class=class="str">"cmt">//定义时间坐标 } } class=class="str">"cmt">// 如果 draw=class="kw">false, 向左侧写入直方图 if(!draw) { class="type">class="kw">string name="- "+h_name+pfx; ObjectCreate(class="num">0,name,OBJ_TREND,swin,time,y); ObjectSetInteger(class="num">0,name,OBJPROP_COLOR,color_L_active); ObjSet; if(StringFind(ObjectGetString(class="num">0,name,OBJPROP_TEXT),"*",class="num">0)<class="num">0) { ObjectSetString(class="num">0,name,OBJPROP_TEXT,"*class="num">1"); ObjectSetInteger(class="num">0,name,OBJPROP_TIME,class="num">1,time-hsize); } else { class="type">class="kw">string str=ObjectGetString(class="num">0,name,OBJPROP_TEXT); class="type">class="kw">string strint=StringSubstr(str,class="num">1); class="type">long n=StringToInteger(strint); n++; ObjectSetString(class="num">0,name,OBJPROP_TEXT,"*"+(class="type">class="kw">string)n); ObjectSetInteger(class="num">0,name,OBJPROP_TIME,class="num">1,time-hsize*n); } } ChartRedraw(); } class=class="str">"cmt">//--- 将直方图平移到新的柱线 if(time[class="num">0]>prevTimeBar) class=class="str">"cmt">// 定义新柱线抵达 { prevTimeBar=time[class="num">0]; class=class="str">"cmt">// 通过所有图形对象传递 for(class="type">int obj=ObjectsTotal(class="num">0,-class="num">1,-class="num">1)-class="num">1;obj>=class="num">0;obj--) { class="type">class="kw">string obj_name=ObjectName(class="num">0,obj,-class="num">1,-class="num">1); class=class="str">"cmt">// 获取已发现对象的名称 if(obj_name[class="num">0]==R) class=class="str">"cmt">// 搜索直方图元素前缀 { class=class="str">"cmt">// 如果已发现直方图元素 ObjectSetInteger(class="num">0,obj_name,OBJPROP_TIME, class=class="str">"cmt">// 设置新坐标值
「用对象文本字段驱动直方图锚点位移」
这段逻辑处理的是图表上两类自定义直方图对象(前缀 R / L)随新 tick 到来的重定位。核心思路是把位移步数编码进对象文本属性的第一个字符之后,运行时再解析出来算新时间坐标。 对 R 类对象,先读 OBJPROP_TEXT,用 StringSubstr 从索引 1 截掉前缀,StringToInteger 转成步数 n,再把锚点 1 的时间设为 time[0]+hsize*n,同时把颜色刷成 color_R_passive。L 类完全对称,只是时间方向改成 time[0]-hsize*n,颜色用 color_L_passive。 验证时可在 MT5 新建一个脚本,画两个前缀分别为 R3、L2 的 OBJ_HISTOGRAM 对象,hsize 取 3600(1 小时秒数),新 tick 后 R 对象右移 3 小时、L 对象左移 2 小时。外汇与贵金属杠杆品种波动剧烈,这类自动重绘对象仅作辅助参考,实盘须自行承担滑点与跳空风险。 最后 ObjectSetInteger 改完坐标和颜色,必须调 ChartRedraw 才会立刻生效,否则要等下一帧重绘。
ObjectSetInteger(class="num">0,obj_name,OBJPROP_TIME,class="num">0,time[class="num">0]); class=class="str">"cmt">// 对于锚点 "class="num">0" class="type">class="kw">string str=ObjectGetString(class="num">0,obj_name,OBJPROP_TEXT);class=class="str">"cmt">// 从对象属性中读取变量 class="type">class="kw">string strint=StringSubstr(str,class="num">1); class=class="str">"cmt">// 从接收的变量中分离出一个子字符串 class="type">long n=StringToInteger(strint); class=class="str">"cmt">// 将字符串转换为长整型变量 ObjectSetInteger(class="num">0,obj_name,OBJPROP_TIME, class=class="str">"cmt">// 计算新坐标值 class="num">1,time[class="num">0]+hsize*n); class=class="str">"cmt">// 对于锚点 "class="num">1" ObjectSetInteger(class="num">0,obj_name,OBJPROP_COLOR, color_R_passive); class=class="str">"cmt">// 改变已移动直方图元素的颜色 } if(obj_name[class="num">0]==L) { ObjectSetInteger(class="num">0,obj_name,OBJPROP_TIME,class="num">0,time[class="num">0]); class="type">class="kw">string str=ObjectGetString(class="num">0,obj_name,OBJPROP_TEXT); class="type">class="kw">string strint=StringSubstr(str,class="num">1); class="type">long n=StringToInteger(strint); ObjectSetInteger(class="num">0,obj_name,OBJPROP_TIME,class="num">1,time[class="num">0]-hsize*n); ObjectSetInteger(class="num">0,obj_name,OBJPROP_COLOR,color_L_passive); } } ChartRedraw(); }