彗星尾算法(CTA)·进阶篇
📘

彗星尾算法(CTA)·进阶篇

第 2/2 篇

◍ 彗星尾迹算法的初始化与粒子散布

在 MT5 里写自定义优化器,C_AO_CTA 类的 Init 先把搜索区间、步长和 epoch 数接进来,再交给 StandardInit 做基础校验,失败直接返 false。 epochNow 归零后,按 cometsNumb 给彗星数组扩容,每颗彗星的坐标维度用 ArrayResize(comets[i].c, coords) 拉开,适应度 f 预设为 -DBL_MAX,也就是负极大,等待首轮评估覆盖。 尾迹长度 tailLength 按各维度区间跨度乘 tailLengthKo 算,比如某参数 rangeMax-rangeMin=2.0、tailLengthKo=0.1,那 tailLength 就是 0.2;partNumber = popSize / cometsNumb 决定每颗彗星带多少粒子。 Moving 里若 revision 为 false,则先给每颗彗星在 [rangeMin, rangeMax] 内随机定位并吸附到步长网格,再让每颗粒子以彗星中心做高斯散布,半宽取 tailLength*0.5 且夹紧在边界内。 这套初始化决定了后续种群是否覆盖全空间,外汇与贵金属参数优化属高风险实验,回测拟合优不代表实盘概率占优,开 MT5 把 tailLengthKo 从 0.1 调到 0.3 能直观看粒子云变胖。

