ZigZag (之字折线) 的力量(第二部分)。 接收、处理和显示数据的示例·综合运用
📐

ZigZag (之字折线) 的力量(第二部分)。 接收、处理和显示数据的示例·综合运用

(3/3)· 从接收数据到图形化统计,教你用 4 个指标和 2 类 EA 榨干之字折线的信息量

含代码示例偏理论 第 3/3 篇
很多交易者把 ZigZag 只当画线辅助,加载完就盯着拐点猜方向。殊不知折线段的频率、时长、百分比差异才是价格行为统计的富矿,不提取出来等于白白浪费了历史数据。

在 MT5 面板里搭按钮和品种扫描表

做多品种监控面板时,GUI 初始化顺序很关键:先挂按钮、再建表格、最后补进度栏,任何一步返回 false 都会导致整个面板加载失败。下面这段初始化尾部代码就是标准套路,按钮放在窗口坐标 (375,0)、宽 50 高 70,表格列间距 2 行间距 75,进度栏贴着状态栏。 //--- 按钮 if(!CWndCreate::CreateButton(m_request,"Request",m_window,0,375,50,70)) return(false); //--- 表格 if(!CreateTable(2,75)) return(false); //--- 进度栏 if(!CWndCreate::CreateProgressBar(m_progress_bar,"Processing:",m_status_bar,0,2,3)) return(false); //--- 结束 GUI 开发 CWndEvents::CompletedGUI(); return(true); } 表格本身用 CreateTable() 定义:4 列 1 行,列宽默认 50 像素、第 0 列拉到 80 像素放品种名,表头写死为 Symbols / M5 / H1 / H8,也就是只扫这四个时间框架。开启 Zebra 行底色(clrWhiteSmoke)和自动 X/Y 拉伸后,面板能跟着窗口变宽变高,右侧留 2 像素、底部留 24 像素边距。 #define COLUMNS1_TOTAL 4 #define ROWS1_TOTAL 1 //--- 保存指向主元素的指针 m_table.MainPointer(m_window); //--- 列宽数组 int width[COLUMNS1_TOTAL]; ::ArrayInitialize(width,50); width[0]=80; //--- 沿 X 轴的列中文本偏移的数组 int text_x_offset[COLUMNS1_TOTAL]; ::ArrayInitialize(text_x_offset,7); //--- 列中的文本对齐数组 ENUM_ALIGN_MODE align[COLUMNS1_TOTAL]; ::ArrayInitialize(align,ALIGN_CENTER); align[0]=ALIGN_LEFT; //--- 属性 m_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL); m_table.TextAlign(align); m_table.ColumnsWidth(width); m_table.TextXOffset(text_x_offset); m_table.ShowHeaders(true); m_table.IsSortMode(true); m_table.IsZebraFormatRows(clrWhiteSmoke); m_table.AutoXResizeMode(true); m_table.AutoYResizeMode(true); m_table.AutoXResizeRightOffset(2); m_table.AutoYResizeBottomOffset(24); //--- 创建控件 if(!m_table.CreateTable(x_gap,y_gap)) return(false); //--- 标题 string headers[]={"Symbols","M5","H1","H8"}; for(uint i=0; i<m_table.ColumnsTotal(); i++) m_table.SetHeaderText(i,headers[i]); //--- 将对象添加到公共对象组数组 CWndContainer::AddToElementsArray(0,m_table); return(true); 品种过滤交给 CheckFilterText(),它按字符串规则筛掉不符合条件的 symbol,再进表。外汇和贵金属波动大、滑点频繁,这种面板只解决「看」的问题,信号真假还得你人工复核,别直接当入场依据。

MQL5 / C++
  if(!CWndCreate::CreateButton(m_request,"Request",m_window,class="num">0,class="num">375,class="num">50,class="num">70))
     class="kw">return(class="kw">false);
  if(!CreateTable(class="num">2,class="num">75))
     class="kw">return(class="kw">false);
  if(!CWndCreate::CreateProgressBar(m_progress_bar,"Processing:",m_status_bar,class="num">0,class="num">2,class="num">3))
     class="kw">return(class="kw">false);
  CWndEvents::CompletedGUI();
  class="kw">return(true);
}
class="type">bool CProgram::CreateTable(class="kw">const class="type">int x_gap,class="kw">const class="type">int y_gap)
  {
class="macro">#define COLUMNS1_TOTAL class="num">4
class="macro">#define ROWS1_TOTAL    class="num">1
  m_table.MainPointer(m_window);
  class="type">int width[COLUMNS1_TOTAL];
  ::ArrayInitialize(width,class="num">50);
  width[class="num">0]=class="num">80;
  class="type">int text_x_offset[COLUMNS1_TOTAL];
  ::ArrayInitialize(text_x_offset,class="num">7);
  ENUM_ALIGN_MODE align[COLUMNS1_TOTAL];
  ::ArrayInitialize(align,ALIGN_CENTER);
  align[class="num">0]=ALIGN_LEFT;
  m_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL);
  m_table.TextAlign(align);
  m_table.ColumnsWidth(width);
  m_table.TextXOffset(text_x_offset);
  m_table.ShowHeaders(true);
  m_table.IsSortMode(true);
  m_table.IsZebraFormatRows(clrWhiteSmoke);
  m_table.AutoXResizeMode(true);
  m_table.AutoYResizeMode(true);
  m_table.AutoXResizeRightOffset(class="num">2);
  m_table.AutoYResizeBottomOffset(class="num">24);
  if(!m_table.CreateTable(x_gap,y_gap))
     class="kw">return(class="kw">false);
  class="type">class="kw">string headers[]={"Symbols","M5","H1","H8"};
  for(class="type">uint i=class="num">0; i<m_table.ColumnsTotal(); i++)
     m_table.SetHeaderText(i,headers[i]);
  CWndContainer::AddToElementsArray(class="num">0,m_table);
  class="kw">return(true);
}
class CProgram : class="kw">public CWndCreate
  {
class="kw">private:
  class="type">bool             CheckFilterText(class="kw">const class="type">class="kw">string symbol_name);
  };
