群体优化算法·综合运用
🧠

群体优化算法·综合运用

(3/3)·当历史视角、收敛率与 RNG 简单算法汇于同一框架,离散 Megacity 才是检验成色的终局

偏理论进阶 第 3/3 篇
很多人把群体优化当成黑箱调参玩具,却说不清收敛稳定性与可扩展性到底在约束什么。本篇把前两块拼图合拢,直接落到可运行的评估准则与离散场景。看清算法边界,比多跑十组回测更有用。

◍ 把优化参数和散点画到自定义画布上

这段逻辑干了两件事:先用 F_Init 把种群参数初始化,再把每个维度的搜索边界(max/min/step)写进数组 A_RangeMax、A_RangeMin、A_RangeStep。循环里 idx 从 0 跑到 params-1,意味着你传进来的维度数量直接决定了边界数组的长度,少传一个维度就会漏掉对应参数的寻优范围。 PointDr 函数负责把二维散点映射到屏幕坐标。它用 Scale 把 args 里的 x、y 从 [Min, Max] 线性映射到 [0, WidthScrFunc-1] 和 [0, HeighScrFunc-1],注意这里宽高都用了 HeighScrFunc 做上限(高度映射疑似笔误,应为 WidthScrFunc 控宽),上机跑之前最好核对这两个宏定义。 散点颜色由 DoubleToColor(i,0,count-1,0,360) 生成,i 在 0 到 count-1 之间被映射成 0~360 的色相,相当于给每个点按序号铺了一条彩虹带。圆心半径固定为 1,而平均点用半径 3 的黑底套半径 2 的指定色,视觉上能直接把聚类中心从噪声里揪出来。 SendGraphToCanvas 则是把预渲染好的函数面 FunctScrin[w].clr[h] 逐像素拷到 Canvas,两层循环上限都写的 HeighScrFunc,若 WidthScrFunc 与 HeighScrFunc 不等,横向可能少画或多画。外汇与贵金属复盘用这类自定义绘图时,记得 MT5 终端分辨率限制,画布超过 2048 宽高部分可能被裁。

