您应当知道的 MQL5 向导技术(第 09 部分):K-Means 聚类与分形波配对·进阶篇
📘

您应当知道的 MQL5 向导技术(第 09 部分):K-Means 聚类与分形波配对·进阶篇

第 2/3 篇

「聚类起手式:三种初始化怎么选」

在 MT5 里做 K 均值聚类,最拖时间的往往不是迭代本身,而是初始质心选得太随缘。AlgLib 给了三条路:纯随机、k-means++、快速贪婪初始化,核心函数分别是 SelectInitialCenters 与后续的 KMeansGenerateInternal。 随机法最直接,给预期聚类数 k 的每一行 ct 直接塞一个从数据集里随机抽的点,ct 的行数等于 k、列数等于特征维度。下面这段就是随机分支的骨架: k-means++ 只随机定第一个质心,其余质心按「离已有质心越远越可能被选中」的概率采样,能明显压低收敛轮数。若某轮所有点到当前质心距离平方和为 0,就退化为随机选一个,避免卡死。 快速贪婪初始化是 k-means++ 的变体,每轮先算最近质心距离,再按约一半聚类规模独立采样(选点概率正比于距离),采样到 2 倍聚类量后做贪婪精选,优先留最远点。计算密集,但出来的质心代表性更好,劳埃德算法接力时也更快。 本系列测试集用的是 GBPUSD 收盘价变化量,外汇品种波动聚集明显,聚类结果仅作结构参考,实盘前务必在 MT5 策略测试器里跑一遍验证,杠杆交易高风险。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//--- Random initialization</span>
&nbsp;&nbsp; <span class="keyword">if</span>(initalgo==<span class="number">class="num">1</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(i=<span class="number">class="num">0</span>; i&lt;k; i++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; j=CHighQualityRand::HQRndUniformI(rs,npoints);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ct.Row(i,xy[j]+<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="comment">class=class="str">"cmt">//--- k-means++ initialization</span>
&nbsp;&nbsp; <span class="keyword">if</span>(initalgo==<span class="number">class="num">2</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- Prepare distances array.</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- Select initial center at random.</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;initbuf.m_ra0=<span class="keyword">vector</span>&lt;<span class="keyword">class="type">class="kw">double</span>&gt;::Full(npoints,CMath::m_maxrealnumber);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ct.Row(<span class="number">class="num">0</span>,xy[ptidx]+<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- For each newly added center repeat:</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- * reevaluate distances from points to best centers</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- * sample points with probability dependent on distance</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- * add new center</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(cidx=<span class="number">class="num">0</span>; cidx&lt;k-<span class="number">class="num">1</span>; cidx++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- Reevaluate distances</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; s=<span class="number">class="num">0.0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(i=<span class="number">class="num">0</span>; i&lt;npoints; i++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;v=<span class="number">class="num">0.0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(j=<span class="number">class="num">0</span>; j&lt;=nvars-<span class="number">class="num">1</span>; j++)
&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; vv=xy.Get(i,j)-ct.Get(cidx,j);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; v+=vv*vv;
&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 class="keyword">if</span>(v&lt;initbuf.m_ra0[i])
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; initbuf.m_ra0.Set(i,v);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;s+=initbuf.m_ra0[i];
&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;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- If all distances are zero, it means that we can not find enough</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- distinct points. In this case we just select non-distinct center</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- at random and class="kw">continue iterations. This issue will be handled</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- later in the FixCenters() function.</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(s==<span class="number">class="num">0.0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ct.Row(cidx+<span class="number">class="num">1</span>,xy[ptidx]+<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">continue</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }

采样轮次里的中心点距离兜底

在 k-means++ 变体初始化里,用均匀随机数 v 选定某个数据点为簇中心时,必须防住浮点取整导致一个都没选中的空窗。代码先累加各点权重 vv,一旦 v 小于累计权重除以 s,就锁定 ptidx;若循环跑完 ptidx 还是 -1,则回退到最后一个非零权重点 lastnz。 下面这段是核心选取逻辑,注意 lastnz 在每次遇到非零权重时被刷新,保证兜底永远有值: //--- Select point as center using its distance. //--- We also handle situation when because of rounding errors //--- no point was selected - in this case, last non-zero one //--- will be used. v=CHighQualityRand::HQRndUniformR(rs); vv=0.0; lastnz=-1; ptidx=-1; for(i=0; i<npoints; i++) { if(initbuf.m_ra0[i]==0.0) continue; lastnz=i; vv+=initbuf.m_ra0[i]; if(v<=vv/s) { ptidx=i; break; } } if(!CAp::Assert(lastnz>=0,__FUNCTION__": integrity error")) return; if(ptidx<0) ptidx=lastnz; ct.Row(cidx+1,xy[ptidx]+0); 另一种初始化算法(initalgo==3)走的是 Scalable k-means++ 的 fast-greedy 思路:每轮按距离概率独立采样约 0.5*K 个点,循环到总样本数达到 2*K 才停。samplescale 直接设为 0.5*k,samplesize 设为 2*k,这两个常量决定了子采样规模,开 MT5 把 k 从 5 改成 10 时,样本上限会从 10 跳到 20,聚类冷启动耗时可能明显拉长。 别把正态当圣经 浮点累加 vv 在 npoints 很大时可能丢精度,导致 v<=vv/s 的命中位置偏移。实盘前用一小撮历史 Tick 数据跑一遍,打印 lastnz 与 ptidx 是否一致,比直接信注释更稳妥。

MQL5 / C++
v=CHighQualityRand::HQRndUniformR(rs);
vv=class="num">0.0;
lastnz=-class="num">1;
ptidx=-class="num">1;
for(i=class="num">0; i<npoints; i++)
  {
  if(initbuf.m_ra0[i]==class="num">0.0)
    class="kw">continue;
  lastnz=i;
  vv+=initbuf.m_ra0[i];
  if(v<=vv/s)
    {
    ptidx=i;
    break;
    }
  }
if(!CAp::Assert(lastnz>=class="num">0,__FUNCTION__": integrity error"))
  class="kw">return;
if(ptidx<class="num">0)
  ptidx=lastnz;
ct.Row(cidx+class="num">1,xy[ptidx]+class="num">0);

◍ k-means++ 采样与贪心初始化的距离处理

这段逻辑是 k-means++ 初始化里最容易被忽略的一环:先调用 KMeansUpdateDistances 刷新新旧样本距离,再把新距离缓冲 m_ra1 与旧缓冲 m_ra0 逐点取最小值合并。合并后累加得到总距离 s,若 s 严格等于 0.0,说明所有点到已有簇心的距离都塌成零,这时只能从 npoints 里随机抽点填满 samplesize 并直接 break,避免死循环。 独立采样阶段按概率 samplescale*m_ra0[i]/s 决定是否把第 i 个原始点塞进采样矩阵 m_rm0,samplescntall 达到 samplesize 就提前退出。这里用的是 CHighQualityRand 的高质量均匀随机,比 MT5 自带 MathRand 的周期与分布更可靠,外汇多变量特征采样时偏差倾向更小。 采样完成后跑贪心版 k-means:先把 m_ra0 填满 double 最大值,随机挑一个采样点作为 0 号簇心,随后对 cidx 从 0 到 k-2 循环,每次重新算各采样点到当前所有簇心的平方距离并刷新 m_ra0 的最小值。平方距离用 vv*vv 累加,不做开方,省掉一半浮点开销,MT5 回测中样本量上万时这步可能少掉十几毫秒。 别把正态当圣经:m_ra0 初始化成 m_maxrealnumber 是为了让第一次比较必然写入,若你改成了 0.0 或遗漏重置,贪心选心会直接失效,簇数 k 再大也只收敛到 1 个心。

MQL5 / C++
KMeansUpdateDistances(xy,class="num">0,npoints,nvars,initbuf.m_rm0,samplescntall-samplescntnew,samplescntall,initbuf.m_ia1,initbuf.m_ra1);
samplescntnew=class="num">0;
class=class="str">"cmt">//--- Merge new distances with old ones.
class=class="str">"cmt">//--- Calculate sum of distances, if sum is exactly zero - fill sample
class=class="str">"cmt">//--- by randomly selected points and terminate.
s=class="num">0.0;
for(i=class="num">0; i<npoints; i++)
  {
  initbuf.m_ra0.Set(i,MathMin(initbuf.m_ra0[i],initbuf.m_ra1[i]));
  s+=initbuf.m_ra0[i];
  }
if(s==class="num">0.0)
  {
  class="kw">while(samplescntall<samplesize)
    {
    ptidx=CHighQualityRand::HQRndUniformI(rs,npoints);
    initbuf.m_rm0.Row(samplescntall,xy[ptidx]+class="num">0);
    samplescntall++;
    samplescntnew++;
    }
  break;
  }
class=class="str">"cmt">//--- Sample points independently.
for(i=class="num">0; i<npoints; i++)
  {
  if(samplescntall==samplesize)
    break;
  if(initbuf.m_ra0[i]==class="num">0.0)
    class="kw">continue;
  if(CHighQualityRand::HQRndUniformR(rs)<=(samplescale*initbuf.m_ra0[i]/s))
    {
    initbuf.m_rm0.Row(samplescntall,xy[i]+class="num">0);
    samplescntall++;
    samplescntnew++;
    }
  }
}
class=class="str">"cmt">//--- Run greedy version of k-means on sampled points
initbuf.m_ra0=vector<class="type">class="kw">double>::Full(samplescntall,CMath::m_maxrealnumber);
ptidx=CHighQualityRand::HQRndUniformI(rs,samplescntall);
ct.Row(class="num">0,initbuf.m_rm0[ptidx]+class="num">0);
for(cidx=class="num">0; cidx<k-class="num">1; cidx++)
  {
  class=class="str">"cmt">//--- Reevaluate distances
  for(i=class="num">0; i<samplescntall; i++)
    {
    v=class="num">0.0;
    for(j=class="num">0; j<nvars; j++)
      {
      vv=initbuf.m_rm0.Get(i,j)-ct.Get(cidx,j);
      v+=vv*vv;
      }
    if(v<initbuf.m_ra0[i])
      initbuf.m_ra0.Set(i,v);

「贪心选点:拿最远样本当聚类中心」

这段逻辑出现在样本初始化收尾阶段,目的是用贪心方式挑出一个聚类中心:在已收集的全部样本里,选半径值最大的那个点作为中心,而不是随机指定。 具体来看,代码先把 ptidx 置 0,再遍历 i 从 0 到 samplescntall-1,只要 initbuf.m_ra0[i] 大于当前 initbuf.m_ra0[ptidx] 就把 ptidx 更新为 i,循环结束 ptidx 指向最远距离样本。 随后 ct.Row(cidx+1, initbuf.m_rm0[ptidx]+0) 把该点坐标写入聚类表第 cidx+1 行。外汇与贵金属行情下这种远点初始化可能让聚类更快覆盖极端波动区,但极端样本本身带噪声,高杠杆品种须警惕过拟合。

MQL5 / C++
      }
      class=class="str">"cmt">//--- Select point as center in greedy manner - most distant
      class=class="str">"cmt">//--- point is selected.
      ptidx=class="num">0;
      for(i=class="num">0; i<samplescntall; i++)
        {
         if(initbuf.m_ra0[i]>initbuf.m_ra0[ptidx])
            ptidx=i;
        }
      ct.Row(cidx+class="num">1,initbuf.m_rm0[ptidx]+class="num">0);
      }
      class="kw">return;
   }

用滞后 y 让无监督聚类能预测

AHC 这类 K-均值本质是无监督分类,想做回归或预测必须补一列滞后 y:y 取收盘价变化,但比聚类用的 x 矩阵早 1 根柱线,才能给聚类打上“已知结果”的标签。y 和 x 在同一 for 循环里填充,省一次遍历。 聚类算完后,取 x 矩阵顶行(当前收盘价变化)对应的聚类索引,去匹配已标记数据点的历史最终变化,求和再取平均。用当前波动范围做常规化,把平均变化压到 0–1 区间,这就是该聚类的方向权重。 LongCondition / ShortCondition 返回 0–100,所以常规化数值要乘 100。下面这段代码里,i>0 时才给 y 赋值,避免最新一根柱还没有“未来”结果;_value 用前一根减当前根算变化,非法值清零后写进 y 数组。 ShortCondition 里先取 GetOutput() 原始输出,再用最近 1 根的高低差做分母常规化(兜底用 Point 防除零),乘 100 得到下跌投票强度;输出为负时夹在 -100 到 0 之间返回。外汇和贵金属波动剧烈,这种权重只代表历史聚类下的倾向概率,实盘须自担高风险。

MQL5 / C++
  if(i>class="num">0)class=class="str">"cmt">//assign classifier only for data points for which eventual bar range is known
  {
    class="type">class="kw">double _value=m_close.GetData(StartIndex()+i-class="num">1)-m_close.GetData(StartIndex()+i);
    if(_dbl_min>=_value||!MathIsValidNumber(_value)||_value>=_dbl_max){ _value=class="num">0.0; }
    m_data.y.Set(i-class="num">1,_value);
    vector _v=m_data.y.ToVector();if(_v.HasNan()){ _v.ReplaceNan(class="num">0.0); }m_data.y=CRowDouble(_v);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Voting" that price will fall.                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSignalKMEANS::ShortCondition(class="type">void)
  {
    ...
    
    class="type">class="kw">double _output=GetOutput();
    
    class="type">int _range_size=class="num">1;
    
    class="type">class="kw">double _range=m_high.GetData(m_high.MaxIndex(StartIndex(),StartIndex()+_range_size))-m_low.GetData(m_low.MinIndex(StartIndex(),StartIndex()+_range_size));
    
    _output/=fmax(_range,m_symbol.Point());
    _output*=class="num">100.0;
    
    if(_output<class="num">0.0){ result=class="type">int(fmax(-class="num">100.0,round(_output)))*-class="num">1; }
    
    ...
  }

常见问题

常用随机、k-means++和贪心三种;样本分散且维度高时优先k-means++,极端值多时贪心更稳,随机仅作快速验证。
距离过近会导致簇重叠、分类失效;可在每轮计算中心点间最小距离,低于阈值时重选或合并,保证簇间 separable。
小布可自动加载品种数据做聚类初始化与滞后y预测,把重复计算和集合图生成交给它,你只看信号结论。
概率偏低但存在;建议先用正态检验看分布,再限制最大迭代轮次,避免把噪声极值锁成永久中心。
黄金类常试1~3根,白银流动性差可放到5;以样本外准确率不再提升为停点,别盲目加长。