禁忌搜索(TS)·进阶篇
📘

禁忌搜索(TS)·进阶篇

第 2/2 篇

「TSm 优化器的种群与坐标初始化」

这套树状空间搜索(TSm)优化器把每一维坐标切成 sectorsPerCoord 个扇区,由 params[1] 读入;params[2] 则给出 bestProbab,作为精英保留的概率阈值。代理个体数组 agents[] 在 Init 时按 popSize 扩容,每个 agent 用 coords 与 sectorsPerCoord 完成自身初始化。 Init 函数接收三组区间参数:rangeMinP 为搜索下界,rangeMaxP 为上界,rangeStepP 为步长,epochsP 默认 0。它先调 StandardInit 做通用校验,失败直接返回 false;通过后 ArrayResize(agents, popSize) 把种群数组撑到设定规模,再循环让每个 agent 绑定坐标维度与扇区数。 Moving 暴露了冷启动逻辑:首次调用且 revision 为 false 时走 InitializePopulation 生成初代种群并置 revision=true,后续调用才进入 GenerateNewCoordinates 做迭代寻优。你在 MT5 里把 popSize 设成 50、sectorsPerCoord 设成 10,能直观看到代理在 10 个扇区内离散跳变,外汇与贵金属参数寻优属高风险操作,结果仅具概率倾向。

