最新的改革·综合运用
(3/3)·把 Renko 之外的非时间图表真正写进 EA 之前,先用脚本跑通点数图的全流程
支撑阻力斜线的矩阵描点逻辑
这段片段在做一件事:把识别出的支撑或阻力区段,按趋势方向在字符矩阵里逐列标成 '+',并可选渲染到 BMP 图像。核心不是画线算法,而是 thBeg/thEnd 的区间推进与 tv 的升降跟随。 先通过 ArrayMaximum 在 tMax 里找窗口内最大体积位置 i,j 取对应体积值;随后 thBeg 锁定为 i,tv 设为 j,进入 while 循环:只要矩阵该格是 '.' 就沿趋势平移(U 涨 j++,D 跌 j--),直到碰上非空格或到右边界 width-1。 若 thBeg+i 仍小于 thEnd,则把 thBeg 再右移 2 列重新扫描,避免相邻噪声段重复占用。最终 thEnd=thBeg+i,若两者相等则 thEnd++ 保底有一列宽度。 for 循环内对 [thBeg, thEnd) 每列调 SetMatrix 写 '+',同时当 toPic==true 时用 bmp.DrawLine 画 1px 主线与偏移 1px 的加宽线:U 趋势用 clrGreen 从左下到右上,D 趋势用 clrRed 从左上到右下。注意 cs 是单元格尺寸、yshift 是纵向偏移,调参时改这两个值就能直接改变输出图比例。 末尾把 trend 翻转(U↔D),让下一轮扫描自动切换支撑/阻力角色;最后 ArrayResize(strDBC,width,0) 按列数重开字符串缓存,并用 TimeToStruct 取首末列时间,当跨度 >=50000000(约 5000 万秒,折合 578 天+)时走分支处理——外汇与贵金属此类长周期图表波动剧烈,实盘验证前务必认清高风险。
i=ArrayMaximum(tMax,thBeg,thEnd-thBeg); j=tMax[i]; } thBeg=i; tv=j; i=class="num">0; class="kw">while(GetMatrix(matrix,j,height,(class="type">long)(thBeg+i))==&class="macro">#x27;.&class="macro">#x27;) { i++; if(trend==&class="macro">#x27;U&class="macro">#x27;) j++; else j--; if(thBeg+i==width-class="num">1) { thEnd=width-class="num">1; class="kw">break; }; }; if(thBeg+i<thEnd) { thBeg=thBeg+class="num">2; i=class="num">0; }; }; thEnd=thBeg+i; if(thEnd==thBeg) thEnd++; for(i=thBeg;i<thEnd;i++) { SetMatrix(matrix,tv,tv,height,(class="type">long)(i),&class="macro">#x27;+&class="macro">#x27;); class=class="str">"cmt">// --------------------------------- BMP ----------------------------------------- if(toPic==true) { class=class="str">"cmt">//--- support and resistance lines if(trend==&class="macro">#x27;U&class="macro">#x27;) { bmp.DrawLine(RowVolWidth+startX+i*cs,yshift+tv*cs, RowVolWidth+startX+(i+class="num">1)*cs,yshift+(tv+class="num">1)*cs,clrGreen); } if(trend==&class="macro">#x27;D&class="macro">#x27;) { bmp.DrawLine(RowVolWidth+startX+i*cs,yshift+(tv+class="num">1)*cs, RowVolWidth+startX+(i+class="num">1)*cs,yshift+(tv)*cs,clrRed); } class=class="str">"cmt">//--- broadening of support/resistance lines if(trend==&class="macro">#x27;U&class="macro">#x27;) { bmp.DrawLine(RowVolWidth+class="num">1+startX+i*cs,yshift+tv*cs, RowVolWidth+class="num">1+startX+(i+class="num">1)*cs,yshift+(tv+class="num">1)*cs,clrGreen); } if(trend==&class="macro">#x27;D&class="macro">#x27;) { bmp.DrawLine(RowVolWidth+class="num">1+startX+i*cs,yshift+(tv+class="num">1)*cs, RowVolWidth+class="num">1+startX+(i+class="num">1)*cs,yshift+(tv)*cs,clrRed); } } class=class="str">"cmt">// --------------------------------- BMP ----------------------------------------- if(trend==&class="macro">#x27;U&class="macro">#x27;) tv++; else tv--; }; if(trend==&class="macro">#x27;U&class="macro">#x27;) trend=&class="macro">#x27;D&class="macro">#x27;; else trend=&class="macro">#x27;U&class="macro">#x27;; i=class="num">0; }; class=class="str">"cmt">//--- displaying data in columns ArrayResize(strDBC,width,class="num">0); TimeToStruct(DateByCol[class="num">0],bMDT); TimeToStruct(DateByCol[width-class="num">1],eMDT); if((DateByCol[width-class="num">1]-DateByCol[class="num">0])>=class="num">50000000) {
◍ 长周期下的坐标轴年份月份标注
当图表横轴跨度超过 5000000 秒(约 57.8 天)时,逐日标注会挤成一团,代码改用「年.月」格式压缩显示。判断依据是 DateByCol 首尾时间差,超过该阈值就走 7 字符宽度的 strDBC 数组分支。 具体逻辑里,先给每个列初始化 7 个空格的缓冲串,再遍历相邻两列:若月份不同,就把结束列的「年.月」写进 strDBC。月份小于 10 补前导 0(如 2024.03),大于等于 10 直接拼(如 2024.11),这样视觉对齐不会被个位数月份打乱。 随后用 0~6 共 7 轮循环,把 strDBC 里每列的第 i 个字符抽出来拼成一行文本,前面垫 12 个空格加冒号、末尾也加冒号,逐行塞进 array 并 ArrayResize 扩容。你在 MT5 里把这段接进自己的坐标轴绘制函数,改 5000000 这个阈值就能控制「切到年月视图」的灵敏度和外汇、贵金属报价屏的高风险始终存在,参数调试仅供验证。
if((DateByCol[width-class="num">1]-DateByCol[class="num">0])>=class="num">5000000) { for(i=class="num">0;i<=width-class="num">1;i++) StringInit(strDBC[i],class="num">7,&class="macro">#x27; &class="macro">#x27;); for(i=class="num">1;i<=width-class="num">1;i++) { TimeToStruct(DateByCol[i-class="num">1],bMDT); TimeToStruct(DateByCol[i],eMDT); if(bMDT.mon!=eMDT.mon) { if(eMDT.mon<class="num">10) strDBC[i]=DoubleToString(eMDT.year,class="num">0)+".class="num">0"+DoubleToString(eMDT.mon,class="num">0); if(eMDT.mon>=class="num">10) strDBC[i]=DoubleToString(eMDT.year,class="num">0)+"."+DoubleToString(eMDT.mon,class="num">0); } }; for(i=class="num">0;i<=class="num">6;i++) { StringInit(s,vd,&class="macro">#x27; &class="macro">#x27;); s=s+" : "; for(j=class="num">0;j<=width-class="num">1;j++) s=s+StringSubstr(strDBC[j],i,class="num">1); s=s+" : "; k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; }; } else {
「用字符阵列拼出跨日坐标与价格标尺」
在 MT5 自定义指标里把时间序列转成文本矩阵,核心是先给每一列初始化固定长度的空白缓冲。下面这段逻辑用 StringInit 把 strDBC 每个元素预置 10 个空格,避免后面拼接日期字符串时越界或错位。 日期标记只在「相邻列跨天」时写入。代码用 TimeToStruct 取出前后两列的年/月/日,一旦 day 不同,就按月份和日期是否小于 10 分别补前导零,拼成如 2024.03.05 或 2024.11.12 的格式——外汇与贵金属的历史回看里,这种零宽对齐能直接决定终端里表头是否整齐。 随后用 0~9 的循环把 strDBC 按字符竖切,每行取各列的第 i 个字符拼进 s,前面补空格与冒号,存进动态 array。这里 k 从 0 自增,ArrayResize 每次扩 1,实测 width=120 时约申请 10 条字符串,内存占用极低。 价格标尺部分另起横轴:price 从 GlobalMax*dBox 起算,HVolumeMax 用 ArrayMaximum 抓出成交量峰值,再按 height 从顶向下循环,每行用 DoubleToString(price,sd) 右侧补空格对齐到 8 位宽,拼出「价格 :」前缀。贵金属跳空夜这类高波动场景,标尺若不对齐,肉眼读柱容易偏一格。
for(i=class="num">0;i<=width-class="num">1;i++) StringInit(strDBC[i],class="num">10,&class="macro">#x27; &class="macro">#x27;); for(i=class="num">1;i<=width-class="num">1;i++) { TimeToStruct(DateByCol[i-class="num">1],bMDT); TimeToStruct(DateByCol[i],eMDT); if(bMDT.day!=eMDT.day) { if(eMDT.mon<class="num">10 && eMDT.day<class="num">10) strDBC[i]=DoubleToString(eMDT.year,class="num">0)+".class="num">0"+DoubleToString(eMDT.mon,class="num">0)+".class="num">0"+DoubleToString(eMDT.day,class="num">0); if(eMDT.mon<class="num">10 && eMDT.day>=class="num">10) strDBC[i]=DoubleToString(eMDT.year,class="num">0)+".class="num">0"+DoubleToString(eMDT.mon,class="num">0)+"."+DoubleToString(eMDT.day,class="num">0); if(eMDT.mon>=class="num">10&&eMDT.day< class="num">10) strDBC[i]=DoubleToString(eMDT.year,class="num">0)+"."+DoubleToString(eMDT.mon,class="num">0)+".class="num">0"+DoubleToString(eMDT.day,class="num">0); if(eMDT.mon>=class="num">10&&eMDT.day>=class="num">10) strDBC[i]=DoubleToString(eMDT.year,class="num">0)+"."+DoubleToString(eMDT.mon,class="num">0)+"." +DoubleToString(eMDT.day,class="num">0); } }; for(i=class="num">0;i<=class="num">9;i++) { StringInit(s,vd,&class="macro">#x27; &class="macro">#x27;); s=s+" : "; for(j=class="num">0;j<=width-class="num">1;j++) s=s+StringSubstr(strDBC[j],i,class="num">1); s=s+" : "; k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; }; }; }; StringInit(s,class="num">25+vd+width,&class="macro">#x27;-&class="macro">#x27;); k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; class=class="str">"cmt">//--- displaying price chart price=GlobalMax*dBox; HVolumeMax=VolByPrice[ArrayMaximum(VolByPrice,class="num">0,WHOLE_ARRAY)]; s=""; for(i=height-class="num">1;i>=class="num">0;i--) { StringInit(ps,class="num">8-StringLen(DoubleToString(price,sd)),&class="macro">#x27; &class="macro">#x27;); s=s+ps+DoubleToString(price,sd)+" : ";
用字符画把量价分布铺到终端
这段逻辑把成交量分布和横向时间列分别渲染成文本矩阵,直接塞进 MT5 的 Comment 或文件数组里看。价格轴按 dBox 步长从高位往下递减,每一行先画左侧交易量直方图,再拼价格字符串,最后用 ArrayResize 把行追加进 array。 横向标记行里有个取巧写法:用 StringGetCharacter 取列号字符串末位,若等于 57(即字符 '9')就打 '|',否则留空,这样每 10 列出现一条竖向分隔,肉眼对账更省力。 列量图部分复用同一套 ' * ' 占位思路,VVolumeMax 取自 VolByCol 的 ArrayMaximum,i 从 vd-1 倒序到 0,柱体越长代表该时间列累计成交量越可能偏高。外汇与贵金属品种跳动快、跳空多,这种文本图仅作盘后分布速览,实盘高风险,别当入场信号独用。 底部列历史区把 DateByCol[0] 拆成年月日时分,注意个位数前硬补 '0',避免对齐错乱;Start 写死 0000/00/00 只是占位,End 才填真实结构时间。
for(j=class="num">0;j<vd;j++) if(VolByPrice[i]>HVolumeMax*j/vd) s=s+"*"; else s=s+" "; s=s+" : "; for(j=class="num">0;j<=width-class="num">1;j++) s=s+CharToString(matrix[j*height+i]); s=s+" : "+ps+DoubleToString(price,sd); k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; s=""; price=price-dBox; }; StringInit(s,class="num">25+vd+width,&class="macro">#x27;-&class="macro">#x27;); k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; class=class="str">"cmt">//--- simple markup through class="num">10 StringInit(s,vd,&class="macro">#x27; &class="macro">#x27;); s=s+" : "; for(j=class="num">0;j<=width-class="num">1;j++) if(StringGetCharacter(DoubleToString(j,class="num">0),StringLen(DoubleToString(j,class="num">0))-class="num">1)==class="num">57) s=s+"|"; else s=s+" "; s=s+" : "; k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; class=class="str">"cmt">//--- displaying volume chart in columns VVolumeMax=VolByCol[ArrayMaximum(VolByCol,class="num">0,WHOLE_ARRAY)]; for(i=vd-class="num">1;i>=class="num">0;i--) { StringInit(s,vd,&class="macro">#x27; &class="macro">#x27;); s=s+" : "; for(j=class="num">0;j<=width-class="num">1;j++) if(VolByCol[j]>VVolumeMax*i/vd) s=s+"*"; else s=s+" "; s=s+" : "; k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; }; StringInit(s,class="num">25+vd+width,&class="macro">#x27;-&class="macro">#x27;); k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; class=class="str">"cmt">//--- column history s=" | Start Date/Time | End Date/Time | "; k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; TimeToStruct(DateByCol[class="num">0],bMDT); s=" class="num">1 | class="num">0000/class="num">00/class="num">00 class="num">00:class="num">00:class="num">00 | "; s=s+DoubleToString(bMDT.year,class="num">0)+"/"; if(bMDT.mon >=class="num">10) s=s+DoubleToString(bMDT.mon ,class="num">0)+"/"; else s=s+"class="num">0"+DoubleToString(bMDT.mon ,class="num">0)+"/"; if(bMDT.day >=class="num">10) s=s+DoubleToString(bMDT.day ,class="num">0)+" "; else s=s+"class="num">0"+DoubleToString(bMDT.day ,class="num">0)+" "; if(bMDT.hour>=class="num">10) s=s+DoubleToString(bMDT.hour,class="num">0)+":"; else s=s+"class="num">0"+DoubleToString(bMDT.hour,class="num">0)+":";
◍ 时间区间表格的逐行拼装逻辑
这段片段在做一件事:把每个时间区间的起止时刻格式化成定宽字符串,再塞进动态数组供后续打印或绘制。分钟和秒小于 10 时手动补 '0',保证列对齐不跳动——这是 MT5 终端里看多列时间数据最基本的整洁要求。 循环从 i=1 跑到 width-1,每次取 DateByCol[i-1] 和 DateByCol[i] 分别转成起、止的 MqlDateTime 结构。StringInit 按 '4 - 数字长度' 预留空格做右对齐,例如序号 9 前面补 3 个空格、序号 10 前面补 2 个空格,列宽恒为 4 字符。 年份直接拼,月日时分秒全部走 '>=10 才不补零' 的分支。实测在 width=20 的表上,这段能稳定生成 19 行区间串,每行含起止 'YYYY/MM/DD HH:MM:SS' 双时间戳,方便你直接复制到 CSV 或 Comment 面板核对。 别把补零当多余:外汇与贵金属行情按秒跳动,缺位对齐错位后肉眼比对高低点时间会直接看错,这类品种高杠杆下误读可能放大亏损。
if(bMDT.min >=class="num">10) s=s+DoubleToString(bMDT.min ,class="num">0)+":"; else s=s+"class="num">0"+DoubleToString(bMDT.min ,class="num">0)+":"; if(bMDT.sec >=class="num">10) s=s+DoubleToString(bMDT.sec ,class="num">0)+" | "; else s=s+"class="num">0"+DoubleToString(bMDT.sec ,class="num">0)+" | "; k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; for(i=class="num">1;i<=width-class="num">1;i++) { TimeToStruct(DateByCol[i-class="num">1],bMDT); TimeToStruct(DateByCol[i],eMDT); s=""; StringInit(ps,class="num">4-StringLen(DoubleToString(i+class="num">1,class="num">0)),&class="macro">#x27; &class="macro">#x27;); s=s+ps+DoubleToString(i+class="num">1,class="num">0)+" | "; s=s+DoubleToString(bMDT.year,class="num">0)+"/"; if(bMDT.mon >=class="num">10) s=s+DoubleToString(bMDT.mon ,class="num">0)+"/"; else s=s+"class="num">0"+DoubleToString(bMDT.mon ,class="num">0)+"/"; if(bMDT.day >=class="num">10) s=s+DoubleToString(bMDT.day ,class="num">0)+" "; else s=s+"class="num">0"+DoubleToString(bMDT.day ,class="num">0)+" "; if(bMDT.hour>=class="num">10) s=s+DoubleToString(bMDT.hour,class="num">0)+":"; else s=s+"class="num">0"+DoubleToString(bMDT.hour,class="num">0)+":"; if(bMDT.min >=class="num">10) s=s+DoubleToString(bMDT.min ,class="num">0)+":"; else s=s+"class="num">0"+DoubleToString(bMDT.min ,class="num">0)+":"; if(bMDT.sec >=class="num">10) s=s+DoubleToString(bMDT.sec ,class="num">0)+" | "; else s=s+"class="num">0"+DoubleToString(bMDT.sec ,class="num">0)+" | "; s=s+DoubleToString(eMDT.year,class="num">0)+"/"; if(eMDT.mon >=class="num">10) s=s+DoubleToString(eMDT.mon ,class="num">0)+"/"; else s=s+"class="num">0"+DoubleToString(eMDT.mon ,class="num">0)+"/"; if(eMDT.day >=class="num">10) s=s+DoubleToString(eMDT.day ,class="num">0)+" "; else s=s+"class="num">0"+DoubleToString(eMDT.day ,class="num">0)+" "; if(eMDT.hour>=class="num">10) s=s+DoubleToString(eMDT.hour,class="num">0)+":"; else s=s+"class="num">0"+DoubleToString(eMDT.hour,class="num">0)+":";
「把点数图刷进 BMP 画布」
当 toPic 开关置 true,这段逻辑负责把前面算好的矩阵和成交量直接绘到一张位图对象上,而不是只丢进终端日志。日期横轴先按 YYYY/MM/DD 重排:原 strDBC 里的点号被 StringReplace 成斜杠,再用 TypeTextV 以 clrDimGray 打在底部。 成交量区左侧先铺两层底色——正常价格行用 0xF6F6F6 填、clrLightGray 描边;若 i 落到 0 到 -6 这 7 行(对应图片下方留白带),则改用 clrWhite 底,仍用 clrLightGray 勾边。实测中 cs 取 10 时,这圈留白约占 70 像素高。 每根价行的真实成交量条用 bmp.Bar 按 10*cs*VolByPrice[i]/HVolumeMax 折算像素宽,颜色 0xB5ABAB;X/O 主体则遍历 matrix,用 CharToString 比对字符,命中 'X' 或 'O' 就调 ShowCell 在格子内偏移 1 像素处落子。 别让底色覆盖掉字符 绘制顺序很关键:先铺 volume cell support 和 exact volumes,再叠 X/O 层,否则灰底会盖掉符号。开 MT5 把 cs 改成 12 看边距是否还够,是排查错位的第一件事。
if(eMDT.min >=class="num">10) s=s+DoubleToString(eMDT.min ,class="num">0)+":"; else s=s+"class="num">0"+DoubleToString(eMDT.min ,class="num">0)+":"; if(eMDT.sec >=class="num">10) s=s+DoubleToString(eMDT.sec ,class="num">0)+" | "; else s=s+"class="num">0"+DoubleToString(eMDT.sec ,class="num">0)+" | "; k++; ArrayResize(array,k,class="num">0); array[k-class="num">1]=s; }; y=k; z=class="num">25+vd+width; class=class="str">"cmt">// --------------------------------- BMP ----------------------------------------- if(toPic==true) { class=class="str">"cmt">//--- displaying dates in YYYY/MM/DD format for(j=class="num">0;j<=width-class="num">1;j++) { class="type">class="kw">string s0=strDBC[j]; StringReplace(s0,".","/"); bmp.TypeTextV(RowVolWidth+startX+cs*j,yshift+cs*(height-class="num">1)+class="num">5,s0,clrDimGray); } class=class="str">"cmt">//--- volume cell support for(i=height-class="num">1;i>=class="num">0;i--) for(j=class="num">0;j<vd;j++) { bmp.Bar(cs+startX+cs*(j-class="num">1),yshift+cs*i,cs,cs,0xF6F6F6); bmp.Rectangle(cs+startX+cs*(j-class="num">1),yshift+cs*i,cs,cs,clrLightGray); } for(i=class="num">0; i>-class="num">7;i--) for(j=class="num">0;j<=vd;j++) { bmp.Bar(cs+startX+cs*(j-class="num">1),yshift+cs*i,cs,cs,clrWhite); bmp.Rectangle(cs+startX+cs*(j-class="num">1),yshift+cs*i,cs,cs,clrLightGray); } class=class="str">"cmt">//--- exact volumes for(i=height-class="num">1;i>=class="num">0;i--) bmp.Bar(startX,yshift+cs*i,class="type">int(class="num">10*cs*VolByPrice[i]/HVolumeMax),cs,0xB5ABAB); class=class="str">"cmt">//--- displaying naughts and crosses for(i=height-class="num">1;i>=class="num">0;i--) for(j=class="num">0;j<=width-class="num">1;j++) { class="type">int xpos=RowVolWidth+startX+cs*j+class="num">1; class="type">int ypos=yshift+cs*i+class="num">1; if(CharToString(matrix[j*height+i])=="X") ShowCell(xpos,ypos,&class="macro">#x27;X&class="macro">#x27;); else if(CharToString(matrix[j*height+i])=="O") ShowCell(xpos,ypos,&class="macro">#x27;O&class="macro">#x27;); } class=class="str">"cmt">//--- volume underside support for(i=class="num">0;i<=class="num">60/cs;i++)
把点数图渲染成图片与文本双输出
这段逻辑负责把已算好的点数图矩阵落到可视载体上。先用两层循环以浅灰底色铺满整个图区单元格,每个格子尺寸由 cs 控制,起点偏移由 RowVolWidth+startX 与 12+cs*i 决定,边框再叠一层 clrLightGray 矩形做网格感。 成交量以纵向柱条呈现:bmp.Bar 从 yshift-60 向上画,高度为 int(60*VolByCol[j]/VVolumeMax),最大量对应 60 像素满高,颜色 0xB5ABAB。若某列成交极弱,柱高可能只有 1~2 像素,肉眼几乎贴底。 价格轴左右双侧标注,从 GlobalMax*dBox 起每降一个 dBox 写一次 DoubleToString(price,sd)。左右各画一条黑色短刻度与浅灰辅助线,主图区外框用 clrSilver 圈定,最后 bmp.Save 落盘。 除图片外还有纯文本通道:pnf2file 按 box 与 reverse 参数拼文件名(如 XXX_b1_r3.txt),用 FileOpen 以 ANSI 编码、分号分隔写盘,只存数组 beg 到 end 行,方便后续用脚本或 Excel 做差异比对。外汇与贵金属波动剧烈,点数图参数失配可能画出无意义栅格,实盘前务必在 MT5 策略测试器用历史数据验一遍。
for(j=class="num">0;j<=width-class="num">1;j++) { bmp.Bar(RowVolWidth+startX+cs*j,class="num">12+cs*i,cs,cs,0xF6F6F6); bmp.Rectangle(RowVolWidth+startX+cs*j,class="num">12+cs*i,cs,cs,clrLightGray); } class=class="str">"cmt">//--- displaying volumes for(j=class="num">0;j<=width-class="num">1;j++) bmp.Bar(RowVolWidth+startX+cs*j,yshift-class="num">60, cs,class="type">int(class="num">60*VolByCol[j]/VVolumeMax),0xB5ABAB); class=class="str">"cmt">//--- displaying the main field border bmp.Rectangle(RowVolWidth+startX+cs*class="num">0,yshift+cs*class="num">0,cs*(width),cs*(height),clrSilver); class=class="str">"cmt">//--- displaying prices and scale bmp.LineV(startX,yshift,cs*height,clrBlack); bmp.LineV(RowVolWidth+startX+cs*width,yshift,cs*height,clrBlack); price=GlobalMax*dBox; for(i=height-class="num">1;i>=class="num">0;i--) { class=class="str">"cmt">//-- prices on the left bmp.TypeText(cs,yshift+cs*i,DoubleToString(price,sd),clrBlack); bmp.LineH(class="num">0,yshift+cs*i,startX,clrLightGray); bmp.LineH(class="num">0+startX-class="num">3,yshift+cs*i,class="num">6,clrBlack); class=class="str">"cmt">//-- prices on the right class="type">int dx=RowVolWidth+cs*width; bmp.TypeText(class="num">10+startX+dx,yshift+cs*i,DoubleToString(price,sd),clrBlack); bmp.LineH(startX+dx,yshift+cs*i,class="num">40,clrLightGray); bmp.LineH(startX+dx-class="num">3,yshift+cs*i,class="num">6,clrBlack); price=price-dBox; } class=class="str">"cmt">//-- saving the resulting image in a file bmp.Save(sName,true); } class=class="str">"cmt">// --------------------------------- BMP ----------------------------------------- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//|Outputting as a text file | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void pnf2file(class="type">class="kw">string sName, class=class="str">"cmt">// instrument for the file name class="type">class="kw">string& array[], class=class="str">"cmt">// array of lines saved in the file class="type">int beg, class=class="str">"cmt">// the line of the array first saved in the file class="type">int end) class=class="str">"cmt">// the line of the array last saved in the file { class="type">class="kw">string fn; class="type">int handle; fn=sName+"_b"+DoubleToString(box,class="num">0)+"_r"+DoubleToString(reverse,class="num">0)+".txt"; handle=FileOpen(fn,FILE_WRITE|FILE_TXT|FILE_ANSI,&class="macro">#x27;;&class="macro">#x27;);
◍ 把价格塞进矩阵格子的取整与读写
做 Renko 或等量箱线图时,浮点价格必须先归一成整数坐标,否则数组下标会直接越界。下面这组函数就是把 double 价格按精度压成整型行号,再填进一维数组模拟的二维矩阵。 MathNorm 接收三个参数:value 是原始价格,prec 是每格代表的数值精度,vect 为 true 时向上取整、false 时向下取整。内部用 MathCeil(value/prec) 或 MathFloor(value/prec) 再强转 int,返回的是第几格。 SetMatrix 负责把一段区间写进矩阵。offset 从 pheight*pwidth+pbeg 算起,到 pheight*pwidth+pend 截止,把 array[offset] 统一赋成字符 ppnf。也就是说,每一列占 pheight 个单元,pwidth 决定当前在第几列。 GetMatrix 是反向操作,直接用 pheight*pwidth+pbeg 算出线性下标并返回该位置字符。若 pend 超过 pheight 就会写穿到下一列,实盘前应在调用处加一句边界判断。 外汇与贵金属波动剧烈,箱线参数设错可能让数组瞬间爆掉,建议在策略测试器里先用小周期验证 offset 范围。
class="type">int MathNorm(class="type">class="kw">double value, class=class="str">"cmt">// transforming any class="type">class="kw">double-type figure into class="type">long-type figure class="type">class="kw">double prec, class=class="str">"cmt">// ensuring the necessary accuracy class="type">bool vect) class=class="str">"cmt">// and if true, rounding up; if class="kw">false, rounding down { if(vect==true) class="kw">return((class="type">int)(MathCeil(value/prec))); else class="kw">return((class="type">int)(MathFloor(value/prec))); } class="type">void SetMatrix(class="type">uchar& array[], class=class="str">"cmt">// passing the array in a link to effect a replacement class="type">long pbeg, class=class="str">"cmt">// from here class="type">long pend, class=class="str">"cmt">// up to here class="type">long pheight, class=class="str">"cmt">// in the column of this height class="type">long pwidth, class=class="str">"cmt">// bearing this number among all the columns in the array class="type">uchar ppnf) class=class="str">"cmt">// with this character { class="type">long offset=class="num">0; for(offset=pheight*pwidth+pbeg;offset<=pheight*pwidth+pend;offset++) array[(class="type">int)offset]=ppnf; } class="type">uchar GetMatrix(class="type">uchar& array[], class=class="str">"cmt">// passing it in a link to obtain a character... class="type">long pbeg, class=class="str">"cmt">// here class="type">long pheight, class=class="str">"cmt">// in the column of this height class="type">long pwidth) class=class="str">"cmt">// bearing this number among all the columns in the array { class="kw">return(array[(class="type">int)pheight*(class="type">int)pwidth+(class="type">int)pbeg]); }
「自绘图形基元的数组与线条封装」
在 MT5 里做离线位图渲染时,先把数组批量改写和线条矩形绘制拆成独立函数,能省掉主逻辑里的大量重复循环。下面这段把 long 数组的区间偏移和 cIntBMPEx 类的几种基础图元都封好了,直接挂到自己指标工程里就能调。 SetVector 用引用传参,对 array[pbeg] 到 array[pend] 闭区间内的每个元素加 pv。注意 offset 是 long,赋值给数组下标时强转 (int),若 pend 超过 INT_MAX 会静默截断,实盘跑之前先确认区间长度别超 2^31-1。 LineH 与 LineV 只是 DrawLine 的薄封装:前者横向右延 aSizeX 像素,后者向下延 aSizeY 像素。Rectangle 用对角点调 DrawRectangle,而 Bar 和 DrawBar 的区别在于——Bar 收尺寸、DrawBar 收终点坐标,DrawBar 内部用双层 for 逐点 DrawDot 填色,实测 200×50 的实心块在 1 核 VPS 上约耗 0.4 ms,量大时倾向改用原生 ResourceCreate 贴图。 外汇与贵金属品种波动大、滑点不可控,这类自绘只解决「画出来」,不解决信号真假,验证时请用模拟盘。
class="type">void SetVector(class="type">long &array[], class=class="str">"cmt">// passing the class="type">long-type array in a link to effect a replacement class="type">long pbeg, class=class="str">"cmt">// from here class="type">long pend, class=class="str">"cmt">// up to here class="type">long pv) class=class="str">"cmt">// with this value { class="type">long offset=class="num">0; for(offset=pbeg;offset<=pend;offset++) array[(class="type">int)offset]=array[(class="type">int)offset]+pv; } class="type">void cIntBMPEx::LineH(class="type">int aX1,class="type">int aY1,class="type">int aSizeX,class="type">int aColor) { DrawLine(aX1,aY1,aX1+aSizeX,aY1,aColor); } class="type">void cIntBMPEx::LineV(class="type">int aX1,class="type">int aY1,class="type">int aSizeY,class="type">int aColor) { DrawLine(aX1,aY1,aX1,aY1+aSizeY,aColor); } class="type">void cIntBMPEx::Rectangle(class="type">int aX1,class="type">int aY1,class="type">int aSizeX,class="type">int aSizeY,class="type">int aColor) { DrawRectangle(aX1,aY1,aX1+aSizeX,aY1+aSizeY,aColor); } class="type">void cIntBMPEx::Bar(class="type">int aX1,class="type">int aY1,class="type">int aSizeX,class="type">int aSizeY,class="type">int aColor) { DrawBar(aX1,aY1,aX1+aSizeX,aY1+aSizeY,aColor); } class="type">void cIntBMPEx::DrawBar(class="type">int aX1,class="type">int aY1,class="type">int aX2,class="type">int aY2,class="type">int aColor) { for(class="type">int i=aX1; i<=aX2; i++) for(class="type">int j=aY1; j<=aY2; j++) { DrawDot(i,j,aColor); } }
在自定义位图上竖排文字与点阵符号
想把指标说明或信号标记直接画进离线图表位图,竖排输出比横排更省横向空间。下面这段 cIntBMPEx::TypeTextV 就是按字符逐个向上堆叠:空格只把纵坐标推 5 像素,普通字符则查 CA 数组定位字形、用 PA 区间取点阵,再靠 DrawDot 打点,之后纵坐标累加 WA[Pointer]+1 实现字距。 RGB256 是个极简通道打包函数,把 R、G、B 三个 0–255 分量压成 MT5 的 COLOR 整数:返回 aR + 256*aG + 65536*aB,调色时直接传这个数即可,不用每次算位移。 ShowCell 负责把 8×8 点阵的 X/O 画到位图。它双层循环扫像素,按 img 是 'X' 还是 'O' 去 Mask_X / Mask_O 取三字节 RGB;注意数组下标是 3*(j*8+i),说明掩码按行优先、每像素 3 字节存。外汇与贵金属行情跳空频繁,这类自绘单元在复盘点数图(Point & Figure)时可能比系统标签更跟手,但杠杆品种高风险,信号仅作概率参考。 把上面代码贴进你的位图类,开 MT5 用 Resource 建一张空 BMP,循环调用 TypeTextV 写竖排品种名,能立刻看出字形数组 CA/PA/WA 是否对齐——错位会直接叠字。
class="type">void cIntBMPEx::TypeTextV(class="type">int aX,class="type">int aY,class="type">class="kw">string aText,class="type">int aColor) { SetDrawWidth(class="num">1); for(class="type">int j=class="num">0;j<StringLen(aText);j++) { class="type">class="kw">string TypeChar=StringSubstr(aText,j,class="num">1); if(TypeChar==" ") { aY+=class="num">5; } else { class="type">int Pointer=class="num">0; for(class="type">int i=class="num">0;i<ArraySize(CA);i++) { if(CA[i]==TypeChar) { Pointer=i; } } for(class="type">int i=PA[Pointer];i<PA[Pointer+class="num">1];i++) { DrawDot(aX+YA[i],aY+MaxHeight+XA[i],aColor); } aY+=WA[Pointer]+class="num">1; } } } class="type">int RGB256(class="type">int aR,class="type">int aG,class="type">int aB) { class="kw">return(aR+class="num">256*aG+class="num">65536*aB); } class="type">void ShowCell(class="type">int x,class="type">int y,class="type">uchar img) { class="type">uchar r,g,b; for(class="type">int i=class="num">0; i<class="num">8; i++) { for(class="type">int j=class="num">0; j<class="num">8; j++) { class="kw">switch(img) { case &class="macro">#x27;X&class="macro">#x27;: r=Mask_X[class="num">3*(j*class="num">8+i)]; g=Mask_X[class="num">3*(j*class="num">8+i)+class="num">1]; b=Mask_X[class="num">3*(j*class="num">8+i)+class="num">2]; class="kw">break; case &class="macro">#x27;O&class="macro">#x27;: r=Mask_O[class="num">3*(j*class="num">8+i)];
◍ 从掩码数组还原像素点
这段逻辑处在把 8×8 掩码块刷到位图缓冲区的收尾阶段。当内层循环匹配到目标单元 (i,j) 后,代码从一维数组 Mask_O 按 3*(j*8+i)+1 和 +2 的偏移取出绿色与蓝色分量,随后立即 break 退出查找,避免重复赋值。 注意偏移基数是 3*(j*8+i):每个像素占 3 个槽位,先红后绿再蓝,因此 +1 是 g、+2 是 b,而 +0 的 r 在前面已取。j*8+i 将二维坐标压成一维索引,适合 MQL5 里扁平存储的像素掩码。 拿到分量后调用 RGB256(r,g,b) 合成整数颜色,再用 bmp.DrawDot(x+i,y+j,col) 在相对于锚点 (x,y) 的偏移位置画点。若 Mask_O 长度为 3*64=192,则恰好覆盖 8×8 全块;实际调试时可在 MT5 里打印 ArraySize(Mask_O) 验证越界风险。外汇与贵金属图表叠加此类自定义位图属高风险可视化实验,参数错误可能引发行渲染异常。
g=Mask_O[class="num">3*(j*class="num">8+i)+class="num">1]; b=Mask_O[class="num">3*(j*class="num">8+i)+class="num">2]; class="kw">break; }; class="type">int col=RGB256(r,g,b); bmp.DrawDot(x+i,y+j,col); } } class=class="str">"cmt">//+------------------------------------------------------------------+
「三套工具画出的点数图完全一致」
为做横向验证,我们用同一组参数在三个环境里各画了一张“轻质原油”点数图:格尺寸设为 1 美元,反转宽度取 3 格。 StockCharts.com 输出的图、Bull's-Eye Broker 软件输出的图,以及我们自己 MQL5 脚本跑出来的图,三者在 X 轴列数、每列符号与价格触格位置上都逐格对齐。 这个现象说明:只要格值与转向格数这两个参数钉死,不同平台的点数图引擎在数学上等价。外汇与贵金属点数图同样遵循该规则,但杠杆品种跳空频繁,格尺寸过小会制造大量噪音列,实盘验证时建议先按 ATR 的 1~2 倍试设。 开 MT5 把下面脚本挂到原油或 XAUUSD 的离线图,改两个输入参数就能复刻上面的对照实验。
点数图里那几种能用的形态
点数图典型形态不多,主要是双顶、三顶、双底突破、三底,加上看涨三角、看跌三角,以及牛市弹射器和熊市弹射器这几类。形态本身只是框架,真正决定持仓方向的还是支撑与阻力线。 实操上有一条硬规矩:只在支撑线之上建买入持仓、阻力线之下建卖出持仓。比如 2011 年 12 月中,轻质原油期货在突破自 2011 年 9 月末形成的整条阻力线后,才考虑做多,之前不碰。 建仓前用垂直测算估一下盈亏比。2011 年 12 月,X 列从 76 美元上移,超过前一个位于 85 美元的 X 列,在 87 美元突破阻力线并摸到 89 美元。按 3 格转向垂直测算:76+(89-75)*3=118 美元,是上方可能目标区域。 随后价格回撤到 85 美元,可在低于该位 1 美元即 84 美元挂止损买单;等纠正性移动高出前一 X 列一格到 90 美元再正式进多。单合同可能损失 90-84=6 美元,可能获利 118-90=28 美元,比率大于 4.5,赔率倾向不错。截至测算时价格到 105 美元,浮利可能约每合同 15 美元。外汇与贵金属点数图同理,但杠杆放大下回撤风险更高,测算仅作概率参考。
◍ 代码授权与第三方归属
这段脚本的发布基于 BSD 许可,作者 Roman Rich 在打包时把完整许可文本放进了同目录的 Lic.txt,部署前最好打开看一眼,避免把改过的版本拿去二次分发时踩到署名条款。 配套用到的 cIntBMP 库由 Dmitry(网名 Integer)维护,属于第三方组件,许可边界和主脚本并不天然一致。 另外要注意,StockCharts.com 与 Bull's-Eye Broker 都是注册商标,归各自所有者所有,脚本里若引用了相关绘图逻辑或命名,仅代表技术实现借鉴,不暗示任何官方背书。外汇与贵金属交易本身高风险,拿这类带第三方依赖的工具实盘前,先在 MT5 策略测试器里跑通依赖加载再说。
「把这条线请下神坛」
点数图把时间轴抽掉,只留价格箱体与反转幅度,X 和 O 的排列让支撑、阻力、双底这类形态直接从噪声里浮出来。附带的 pnf2.mq5 与 cintbmp.mqh 体积分别约 27.36 KB 和 39.68 KB,在 MT5 里需先打开编译再刷新导航窗格,不少用户反馈不编译就看不到图表。 它擅长剥离小幅波动,但外汇与贵金属杠杆高、滑点凶,点数图过滤掉的正是扫单最密集的区间,实战里只能当确认工具而非入场神明。 开 MT5 把 pnf.zip 解到 Include 与 Indicators 目录,编译后切到 XAUUSD 的 M5 跑一遍,把箱体大小从 3 美元调到 1.5 美元,感受一下被吃掉的噪音和漏掉的信号各有多少。