禁忌搜索(TS)(基础篇)
📘

禁忌搜索(TS)(基础篇)

第 1/2 篇

禁忌搜索在 MT5 里的落地形态

禁忌搜索(Tabu Search,简称 TS)是一类带记忆机制的局部搜索启发式算法,靠一张「禁忌表」避免刚走过的劣解被反复试探,从而在邻域里跳出局部最优。把它搬进 MetaTrader 5,本质是用 EA 或脚本在参数空间里做组合寻优,而不是靠人工调参盲猜。 Andrey Dik 在 2025 年 4 月 14 日发布的 MT5 测试者条目下,该小节标注了 712 次访问、0 条评论,说明这套思路已有一定关注但社区讨论还不充分。原文给出的技术骨架只有三块:概述、算法实现、测试结果——意味着代码层要先定义邻域生成与禁忌期限,再接回测接口。 外汇与贵金属品种波动受杠杆与隔夜风险影响,用 TS 寻优得到的参数集只在历史样本内「可能」降低过拟合概率,实盘仍需小资金验证。开 MT5 把策略测试器的优化模式切到自定义、接一段禁忌逻辑,是验证它是否值得用的直接办法。

「禁忌搜索靠记忆绕开死循环」

禁忌搜索(Tabu Search)由 Fred Glover 在 1980 年代提出,是组合优化里最早成型的元启发式之一。它和遗传算法同代,但靠一张“禁忌表”记住已被禁止的移动,直接切断回到劣质旧解的路径,这一点在当时很反直觉。 算法从某个初始解出发,在邻域里挑能改善目标的移动;一旦某步被记进禁忌表,后续迭代就不能原样重走。以 0-1 背包为例,每次尝试增删一件物品,禁忌表拦住已经试过的组合,搜索空间不会被自己绕晕。 后续 Manuel Laguna、Rafael Marti 把这套自适应记忆机制扩到了生产排程、财务分析甚至电信路由。原始版本主攻 TSP、背包这类离散问题,但本文要改的正是它——把禁忌逻辑搬到连续空间,让 MT5 上的参数寻优也能用上。

◍ 把禁忌搜索改造成连续空间的白黑名单机制

经典禁忌搜索原本处理离散决策(比如找节点间最短路径),直接套到参数优化这种连续空间会崩:不可能给每一个浮点解打标签,组合数大到不可算。原文的改法很直接——把每个待优化参数的可行域切成固定数量的区域,用「白名单计数」和「黑名单计数」代替传统禁忌表。 成功解对应的区域白名单 +1,相比上一步没改善的解对应区域黑名单 +1。有希望的区域会累积白名单标记,从而被更彻底探索;但同一区域也可能因为藏着极差决策而叠了黑名单标记。选下一解时,按白名单标记数占该坐标全部区域白名单总和的比例抽区域;生成后发现落在黑名单上,再按黑名单标记占比的概率决定是否换一个随机区域。这样算法不会钉死在某一区,即便逼近全局最优,黑名单堆积也会逼它换方向。 原文给了一个直观例子:某坐标分 4 个区域,区域「3」白名单有 5 个标记(该坐标最多),但黑名单有 6 个标记。下次迭代它被选中的基础概率最高,但生成新解时被替换的概率也因黑名单而抬高——潜力小的区域反而可能以更低概率被换掉。整个过程概率持续再平衡,外部参数极少,偏向自适应。 代码层用两个结构落地:S_TSmSector 存区域计数器数组;S_TSmAgent 管每个坐标的 blacklist[] / whitelist[] 与 fPrev(前一次适应度,初始化为 -DBL_MAX)。C_AO_TSm 类默认 popSize=50、sectorsPerCoord=100、bestProbab=0.8。Init 先 StandardInit 设范围,再按 popSize 开 agents 数组并逐个 Init;Moving 首次跑 InitializePopulation,之后每代 GenerateNewCoordinates;Revision 更新最优解并调 UpdateLists 刷名单。

MQL5 / C++
class="kw">struct S_TSmSector
{
   class="type">int sector[]; class=class="str">"cmt">// 区域标记计数器数组
};

class="kw">struct S_TSmAgent
{
   class="type">int blacklist[]; class=class="str">"cmt">// 每坐标的区域黑名单数组
   class="type">int whitelist[]; class=class="str">"cmt">// 每坐标的区域白名单数组
   class="type">class="kw">double fPrev;    class=class="str">"cmt">// 智能体之前的适应度值