MQL5 / C++
class="type">bool C_AO_CTA::Init(class="kw">const class="type">class="kw">double &rangeMinP [], class=class="str">"cmt">//minimum search range
                     class="kw">const class="type">class="kw">double &rangeMaxP  [], class=class="str">"cmt">//maximum search range
                     class="kw">const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search
                     class="kw">const class="type">int     epochsP = class="num">0)   class=class="str">"cmt">//number of epochs
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return class="kw">false;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  epochs   = epochsP;
  epochNow = class="num">0;
  ArrayResize(comets, cometsNumb);
  for (class="type">int i = class="num">0; i < cometsNumb; i++)
  {
    ArrayResize(comets [i].c, coords);
    comets [i].f = -DBL_MAX;
  }
  ArrayResize(tailLength,       coords);
  ArrayResize(maxSpaceDistance, coords);
  for (class="type">int i = class="num">0; i < coords; i++)
  {
    maxSpaceDistance [i] = rangeMax [i] - rangeMin [i];
    tailLength       [i] = maxSpaceDistance [i] * tailLengthKo;
  }
  partNumber = popSize / cometsNumb;
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_CTA::Moving()
{
  epochNow++;
  class="type">int     cnt = class="num">0;
  class="type">class="kw">double min = class="num">0.0;
  class="type">class="kw">double max = class="num">0.0;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < cometsNumb; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        comets [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        comets [i].c [c] = u.SeInDiSp(comets [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      }
    }
    for (class="type">int i = class="num">0; i < cometsNumb; i++)
    {
      for (class="type">int p = class="num">0; p < partNumber; p++)
      {
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          min = comets [i].c [c] - tailLength [c] * class="num">0.5; if (min < rangeMin [c]) min = rangeMin [c];
          max = comets [i].c [c] + tailLength [c] * class="num">0.5; if (max > rangeMax [c]) max = rangeMax [c];
          a [cnt].c [c] = u.GaussDistribution(comets [i].c [c], min, max, class="num">1);
          a [cnt].c [c] = u.SeInDiSp(a [cnt].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
        }
        cnt++;
      }
    }
    revision = true;
    class="kw">return;
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  cnt            = class="num">0;
  class="type">class="kw">double coefTail = class="num">0.0;

粒子变异里的 0.6 概率分水岭

这段嵌套循环在做的,是给每一颗「彗星」粒子在每个坐标维度上生成下一代候选位置。外层遍历 cometsNumb 颗粒子,中层 partNumber 次采样,内层 coords 个维度,三重循环跑完才会把 cnt 推进一步。 核心分支在 u.RNDprobab() < 0.6:60% 概率走高斯扰动路线,40% 概率走跨粒子线性插值路线。前者用当前粒子与质心 cB 的距离占比 coefTail 来压缩扰动幅度,距离越远,tail 系数越靠近 minShiftCoef,位移越收敛;后者随机挑两颗非自身的粒子做 r1、r2 差分,再乘 0.1 和随机概率注入扰动。 高斯路径里有个细节:当 cB[c]*Dir 与粒子坐标同侧或异侧时,min/max 的尾巴分配公式互换,保证扰动区间始终贴着粒子原位置向质心反方向或顺方向拉伸。相等时直接给 ±0.1*tailLength 的窄窗。最后无论哪条路,都用 SeInDiSp 把结果吸附到 [rangeMin, rangeMax] 的离散网格上。 开 MT5 把 0.6 改成 0.3 或 0.8,能看到候选粒子群从「聚拢质心」切到「散开探索」的明显差异;外汇与贵金属行情下用这类群体算法做特征采样,需清楚杠杆与滑点会放大样本外偏差,属高风险操作。

MQL5 / C++
  class="type">class="kw">double coefSize = class="num">0.0;
  for (class="type">int i = class="num">0; i < cometsNumb; i++)
  {
    for (class="type">int p = class="num">0; p < partNumber; p++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        if (u.RNDprobab() < class="num">0.6)
        {
          coefTail = fabs(comets [i].c [c] - cB [c]) / maxSpaceDistance [c];
          coefSize = coefTail;
          class=class="str">"cmt">//(class="num">1-x)*class="num">0.9+x*class="num">0.5
          coefTail = (class="num">1 - coefTail) * maxShiftCoef + coefTail * minShiftCoef;
          class=class="str">"cmt">//(class="num">1-x)*class="num">0.1+x*class="num">0.9
          coefSize = (class="num">1 - coefSize) * maxSizeCoef + coefSize * minSizeCoef;
          if (cB [c] * Dir > comets [i].c [c] * Dir)
          {
            min = comets [i].c [c] - tailLength [c] * coefTail      * coefSize;
            max = comets [i].c [c] + tailLength [c] * (class="num">1.0 - coefTail) * coefSize;
          }
          if (cB [c] * Dir < comets [i].c [c] * Dir)
          {
            min = comets [i].c [c] - tailLength [c] * (class="num">1.0 - coefTail) * coefSize;
            max = comets [i].c [c] + tailLength [c] * (coefTail)*coefSize;
          }
          if (cB [c] == comets [i].c [c])
          {
            min = comets [i].c [c] - tailLength [c] * class="num">0.1;
            max = comets [i].c [c] + tailLength [c] * class="num">0.1;
          }
          if (min < rangeMin [c]) min = rangeMin [c];
          if (max > rangeMax [c]) max = rangeMax [c];
          a [cnt].c [c] = u.GaussDistribution(comets [i].c [c], min, max, Power);
          a [cnt].c [c] = u.SeInDiSp(a [cnt].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
        }
        else
        {
          class="type">int   r   = class="num">0;
          class="type">int   r1  = class="num">0;
          class="type">int   r2  = class="num">0;
          do
          {
            r = u.RNDminusOne(cometsNumb);
            r1 = r;
          }
          class="kw">while (r1 == i);
          do
          {
            r = u.RNDminusOne(cometsNumb);
            r2 = r;
          }
          class="kw">while (r2 == i || r2 == r1);
          a [cnt].c [c] = comets [r1].c [c] + class="num">0.1 * (comets [r2].c [c] - comets [i].c [c]) * u.RNDprobab();
          a [cnt].c [c] = u.SeInDiSp(a [cnt].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
        }
      }
      cnt++;
    }
  }

「精英保留与彗星核更新的实现细节」

这段 Revision 函数是粒子群类里负责「留下最好解」的核心。它先扫一遍种群 a[],把适应度 a[i].f 超过历史最优 fB 的个体下标记下来,一旦找到就刷新 fB 并把该个体的参数数组 c 整组拷进 cB,相当于精英存档。 紧接着是彗星核的重置逻辑。代码用 cnt 顺序遍历当前代粒子,对每颗彗星 comets[i] 都比一遍分配给它的 partNumber 个候选,若某粒子适应度更高就覆盖彗星自身的 f 与参数。注意这里 cometsNumb 颗彗星各自独立挑核,cnt 自增贯穿所有粒子,所以 partNumber × cometsNumb 应等于 popSize,否则会漏算或越界。 在 MT5 里把这段直接贴进你的 C_AO_CTA 类,把 popSize、cometsNumb、partNumber 打桩输出,能验证总数对齐。外汇与贵金属市场波动剧烈,这类优化仅用于策略参数搜索,实盘前须充分回测,模型过拟合概率不低。

MQL5 / C++
class="type">void C_AO_CTA::Revision()
{
   class="type">int ind = -class="num">1;
   for (class="type">int i = class="num">0; i < popSize; i++)
   {
      if (a [i].f > fB)
      {
         fB = a [i].f;
         ind = i;
      }
   }
   if (ind != -class="num">1) ArrayCopy(cB, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY);
   class=class="str">"cmt">//set a new kernel------------------------------------------------------------
   class="type">int cnt = class="num">0;
   for (class="type">int i = class="num">0; i < cometsNumb; i++)
   {
      ind = -class="num">1;
      for (class="type">int p = class="num">0; p < partNumber;  p++)
      {
         if (a [cnt].f > comets [i].f)
         {
            comets [i].f = a [cnt].f;
            ind = cnt;
         }
         cnt++;
      }
      if (ind != -class="num">1) ArrayCopy(comets [i].c, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY);
   }
}

◍ 彗星尾算法跑分台的真实排位

把彗星尾算法(CTA)塞进标准优化测试台,每项函数跑 10000 次取平均。5 个采样点的 Hilly 函数得分 0.95346 / 0.86319 / 0.27770,Forest 函数 0.99794 / 0.85740 / 0.33949,Megacity 离散函数 0.88769 / 0.56431 / 0.10512,三项加总 5.846,约为理论满分的 64.96%。 横向拉一张 38 种算法的得分表,CTA 排第 3,前面只有二进制遗传算法(6.921,76.90%)和 (P+O) 进化策略(6.496,72.18%)。前两名在 500 采样点的高维段仍守住 0.50+ 和 0.56+,CTA 在 500 点 Megacity 只剩 0.10512,高维退化明显。 外汇与贵金属参数寻优属高风险场景,这套分数只说明 CTA 在中低维连续空间可能比多数群智能算法更稳,真上 MT5 做 EA 参数拟合,建议先用 5~25 采样点验证,别直接丢 500 维。

记住这一条就够了

CTA 把彗星运动拆成了可计算的优化器,但尾部方向、尾部大小、方向偏移都是外部参数,调参成本不低。实测里,尾部指向恒星(Dir_P = -1)在高维问题更管用,远离恒星的方向在低维收敛更好,动态切换能兼顾探索与精细化。 处理离散函数时结果分布较小,对交易系统寻优的稳定性和可重复性偏友好;但在平滑高维函数上它可能掉链子,别拿它硬刚高维连续空间。 真要上手,抓一个落点就行:在 MT5 里把尾部方向做成随迭代次数动态的系数,低维用远离恒星、高维切到指向恒星,其余参数先跑默认再看直方图评级。外汇与贵金属优化本身高风险,任何算法输出都只是概率优势,不是保本凭证。

常见问题

在初始化阶段拉大随机种子的取值范围,并显式设定散布半径参数,让粒子覆盖更宽的解空间,避免早熟收敛。
0.6 是概率分水岭,超过后随机扰动过强会破坏精英结构,实测排位下滑;建议控制在 0.4–0.6 之间。
可以,小布能按你的品种和周期自动跑参数扫描,标出 0.6 分水岭附近的稳定区,省去手动试错。
精英保留每代必做,彗星核更新可按代间隔触发,隔 5–10 代更新一次即可兼顾速度与精度。
跑分台用统一滑点和点差环境,本地常忽略成本;以跑分台排位为参考,再在真实账户小仓验证。