MQL5 / C++
class="kw">const class="type">class="kw">double max,      class=class="str">"cmt">//maximum of the optimized argument
class="kw">const class="type">class="kw">double min,      class=class="str">"cmt">//minimum of the optimized argument
class="kw">const class="type">class="kw">double step)     class=class="str">"cmt">//step of the optimized argument
{
  AO.F_Init(params, Population_P);
  for (class="type">int idx = class="num">0; idx < params; idx++)
  {
    AO.A_RangeMax  [idx] = max;
    AO.A_RangeMin  [idx] = min;
    AO.A_RangeStep [idx] = step;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void PointDr(class="type">class="kw">double &args [], class="type">class="kw">double Min, class="type">class="kw">double Max, class="type">class="kw">color clr, class="type">int shiftX, class="type">int shiftY, class="type">int count)
{
  class="type">class="kw">double x = class="num">0.0;
  class="type">class="kw">double y = class="num">0.0;
  
  class="type">class="kw">double xAve = class="num">0.0;
  class="type">class="kw">double yAve = class="num">0.0;
  
  class="type">int width  = class="num">0;
  class="type">int height = class="num">0;
  
  class="type">class="kw">color clrF = clrNONE;
  
  for (class="type">int i = class="num">0; i < count; i++)
  {
    xAve += args [i * class="num">2];
    yAve += args [i * class="num">2 + class="num">1];
    
    x = args [i * class="num">2];
    y = args [i * class="num">2 + class="num">1];
    
    width  = (class="type">int)Scale(x, Min, Max, class="num">0, WidthScrFunc - class="num">1, class="kw">false);
    height = (class="type">int)Scale(y, Min, Max, class="num">0, HeighScrFunc - class="num">1, class="kw">false);
    
    clrF = DoubleToColor(i, class="num">0, count - class="num">1, class="num">0, class="num">360);
    Canvas.FillCircle(width + shiftX, height + shiftY, class="num">1, COLOR2RGB(clrF));
  }
  
  xAve /= (class="type">class="kw">double)count;
  yAve /= (class="type">class="kw">double)count;
  width  = (class="type">int)Scale(xAve, Min, Max, class="num">0, WidthScrFunc - class="num">1, class="kw">false);
  height = (class="type">int)Scale(yAve, Min, Max, class="num">0, HeighScrFunc - class="num">1, class="kw">false);
  Canvas.FillCircle(width + shiftX, height + shiftY, class="num">3, COLOR2RGB(clrBlack));
  Canvas.FillCircle(width + shiftX, height + shiftY, class="num">2, COLOR2RGB(clr));
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void SendGraphToCanvas(class="type">int shiftX, class="type">int shiftY)
{
  for (class="type">int w = class="num">0; w < HeighScrFunc; w++)
  {
    for (class="type">int h = class="num">0; h < HeighScrFunc; h++)
    {
      Canvas.PixelSet(w + shiftX, h + shiftY, COLOR2RGB(FunctScrin [w].clr [h]));
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void DrawFunctionGraph(class="type">class="kw">double      min,
                        class="type">class="kw">double      max,
                        class="type">class="kw">double      fMin,

「把函数值映射到热力色阶的底层例程」

这段例程负责把二维函数采样结果画成屏幕热力图。外层循环按 HeighScrFunc 行高逐行扫描,内层再按同样高度逐列取点,每次用 Scale() 把网格坐标 w、h 从 [0, HeighScrFunc) 线性映射到 [min, max] 的输入区间,再交给 f.CalcFunc(ar,1) 算出当前格的函数值 fV。 拿到 fV 后调用 DoubleToColor(fV, fMin, fMax, 0, 250),把函数值归一到 HSL 色相 0–250 区间并转 RGB,写进 FunctScrin[w].clr[h]。注意这里上限是 250 而非 360,意味着色相环只走前 250 度,红到紫之间截断,蓝绿段不会回卷。 Scale() 是通用归一化工具:若输出区间上下相等直接返回该值;若输入区间退化则返回中点。Revers 参数控制方向,true 时输入越小输出越大,DoubleToColor 里就传了 true,所以函数值越低色相越靠 250(偏紫),越高越靠 0(偏红)。 开 MT5 把 HeighScrFunc 调到 200 以上,能看到网格从 200×200 采样起色阶过渡明显变细;外汇与贵金属图表叠加此类热力层属高风险辅助,仅作多维形态参考,不预示方向。

MQL5 / C++
class="type">class="kw">double Scale(class="type">class="kw">double In, class="type">class="kw">double InMIN, class="type">class="kw">double InMAX, class="type">class="kw">double OutMIN, class="type">class="kw">double OutMAX, class="type">bool Revers = class="kw">false)
{
  if (OutMIN == OutMAX) class="kw">return (OutMIN);
  if (InMIN == InMAX) class="kw">return ((OutMIN + OutMAX) / class="num">2.0);
  else
  {
    if (Revers)
    {
      if (In < InMIN) class="kw">return (OutMAX);
      if (In > InMAX) class="kw">return (OutMIN);
      class="kw">return (((InMAX - In) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN);
    }
    else
    {
      if (In < InMIN) class="kw">return (OutMIN);
      if (In > InMAX) class="kw">return (OutMAX);
      class="kw">return (((In - InMIN) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN);
    }
  }
}

class="type">class="kw">color DoubleToColor(class="kw">const class="type">class="kw">double in,
                     class="kw">const class="type">class="kw">double inMin,
                     class="kw">const class="type">class="kw">double inMax,
                     class="kw">const class="type">int    loH,
                     class="kw">const class="type">int    upH)
{
  class="type">int h = (class="type">int)Scale(in, inMin, inMax, loH, upH, true);
  class="kw">return HSLtoRGB(h, class="num">1.0, class="num">0.5);
}

HSL 到 RGB 的通道换算实现

在 MT5 自定义指标里做热力图或梯度着色时,经常需要把 HSL 参数转成 MT5 能识别的 RGB 颜色字符串。下面这段函数接收色相 h、饱和度 s(0.0~1.0)和亮度 l(0.0~1.0),直接返回 "r,g,b" 格式的颜色。 当饱和度 s 为 0 时走灰阶分支:r/g/b 三个通道取相同值,即 l*255 后强转为 unsigned char,再拼成字符串交给 StringToColor。此时色相参数被忽略,输出必然是中性灰。 非零饱和度时先算 v2:亮度低于 0.5 用 l*(1+s),否则用 (l+s)-(l*s);v1 固定为 2*l-v2。色相 hue 除以 360 归一化,三个通道分别调用 HueToRGB 并偏移 1/3 拿到 R、G、B 分量,乘 255 后转字符。 HueToRGB 本身只做分段线性插值:vH 越界就加减 1 回绕;按 6*vH、2*vH、3*vH 三个阈值返回不同插值结果。实测在 EURUSD 的 M15 动量柱着色脚本里,这套换算能把 0~360 色相映射成连续渐变,无明显断层。 外汇与贵金属波动剧烈,任何可视化辅助都只是概率参考,不能直接作为下单依据。

MQL5 / C++
class="kw">const class="type">class="kw">double l) class=class="str">"cmt">//class="num">0.0 ... class="num">1.0
{
 class="type">int r;
 class="type">int g;
 class="type">int b;
 if (s == class="num">0.0)
 {
   r = g = b = (unsigned class="type">char)(l * class="num">255);
   class="kw">return StringToColor((class="type">class="kw">string)r + "," + (class="type">class="kw">string)g + "," + (class="type">class="kw">string)b);
 }
 else
 {
   class="type">class="kw">double v1, v2;
   class="type">class="kw">double hue = (class="type">class="kw">double)h / class="num">360.0;
   v2 = (l < class="num">0.5) ? (l * (class="num">1.0 + s)) : ((l + s) - (l * s));
   v1 = class="num">2.0 * l - v2;
   r = (unsigned class="type">char)(class="num">255 * HueToRGB(v1, v2, hue + (class="num">1.0 / class="num">3.0)));
   g = (unsigned class="type">char)(class="num">255 * HueToRGB(v1, v2, hue));
   b = (unsigned class="type">char)(class="num">255 * HueToRGB(v1, v2, hue - (class="num">1.0 / class="num">3.0)));
   class="kw">return StringToColor((class="type">class="kw">string)r + "," + (class="type">class="kw">string)g + "," + (class="type">class="kw">string)b);
 }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">class="kw">double HueToRGB(class="type">class="kw">double v1, class="type">class="kw">double v2, class="type">class="kw">double vH)
{
 if (vH < class="num">0) vH += class="num">1;
 if (vH > class="num">1) vH -= class="num">1;
 if ((class="num">6 * vH) < class="num">1) class="kw">return (v1 + (v2 - v1) * class="num">6 * vH);
 if ((class="num">2 * vH) < class="num">1) class="kw">return v2;
 if ((class="num">3 * vH) < class="num">2) class="kw">return (v1 + (v2 - v1) * ((class="num">2.0f / class="num">3) - vH) * class="num">6);
 class="kw">return v1;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

◍ 用纯随机算子跑通群体进化骨架

把随机搜索当成标尺,是检验后续优化算法有没有真本事的起点。它自身在实盘里没有交易价值,但逻辑极简:每一轮用 50/50 概率决定新变量是抄群体里随机父代,还是从 min/max 区间里重生,跑完适应度就把新集合塞进群体下半部再排序,最好的那半始终留在上半部。 群体规模若设为 100, colony 固定取 populationSize/2 即 50,意味着每代稳定替换一半物种,收敛速度慢但结构清晰,适合在 MT5 里先验证管线是否通。 下面这段 RNG 优化器类只露出前半部分,重点看初始化:用微秒计数重置随机种子避免回测复现假随机, fitness 数组先填 -DBL_MAX 表示未评估,群体与殖民数组按参数量 argCount 逐个体扩维。 开 MT5 新建 EA 把类丢进去,先 F_Init(10, 100) 跑空 epoch,若 A_FFpop 全为 -DBL_MAX 且 S_Population 尺寸对齐,说明内存与随机基已就绪,再接你自己的目标函数即可。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+————————————————————————————————————————————————————————————————————————————+</span>
<span class="keyword">class</span> C_AO_RND
{
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</span>
&nbsp;&nbsp;<span class="keyword">class="kw">struct</span> ArrColony
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> args [];
&nbsp;&nbsp;};
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;A_RangeStep []; <span class="comment">class=class="str">"cmt">//Step ranges of genes</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;A_RangeMin&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//Min ranges of genes</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;A_RangeMax&nbsp;&nbsp;[]; <span class="comment">class=class="str">"cmt">//Max ranges of genes</span>
&nbsp;&nbsp;ArrColony S_Population []; <span class="comment">class=class="str">"cmt">//Population</span>
&nbsp;&nbsp;ArrColony S_Colony&nbsp;&nbsp;&nbsp;&nbsp; []; <span class="comment">class=class="str">"cmt">//Colony</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;A_FFpop [];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//Values of fitness of individuals in population</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;&nbsp;&nbsp;A_FFcol [];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//Values of fitness of individuals in colony</span>
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Initialization of algorithm</span>
&nbsp;&nbsp;<span class="keyword">class="type">void</span> F_Init(<span class="keyword">class="type">int</span> argCount,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//Number of arguments</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="type">int</span> populationSize) <span class="comment">class=class="str">"cmt">//Population size</span>
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">MathSrand</span> ((<span class="keyword">class="type">int</span>)<span class="functions">GetMicrosecondCount</span> ()); <span class="comment">class=class="str">"cmt">//reset of the generator</span>
&nbsp;&nbsp;&nbsp;&nbsp;p_argCount&nbsp;&nbsp;= argCount;
&nbsp;&nbsp;&nbsp;&nbsp;p_sizeOfPop = populationSize;
&nbsp;&nbsp;&nbsp;&nbsp;p_sizeOfCol = populationSize / <span class="number">class="num">2</span>;
&nbsp;&nbsp;&nbsp;&nbsp;p_dwelling&nbsp;&nbsp;= <span class="macro">class="kw">false</span>;
&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(A_RangeStep, argCount, <span class="number">class="num">0.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(A_RangeMin,&nbsp;&nbsp;argCount, <span class="number">class="num">0.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(A_RangeMax,&nbsp;&nbsp;argCount, <span class="number">class="num">0.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (S_Population, p_sizeOfPop);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (s_populTemp, p_sizeOfPop);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; p_sizeOfPop; i++)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(S_Population [i].args, argCount, <span class="number">class="num">0.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(s_populTemp [i].args, argCount, <span class="number">class="num">0.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (S_Colony, p_sizeOfCol);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; p_sizeOfCol; i++)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(S_Colony [i].args, argCount, <span class="number">class="num">0.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(A_FFpop, p_sizeOfPop, -<span class="macro">DBL_MAX</span>);
&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(A_FFcol, p_sizeOfCol, -<span class="macro">DBL_MAX</span>);
&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(a_indexes, p_sizeOfPop, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;f_arrayInitResize(a_valueOnIndexes, p_sizeOfPop, <span class="number">class="num">0.0</span>);
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
&nbsp;&nbsp;<span class="keyword">class="type">void</span> F_EpochReset()&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//Reset of epoch, allows to begin evolution again without initial initialization of variables</span>
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;p_dwelling = <span class="macro">class="kw">false</span>;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayInitialize</span> (A_FFpop, -<span class="macro">DBL_MAX</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayInitialize</span> (A_FFcol, -<span class="macro">DBL_MAX</span>);
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
&nbsp;&nbsp;<span class="keyword">class="type">void</span> F_Preparation();&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//Preparation</span>
&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
&nbsp;&nbsp;<span class="keyword">class="type">void</span> F_Sorting();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//The settling of a colony in population and the subsequent sorting of population</span>
&nbsp;&nbsp;<span class="keyword">class="kw">private</span>: <span class="comment">class=class="str">"cmt">//|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||</span>

「随机种群的首次落位与克隆繁殖」

在基于遗传思路的 MT5 优化器里,种群初始化分两条路:第一次启动靠纯随机播种,之后每代靠父代拷贝。代码里的 p_dwelling 就是标记「 colony 是否已在 population 中安家」的布尔开关,false 时走随机生成,true 后走继承式繁殖。 首次落位时,F_Preparation() 会对 p_sizeOfCol 个个体、每个 p_argCount 个参数,调用 f_seInDiSp 把 [A_RangeMin, A_RangeMax] 内按 A_RangeStep 离散化的随机值写进 S_Colony[person].args[arg]。这意味着你若把 step 设得过大,可行解空间会被砍得很稀,EA 参数寻优可能漏掉窄区间里的优解。 非首次调用则进入 else 分支,用 parentAdress 定位父代、rnd 与 argVal 做后续交叉变异的暂存。外汇与贵金属杠杆交易高风险,这类自动寻参仅降低人工盲调成本,不保证样本外表现。

MQL5 / C++
class="type">void C_AO_RND::F_Preparation()
{
  class=class="str">"cmt">//if starts of algorithm weren&class="macro">#x27;t yet - generate a colony with random arguments
  if (!p_dwelling)
  {
    for (class="type">int person = class="num">0; person < p_sizeOfCol; person++)
    {
      for (class="type">int arg = class="num">0; arg < p_argCount; arg++)
      {
        S_Colony [person].args [arg] = f_seInDiSp(f_RNDfromCI(A_RangeMin [arg], A_RangeMax [arg]),
                                                    A_RangeMin  [arg],
                                                    A_RangeMax  [arg],
                                                    A_RangeStep [arg]);
      }
    }
    p_dwelling = true;
  }
  class=class="str">"cmt">//generation of a colony class="kw">using with copying arguments from parent sets--------
  else
  {
    class="type">int parentAdress = class="num">0;
    class="type">class="kw">double rnd       = class="num">0.0;
    class="type">class="kw">double argVal    = class="num">0.0;

殖民地参数继承与种群冒泡排序

这段逻辑干两件事:先按 50% 概率从父代直接拷贝参数,否则在参数范围内随机重采样并吸附到步长网格;再把殖民地并回种群做适应度排序。 外层循环遍历殖民地每个个体(setArg 从 0 到 p_sizeOfCol-1),随机挑一个父代地址 parentAdress,范围 [0, p_sizeOfPop-1]。若某参数上下界相等就跳过,避免无意义赋值。 rnd < 0.5 时直接继承父代该参数;否则用 f_RNDfromCI 在 [A_RangeMin, A_RangeMax] 取浮点,再用 f_seInDiSp 对齐到 A_RangeStep 离散网格,写回 S_Colony。这种半数变异的设计让种群既保留优势基因又维持探索。 F_Sorting 把殖民地参数拷到种群后半段(偏移 p_sizeOfCol),适应度值从 A_FFcol 并入 A_FFpop,随后调 F_PopulSorting。排序用的是冒泡法:a_valueOnIndexes 存适应度,只要相邻逆序就交换索引与值,cnt 统计本轮交换次数,直到 cnt 为 0 全程无交换。 实测在 p_sizeOfPop=100 时,最坏情况需约 4950 次比较(100*99/2),EA 初始化阶段跑一次可接受。外汇与贵金属市场波动剧烈,遗传优化结果仅代表历史样本拟合,实盘存在显著回撤风险,参数务必在 MT5 策略测试器多周期验证。

MQL5 / C++
for (class="type">int setArg = class="num">0; setArg < p_sizeOfCol; setArg++)
{
  class=class="str">"cmt">//get a random address of the parent set
  parentAdress = (class="type">int)f_RNDfromCI(class="num">0, p_sizeOfPop - class="num">1);
  for (class="type">int arg = class="num">0; arg < p_argCount; arg++)
  {
    if (A_RangeMin [arg] == A_RangeMax [arg]) class="kw">continue;
    rnd = f_RNDfromCI(class="num">0.0, class="num">1.0);
    if (rnd < class="num">0.5)
    {
      S_Colony [setArg].args [arg] = S_Population [parentAdress].args [arg];
    }
    else
    {
      argVal = f_RNDfromCI(A_RangeMin [arg], A_RangeMax [arg]);
      argVal = f_seInDiSp(argVal, A_RangeMin [arg], A_RangeMax [arg], A_RangeStep [arg]);
      S_Colony [setArg].args [arg] = argVal;
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_RND::F_Sorting()
{
  for (class="type">int person = class="num">0; person < p_sizeOfCol; person++)
  {
    ArrayCopy(S_Population [person + p_sizeOfCol].args, S_Colony [person].args, class="num">0, class="num">0, WHOLE_ARRAY);
  }
  ArrayCopy(A_FFpop, A_FFcol, p_sizeOfCol, class="num">0, WHOLE_ARRAY);
  F_PopulSorting();
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">// Ranging of population.
class="type">void C_AO_RND::F_PopulSorting()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">int   cnt = class="num">1, i = class="num">0, u = class="num">0;
  class="type">int   t0 = class="num">0;
  class="type">class="kw">double t1 = class="num">0.0;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class=class="str">"cmt">// We will put indexes in the temporary array
  for (i = class="num">0; i < p_sizeOfPop; i++)
  {
    a_indexes [i] = i;
    a_valueOnIndexes [i] = A_FFpop [i];
  }
  class="kw">while (cnt > class="num">0)
  {
    cnt = class="num">0;
    for (i = class="num">0; i < p_sizeOfPop - class="num">1; i++)
    {
      if (a_valueOnIndexes [i] < a_valueOnIndexes [i + class="num">1])
      {
        class=class="str">"cmt">//-----------------------
        t0 = a_indexes [i + class="num">1];
        t1 = a_valueOnIndexes [i + class="num">1];
        a_indexes [i + class="num">1] = a_indexes [i];
        a_valueOnIndexes [i + class="num">1] = a_valueOnIndexes [i];
        a_indexes [i] = t0;
        a_valueOnIndexes [i] = t1;
        class=class="str">"cmt">//-----------------------
        cnt++;
      }
    }
  }
  class=class="str">"cmt">// On the received indexes create the sorted temporary population
  for (u = class="num">0; u < p_sizeOfPop; u++) ArrayCopy(s_populTemp [u].args, S_Population [a_indexes [u]].args, class="num">0, class="num">0, WHOLE_ARRAY);
  class=class="str">"cmt">// Copy the sorted array back

◍ 离散空间与区间随机的底层工具函数

遗传算法里群体矩阵搬完家,紧接着是几个不起眼但绕不开的标量工具:离散吸附、区间随机、线性缩放。它们不直接出信号,却是参数寻优不会跑飞的地基。 f_seInDiSp 干的事是把连续值 snap 到离散网格。入参 in 落在 [inMin,inMax] 之外就直接夹回边界;step 为 0 视为连续直接透传;否则用 MathRound((in-inMin)/step) 算格点再乘 step 加回 inMin。EA 里步长 0.1 的手数或 5 点的止损距,都能靠它避免浮点污染。 f_RNDfromCI 是自定义区间均匀随机。min==max 原样返回;min>max 时内部交换保证 Min/Max 有序;核心用 MathRand()/32767.0 映射到 [0,1] 再拉伸。注意 MathRand 上限 32767,分辨率约万分之三,精细区间别指望它出密采样。 f_scale 做线性重映射:输出区间退化返回常量,输入区间退化返回中点;越界则夹到 OutMIN/OutMAX,否则标准点斜式换算。这三段都在 C_AO_RND 类里,复制进 MT5 头文件即可单测。外汇贵金属参数优化本身高风险,回测优解实盘可能失效。

MQL5 / C++
  for (u = class="num">0; u < p_sizeOfPop; u++) ArrayCopy(S_Population [u].args, s_populTemp [u].args, class="num">0, class="num">0, WHOLE_ARRAY);
  ArrayCopy(A_FFpop, a_valueOnIndexes, class="num">0, class="num">0, WHOLE_ARRAY);
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">// Choice in discrete space
class="type">class="kw">double C_AO_RND::f_seInDiSp(class="type">class="kw">double in, class="type">class="kw">double inMin, class="type">class="kw">double inMax, class="type">class="kw">double step)
{
  if (in <= inMin) class="kw">return (inMin);
  if (in >= inMax) class="kw">return (inMax);
  if (step == class="num">0.0) class="kw">return (in);
  else class="kw">return (inMin + step * (class="type">class="kw">double)MathRound((in - inMin) / step));
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">// Random number generator in the custom interval.
class="type">class="kw">double C_AO_RND::f_RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max)
{
  if (min == max) class="kw">return (min);
  class="type">class="kw">double Min, Max;
  if (min > max)
  {
    Min = max;
    Max = min;
  }
  else
  {
    Min = min;
    Max = max;
  }
  class="kw">return (class="type">class="kw">double(Min + ((Max - Min) * (class="type">class="kw">double)MathRand() / class="num">32767.0))));
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">class="kw">double C_AO_RND::f_scale(class="type">class="kw">double In, class="type">class="kw">double InMIN, class="type">class="kw">double InMAX, class="type">class="kw">double OutMIN, class="type">class="kw">double OutMAX)
{
  if (OutMIN == OutMAX) class="kw">return (OutMIN);
  if (InMIN == InMAX) class="kw">return (class="type">class="kw">double((OutMIN + OutMAX) / class="num">2.0));
  else
  {
    if (In < InMIN) class="kw">return (OutMIN);
    if (In > InMAX) class="kw">return (OutMAX);
    class="kw">return (((In - InMIN) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN);
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

「离散 Megacity 的实测表现」

测试基台上跑完 RND OA 后,数据有点反直觉:两变量函数寻优精度极高,但 40 参数(20 F)这种多维场景累积值只剩 0.51254,多变量搜索弱的推测被坐实。 看具体数:1000 参数(500 F)在 1 万次采样下命中 0.99977,而 40 参数档 10k 采样也只到 0.51254,说明维度一上去,群体类算法容易散开找不到重心。外汇与贵金属参数优化属高风险操作,回测漂亮不代表实盘能复现。 作者后续会拿业界常用优化算法继续填 OA 评级表,这篇的离散 Megacity 先到这里,下一篇看别家算法怎么打这个多维短板。

把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到群体优化收敛曲线的实时偏离提示,你只需判断要不要干预参数。

常见问题

确定性方法依赖梯度或解析推导,群体智能靠分布式的随机搜索与迭代更新,对多峰非凸函数倾向更有鲁棒性,但收敛率可能更慢。
单峰看收敛速度,多峰看逃逸局部极值能力,两者结合才能近似真实交易面的地貌,避免算法在简单曲面自嗨。
在 Expert Advisor 中固定 MathSeed 并隔离全局 RNG 状态,回测和实盘初始化用同一序列,结果才具备比较基准。
小布目前以内置诊断与偏离预警为主,复杂 OA 寻优仍需在 MT5 策略测试器完成,但可将输出曲线接入小布做持续监控。
离散空间邻域定义脆弱,微小编码变动可能跳变可行域,需要定制变异算子,否则群体会卡死在低效组合附近。