最新的改革·综合运用
📊

最新的改革·综合运用

(3/3)·把 Renko 之外的非时间图表真正写进 EA 之前,先用脚本跑通点数图的全流程

实战向 第 3/3 篇
多数交易者盯着时间轴 K 线,却忽略价格本身的堆积节奏。点数图把时间变量拿掉,只保留格值与转向,适合识别支撑阻力密度。本篇承接前两篇基础,直接把算法落成可跑的脚本并比对商业软件输出。

支撑阻力斜线的矩阵描点逻辑

这段片段在做一件事:把识别出的支撑或阻力区段,按趋势方向在字符矩阵里逐列标成 '+',并可选渲染到 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 天+)时走分支处理——外汇与贵金属此类长周期图表波动剧烈,实盘验证前务必认清高风险。

MQL5 / C++
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 这个阈值就能控制「切到年月视图」的灵敏度和外汇、贵金属报价屏的高风险始终存在,参数调试仅供验证。

MQL5 / C++
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 位宽,拼出「价格 :」前缀。贵金属跳空夜这类高波动场景,标尺若不对齐,肉眼读柱容易偏一格。

MQL5 / C++
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 才填真实结构时间。

MQL5 / C++
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 面板核对。 别把补零当多余:外汇与贵金属行情按秒跳动,缺位对齐错位后肉眼比对高低点时间会直接看错,这类品种高杠杆下误读可能放大亏损。

MQL5 / C++
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 看边距是否还够,是排查错位的第一件事。

MQL5 / C++
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 策略测试器用历史数据验一遍。

MQL5 / C++
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 范围。

MQL5 / C++
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 贴图。 外汇与贵金属品种波动大、滑点不可控,这类自绘只解决「画出来」,不解决信号真假,验证时请用模拟盘。

MQL5 / C++
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 是否对齐——错位会直接叠字。

MQL5 / C++
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) 验证越界风险。外汇与贵金属图表叠加此类自定义位图属高风险可视化实验,参数错误可能引发行渲染异常。

MQL5 / C++
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 美元,感受一下被吃掉的噪音和漏掉的信号各有多少。

让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到点数图密度与转向提示,把重复劳动交给小布,你专注决策。

常见问题

通常取该品种近期平均真实波幅的 1/3 到 1/2 作为格值,外汇贵金属因杠杆高风险需先用模拟盘验证,再上真实账户。
因两者都基于最高-最低价取整到格尺寸并套用相同转向格数,差异只可能来自历史报价源与取整边界处理。
目前小布盯盘内置的是通用非时间图表诊断,自写脚本需在本地面板跑完再对照小布的密度标注,减少手动刷图。
在黄金期货上,三联底突破第三列顶部可能倾向反转概率上升,但外汇贵金属高风险,须结合仓位控制。
只能用 Bull's-Eye Broker 等支持外汇的离线软件,或自写脚本读本地报价,无法用该网站交叉验证。