龟壳演化算法(TSEA)·进阶篇
🐢

龟壳演化算法(TSEA)·进阶篇

(2/3)·把龟壳分层生长搬进 EA,内层保留劣解的思路为何能躲过局部最优

实战向 第 2/3 篇
很多交易者把优化跑完直接取最优参数就上线,内层那些次优解全被丢弃。TSEA 反其道而行,把不太理想的方案压进壳内继续演化,忽略这层结构很容易在行情切换时集体失效。

「KNN 投票落点与 TSEA 默认参数」

上面那段闭括号收尾的是 K 近邻分类的归票逻辑:先对距离索引做冒泡排序,再开一个长度为 n_clusters 的 votes 数组并清零,随后只遍历前 k_neighbors 个近邻,把带有效标签(非 -1 且在簇数范围内)的样本对应簇票数加一。 最后两轮线性扫描找最大票簇——max_votes 初始 0、max_votes_cluster 初始 -1,只要某簇票数比当前最大值高就刷新,函数最终返回得票最高的簇编号;若所有近邻都无标签,返回值是 -1,调用方需自行处理这种「无监督邻域」情形。 紧跟着的 C_AO_TSEA 类把这套聚类骨架包装成可调参的分析对象。构造函数里写死了 5 个关键数:种群规模 popSize=100、纵向簇 vClusters=3、横向簇 hClusters=10、近邻数 neighbNumb=5、单格最大代理 maxAgentsInCell=3。 这些字面量同时被写进 params 数组,所以你在 MT5 里改面板参数后,必须走 SetParams() 把 double 型的 val 强转回 int 覆盖成员变量,否则实际跑的还是构造时的旧值。外汇与贵金属波动剧烈,这类聚类参数对样本窗口敏感,实盘前建议在历史数据上多次重算验证。

MQL5 / C++
BubbleSort(distances_indices, class="num">0, n - class="num">1);
class=class="str">"cmt">// Define the cluster for the point
class="type">int votes [];
ArrayResize(votes, n_clusters);
ArrayInitialize(votes, class="num">0);
for (class="type">int j = class="num">0; j < k_neighbors; j++)
{
  class="type">int label = data [distances_indices [j].index].label;
  if (label != -class="num">1 && label < n_clusters)
  {
    votes [label]++;
  }
}
class="type">int max_votes = class="num">0;
class="type">int max_votes_cluster = -class="num">1;
for (class="type">int j = class="num">0; j < n_clusters; j++)
{
  if (votes [j] > max_votes)
  {
    max_votes = votes [j];
    max_votes_cluster = j;
  }
}
class="kw">return max_votes_cluster;
};

TSEA 优化器的初始化与种群落格

这套树状空间探索算法(TSEA)把搜索空间切成纵向 vClusters 与横向 hClusters 两套簇,再用 agent 数组承载 popSize 个搜索个体。Init 函数先调 StandardInit 校验传入的 rangeMinP / rangeMaxP / rangeStepP 三套边界数组,任一不合法就直接返回 false,MT5 上你会看到EA初始化失败。 下面这段是 Init 的核心内存布局逻辑,建议直接拷进 MT5 看数组怎么铺开: bool C_AO_TSEA::Init (const double &rangeMinP [], //minimum search range const double &rangeMaxP [], //maximum search range const double &rangeStepP [], //step search const int epochsP = 0) //number of epochs { if (!StandardInit (rangeMinP, rangeMaxP, rangeStepP)) return false; ArrayResize (agent, popSize); for (int i = 0; i < popSize; i++) agent [i].Init (coords); ArrayResize (clusters, hClusters); for (int i = 0; i < hClusters; i++) clusters [i].Init (coords); ArrayResize (cell, vClusters); for (int i = 0; i < vClusters; i++) { ArrayResize (cell [i].cell, hClusters); for (int c = 0; c < hClusters; c++) ArrayResize (cell [i].cell [c].agent, 0, maxAgentsInCell); } minFval = DBL_MAX; stepF = 0.0; epochs = epochsP; epochsNow = 0; return true; } 逐行拆一下:ArrayResize(agent, popSize) 给种群分配槽位,随后每个 agent[i] 用 coords 维度初始化;clusters 按 hClusters 横向簇数铺开。cell 是二维网格,纵轴 vClusters、横轴 hClusters,每个格子的 agent 子数组预留 maxAgentsInCell 容量——这一步决定了网格内最多塞多少个体,外汇参数寻优时若 maxAgentsInCell 太小,局部簇容易溢出丢解。 minFval 初始化成 DBL_MAX 意味着首轮适应度必被刷新,epochsNow 从 0 计起。默认 epochsP=0 表示不限制迭代轮数,实盘跑黄金 XAUUSD 这类高波动品种时,可能因为永不收敛而卡死回测,建议显式传一个整数上限。

MQL5 / C++
class="type">bool C_AO_TSEA::Init(const class="type">class="kw">double &rangeMinP  [], class=class="str">"cmt">//minimum search range
                      const class="type">class="kw">double &rangeMaxP  [], class=class="str">"cmt">//maximum search range
                      const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search
                      const class="type">int    epochsP = class="num">0)   class=class="str">"cmt">//number of epochs
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
  ArrayResize(agent, popSize);
  for (class="type">int i = class="num">0; i < popSize; i++) agent [i].Init(coords);
  ArrayResize(clusters, hClusters);
  for (class="type">int i = class="num">0; i < hClusters; i++) clusters [i].Init(coords);
  ArrayResize(cell, vClusters);
  for (class="type">int i = class="num">0; i < vClusters; i++)
  {
    ArrayResize(cell [i].cell, hClusters);
    for (class="type">int c = class="num">0; c < hClusters; c++) ArrayResize(cell [i].cell [c].agent, class="num">0, maxAgentsInCell);
  }
  minFval   = DBL_MAX;
  stepF     = class="num">0.0;
  epochs    = epochsP;
  epochsNow = class="num">0;
  class="kw">return true;
}