   class="type">void Init(class="type">int coords, class="type">int sectorsPerCord)
   {
      ArrayResize(blacklist, coords);
      ArrayResize(whitelist, coords);
      for(class="type">int c = class="num">0; c < coords; c++)
      {
         ArrayResize(blacklist[c].sector, sectorsPerCord);
         ArrayResize(whitelist[c].sector, sectorsPerCord);
         ArrayInitialize(blacklist[c].sector, class="num">0);
         ArrayInitialize(whitelist[c].sector, class="num">0);
      }
      fPrev = -DBL_MAX; class=class="str">"cmt">// 尚未获得适应度值
   }
};
逐行拆解:S_TSmSector 只包一个 int 数组 sector[],每个元素是该区域的计数。S_TSmAgent 里 blacklist / whitelist 是「坐标数 × 区域数」的二级数组;Init 先按坐标数扩维,再循环每个坐标把区域数组扩到 sectorsPerCord 并清零——这就是白黑名单计数器的初始态。fPrev 设 -DBL_MAX 是双精度最小,保证第一次评估必然「优于前次」从而能写白名单。 UpdateLists 的核心循环:对每个智能体 i、每个坐标 c,用 GetSectorIndex 算区域 ind;若 a[i].f > a[i].fPrev 则 whitelist[c].sector[ind]++,否则 blacklist[c].sector[ind]++,末了把 fPrev 刷成当前值。GetSectorIndex 里若 max==min 直接返 0;否则 sL=(max-min)/sectorsPerCoord,ind=floor((coord-min)/sL),越界夹到 [0, sectorsPerCoord-1]。ChooseSectorFromWhiteList 按白名单计数做轮盘赌:totalCount 为 0 就纯随机,否则 randomValue 落到的累积权重区间决定选哪个区域。 外汇 / 贵金属参数优化属高风险场景,这套机制只是降低陷入局部劣解的概率,不保证实盘收益;建议直接把 sectorsPerCoord 从 100 调到 20~50 在 MT5 策略测试器里跑同一 EA,观察黑名单堆积换区的频率再定参。

MQL5 / C++
class="kw">struct S_TSmSector
{
   class="type">int sector[]; class=class="str">"cmt">// 区域标记计数器数组
};

class="kw">struct S_TSmAgent
{
   class="type">int blacklist[]; class=class="str">"cmt">// 每坐标的区域黑名单数组
   class="type">int whitelist[]; class=class="str">"cmt">// 每坐标的区域白名单数组
   class="type">class="kw">double fPrev;    class=class="str">"cmt">// 智能体之前的适应度值

   class="type">void Init(class="type">int coords, class="type">int sectorsPerCord)
   {
      ArrayResize(blacklist, coords);
      ArrayResize(whitelist, coords);
      for(class="type">int c = class="num">0; c < coords; c++)
      {
         ArrayResize(blacklist[c].sector, sectorsPerCord);
         ArrayResize(whitelist[c].sector, sectorsPerCord);
         ArrayInitialize(blacklist[c].sector, class="num">0);
         ArrayInitialize(whitelist[c].sector, class="num">0);
      }
      fPrev = -DBL_MAX; class=class="str">"cmt">// 尚未获得适应度值
   }
};

黑名单概率怎么逼着智能体换区