MQL5 / C++
sectorsPerCoord = (class="type">int)params [class="num">1].val;
bestProbab       = params       [class="num">2].val;
}
class="type">bool 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="type">void Moving();
class="type">void Revision();
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">int    sectorsPerCoord;
class="type">class="kw">double bestProbab;
S_TSmAgent agents [];
class="kw">private: class=class="str">"cmt">//-------------------------------------------------------------------
class="type">void   InitializePopulation();
class="type">void   UpdateLists();
class="type">void   GenerateNewCoordinates();
class="type">int    GetSectorIndex(class="type">class="kw">double coord, class="type">int dimension);
class="type">int    ChooseSectorFromWhiteList(class="type">int agentIndex, class="type">int dimension);
class="type">class="kw">double GenerateCoordInSector(class="type">int sectorIndex, class="type">int dimension);
class="type">bool   IsInBlackList(class="type">int agentIndex, class="type">int dimension, class="type">int sectorIndex);
};
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">bool C_AO_TSm::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)
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
  class=class="str">"cmt">//----------------------------------------------------------------------------
  ArrayResize(agents, popSize);
  for (class="type">int i = class="num">0; i < popSize; i++) agents [i].Init(coords, sectorsPerCoord);
  class="kw">return true;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSm::Moving()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  if (!revision)
  {
    InitializePopulation();
    revision = true;
    class="kw">return;
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  GenerateNewCoordinates();
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSm::Revision()
{

◍ 群体寻优里的坐标更新与黑白名单

这套基于代理的优化器把每个候选解拆成多维度坐标,在每一代里先扫一遍种群,找出当前适应度最高的个体并记录进 cB 数组。下面这段就是扫描逻辑:遍历 i 从 0 到 popSize-1,一旦 a[i].f 超过暂存最优值 fB,就刷新 fB 并把该个体的坐标数组拷进 cB。

MQL5 / C++
class=class="str">"cmt">//----------------------------------------------------------------------------
for (class="type">int i = class="num">0; i < popSize; i++)
{
  if (a [i].f > fB)
  {
    fB = a [i].f;
    ArrayCopy(cB, a [i].c);
  }
}
class=class="str">"cmt">//----------------------------------------------------------------------------
UpdateLists();
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSm::InitializePopulation()
{
  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]);
    }
    agents [i].fPrev = -DBL_MAX;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSm::UpdateLists()
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class="type">int sectorIndex = GetSectorIndex(a [i].c [c], c);
      if (a [i].f > agents [i].fPrev)
      {
        agents [i].whitelist [c].sector [sectorIndex]++;
      }
      else
        if (a [i].f < agents [i].fPrev)
        {
          agents [i].blacklist [c].sector [sectorIndex]++;
        }
    }
    agents [i].fPrev = a [i].f;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSm::GenerateNewCoordinates()
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      if (u.RNDprobab() < bestProbab)
      {
        a [i].c [c] = cB [c];
      }
      else
      {
        class="type">int sectorIndex = ChooseSectorFromWhiteList(i, c);
        class="type">class="kw">double newCoord = GenerateCoordInSector(sectorIndex, c);
        if (IsInBlackList(i, c, sectorIndex))
        {
          sectorIndex = u.RNDminusOne(sectorsPerCoord);
          newCoord = GenerateCoordInSector(sectorIndex, c);
        }
        newCoord = u.SeInDiSp(newCoord, rangeMin [c], rangeMax [c], rangeStep [c]);
        a [i].c [c] = newCoord;
      }
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">int C_AO_TSm::GetSectorIndex(class="type">class="kw">double coord, class="type">int dimension)
{
  if (rangeMax [dimension] == rangeMin [dimension]) class="kw">return class="num">0;
  class="type">class="kw">double sL =  (rangeMax [dimension] - rangeMin [dimension]) / sectorsPerCoord;
  class="type">int ind = (class="type">int)MathFloor((coord - rangeMin [dimension]) / sL);
  class=class="str">"cmt">// Special handling for max value
初始化种群时,每个坐标先在区间内均匀随机(RNDfromCI),再吸附到离散步长网格(SeInDiSp),并把 fPrev 设为 -DBL_MAX,保证第一代必进白名单。UpdateLists 按维度把坐标映射到 sectorIndex,若适应度优于上代则白名单计数 +1,劣于上代则黑名单 +1——这是后续采样偏置的依据。 GenerateNewCoordinates 里有个可验证的概率开关:以 bestProbab 概率直接复制全局最优 cB[c],否则从白名单选扇区生成新坐标;若落进黑名单扇区,就随机重投一个扇区。外汇与贵金属参数寻优属高风险实验,实盘前务必在 MT5 策略测试器跑多代回测看收敛倾向。 GetSectorIndex 用区间长度除以 sectorsPerCoord 得到扇区宽度 sL,再对偏移量向下取整。当上下界相等时直接返回 0,避免除零;边界最大值通常需要额外处理,否则可能溢出最后一个扇区索引。

MQL5 / C++
class=class="str">"cmt">//----------------------------------------------------------------------------
for (class="type">int i = class="num">0; i < popSize; i++)
{
  if (a [i].f > fB)
  {
    fB = a [i].f;
    ArrayCopy(cB, a [i].c);
  }
}
class=class="str">"cmt">//----------------------------------------------------------------------------
UpdateLists();
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSm::InitializePopulation()
{
  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]);
    }
    agents [i].fPrev = -DBL_MAX;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSm::UpdateLists()
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      class="type">int sectorIndex = GetSectorIndex(a [i].c [c], c);
      if (a [i].f > agents [i].fPrev)
      {
        agents [i].whitelist [c].sector [sectorIndex]++;
      }
      else
        if (a [i].f < agents [i].fPrev)
        {
          agents [i].blacklist [c].sector [sectorIndex]++;
        }
    }
    agents [i].fPrev = a [i].f;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_TSm::GenerateNewCoordinates()
{
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      if (u.RNDprobab() < bestProbab)
      {
        a [i].c [c] = cB [c];
      }
      else
      {
        class="type">int sectorIndex = ChooseSectorFromWhiteList(i, c);
        class="type">class="kw">double newCoord = GenerateCoordInSector(sectorIndex, c);
        if (IsInBlackList(i, c, sectorIndex))
        {
          sectorIndex = u.RNDminusOne(sectorsPerCoord);
          newCoord = GenerateCoordInSector(sectorIndex, c);
        }
        newCoord = u.SeInDiSp(newCoord, rangeMin [c], rangeMax [c], rangeStep [c]);
        a [i].c [c] = newCoord;
      }
    }
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">int C_AO_TSm::GetSectorIndex(class="type">class="kw">double coord, class="type">int dimension)
{
  if (rangeMax [dimension] == rangeMin [dimension]) class="kw">return class="num">0;
  class="type">class="kw">double sL =  (rangeMax [dimension] - rangeMin [dimension]) / sectorsPerCoord;
  class="type">int ind = (class="type">int)MathFloor((coord - rangeMin [dimension]) / sL);
  class=class="str">"cmt">// Special handling for max value

白黑名单下的坐标抽样逻辑

这段实现把优化代理的搜索空间切成 sectorsPerCoord 个扇区,再用白黑名单做偏向性抽样。ChooseSectorFromWhiteList 先统计某维度下白名单总计数,若为 0 就随机返回一个扇区;否则按累计权重做轮盘赌式选取,randomValue 落在哪个累计区间就返回对应 s。 GenerateCoordInSector 负责把扇区索引还原成真实坐标:扇区宽度 = (rangeMax-rangeMin)/sectorsPerCoord,起点加索引乘宽度,再在 [sectorStart, sectorEnd] 内均匀随机取点。若 rangeMin=0、rangeMax=1、sectorsPerCoord=10,则第 3 扇区坐标落在 0.3~0.4 之间。 IsInBlackList 用黑、白计数算黑名单概率 blackCount/(blackCount+whiteCount),当两者总和为 0 直接放行。这种机制让外汇或贵金属参数寻优时,历史亏损区被采样的概率倾向压低,但高频交易参数空间仍属高风险,实盘前务必在 MT5 策略测试器回测。 边界处理上,coord 等于 rangeMax 或 ind 越界时统一夹到 sectorsPerCoord-1,ind 为负则归 0,避免数组越界导致 EA 崩溃。

MQL5 / C++
  if (coord == rangeMax [dimension]) class="kw">return sectorsPerCoord - class="num">1;
  if (ind >= sectorsPerCoord) class="kw">return sectorsPerCoord - class="num">1;
  if (ind < class="num">0) class="kw">return class="num">0;
  class="kw">return ind;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">int C_AO_TSm::ChooseSectorFromWhiteList(class="type">int agentIndex, class="type">int dimension)
{
  class="type">int totalCount = class="num">0;
  for (class="type">int s = class="num">0; s < sectorsPerCoord; s++)
  {
    totalCount += agents [agentIndex].whitelist [dimension].sector [s];
  }
  if (totalCount == class="num">0)
  {
    class="type">int randomSector = u.RNDminusOne(sectorsPerCoord);
    class="kw">return randomSector;
  }
  class="type">int randomValue = u.RNDminusOne(totalCount);
  class="type">int cumulativeCount = class="num">0;
  for (class="type">int s = class="num">0; s < sectorsPerCoord; s++)
  {
    cumulativeCount += agents [agentIndex].whitelist [dimension].sector [s];
    if (randomValue <= cumulativeCount)
    {
      class="kw">return s;
    }
  }
  class="kw">return sectorsPerCoord - class="num">1;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">class="kw">double C_AO_TSm::GenerateCoordInSector(class="type">int sectorIndex, class="type">int dimension)
{
  class="type">class="kw">double sectorSize  = (rangeMax [dimension] - rangeMin [dimension]) / sectorsPerCoord;
  class="type">class="kw">double sectorStart = rangeMin [dimension] + sectorIndex * sectorSize;
  class="type">class="kw">double sectorEnd   = sectorStart + sectorSize;
  class="type">class="kw">double newCoord = u.RNDfromCI(sectorStart, sectorEnd);
  class="kw">return newCoord;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">bool C_AO_TSm::IsInBlackList(class="type">int agentIndex, class="type">int dimension, class="type">int sectorIndex)
{
  class="type">int blackCount = agents [agentIndex].blacklist [dimension].sector [sectorIndex];
  class="type">int whiteCount = agents [agentIndex].whitelist [dimension].sector [sectorIndex];
  class="type">int totalCount = blackCount + whiteCount;
  if (totalCount == class="num">0) class="kw">return false;
  class="type">class="kw">double blackProbability = (class="type">class="kw">double)blackCount / totalCount;
  class="kw">return u.RNDprobab() < blackProbability;
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————

「禁忌搜索M跑分落在中游」

把 TSm(禁忌搜索 M)塞进标准测试台,每组函数跑 10000 次取均值:Hilly 在 5/25/500 维分别拿到 0.878 / 0.614 / 0.291,Forest 对应 0.929 / 0.518 / 0.191,Megacity 离散函数则是 0.611 / 0.382 / 0.122。三项加总 4.536,折算完成度约 50.40%。 横向摆进 40 个优化器大表,TSm 排第 18 位,刚好压过均值线。榜首 ANS 跨邻域搜索总分 6.134(68.15%),垫底 PSO 只有 2.230(24.77%),差距接近三倍。TSm 在 Hilly 低维不差,但 500 维掉到 0.291,长维度退化明显。 值得留意的是,小维度函数上分数普遍发散,这不是 TSm 独有问题,表里前十几名也都有类似毛刺。若你想在 MT5 里拿它做参数寻优,建议先锁低维区间,别直接丢 500 维空间。外汇与贵金属市场高杠杆、高风险,任何优化结果都只是概率倾向,实盘前务必用历史数据复验。

◍ 群体智能算法的回测数值对比

把五种优化算法塞进同一套 MQL5 参数寻优框架,跑出来的统计分布能直接看出差距。Boids 虚拟生物算法在均值列拿到 0.43340,明显低于 MA 猴群算法的 0.59107,但 Boids 的极值上限 0.99346 又是这组里最高的,说明它的解空间跳跃更野。 RND 随机搜索的均值只有 0.52033,综合得分 2.014、排名垫底,比 FSS 鱼群搜索的 2.056 还低一截。SFL 混合蛙跳算法综合得分 2.104,在群体算法里排中间,没有哪一项特别拉垮。 这些数字都是单周期回测产出,外汇与贵金属市场高风险,算法历史表现不预示未来概率。开 MT5 把同一 ea 分别挂这五种优化器跑一遍,重点看 Boids 的极值分布是否值得用更宽约束去赌。

一点提醒

这套基于禁忌搜索思路改出来的 TSm,在平滑的 Hilly 高维函数上会明显吃力,其余测试里评级普遍落在 0.99 以上(图1 白块即该阈值),直方图标尺 0 到 100 且 100 为理论极值,说明多数场景下它够用但不是全能。 它只暴露两个直观参数(不含种群大小),扩展性强,可作为你写新优化器的底层骨架;收敛精度仍偏粗,真要上生产建议再叠此前聊过的精炼手段。 文章附 TSm.zip(35.52 KB)含当前版 MQL5 代码,外汇与贵金属优化属高风险实验,回测好看不等于实盘能复现,跑之前先在 MT5 策略测试器里用自己的品种验证一遍。

常见问题

先用均匀随机撒点覆盖解空间,种群规模取 20~50 较稳;坐标初始化别全挤在局部,否则后期难跳出。
白名单强制保留优质坐标,黑名单禁走近期访问点;每轮更新前先查名单再决定新坐标,避免重复搜索。
可以,小布能按你给的种群与名单参数自动跑多组回测,并把跑分和中游表现直接生成对比看板。
黑名单过宽会掐掉邻域,抽样概率要随迭代衰减;建议每 50 代松绑一次黑名单再抽。
先调禁忌长度和种群再判;若对比回测里蚁群、粒子群稳定领先 15% 以上,再考虑换不迟。