class="type">bool CProgram::CheckFilterText(class="kw">const class="type">class="kw">string symbol_name)
  {

◍ 用过滤器筛出可交易外汇品种

在 MT5 面板工具里做品种扫描时,先靠一个文本过滤器决定哪些符号进入后续列表。过滤器没按下、或输入框为空,函数直接返回 true,等于不过滤全放行;一旦有逗号分隔的关键词,就按子串匹配品种名,命中即标记 check 并跳出循环。 下面的 GetSymbols 方法负责真正拉取品种。它先调用 SymbolsTotal(false) 拿到当前终端可见品种总数,再逐个用 SymbolName(i,false) 取名字,并顺手 SymbolSelect(symbol_name,false) 把该品种从市场观察窗口隐藏,避免界面被刷屏。 只保留纯外汇品种:用 SymbolInfoInteger 查 SYMBOL_TRADE_CALC_MODE,不等于 SYMBOL_CALC_MODE_FOREX 的直接 continue;同时剔掉 SYMBOL_TRADE_MODE 为 DISABLED 的禁止交易品种。最后过一遍 CheckFilterText 的名称过滤,剩下的才写进 m_symbols 数组供后面跑回测或盯盘用。 外汇与贵金属杠杆高、点差跳变频繁,这类自动隐藏和过滤逻辑能降低误选冷门或禁交易品种的概率,但终端品种权限仍受券商限制,实际可交易集可能少于代码筛出的数量。

MQL5 / C++
class="type">bool check=class="kw">false;
class=class="str">"cmt">//--- 如果启用了品种名称过滤器
if(!m_filter.IsPressed())
   class="kw">return(true);
class=class="str">"cmt">//--- 如果输入了文本
class="type">class="kw">string text=m_filter.GetValue();
if(text=="")
   class="kw">return(true);
class=class="str">"cmt">//--- 切分为子串
class="type">class="kw">string elements[];
class="type">class="kw">ushort sep=::StringGetCharacter(",",class="num">0);
::StringSplit(text,sep,elements);
class=class="str">"cmt">//--- 匹配检查
class="type">int elements_total=::ArraySize(elements);
for(class="type">int e=class="num">0; e<elements_total; e++)
   {
   class=class="str">"cmt">//--- 删除边外空白
   ::StringTrimLeft(elements[e]);
   ::StringTrimRight(elements[e]);
   class=class="str">"cmt">//--- 如果检测到匹配
   if(::StringFind(symbol_name,elements[e])>-class="num">1)
      {
      check=true;
      class="kw">break;
      }
   }
class=class="str">"cmt">//--- 结果
class="kw">return(check);
}
class CProgram : class="kw">public CWndCreate
   {
class="kw">private:
   class=class="str">"cmt">//--- 品种数组
   class="type">class="kw">string            m_symbols[];
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 获取品种
   class="type">void              GetSymbols(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取品种                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::GetSymbols(class="type">void)
   {
class=class="str">"cmt">//--- 进度
   m_progress_bar.LabelText("Get symbols...");
   m_progress_bar.Update(class="num">0,class="num">1);
class=class="str">"cmt">//--- 清楚品种数组
   ::ArrayFree(m_symbols);
class=class="str">"cmt">//--- 收集外汇品种的数组
   class="type">int symbols_total=::SymbolsTotal(class="kw">false);
   for(class="type">int i=class="num">0; i<symbols_total; i++)
      {
      class=class="str">"cmt">//--- 获取品种名称
      class="type">class="kw">string symbol_name=::SymbolName(i,class="kw">false);
      class=class="str">"cmt">//--- 在市场观察窗口中隐藏它
      ::SymbolSelect(symbol_name,class="kw">false);
      class=class="str">"cmt">//--- 如果这不是外汇代码,则转到下一个
      if(::SymbolInfoInteger(symbol_name,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX)
         class="kw">continue;
      class=class="str">"cmt">//--- 如果禁止交易,则转到下一个
      if(::SymbolInfoInteger(symbol_name,SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_DISABLED)
         class="kw">continue;
      class=class="str">"cmt">//--- 按照过滤器检查品种
      if(!CheckFilterText(symbol_name))
         class="kw">continue;
      class=class="str">"cmt">//--- 将品种保存到数组

「动态装载品种与等待指标计算就绪」

在面板初始化阶段,把外部传入的 symbol_name 追加进 m_symbols 数组:先取原长度 array_size,用 ArrayResize 预留 1000 个冗余单元避免频繁重分配,再把新名写进末位。若数组为空,则把当前图表 _Symbol 作为保底默认值塞进去,防止后续遍历空数组出错。 品种收集完后,用 SymbolSelect(m_symbols[i], true) 逐个把它们在市场观察窗口置为可见,这样后续 iCustom 取数才不会因品种未订阅而返回空值。 GetSymbolsData 里调的是自定义指标 PercentageSegmentsZZ.ex5,路径写死在 ::Indicators\Custom\ZigZag\ 下,iCustom 第 4~6 参分别是 0、0、5,buffer_index 取 2 号缓冲。 这里有个实战坑:Bars(symbol,period,start_time,end_time) 算出的 bars_total 可能大于指标已算出的 bars_calculated,此时若不等待直接 CopyBuffer 会丢数据。代码用 Sleep(100) 轮询,直到 bars_calculated>=bars_total 才跳出,拷贝窗口精确到起止日期。外汇与贵金属行情跳空频繁,这种等待机制能降低自定义 ZigZag 类指标回测时前段样本缺失的概率,但高频切换品种仍属高风险操作。

MQL5 / C++
class="type">int array_size=::ArraySize(m_symbols);
::ArrayResize(m_symbols,array_size+class="num">1,class="num">1000);
m_symbols[array_size]=symbol_name;
class=class="str">"cmt">//--- 如果数组为空,则将当前品种设置为默认值
class="type">int array_size=::ArraySize(m_symbols);
if(array_size<class="num">1)
  {
   ::ArrayResize(m_symbols,array_size+class="num">1);
   m_symbols[array_size]=_Symbol;
  }
class=class="str">"cmt">//--- 在市场观察窗口中显示
class="type">int selected_symbols_total=::ArraySize(m_symbols);
for(class="type">int i=class="num">0; i<selected_symbols_total; i++)
   ::SymbolSelect(m_symbols[i],true);
}
class CProgram : class="kw">public CWndCreate
  {
class="kw">private:
  class=class="str">"cmt">//--- 获取品种数据
  class="type">class="kw">double          GetSymbolsData(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES period);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  获取品种数据                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CProgram::GetSymbolsData(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES period)
  {
   class="type">class="kw">double result       =class="num">0.0;
   class="type">int    buffer_index =class="num">2;
class=class="str">"cmt">//--- 获取指标句柄
   class="type">class="kw">string path  ="::Indicators\\Custom\\ZigZag\\PercentageSegmentsZZ.ex5";
   class="type">int    handle =::iCustom(symbol,period,path,class="num">0,class="num">0,class="num">5);
   if(handle!=INVALID_HANDLE)
     {
      class=class="str">"cmt">//--- 复制指定范围内的数据
      class="type">class="kw">double  data[];
      class="type">class="kw">datetime start_time =m_from_date.SelectedDate();
      class="type">class="kw">datetime end_time   =m_to_date.SelectedDate();
      class=class="str">"cmt">//--- 指定范围内的柱线数量
      class="type">int bars_total=::Bars(symbol,period,start_time,end_time);
      class=class="str">"cmt">//--- 指定范围内的柱线数量
      class="type">int bars_calculated=::BarsCalculated(handle);
      if(bars_calculated<bars_total)
        {
         class="kw">while(true)
           {
            ::Sleep(class="num">100);
            bars_calculated=::BarsCalculated(handle);
            if(bars_calculated>=bars_total)
               class="kw">break;
           }
        }
      class=class="str">"cmt">//--- 获取数据
      class="type">int copied=::CopyBuffer(handle,buffer_index,start_time,end_time,data);
      if(copied<class="num">1)
        {
         class="kw">while(true)
           {

指标缓冲读取与超阈占比统计

这段逻辑先处理指标数据拉取失败时的兜底:若 CopyBuffer 在重试后拿到的数据量仍不足 bars_total,就靠 Sleep(100) 做 100 毫秒级退让再补一次;当 total<1 直接带着空 result 退出,避免后面除零。 核心统计在标红块里:用 for 循环扫完 data 数组,凡是 data[k] 大于 m_level.GetValue() 返回的阈值就 counter++,最后 result = (counter/total)*100,得出该品种在当前周期里「突破阈值」的柱线百分比。外汇与贵金属波动随机,这个百分比只反映历史样本分布,不预示后续方向。 表格刷新由 RebuildingTables 负责,先 DeleteAllRows 清空,再按 m_symbols 数量从 1 开始 AddRow;SetData 则对每一行调用 GetSymbolsData 取数,用 m_table.SetValue(column_index,r,string(value),2,true) 写两位小数并触发重绘。开 MT5 把 m_level 设成 ATR 倍数,就能直接看到哪些品种近期异动概率偏高。

MQL5 / C++
::Sleep(class="num">100);
copied=::CopyBuffer(handle,buffer_index,start_time,end_time,data);
if(copied>=bars_total)
  class="kw">break;
class=class="str">"cmt">//--- 如果没有收到数据则退出
class="type">int total=::ArraySize(data);
if(total<class="num">1)
  class="kw">return(result);
class=class="str">"cmt">//--- 计算重复次数
class="type">int counter=class="num">0;
for(class="type">int k=class="num">0; k<total; k++)
  {
   if(data[k]>(class="type">class="kw">double)m_level.GetValue())
     counter++;
  }
class=class="str">"cmt">//--- 百分比
result=((class="type">class="kw">double)counter/(class="type">class="kw">double)total)*class="num">100;
::IndicatorRelease(handle);
class="kw">return(result);
class="type">void CProgram::RebuildingTables(class="type">void)
{
m_table.DeleteAllRows();
class="type">int symbols_total=::ArraySize(m_symbols);
for(class="type">int i=class="num">1; i<symbols_total; i++)
  m_table.AddRow(i);
}
class="type">void CProgram::SetData(class="kw">const class="type">int column_index,class="kw">const ENUM_TIMEFRAMES period)
{
 for(class="type">uint r=class="num">0; r<(class="type">uint)m_table.RowsTotal(); r++)
  {
   class="type">class="kw">double value=GetSymbolsData(m_symbols[r],period);
   m_table.SetValue(column_index,r,class="type">class="kw">string(value),class="num">2,true);
   m_table.Update();
  }
}

◍ 多周期数据落表的进度与填充逻辑

在 MT5 面板程序里,把多品种多周期数据塞进表格不能一把梭,得先让进度条接管界面反馈。CProgram::GetData 的调用顺序是 StartProgress → GetSymbols → SetDataToTable → EndProgress,其中 SetDataToTable 负责真正的表结构重建与写值。 SetDataToTable 先调 RebuildingTable 清掉旧结构,再写死表头为 Symbols / M5 / H1 / H8 四列,并把第 1 列以后的列数据类型设为 TYPE_DOUBLE。第一列用 m_table.SetValue(0,r,m_symbols[r],0,true) 逐行填品种名,随后 m_table.Update(true) 强制重绘。 剩余三列分别由 SetData(1,PERIOD_M5)、SetData(2,PERIOD_H1)、SetData(3,PERIOD_H8) 填充,周期名通过 GetPeriodName 截取 EnumToString(period) 从第 7 个字符起的子串得到,比如 PERIOD_M5 会变成 M5。 进度反馈分两层:外层 GetData 用 StartProgress / EndProgress 包住整体;内层循环里 m_progress_bar.LabelText 拼出 "Data preparation ["+品种+","+周期名+"]...",再用 Update(r,总行数) 推进百分比。外汇与贵金属行情多周期抓取受点差和报价中断影响,实盘跑这套填充逻辑时滑点风险偏高,建议在策略测试器用历史数据先验证表结构无误。

MQL5 / C++
class=class="str">"cmt">//--- 进度
   m_progress_bar.LabelText("Data preparation ["+m_symbols[r]+","+GetPeriodName(period)+"]...");
   m_progress_bar.Update(r,m_table.RowsTotal());
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回周期字符串对应的值                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CProgram::GetPeriodName(class="kw">const ENUM_TIMEFRAMES period)
  {
   class="kw">return(::StringSubstr(::EnumToString(period),class="num">7));
  }
class CProgram : class="kw">public CWndCreate
  {
class="kw">private:
   class=class="str">"cmt">//--- 在表格中填充数据
   class="type">void              SetDataToTable(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 在表格中填充数据                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::SetDataToTable(class="type">void)
  {
class=class="str">"cmt">//--- 进度
   m_progress_bar.LabelText("Data preparation...");
   m_progress_bar.Update(class="num">0,class="num">1);
class=class="str">"cmt">//--- 重新构建表格
   RebuildingTable();
class=class="str">"cmt">//--- 标题
   class="type">class="kw">string headers[]={"Symbols","M5","H1","H8"};
   for(class="type">uint i=class="num">0; i<m_table.ColumnsTotal(); i++)
      m_table.SetHeaderText(i,headers[i]);
   for(class="type">uint i=class="num">1; i<m_table.ColumnsTotal(); i++)
      m_table.DataType(i,TYPE_DOUBLE);
class=class="str">"cmt">//--- 在第一列里设置数值
   for(class="type">uint r=class="num">0; r<(class="type">uint)m_table.RowsTotal(); r++)
      m_table.SetValue(class="num">0,r,m_symbols[r],class="num">0,true);
class=class="str">"cmt">//--- 显示表格
   m_table.Update(true);
class=class="str">"cmt">//--- 用数据填充剩余的列
   SetData(class="num">1,PERIOD_M5);
   SetData(class="num">2,PERIOD_H1);
   SetData(class="num">3,PERIOD_H8);
  }
class CProgram : class="kw">public CWndCreate
  {
class="kw">private:
   class=class="str">"cmt">//--- 获取数据
   class="type">void              GetData(class="type">void);
   class=class="str">"cmt">//--- 进度(class="num">1)开始,(class="num">2)结束
   class="type">void              StartProgress(class="type">void);
   class="type">void              EndProgress(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取数据                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::GetData(class="type">void)
  {
class=class="str">"cmt">//--- 进度开始
   StartProgress();
class=class="str">"cmt">//--- 获取品种列表
   GetSymbols();
class=class="str">"cmt">//--- 在表格中填充数据
   SetDataToTable();
class=class="str">"cmt">//--- 进度结束

「进度条与事件响应的衔接写法」

在 MT5 面板程序里,长按按钮触发数据拉取前,先用 StartProgress 把进度栏亮出来,能避免界面假死被用户误判为崩了。它的实现很直白:写标签 "Please wait...",把进度初值设成 0 和 1,Show 之后立刻 Redraw 强制重绘。 EndProgress 负责收尾,Hide 掉进度栏只是第一步,还要把按钮的 MouseFocus 置 false 并 Update(true),否则按钮会卡在悬停态。这两段配对使用,界面状态才闭环。 事件分发集中在 OnEvent:CHARTEVENT_CUSTOM+ON_CLICK_BUTTON 里比对 lparam 和 m_request.Id(),命中才跑 GetData;表格排序则走 ON_SORT_DATA 分支,只认 m_table.Id()。这种按控件 ID 分流的写法,在多控件面板里不容易串事件。 表格配色可用 CColors 做渐变填充,声明 color out_colors[] 接收输出。外汇与贵金属行情波动剧烈,这类面板仅作辅助观察,实盘决策须自行承担高风险。

MQL5 / C++
class="type">void CProgram::StartProgress(class="type">void)
  {
  m_progress_bar.LabelText("Please wait...");
  m_progress_bar.Update(class="num">0,class="num">1);
  m_progress_bar.Show();
  m_chart.Redraw();
  }
class="type">void CProgram::EndProgress(class="type">void)
  {
class=class="str">"cmt">//--- 隐藏进度栏
  m_progress_bar.Hide();
class=class="str">"cmt">//--- 更新按钮
  m_request.MouseFocus(class="kw">false);
  m_request.Update(true);
  m_chart.Redraw();
  }
class="type">void CProgram::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- 按下按钮事件
  if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
    {
    if(lparam==m_request.Id())
      {
      class=class="str">"cmt">//--- 获取数据
      GetData();
      class="kw">return;
      }
    class=class="str">"cmt">//---
    class="kw">return;
    }
class=class="str">"cmt">//--- 表格排序事件
  if(id==CHARTEVENT_CUSTOM+ON_SORT_DATA)
    {
    if(lparam==m_table.Id())
      {
      m_table.Update(true);
      class="kw">return;
      }
    class=class="str">"cmt">//---
    class="kw">return;
    }
  }

用三色渐变给表格数值上色

在 MT5 面板里把数字直接染成热力图,比盯着裸数字看相关性快得多。下面这段逻辑先抓出表格里的最大和最小值,再映射成一条从番茄红到矢车菊蓝的渐变带。 核心做法是用两层循环扫遍所有单元格(列从 1 起跳,跳过表头列;行从 0 开始)。max 初值设 0、min 初值设 100,靠 fmax / fmin 实时刷新边界,所以表格里只要出现大于 0 或小于 100 的值就会被正确捕获。 边界拿到后先用 floor 向下取整,再算 range = int(max-min)+1,这个 +1 保证了极值本身也能落进渐变数组下标。Gradient 按 range 长度生成 out_colors,之后第二轮循环里用「当前值减 min」当索引取色,写回 BackColor。 外汇与贵金属行情跳动快,这种热力表若挂在实时信号面板上,颜色突变可能预示某周期相关性反转,但样本外仍可能失效,只看图不做仓位判断。开 MT5 把下面代码丢进你的 CTable 派生类,改一下 colors 数组就能换配色方案。

MQL5 / C++
class=class="str">"cmt">//--- 三色渐变
   class="type">class="kw">color colors[class="num">3]={clrTomato,clrWhite,clrCornflowerBlue};
class=class="str">"cmt">//--- 查找最低和最高表格数值
   class="type">class="kw">double max =class="num">0;
   class="type">class="kw">double min =class="num">100;
   for(class="type">uint c=class="num">1; c<(class="type">uint)m_table.ColumnsTotal(); c++)
     {
       for(class="type">uint r=class="num">0; r<(class="type">uint)m_table.RowsTotal(); r++)
         {
          max =::fmax(max,(class="type">class="kw">double)m_table.GetValue(c,r));
          min =::fmin(min,(class="type">class="kw">double)m_table.GetValue(c,r));
         }
     }
class=class="str">"cmt">//--- 调整到最接近的较低整数
   max =::floor(max);
   min =::floor(min);
class=class="str">"cmt">//--- 获得范围
   class="type">int range =class="type">int(max-min)+class="num">1;
class=class="str">"cmt">//--- 获得颜色渐变数组
   clr.Gradient(colors,out_colors,range);
class=class="str">"cmt">//--- 设置单元格的背景颜色
   for(class="type">uint c=class="num">1; c<(class="type">uint)m_table.ColumnsTotal(); c++)
     {
       for(class="type">uint r=class="num">0; r<(class="type">uint)m_table.RowsTotal(); r++)
         {
          class="type">int index=(class="type">int)m_table.GetValue(c,r)-(class="type">int)min;
          m_table.BackColor(c,r,out_colors[index],true);
         }
     }
class=class="str">"cmt">//--- 更新表格
   m_table.Update();
   }

◍ 统计各价位区间内的线段密度

在上一版 EA 基础上改出双表结构:第一张表只放品种清单,第二张表两列,分别是「点数区间」与「该区间内的线段数」。点「请求」拉品种,「计算」才在指定时间窗内抓 ZigZag 数据并落表。 核心差异在第二张表。CProgram::GetIndicatorData() 先连 ExactZZ_Plus 指标,再按起止日期拷贝线段;CProgram::GetLevels() 负责按步长切分区间,逻辑是先取最大线段点数,再循环填数组到顶。 填表由 CProgram::SetDataToTable2() 完成:先判第一张表有无高亮品种,无则退并写日志;有则取数据、调上述两法,再逐区间累加计数器。 实测 M5 上 EURUSD 自 2010 年至今、步长 100 点(五位报价),线段总数 302145,最大线段约 2400 点。0–100 点区间最密,数量随区间放大而递减。外汇与贵金属波动剧烈,此类统计仅描述历史分布,不预示后续线段规模。

MQL5 / C++
class CProgram : class="kw">public CWndCreate
  {
class="kw">private:
   class=class="str">"cmt">//--- 获取指标数据
   class="type">void                GetIndicatorData(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES period);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取指标数据                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::GetIndicatorData(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES period)
  {
class=class="str">"cmt">//--- 获取指标句柄
   class="type">class="kw">string path   ="::Indicators\\Custom\\ZigZag\\ExactZZ_Plus.ex5";
   class="type">int    handle =::iCustom(symbol,period,path,class="num">0,class="num">0);
   if(handle!=INVALID_HANDLE)
     {
      class=class="str">"cmt">//--- 复制指定范围内的数据
      class="type">class="kw">datetime start_time =m_from_date.SelectedDate();
      class="type">class="kw">datetime end_time   =m_to_date.SelectedDate();
      m_zz.GetZigZagData(handle,class="num">2,class="num">3,symbol,period,start_time,end_time);
      class=class="str">"cmt">//--- 在状态栏中显示数据
      class="type">class="kw">string text="["+symbol+","+(class="type">class="kw">string)GetPeriodName(period)+"] - Segments total: "+(class="type">class="kw">string)m_zz.SegmentsTotal();
      m_status_bar.SetValue(class="num">0,text);
      m_status_bar.GetItemPointer(class="num">0).Update(true);
     }
class=class="str">"cmt">//--- 释放指标
   ::IndicatorRelease(handle);
   }
class CProgram : class="kw">public CWndCreate
  {
class="kw">private:
   class=class="str">"cmt">//--- 数组范围
   class="type">int                m_levels_array[];
   class=class="str">"cmt">//---
class="kw">private:
   class=class="str">"cmt">//--- 获取级别
   class="type">void                GetLevels(class="type">void);
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 获取级别                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::GetLevels(class="type">void)
  {
class=class="str">"cmt">//--- 释放数组
   ::ArrayFree(m_levels_array);
class=class="str">"cmt">//--- 获取最大线段大小
   class="type">int max_value=class="type">int(m_zz.LargestSegment()/m_symbol.Point());
class=class="str">"cmt">//--- 用级别填充数组
   class="type">int counter_levels=class="num">0;

「表格联动下的分形段长统计」

左侧表格没选中品种时,SetDataToTable2 会直接 Print 提示并 return,不会往下跑指标抓取。这一道闸能避免空 symbol 调用指标接口导致的运行时报错。 核心逻辑是先拿选中行的 symbol 名传给 m_symbol,再调 GetIndicatorData 与 GetLevels 把级别数组 m_levels_array 填满;之后用双层循环统计之字转向(ZigZag)每段点数落在区间的频次。 统计段用 m_zz.SegmentSize(s)/m_symbol.Point() 把段长换算成点数,再判断 size 是否落在 m_levels_array[i] 与 m_levels_array[i+1] 之间,命中就把第二列计数 +1。若你的 ZZ 深度参数不同,段数分布会明显偏移,建议在 MT5 里改 Input 后重跑观察。 循环里每步都更新 m_progress_bar,标签动态显示当前级别如 'Get data [50]...',行数多时你能直接看到卡在哪一级。外汇与贵金属波动剧烈,段长统计仅反映历史形态概率,不构成方向判断。

MQL5 / C++
class="kw">while(true)
  {
  class="type">int size=::ArraySize(m_levels_array);
  ::ArrayResize(m_levels_array,size+class="num">1);
  m_levels_array[size]=counter_levels;
  class=class="str">"cmt">//---
  if(counter_levels>max_value)
     class="kw">break;
  class=class="str">"cmt">//---
  counter_levels+=(class="type">int)m_step.GetValue();
  }
}
class CProgram : class="kw">public CWndCreate
 {
class="kw">private:
  class=class="str">"cmt">//--- 用数据填写第二张表格
  class="type">void                SetDataToTable2(class="type">void);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 用数据填写第二张表格                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::SetDataToTable2(class="type">void)
 {
class=class="str">"cmt">//--- 如果不是高亮显示得行,则退出
  if(m_table1.SelectedItem()==WRONG_VALUE)
    {
      ::Print(__FUNCTION__," > Select a symbol in the table on the left!");
      class="kw">return;
    }
class=class="str">"cmt">//--- 进度开始
  StartProgress();
class=class="str">"cmt">//--- 隐藏表格
  m_table2.Hide();
class=class="str">"cmt">//--- 从第一张表格中获取品种
  class="type">class="kw">string symbol=m_table1.GetValue(class="num">0,m_table1.SelectedItem());
  m_symbol.Name(symbol);
class=class="str">"cmt">//--- 获取指标数据
  GetIndicatorData(symbol,_Period);
class=class="str">"cmt">//--- 获取级别
  GetLevels();
class=class="str">"cmt">//--- 重新构建表格
  RebuildingTable2();
class=class="str">"cmt">//--- 在第一列中设置范围
  for(class="type">uint r=class="num">0; r<(class="type">uint)m_table2.RowsTotal(); r++)
      m_table2.SetValue(class="num">0,r,(class="type">class="kw">string)m_levels_array[r],class="num">0);
class=class="str">"cmt">//--- 获取第二列的值
  class="type">int items_total=::ArraySize(m_levels_array);
  class="type">int segments_total=m_zz.SegmentsTotal();
  for(class="type">int i=class="num">0; i<items_total-class="num">1; i++)
    {
      class=class="str">"cmt">//--- 进度
      m_progress_bar.LabelText("Get data ["+(class="type">class="kw">string)m_levels_array[i]+"]...");
      m_progress_bar.Update(i,m_table2.RowsTotal());
      class=class="str">"cmt">//---
      for(class="type">int s=class="num">0; s<segments_total; s++)
        {
         class="type">int size=class="type">int(m_zz.SegmentSize(s)/m_symbol.Point());
         if(size>m_levels_array[i] && size<m_levels_array[i+class="num">1])
           {
            class="type">int value=(class="type">int)m_table2.GetValue(class="num">1,i)+class="num">1;
            m_table2.SetValue(class="num">1,i,(class="type">class="kw">string)value,class="num">0);
           }
        }
    }
class=class="str">"cmt">//--- 显示表格
  m_table2.Update(true);
class=class="str">"cmt">//--- 结束进度
  EndProgress();
  }

按持续柱数给线段分桶

线段持续时间(以柱线计)本身就是一种分组维度。把上一版 EA 复制一份,再往 GUI 加一张两列表格:第一列是柱线数量,第二列是该持续时间内出现的线段数,就能直接看形态在时间轴上的分布密度。 操作流程是:先点「请求」拿品种列表,在第一张表选品种,点「计算」出第二张表的范围数据;要填第三张表,得在第二张表里高亮某一行范围,程序才按这个范围统计线段持续时间。高亮行会触发 ON_CLICK_LIST_ITEM 事件,这里用第二张表 ID 来捕获。 换品种或重算时,必须清掉第三张表旧数据,否则前后数据混在一起容易误读。实盘挂 EA 后,用 USD 系货币对测:GBPUSD 从 2018 年起、步长 100 拉范围,高亮第二表「200」那行,得到范围 200–300 内共 1922 个线段。第三表展开看,持续 10 根柱线的线段只有 11 个——这种长尾分布是肉眼调参前先要心里有数的。 外汇和贵金属波动受杠杆与消息面影响大,此类统计只反映历史样本,实盘信号概率会随 regime 漂移,别直接当进场依据。 下面这段 CProgram::SetDataToTable3 是填第三表的核心。逐行看:类里先声明私有方法 SetDataToTable3;方法开头若第二表没高亮行就打印提示并 return;StartProgress 起进度条,Hide 隐藏表格避免闪烁;selected_row_index 取高亮行号,黄底行 selected_range 从表头 0 列拿范围值。 RebuildingTable3 重建表结构,随后第一列填 1、2、3… 序号;segments_total 取 zigzag 线段总数,外层按行、内层按线段遍历。绿底判断是重点:线段 size 落在 selected_range 到 range+步长之间、且 bars 等于当前行号+1 时,才把第二列对应行计数 +1。最后 Update 显示、EndProgress 收尾。

MQL5 / C++
<span class="keyword">class</span> CProgram : <span class="keyword">class="kw">public</span> CWndCreate
&nbsp;&nbsp;{
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- 用数据填充表格三</span>
&nbsp;&nbsp; <span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SetDataToTable3(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp;};
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| 用数据填充表格三&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;   |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CProgram::SetDataToTable3(<span class="keyword">class="type">void</span>)
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//--- 如果不是高亮显示得行,则退出</span>
&nbsp;&nbsp; <span class="keyword">if</span>(m_table2.SelectedItem()==<span class="macro">WRONG_VALUE</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;::<span class="functions">Print</span>(<span class="keyword">__FUNCTION__</span>,<span class="class="type">class="kw">string">" &gt; Select a range in the table on the left!"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="comment">class=class="str">"cmt">//--- 进度开始</span>
&nbsp;&nbsp; StartProgress();
<span class="comment">class=class="str">"cmt">//--- 隐藏表格</span>
&nbsp;&nbsp; m_table3.Hide();
<span class="comment">class=class="str">"cmt">//--- 获取高亮显示的行</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span> selected_row_index=m_table2.SelectedItem();
<span class="comment">class=class="str">"cmt">//--- 范围</span>
&nbsp;&nbsp; <span style="background-class="type">class="kw">color:rgb(class="num">255, class="num">242, class="num">153);"><span class="keyword">class="type">int</span> selected_range=(<span class="keyword">class="type">int</span>)m_table2.GetValue(<span class="number">class="num">0</span>,selected_row_index);</span>
<span class="comment">class=class="str">"cmt">//--- 重新构建表格</span>
&nbsp;&nbsp; RebuildingTable3();
<span class="comment">class=class="str">"cmt">//--- 在第一列中设置数值</span>
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">uint</span> r=<span class="number">class="num">0</span>; r&lt;(<span class="keyword">class="type">uint</span>)m_table3.RowsTotal(); r++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_table3.SetValue(<span class="number">class="num">0</span>,r,(<span class="keyword">class="type">class="kw">string</span>)(r+<span class="number">class="num">1</span>),<span class="number">class="num">0</span>);
<span class="comment">class=class="str">"cmt">//--- 获取第二列的值</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span> segments_total=m_zz.SegmentsTotal();
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">uint</span> r=<span class="number">class="num">0</span>; r&lt;(<span class="keyword">class="type">uint</span>)m_table3.RowsTotal(); r++)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- 进度</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_progress_bar.LabelText(<span class="class="type">class="kw">string">"Get data ["</span>+(<span class="keyword">class="type">class="kw">string</span>)r+<span class="class="type">class="kw">string">"]..."</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_progress_bar.Update(r,m_table3.RowsTotal());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> s=<span class="number">class="num">0</span>; s&lt;segments_total; s++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> size =<span class="keyword">class="type">int</span>(m_zz.SegmentSize(s)/m_symbol.<span class="predefines">Point</span>());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> bars =m_zz.SegmentBars(s);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
<span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(size&gt;selected_range &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;size&lt;selected_range+(<span class="keyword">class="type">int</span>)m_step.GetValue() &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;bars==r+<span class="number">class="num">1</span>)</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> value=(<span class="keyword">class="type">int</span>)m_table3.GetValue(<span class="number">class="num">1</span>,r)+<span class="number">class="num">1</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_table3.SetValue(<span class="number">class="num">1</span>,r,(<span class="keyword">class="type">class="kw">string</span>)value,<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="comment">class=class="str">"cmt">//--- 显示表格</span>
&nbsp;&nbsp; m_table3.Update(<span class="macro">true</span>);
<span class="comment">class=class="str">"cmt">//--- 进度结束</span>
&nbsp;&nbsp; EndProgress();
&nbsp;&nbsp;}

◍ 表格联动的事件路由怎么写

在 MT5 自定义面板里,多表格联动靠的是图表自定义事件。单击第二张表(m_table2)的某一行时,程序通过 lparam == m_table2.Id() 判断来源,随即调用 SetDataToTable3() 刷新第三张表,避免误触其他控件。 点「请求」按钮(m_request)会重抓第一张表数据,并立即清空第二、第三张表:m_table2.DeleteAllRows(true) 与 m_table3.DeleteAllRows(true)。点「计算」按钮(m_calculate)只刷第二张表并清掉第三张,保留已请求的原始数据。 外汇与贵金属行情跳动快,这种「按来源清旧数据」的逻辑能降低界面显示错位的概率,但高频点击仍可能带来渲染延迟,属正常现象。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理器                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
...
class=class="str">"cmt">//--- 单击行的事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      class=class="str">"cmt">//--- 单击表格行
      if(lparam==m_table2.Id())
        {
         class=class="str">"cmt">//--- 获取第三张表格的数据
         SetDataToTable3();
         class="kw">return;
        }
      class=class="str">"cmt">//---
      class="kw">return;
     }
...
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理器                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//--- 单击按钮的事件
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      class=class="str">"cmt">//--- 点击请求按钮
      if(lparam==m_request.Id())
        {
         class=class="str">"cmt">//--- 获取第一张表格的数据
         SetDataToTable1();
         class=class="str">"cmt">//--- 从表格中删除不相关的数据
         m_table2.DeleteAllRows(true);
         m_table3.DeleteAllRows(true);
         class="kw">return;
        }
      class=class="str">"cmt">//--- 单击计算按钮
      if(lparam==m_calculate.Id())
        {
         class=class="str">"cmt">//--- 获取第二张表格的数据
         SetDataToTable2();
         class=class="str">"cmt">//--- 从表格中删除不相关的数据
         m_table3.DeleteAllRows(true);
        }
      class=class="str">"cmt">//---
      class="kw">return;
     }
...
  }

「图表品种切换时别重建界面」

MT5 面板若挂了十几个控件,每次切品种或周期都 Destroy 再 Create,初始化整组 GUI 的耗时可能卡掉 200~500ms,频繁操作时体验明显发涩。 其实图表品种 / 周期变更触发的是 REASON_CHARTCHANGE,界面本身没必要拆了重来。只要在程序主类里留一个字段记最后一次逆初始化原因,就能判断是不是单纯换图。 逆初始化时只在这种原因下跳过销毁,其余情况(比如卸载 EA、改参数)照常清界面;创建 GUI 的方法开头查一下这个字段,若是图表变更直接 return true,控件原样保留。 下面这段是核心写法:m_last_deinit_reason 存原因,OnDeinitEvent 里赋值并条件销毁,CreateGUI 里提前拦截。外汇和贵金属行情跳动快,这类细节能减少界面闪烁,但 EA 逻辑错误仍可能导致宕机,属高风险操作。

MQL5 / C++
class CProgram : class="kw">public CWndCreate
  {
class="kw">private:
  class=class="str">"cmt">//--- 逆初始化的最终原因
  class="type">int               m_last_deinit_reason;
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CProgram::CProgram(class="type">void) : m_last_deinit_reason(WRONG_VALUE)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 逆初始化                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::OnDeinitEvent(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//--- 记住最后的逆初始化原因
   m_last_deinit_reason=reason;
class=class="str">"cmt">//--- 如果原因与变更品种和周期无关,则删除 GUI
   if(reason!=REASON_CHARTCHANGE)
     {
       CWndEvents::Destroy();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建 GUI                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CreateGUI(class="type">void)
  {
class=class="str">"cmt">//--- 如果图表或时间帧已变更,则退出
   if(m_last_deinit_reason==REASON_CHARTCHANGE)
     class="kw">return(true);
...
   class="kw">return(true);
  }

把 ZigZag 数据接进多周期统计

论坛上常有人说 ZigZag 不能拿来出信号,这其实是个误解。撇开信号不谈,单就刻画价格行为性质而言,很少有指标能像它一样给出这么密的结构信息——只要你能把折线数据抓出来做二次统计。 前文已经把取数工具铺好了,这一节直接看怎么把高周期 ZigZag 和当前周期分段接起来。下方代码来自 MultiPercentageSegmentsZZ 指标里的缓冲区填充函数,它从更高时间框架拉取 1000 根 K 线范围内的折线缓冲,再按 0、1、2 三段分别取当前周期数据。 [CODE] //+------------------------------------------------------------------+

//填入指标缓冲区

//+------------------------------------------------------------------+ void FillIndicatorBuffers(const int i,const int total,const datetime &time[]) { int index=total-i-1; int copy_total=1000; int h_buff=2,l_buff=3; datetime start_time_in =NULL; datetime stop_time_in =NULL; //--- 从更高时间段获取源数据 datetime stop_time=time[i]-(PeriodSeconds(HigherTimeframe)*copy_total); CopyBuffer(zz_handle_htf,2,time[i],stop_time,h_zz_buffer_temp); CopyBuffer(zz_handle_htf,3,time[i],stop_time,l_zz_buffer_temp); CopyTime(_Symbol,HigherTimeframe,time[i],stop_time,t_zz_buffer_temp); //--- 从更高的时间框架中获取最终数据 zz_higher_tf.GetZigZagData(h_zz_buffer_temp,l_zz_buffer_temp,t_zz_buffer_temp); double htf_value=zz_higher_tf.PercentSumSegmentsDifference(); //--- 第一分段数据 zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,0,start_time_in,stop_time_in); zz_current0.GetZigZagData(zz_handle_current,h_buff,l_buff,_Symbol,_Period,start_time_in,stop_time_in); //--- 第二段数据 zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,1,start_time_in,stop_time_in); zz_current1.GetZigZagData(zz_handle_current,h_buff,l_buff,_Symbol,_Period,start_time_in,stop_time_in); //--- 第三分段数据 zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,2,start_time_in,stop_time_in); zz_current2.GetZigZagData(zz_handle_current,h_buff,l_buff,_Symbol,_Period,start_time_in,stop_time_in); 逐行拆一下:h_buff=2、l_buff=3 是高周期 ZigZag 指标里高/低拐点对应的缓冲区序号;stop_time 用 PeriodSeconds(HigherTimeframe)*1000 反推起点,确保拉满 1000 根高周期 K 线。CopyBuffer 把高周期折线的高低缓冲和 CopyTime 的时间数组暂存,交给 GetZigZagData 解析出线段集合,PercentSumSegmentsDifference 算出的 htf_value 就是高周期线段百分比差值。后面三段 SegmentTimes 依次用分段索引 0/1/2 切出当前周期对应时段,再喂给 zz_current0~2 做各自取数。 随文给了一套可直接丢进 MT5 的文件:FrequencyChangeZZ 算反向线段形成频率,SumSegmentsZZ 算线段总和与均值,PercentageSegmentsZZ 和 MultiPercentageSegmentsZZ 做百分比差值,四个 EA(TestZZ_05、ZZ_Scanner_01~03)分别验不同维度的线段扫描。外汇和贵金属波动大、杠杆高,用这些工具做统计倾向辅助判断即可,别当成下单依据。 下一篇会接着演示怎么用上面这套取数接口榨出更多衍生数据,比如跨价格区间的线段分布。先把 ZZ_Scanner_02 挂上黄金 M15,看不同价格带里线段密度差多少,比空谈参数直观得多。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 填入指标缓冲区|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FillIndicatorBuffers(class="kw">const class="type">int i,class="kw">const class="type">int total,class="kw">const class="type">class="kw">datetime &time[])
  {
   class="type">int index=total-i-class="num">1;
   class="type">int copy_total=class="num">1000;
   class="type">int h_buff=class="num">2,l_buff=class="num">3;
   class="type">class="kw">datetime start_time_in =NULL;
   class="type">class="kw">datetime stop_time_in  =NULL;
class=class="str">"cmt">//--- 从更高时间段获取源数据
   class="type">class="kw">datetime stop_time=time[i]-(PeriodSeconds(HigherTimeframe)*copy_total);
   CopyBuffer(zz_handle_htf,class="num">2,time[i],stop_time,h_zz_buffer_temp);
   CopyBuffer(zz_handle_htf,class="num">3,time[i],stop_time,l_zz_buffer_temp);
   CopyTime(_Symbol,HigherTimeframe,time[i],stop_time,t_zz_buffer_temp);
class=class="str">"cmt">//--- 从更高的时间框架中获取最终数据
   zz_higher_tf.GetZigZagData(h_zz_buffer_temp,l_zz_buffer_temp,t_zz_buffer_temp);
   class="type">class="kw">double htf_value=zz_higher_tf.PercentSumSegmentsDifference();
class=class="str">"cmt">//--- 第一分段数据
   zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,class="num">0,start_time_in,stop_time_in);
   zz_current0.GetZigZagData(zz_handle_current,h_buff,l_buff,_Symbol,_Period,start_time_in,stop_time_in);
class=class="str">"cmt">//--- 第二段数据
   zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,class="num">1,start_time_in,stop_time_in);
   zz_current1.GetZigZagData(zz_handle_current,h_buff,l_buff,_Symbol,_Period,start_time_in,stop_time_in);
class=class="str">"cmt">//--- 第三分段数据
   zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,class="num">2,start_time_in,stop_time_in);
   zz_current2.GetZigZagData(zz_handle_current,h_buff,l_buff,_Symbol,_Period,start_time_in,stop_time_in);

◍ 画得少,看得清

这段收尾逻辑只处理两件事:当前未走完的条形图直接沿用高 timeframe 取值与三段 ZigZag 的百分比差值;一旦较高周期冒出新的 bar,就把上一根已定稿的数据整体前移一格,避免历史重绘时图形抖动。 具体看,i<total-1 时每根都实时写 buffer_zz_higher_tf 与三个 segment 差值;else 分支里靠 new_bar_time 是否等于 t_zz_buffer_temp[0] 判断换柱,i>2 才做前移,少了前两根的缓冲就可能越界。 实盘接这思路,MT5 里把 indicator 的 buffers 数设成 4(1 个高周期 + 3 个分段差),周期选 H1 加载、ZigZag 深度分别 12/24/48,肉眼能看出多周期共振段明显变稀疏。外汇与贵金属杠杆高,信号只是概率倾向,验证时先用策略测试器跑半年 tick 数据再上真仓。

MQL5 / C++
if(i<total-class="num">1)
  {
  buffer_zz_higher_tf[i] =htf_value;
  buffer_segment_0[i]     =zz_current0.PercentSumSegmentsDifference();
  buffer_segment_1[i]     =zz_current1.PercentSumSegmentsDifference();
  buffer_segment_2[i]     =zz_current2.PercentSumSegmentsDifference();
  }
class=class="str">"cmt">//--- 关于历史
else
  {
  class=class="str">"cmt">//--- 如果出现了较高时间框架的新条形图
  if(new_bar_time!=t_zz_buffer_temp[class="num">0])
    {
    new_bar_time=t_zz_buffer_temp[class="num">0];
    class=class="str">"cmt">//---
    if(i>class="num">2)
      {
      class="type">int f=class="num">1,s=class="num">2;
      buffer_zz_higher_tf[i-f] =buffer_zz_higher_tf[i-s];
      buffer_segment_0[i-f]     =buffer_segment_0[i-s];
      buffer_segment_1[i-f]     =buffer_segment_1[i-s];
      buffer_segment_2[i-f]     =buffer_segment_2[i-s];
      }
    }
  else
    {
    buffer_zz_higher_tf[i] =htf_value;
    buffer_segment_0[i]     =zz_current0.PercentSumSegmentsDifference();
    buffer_segment_1[i]     =zz_current1.PercentSumSegmentsDifference();
    buffer_segment_2[i]     =zz_current2.PercentSumSegmentsDifference();
    }
  }
把线段统计交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 ZigZag 线段分布与频率异动,你只需判断概率倾向,重复计算交给小布。

常见问题

参数取决于你想要的统计样本长度,默认按所有可用数据计算即可;极值数量越多,对早期线段的平滑越强,但响应会变慢,需按品种波动率权衡。
可以,小布盯盘的品种页内置了基于 AIGC 的线段频率与百分比诊断,免去了自己在 MT5 里写 EA 收集数据的步骤,适合快速验证想法。
前者引用更高时间帧的若干线段性质,后者只算当前帧线段总和的百分比率与差值;跨周期确认时多用前者,单周期结构分析用后者。
持续时间能暴露盘整与突破的节奏,大小只反映空间;外汇贵金属高风险,时间维度异常往往先于价格破位出现,概率上更值得跟踪。
重点在重绘时机与事件绑定,避免在 OnCalculate 里频繁创建对象;用定时器分离数据刷新与界面渲染,否则卡顿会拖慢 EA 逻辑。