◍ 网格里挑种子与 0.8 概率的邻域借用

种群初始化只在非修订模式下跑一次:给 popSize 个个体在每个坐标维度上先丢入均匀随机数,再用 SeInDiSp 按 rangeStep 吸附到离散格点,写进 agent 数组后直接 return,不在每代重复采样。 修订模式下,代码先扫 vClusters × hClusters 的网格,对每个非空 cell 找适应度 f 最大的 agent,把下标存进 indBest。实测中若某 cell 有 12 个 agent、最大 f 落在第 3 个,indBest 就记 3,供后续交叉直接调用。 主干循环对每个个体以 0.8 概率进入邻域借用:用 (-rnd²+1)·vClusters 做偏向 0 行的垂直抽样,水平向随机挑列,直到命中非空 cell。抽中后 50% 概率直接取该 cell 的 indBest 坐标,否则随机选一个 agent。 坐标赋值阶段有 0.6 概率沿用邻居值、否则回退全局最优 cB;扰动幅度固定为 (rangeMax-rangeMin)·0.1。外汇与贵金属参数空间里这种 10% 步长可能让 EA 在欧美盘切换时跳过关键阈值,建议开 MT5 把 0.1 改成 0.05 观察收敛节奏。

MQL5 / C++
class=class="str">"cmt">//----------------------------------------------------------------------------
class=class="str">"cmt">//class="num">1. Generate random individuals for the population
if (!revision)
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
      a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
      agent [i].c [c] = a [i].c [c];
    }
  }
  class="kw">return;
}
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">int     vPos = class="num">0;
class="type">int     hPos = class="num">0;
class="type">int     pos  = class="num">0;
class="type">int     size = class="num">0;
class="type">class="kw">double val  = class="num">0.0;
class="type">class="kw">double rnd  = class="num">0.0;
class="type">class="kw">double min  = class="num">0.0;
class="type">class="kw">double max  = class="num">0.0;
for (class="type">int v = class="num">0; v < vClusters; v++)
{
  for (class="type">int h = class="num">0; h < hClusters; h++)
  {
    size = ArraySize(cell [v].cell [h].agent);
    if (size > class="num">0)
    {
      max = -DBL_MAX;
      pos = -class="num">1;
      for (class="type">int c = class="num">0; c < size; c++)
      {
        if (cell [v].cell [h].agent [c].f > max)
        {
          max = cell [v].cell [h].agent [c].f;
          pos = c;
          cell [v].cell [h].indBest = c;
        }
      }
    }
  }
}
for (class="type">int i = class="num">0; i < popSize; i++)
{
  if (u.RNDprobab() < class="num">0.8)
  {
    while (true)
    {
      rnd = u.RNDprobab();
      rnd = (-rnd * rnd + class="num">1.0) * vClusters;
      vPos = (class="type">int)rnd;
      if (vPos > vClusters - class="num">1) vPos = vClusters - class="num">1;
      hPos = u.RNDminusOne(hClusters);
      size = ArraySize(cell [vPos].cell [hPos].agent);
      if (size > class="num">0) break;
    }
    pos = u.RNDminusOne(size);
    if (u.RNDprobab() < class="num">0.5) pos = cell [vPos].cell [hPos].indBest;
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      if (u.RNDprobab() < class="num">0.6) val = cell [vPos].cell [hPos].agent [pos].c [c];
      else                     val = cB [c];
      class="type">class="kw">double dist = (rangeMax [c] - rangeMin [c]) * class="num">0.1;

「适应度分层与聚类修订的实现细节」

这段逻辑紧接种群生成之后,负责把每一代 agent 的适应度映射到二维网格,并为后续交叉挑选父代。Revision() 先遍历 popSize 个个体,把 a[i].f 同步给 agent[i].f,同时记录全局最优 fB 与最差 minFval,若发现更优个体就用 ArrayCopy 把它的坐标 c 写进 cB。 步长 stepF 直接由 (fB - minFval) / vClusters 算得,这意味着纵向被切成 vClusters 条等高带。每个 agent 的 labelClustV 按 (fB 除外) (agent[i].f - minFval) / stepF 取整得到,越界统一钳到 vClusters-1;最优个体强制落最顶层。 非首轮时 revision 已为 true,跳过 KMeans 初始化,直接复用上一轮聚类中心;首轮则 KMeansPlusPlusInit 加 KMeans 跑一遍 clusters 类。外汇与贵金属市场高波动,这类自适应群体算法仅用于历史回测与参数搜索,实盘信号概率仍受滑点与点差侵蚀。 父代抽取在 else 分支:用 (-rnd*rnd+1.0)*vClusters 做偏向低索引的随机,锁定 vPos 后随机选两个横向簇 hPos 与 hPos2,要求两边都有 agent 才 break。取各自随机成员做坐标均值,再经 SeInDiSp 离散化写回 a[i].c。

MQL5 / C++
min = val - dist; if (min < rangeMin [c]) min = rangeMin [c];
max = val + dist; if (max > rangeMax [c]) max = rangeMax [c];
val = u.PowerDistribution(val, min, max, class="num">30);
a [i].c [c] = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]);
agent [i].c [c] = a [i].c [c];
}
}
else
{
class="type">int size2 = class="num">0;
class="type">int hPos2 = class="num">0;
class="type">int pos2  = class="num">0;
while (true)
{
rnd = u.RNDprobab();
rnd = (-rnd * rnd + class="num">1.0) * vClusters;
vPos = (class="type">int)rnd;
if (vPos > vClusters - class="num">1) vPos = vClusters - class="num">1;
hPos = u.RNDminusOne(hClusters);
size = ArraySize(cell [vPos].cell [hPos].agent);
hPos2 = u.RNDminusOne(hClusters);
size2 = ArraySize(cell [vPos].cell [hPos2].agent);
if (size > class="num">0 && size2 > class="num">0) break;
}
pos  = u.RNDminusOne(size);
pos2 = u.RNDminusOne(size2);
for (class="type">int c = class="num">0; c < coords; c++)
{
val = (cell [vPos].cell [hPos ].agent [pos ].c [c] +
       cell [vPos].cell [hPos2].agent [pos2].c [c]) * class="num">0.5;
a [i].c [c] = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]);
agent [i].c [c] = a [i].c [c];
}
}
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSEA::Revision()
{
class=class="str">"cmt">//get fitness--------------------------------------------------
class="type">int pos = -class="num">1;
for (class="type">int i = class="num">0; i < popSize; i++)
{
agent [i].f = a [i].f;
if (a [i].f > fB)
{
fB = a [i].f;
pos = i;
}
if (a [i].f < minFval) minFval = a [i].f;
}
if (pos != -class="num">1) ArrayCopy(cB, a [pos].c, class="num">0, class="num">0, WHOLE_ARRAY);
stepF = (fB - minFval) / vClusters;
class=class="str">"cmt">//Vertical layout of the child population-----------------------------------
for (class="type">int i = class="num">0; i < popSize; i++)
{
if (agent [i].f == fB) agent [i].labelClustV = vClusters - class="num">1;
else
{
agent [i].labelClustV = class="type">int((agent [i].f - minFval) / stepF);
if (agent [i].labelClustV > vClusters - class="num">1) agent [i].labelClustV = vClusters - class="num">1;
}
}
class=class="str">"cmt">//----------------------------------------------------------------------------
if (!revision)
{
km.KMeansPlusPlusInit(agent, popSize, clusters);
km.KMeans(agent, popSize, clusters);
revision = true;
}
class=class="str">"cmt">//----------------------------------------------------------------------------
把重复劳动交给小布
这些多层集群的合并与演化诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到每层参数的存活状态,你只管判断该不该换壳。

常见问题

通常背甲映射为向外暴露的硬解集群,腹甲对应底层软解缓存,二者通过垂直索引关联,具体实现见算法实现节。
内层劣解在环境变化后可能重新变优,保留它们相当于留了退化缓冲,避免算法过早收敛到局部最优。
可以,小布盯盘品种页支持导入集群参数并标注每层硬度,省去自己写绘图脚本的功夫。
星形或豹纹等集群可类比参数敏感区与稳定区,演化导致集群迁移即对应参数失效重估,细节在测试结果节展开。