区域边界先靠最小值加偏移量算出来:sectorStart = 维度范围下限 + 区域索引 × 区域大小,sectorEnd 再叠一层区域大小。边界一定,RNDfromCI 就在 [sectorStart, sectorEnd] 里丢一个随机坐标,智能体下一脚踩哪儿基本就圈在这条缝里。 IsInBlackList 是这套禁忌搜索的闸门。它吃三个参数:agentIndex(查哪个智能体)、dimension(哪根坐标轴)、sectorIndex(哪个区域)。逻辑很直白——blackCount 和 whiteCount 分别数出该智能体在这个维度、这个区域里被记黑、记白的次数,totalCount 是两者之和;totalCount 为 0 直接返回 false,说明没任何样本就别谈拉黑。 真要算概率时,blackProbability = blackCount / totalCount。RNDprobab() 吐一个 [0,1) 的随机数,只要它小于 blackProbability 就返回 true,逼着智能体跳去别的随机区域。白条越多、黑条越堆,换区概率越往上飘,这是拿频率当后验在调探索倾向。 上面那张 ASCII 图里,V 行是白名单命中、X 行是黑名单命中,能直接看到维度 0 的区域 3 黑条几乎铺满(XXXXXX),对应的 blackProbability 会明显偏高,智能体在这个区被踢走的概率就大。外汇与贵金属参数优化用这套时,样本不足就别急着信换区信号,过拟合风险不低。 代码里 S_TSmAgent 的 Init 把 blacklist / whitelist 都按坐标数和每坐标区域数(默认 sectorsPerCoord=100)开好,并清零;C_AO_TSm 构造函数把 popSize 设 50、bestProbab 设 0.8,这几个值你开 MT5 跑之前可以先手调,看换区节奏会不会更黏。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class="kw">struct</span> S_TSmSector
{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> sector [];
};
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class="kw">struct</span> S_TSmAgent
{
&nbsp;&nbsp;&nbsp;&nbsp;S_TSmSector blacklist []; <span class="comment">class=class="str">"cmt">//black list by sectors of each coordinate</span>
&nbsp;&nbsp;&nbsp;&nbsp;S_TSmSector whitelist []; <span class="comment">class=class="str">"cmt">//white list by sectors of each coordinate</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> fPrev;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//previous fitness</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">void</span> Init(<span class="keyword">class="type">int</span> coords, <span class="keyword">class="type">int</span> sectorsPerCord)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (blacklist, coords);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (whitelist, coords);
&nbsp;&nbsp;&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; coords; i++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (blacklist [i].sector, sectorsPerCord);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span> (whitelist [i].sector, sectorsPerCord);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayInitialize</span> (blacklist [i].sector, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayInitialize</span> (whitelist [i].sector, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fPrev = -<span class="macro">DBL_MAX</span>;
&nbsp;&nbsp;&nbsp;&nbsp;}
};
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class</span> C_AO_TSm : <span class="keyword">class="kw">public</span> C_AO
{
&nbsp;&nbsp;<span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//--------------------------------------------------------------------</span>
&nbsp;&nbsp;C_AO_TSm()
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;ao_name = <span class="class="type">class="kw">string">"TSm"</span>;
&nbsp;&nbsp;&nbsp;&nbsp;ao_desc = <span class="class="type">class="kw">string">"Tabu Search M"</span>;
&nbsp;&nbsp;&nbsp;&nbsp;ao_link = <span class="class="type">class="kw">string">"[MQL5官方文档]
&nbsp;&nbsp;&nbsp;&nbsp;popSize&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = <span class="number">class="num">50</span>;
&nbsp;&nbsp;&nbsp;&nbsp;sectorsPerCoord = <span class="number">class="num">100</span>;
&nbsp;&nbsp;&nbsp;&nbsp;bestProbab&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;= <span class="number">class="num">0.8</span>;
&nbsp;&nbsp;&nbsp;&nbsp;ArrayResize(<span class="keyword">params</span>, <span class="number">class="num">3</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">params</span> [<span class="number">class="num">0</span>].name = <span class="class="type">class="kw">string">"popSize"</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">params</span> [<span class="number">class="num">0</span>].val = popSize;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">params</span> [<span class="number">class="num">1</span>].name = <span class="class="type">class="kw">string">"sectorsPerCoord"</span>; <span class="keyword">params</span> [<span class="number">class="num">1</span>].val = sectorsPerCoord;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">params</span> [<span class="number">class="num">2</span>].name = <span class="class="type">class="kw">string">"bestProbab"</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">params</span> [<span class="number">class="num">2</span>].val = bestProbab;
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">class="type">void</span> SetParams()
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;popSize&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = (<span class="keyword">class="type">int</span>)<span class="keyword">params</span> [<span class="number">class="num">0</span>].val;

常见问题

可以。把近期刚试过且亏损的价格区间写进黑名单,强制下一轮选点绕开,减少死循环式重复止损。
连续品种建议从 8~15 根 K 线起步,波动大的贵金属可拉到 20;观察连亏是否下降再微调。
小布可在品种页标记近期黑名单区与冷却剩余时间,你只需看提示决定是否手动避单或换周期。
禁忌搜索主要砍掉重复踩坑,不提高信号质量;回撤缩小是因坏区域被限次访问,属正常表现。
可能。建议黑名单只禁最近 N 根内的亏损区,保留更早期突破位白名单,降低误杀概率。