最新的改革·进阶篇
点阵图批量遍历与单品种落盘
这段逻辑解决的是 PNF 点阵图(点数图)在 MT5 里「画谁、怎么画、画完放哪」的问题。核心入口是 OnStart:当全局开关 mw 为 true 时,脚本会扫一遍 SymbolsTotal(true) 返回的全部可见品种,逐个生成点阵图并写文件;mw 为 false 时只处理当前图表品种 Symbol()。 循环里每跑一个品种都先 ArrayFree(pnfArray) 再 ArrayResize(pnfArray,0,0),把承接点阵数据的动态数组彻底清空,避免上一种品种的余柱污染下一品种。随后调用 PNF(symb,...) 计算、pnf2file(...) 落盘,品种游标 mwSymb 自增直到覆盖全部。 文首那两行 RGB 三元组(如 255,255,255 与 230,177,177)是 pic 调色板里的配色序列,对应点阵图里涨柱、跌柱与背景的绘制色,共 24 组数值、覆盖 8 个色块循环。外汇与贵金属品种波动属性差异大,同一 cellsize 在不同品种上生成的列数可能差出数倍,实盘前建议先对 XAUUSD 与 EURUSD 各跑一次比对。 想验证很简单:把 mw 设 false,在 XAUUSD 的 H1 图上挂这段,跑完 Alert("Ok.") 弹窗后即去 MQL5/Files 翻对应 .csv 或位图,看列高是否随近 20 根 K 的箱体收敛而压缩。
class="num">255,class="num">255,class="num">255,class="num">230,class="num">177,class="num">177,class="num">185,class="num">36,class="num">36,class="num">255,class="num">255,class="num">255,class="num">255,class="num">255,class="num">255,class="num">189,class="num">51,class="num">51,class="num">222,class="num">153,class="num">153,class="num">255,class="num">255,class="num">255, class="num">255,class="num">255,class="num">255,class="num">240,class="num">207,class="num">207,class="num">200,class="num">84,class="num">84,class="num">255,class="num">255,class="num">255,class="num">255,class="num">255,class="num">255,class="num">227,class="num">168,class="num">168,class="num">211,class="num">117,class="num">117,class="num">255,class="num">255,class="num">255 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Instrument selection | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">int mwSymb; class="type">class="kw">string symb; class="type">int height=class="num">0,width=class="num">0; class="type">class="kw">string pnfArray[]; if(mw==true) { mwSymb=class="num">0; class="kw">while(mwSymb<SymbolsTotal(true)) { symb=SymbolName(mwSymb,true); ArrayFree(pnfArray); ArrayResize(pnfArray,class="num">0,class="num">0); PNF(symb,pnfArray,height,width,pic,cellsize); pnf2file(symb,pnfArray,class="num">0,height); mwSymb++; }; } else { symb=Symbol(); ArrayFree(pnfArray); ArrayResize(pnfArray,class="num">0,class="num">0); PNF(symb,pnfArray,height,width,pic,cellsize); pnf2file(symb,pnfArray,class="num">0,height); }; Alert("Ok."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Chart calculation and drawing | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void PNF(class="type">class="kw">string sName, class=class="str">"cmt">// instrument class="type">class="kw">string& array[], class=class="str">"cmt">// array for the output class="type">int& y, class=class="str">"cmt">// array height class="type">int& z, class=class="str">"cmt">// array width class="type">bool toPic, class=class="str">"cmt">// if true-output and draw class="type">int cs) class=class="str">"cmt">// set the cell size for drawing { class="type">class="kw">string s,ps;
「成交量分布图的数组骨架与初始化」
要在 MT5 里手搓一个成交量分布(Volume Profile)矩阵,第一步是把 K 线时间和 OHLCV 拉进一堆动态数组。上面这段声明里,d[] 存时间、o[]/h[]/l[]/c[] 存开高低收、v[] 是 tick 成交量;matrix[] 是二维像素矩阵,VolByPrice[] 与 VolByCol[] 分别按价格档和列统计成交量,HVolumeMax/VVolumeMax 记录横向与纵向最大量。 dateTime 型 DateByCol[] 把每一列对应到具体时间,MqlDateTime 的 bMDT/eMDT 框定起止日期;tMin[]/tMax[] 则缓存每列的最小/最大tick索引。GlobalMin/GlobalMax 等整型负责全局与游标区间的价格映射,height/width 决定画布尺寸,beg/end 控制渲染起止列。 初始化时先按品种小数位定 sd:SymbolInfoInteger 取 SYMBOL_DIGITS,≤3 位(如 XAUUSD 通常 2 位)设 sd=2,否则 sd=4。再用 b=MathMin(Bars(sName,tf),depth) 限制拉取根数,避免深度过大拖垮终端——depth 由外部传入,实操中设 500~2000 都常见。 所有数组先 ArrayFree 释放旧内存再 ArrayResize 到 b,并用 ArrayInitialize(...,NULL) 清场,随后 CopyTime/CopyOpen…CopyTickVolume 一次性灌入。box 参数非 0 时,dBox=box/MathPow(10.0,sd) 把「点数」转成真实价格步长;为 0 则走后续自动计算分支。外汇与贵金属杠杆高、滑点跳空频繁,复制历史数据后务必在策略测试器用不同品种验证数组边界,防止 depth 超 Bars 导致矩阵错位。
class="type">class="kw">datetime d[]; class="type">class="kw">double o[],h[],l[],c[]; class="type">long v[]; class="type">uchar matrix[]; class="type">long VolByPrice[],VolByCol[],HVolumeMax,VVolumeMax; class="type">int tMin[],tMax[]; class="type">class="kw">datetime DateByCol[]; class="type">MqlDateTime bMDT,eMDT; class="type">class="kw">string strDBC[]; class="type">uchar pnf=&class="macro">#x27;.&class="macro">#x27;; class="type">int sd; class="type">int b,i,j,k=class="num">0,m=class="num">0; class="type">int GlobalMin,GlobalMax,StartMin,StartMax,CurMin,CurMax,RevMin,RevMax,ContMin,ContMax; class="type">int height,width,beg=class="num">0,end=class="num">0; class="type">class="kw">double dBox,price; class="type">int thBeg=class="num">1,thEnd=class="num">2,tv=class="num">0; class="type">uchar trend=&class="macro">#x27;.&class="macro">#x27;; class=class="str">"cmt">// --------------------------------- BMP ----------------------------------------- class="type">int RowVolWidth=class="num">10*cs; class=class="str">"cmt">//--- shift for prices class="type">int startX=class="num">5*cs; class="type">int yshift=cs*class="num">7; class=class="str">"cmt">// --------------------------------- BMP ----------------------------------------- if(SymbolInfoInteger(sName,SYMBOL_DIGITS)<=class="num">3) sd=class="num">2; else sd=class="num">4; b=MathMin(Bars(sName,tf),depth); ArrayFree(d); ArrayFree(o); ArrayFree(h); ArrayFree(l); ArrayFree(c); ArrayFree(v); ArrayFree(matrix); ArrayFree(VolByPrice); ArrayFree(VolByCol); ArrayFree(DateByCol); ArrayFree(tMin); ArrayFree(tMax); ArrayResize(d,b,class="num">0); ArrayResize(o,b,class="num">0); ArrayResize(h,b,class="num">0); ArrayResize(l,b,class="num">0); ArrayResize(c,b,class="num">0); ArrayResize(v,b,class="num">0); ArrayInitialize(d,NULL); ArrayInitialize(o,NULL); ArrayInitialize(h,NULL); ArrayInitialize(l,NULL); ArrayInitialize(c,NULL); ArrayInitialize(v,NULL); CopyTime(sName,tf,class="num">0,b,d); CopyOpen(sName,tf,class="num">0,b,o); CopyHigh(sName,tf,class="num">0,b,h); CopyLow(sName,tf,class="num">0,b,l); CopyClose(sName,tf,class="num">0,b,c); CopyTickVolume(sName,tf,class="num">0,b,v); if(box!=class="num">0) { dBox=box/MathPow(class="num">10.0,(class="type">class="kw">double)sd); } else {
◍ 点阵图里的坐标归一与转向判定
这段逻辑在干一件事:把高低价数组塞进一个归一化网格,再用点数图(Pnf)规则判断该画 X 还是 O。dBox 先算单格代表的价格跨度,用最高价与最低价差除以 count,再按精度 sd 做十位幂缩放,整个矩阵的高度就是 GlobalMax 减 GlobalMin 的整数差。 归一函数 MathNorm 把真实价格映射到行号:GlobalMin 从下边界减 reverse 取整,GlobalMax 从上边界加 reverse 取整,StartMin / StartMax 则锁定 0 号 K 线的落点。reverse 是转向所需格数,外汇与贵金属波动大,reverse 设太小会频繁假突破,实盘前建议在 MT5 用周线先跑一遍看噪声密度。 数组初始化全是铺垫:matrix 用 '.' 填满,VolByPrice 和 VolByCol 归零,DateByCol 给个 1971 年占位日期,tMin / tMax 清零。之后 for 循环从 i=1 扫到 b-1,每次取 CurMin / CurMax 做归一。 当 pnf 还是 '.' 时,若 CurMax 越过 RevMax 就切 'X' 并向上补一列矩阵;若 CurMin 跌破 RevMin 就切 'O'。这里 RevMax = StartMax - reverse,RevMin = StartMin + reverse,意味着初始方向由两个边界各退 reverse 格来确认,属于典型的三格转向变体。
dBox=MathNorm((h[ArrayMaximum(h,class="num">0,WHOLE_ARRAY)]-l[ArrayMinimum(l,class="num">0,WHOLE_ARRAY)])/count,class="num">1/MathPow(class="num">10.0,(class="type">class="kw">double)sd),true)/MathPow(class="num">10.0,(class="type">class="kw">double)sd); }; GlobalMin=MathNorm(l[ArrayMinimum(l,class="num">0,WHOLE_ARRAY)],dBox,true)-(class="type">int)(reverse); GlobalMax=MathNorm(h[ArrayMaximum(h,class="num">0,WHOLE_ARRAY)],dBox,class="kw">false)+(class="type">int)(reverse); StartMin=MathNorm(l[class="num">0],dBox,true); StartMax=MathNorm(h[class="num">0],dBox,class="kw">false); ContMin=(class="type">int)(StartMin-class="num">1); ContMax=(class="type">int)(StartMax+class="num">1); RevMin=(class="type">int)(StartMax-reverse); RevMax=(class="type">int)(StartMin+reverse); height=(class="type">int)(GlobalMax-GlobalMin); width=class="num">1; ArrayResize(matrix,height*width,class="num">0); ArrayInitialize(matrix,&class="macro">#x27;.&class="macro">#x27;); ArrayResize(VolByPrice,height,class="num">0); ArrayInitialize(VolByPrice,class="num">0); ArrayResize(VolByCol,width,class="num">0); ArrayInitialize(VolByCol,class="num">0); ArrayResize(DateByCol,width,class="num">0); ArrayInitialize(DateByCol,D&class="macro">#x27;class="num">01.01.class="num">1971&class="macro">#x27;); ArrayResize(tMin,width,class="num">0); ArrayInitialize(tMin,class="num">0); ArrayResize(tMax,width,class="num">0); ArrayInitialize(tMax,class="num">0); for(i=class="num">1;i<b;i++) { CurMin=MathNorm(l[i],dBox,true); CurMax=MathNorm(h[i],dBox,class="kw">false); class="kw">switch(pnf) { case &class="macro">#x27;.&class="macro">#x27;: { if(CurMax>=RevMax) { pnf=&class="macro">#x27;X&class="macro">#x27;; ContMax=(class="type">int)(CurMax+class="num">1); RevMin=(class="type">int)(CurMax-reverse); beg=(class="type">int)(StartMin-GlobalMin-class="num">1); end=(class="type">int)(CurMax-GlobalMin-class="num">1); SetMatrix(matrix,beg,end,height,(class="type">int)(width-class="num">1),pnf); SetVector(VolByPrice,beg,end,v[i]); VolByCol[width-class="num">1]=VolByCol[width-class="num">1]+v[i]; DateByCol[width-class="num">1]=d[i]; trend=&class="macro">#x27;D&class="macro">#x27;; class="kw">break; }; if(CurMin<=RevMin) { pnf=&class="macro">#x27;O&class="macro">#x27;;
点数图列切换时的矩阵与向量填充
这段逻辑处理点数图(PNF)在 UP 列与 X 列之间的状态切换与数据存储。当趋势标记为 'U' 且当前最小价跌破反转阈值时,代码将趋势改写为 'O',并重新计算 ContMin、RevMax、beg、end 等边界,随后调用 SetMatrix 把该 K 线映射到二维矩阵的对应列高位置,SetVector 则把成交量 v[i] 累加到 VolByPrice 的对应价格区间。 在 'X' 分支里,若 CurMax 继续抬高则只更新 ContMax 与 RevMin,矩阵列宽不变;一旦 CurMin 跌破 RevMin,说明发生向下反转,此时 width++ 并连续对 matrix、VolByCol、DateByCol、tMin、tMax 做 ArrayResize,为新列腾出 height*width 的连续内存。 可直接把这段代码粘进 MT5 自定义指标的 PNF 构建函数里验证:当 reverse 参数设为 3、GlobalMin 取历史最低价归一化值,观察 width 在反转瞬间是否按预期 +1,以及 VolByCol[width-1] 的累加是否与成交量的 Tick 量一致。外汇与贵金属市场波动剧烈,这类基于反转的统计映射仅反映历史概率,实盘需警惕滑点与跳空风险。
ContMin=(class="type">int)(CurMin-class="num">1); RevMax=(class="type">int)(CurMin+reverse); beg=(class="type">int)(CurMin-GlobalMin-class="num">1); end=(class="type">int)(StartMax-GlobalMin-class="num">1); SetMatrix(matrix,beg,end,height,(class="type">int)(width-class="num">1),pnf); SetVector(VolByPrice,beg,end,v[i]); VolByCol[width-class="num">1]=VolByCol[width-class="num">1]+v[i]; DateByCol[width-class="num">1]=d[i]; trend=&class="macro">#x27;U&class="macro">#x27;; class="kw">break; }; class="kw">break; }; case &class="macro">#x27;X&class="macro">#x27;: { if(CurMax>=ContMax) { pnf=&class="macro">#x27;X&class="macro">#x27;; ContMax=(class="type">int)(CurMax+class="num">1); RevMin=(class="type">int)(CurMax-reverse); end=(class="type">int)(CurMax-GlobalMin-class="num">1); SetMatrix(matrix,beg,end,height,(class="type">int)(width-class="num">1),pnf); SetVector(VolByPrice,beg,end,v[i]); VolByCol[width-class="num">1]=VolByCol[width-class="num">1]+v[i]; DateByCol[width-class="num">1]=d[i]; class="kw">break; }; if(CurMin<=RevMin) { pnf=&class="macro">#x27;O&class="macro">#x27;; ContMin=(class="type">int)(CurMin-class="num">1); RevMax=(class="type">int)(CurMin+reverse); tMin[width-class="num">1]=beg-class="num">1; tMax[width-class="num">1]=end+class="num">1; beg=(class="type">int)(CurMin-GlobalMin-class="num">1); end--; width++; ArrayResize(matrix,height*width,class="num">0); ArrayResize(VolByCol,width,class="num">0); ArrayResize(DateByCol,width,class="num">0); ArrayResize(tMin,width,class="num">0); ArrayResize(tMax,width,class="num">0);
「点数图转向时的矩阵与列数组操作」
在 MQL5 里画点数图(PNF)时,遇到转向信号需要同步重写矩阵和每列附属数组。上面这段逻辑处理的是当前列还没到反转阈值、以及从 O 列翻成 X 列两种情形,核心是把价格坐标映射到二维矩阵的行,再把成交量、时间边界塞进对应列向量。 当 CurMin<=ContMin 时,说明还在原列内延续,代码把 pnf 标记为 'O',并用 SetMatrix 把 beg 到 end 区间的行写为 'O',同时 SetVector 把这段成交量累加进 VolByPrice。此时 VolByCol[width-1] 只做加法,DateByCol 记录当前柱时间,不新增列宽。 一旦 CurMax>=RevMax,意味着 O 列结束、X 列开启。代码先把原列的 tMin/tMax 边界定好,beg 加 1、end 重算为 CurMax-GlobalMin-1,width 自增后立刻对 matrix、VolByCol、DateByCol、tMin、tMax 五个数组调用 ArrayResize。注意 ArrayResize 的预留参数都是 0,代表不保留额外内存余量,实盘若品种跳空大,列数可能短时暴涨,需关注内存重分配频率。 外汇与贵金属市场跳空频繁,这类 PNF 重建逻辑在高波动时段可能每根新柱都触发 width++,建议在 MT5 用 Print(width) 打点观察单次加载的最大列数,再决定是否给 ArrayResize 加缓冲尺寸。
SetMatrix(matrix,class="num">0,(class="type">int)(height-class="num">1),height,(class="type">int)(width-class="num">1),&class="macro">#x27;.&class="macro">#x27;); SetMatrix(matrix,beg,end,height,(class="type">int)(width-class="num">1),pnf); SetVector(VolByPrice,beg,end,v[i]); VolByCol[width-class="num">1]=class="num">0; VolByCol[width-class="num">1]=VolByCol[width-class="num">1]+v[i]; DateByCol[width-class="num">1]=d[i]; tMin[width-class="num">1]=beg-class="num">1; tMax[width-class="num">1]=end+class="num">1; class="kw">break; }; class="kw">break; }; case &class="macro">#x27;O&class="macro">#x27;: { if(CurMin<=ContMin) { pnf=&class="macro">#x27;O&class="macro">#x27;; ContMin=(class="type">int)(CurMin-class="num">1); RevMax=(class="type">int)(CurMin+reverse); beg=(class="type">int)(CurMin-GlobalMin-class="num">1); SetMatrix(matrix,beg,end,height,(class="type">int)(width-class="num">1),pnf); SetVector(VolByPrice,beg,end,v[i]); VolByCol[width-class="num">1]=VolByCol[width-class="num">1]+v[i]; DateByCol[width-class="num">1]=d[i]; class="kw">break; }; if(CurMax>=RevMax) { pnf=&class="macro">#x27;X&class="macro">#x27;; ContMax=(class="type">int)(CurMax+class="num">1); RevMin=(class="type">int)(CurMax-reverse); tMin[width-class="num">1]=beg-class="num">1; tMax[width-class="num">1]=end+class="num">1; beg++; end=(class="type">int)(CurMax-GlobalMin-class="num">1); width++; ArrayResize(matrix,height*width,class="num">0); ArrayResize(VolByCol,width,class="num">0); ArrayResize(DateByCol,width,class="num">0); ArrayResize(tMin,width,class="num">0); ArrayResize(tMax,width,class="num">0);
◍ 把点数图矩阵吐成 BMP 与趋势锚点
这一段收在矩阵填充之后,负责把内存里的 matrix 和每列成交量真正落地成图。若 toPic 开关为 true,先按 cs*width+2*startX+RowVolWidth 算横向像素、cs*height+yshift+70 算纵向像素,用 bmp.Create 建一张白底图;其中 70 是留给底部文字说明的预留高度,调小会导致授权与参数串被截掉。 双层 for 从右下角往左上角刷格子:bmp.Bar 先铺白、bmp.Rectangle 再描 clrLightGray 边,形成标准点数图单元格。底部两行 TypeText 分别打 array[k-2](品种描述+Box/Reverse 参数)与 array[k-1](BSD 授权信息),坐标 yshift+cs*height+50 和 +35 是硬编码偏移,字体不随 cs 缩放,cs 设低于 6 时文字会叠进图体。 图生成完并不结束,后面紧接趋势线抽取:while(thEnd<width-1) 里若 trend=='U' 就用 ArrayMinimum(tMin,thBeg,thEnd-thBeg) 找这段区间最低时间锚,tMin 存的是列首偏移减 1。外汇与贵金属点位图对参数极敏感,box 与 reverse 设错会让 thEnd 永远走不到 width-1,趋势线一段都画不出,开 MT5 跑前先把这两个值打印出来核对。
SetMatrix(matrix,class="num">0,(class="type">int)(height-class="num">1),height,(class="type">int)(width-class="num">1),&class="macro">#x27;.&class="macro">#x27;); SetMatrix(matrix,beg,end,height,(class="type">int)(width-class="num">1),pnf); SetVector(VolByPrice,beg,end,v[i]); VolByCol[width-class="num">1]=class="num">0; VolByCol[width-class="num">1]=VolByCol[width-class="num">1]+v[i]; DateByCol[width-class="num">1]=d[i]; tMin[width-class="num">1]=beg-class="num">1; tMax[width-class="num">1]=end+class="num">1; class="kw">break; }; class="kw">break; }; }; }; class=class="str">"cmt">//--- credits s="BSD License, class="num">2012, FXRays.info by Roman Rich"; k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; s=SymbolInfoString(sName,SYMBOL_DESCRIPTION)+", Box-"+DoubleToString(box,class="num">0)+",Reverse-"+DoubleToString(reverse,class="num">0); k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; class=class="str">"cmt">// --------------------------------- BMP ----------------------------------------- if(toPic==true) { class=class="str">"cmt">//-- BMP image size on the chart display class="type">int XSize=cs*width+class="num">2*startX+RowVolWidth; class="type">int YSize=cs*height+yshift+class="num">70; class=class="str">"cmt">//-- creating a bmp image sized XSize x YSize with the background class="type">class="kw">color clrWhite bmp.Create(XSize,YSize,clrWhite); class=class="str">"cmt">//-- displaying cells of the main field for(i=height-class="num">1;i>=class="num">0;i--) for(j=class="num">0;j<=width-class="num">1;j++) { bmp.Bar(RowVolWidth+startX+cs*j,yshift+cs*i,cs,cs,clrWhite); bmp.Rectangle(RowVolWidth+startX+cs*j,yshift+cs*i,cs,cs,clrLightGray); } bmp.TypeText(class="num">10,yshift+cs*(height)+class="num">50,array[k-class="num">2],clrDarkGray); bmp.TypeText(class="num">10,yshift+cs*(height)+class="num">35,array[k-class="num">1],clrGray); } class=class="str">"cmt">// --------------------------------- BMP ----------------------------------------- class=class="str">"cmt">//--- calculating trend lines i=class="num">0; class="kw">while(thEnd<width-class="num">1) { class="kw">while(thBeg+i<thEnd) { if(trend==&class="macro">#x27;U&class="macro">#x27;) { i=ArrayMinimum(tMin,thBeg,thEnd-thBeg); j=tMin[i]; } else {