禁忌搜索(TS)(基础篇)
禁忌搜索在 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 刷名单。
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">// 尚未获得适应度值 } };
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 跑之前可以先手调,看换区节奏会不会更黏。
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class="kw">struct</span> S_TSmSector { <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 { S_TSmSector blacklist []; <span class="comment">class=class="str">"cmt">//black list by sectors of each coordinate</span> S_TSmSector whitelist []; <span class="comment">class=class="str">"cmt">//white list by sectors of each coordinate</span> <span class="keyword">class="type">class="kw">double</span> fPrev; <span class="comment">class=class="str">"cmt">//previous fitness</span> <span class="keyword">class="type">void</span> Init(<span class="keyword">class="type">int</span> coords, <span class="keyword">class="type">int</span> sectorsPerCord) { <span class="functions">ArrayResize</span> (blacklist, coords); <span class="functions">ArrayResize</span> (whitelist, coords); <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < coords; i++) { <span class="functions">ArrayResize</span> (blacklist [i].sector, sectorsPerCord); <span class="functions">ArrayResize</span> (whitelist [i].sector, sectorsPerCord); <span class="functions">ArrayInitialize</span> (blacklist [i].sector, <span class="number">class="num">0</span>); <span class="functions">ArrayInitialize</span> (whitelist [i].sector, <span class="number">class="num">0</span>); } fPrev = -<span class="macro">DBL_MAX</span>; } }; <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 { <span class="keyword">class="kw">public</span>: <span class="comment">class=class="str">"cmt">//--------------------------------------------------------------------</span> C_AO_TSm() { ao_name = <span class="class="type">class="kw">string">"TSm"</span>; ao_desc = <span class="class="type">class="kw">string">"Tabu Search M"</span>; ao_link = <span class="class="type">class="kw">string">"[MQL5官方文档] popSize = <span class="number">class="num">50</span>; sectorsPerCoord = <span class="number">class="num">100</span>; bestProbab = <span class="number">class="num">0.8</span>; ArrayResize(<span class="keyword">params</span>, <span class="number">class="num">3</span>); <span class="keyword">params</span> [<span class="number">class="num">0</span>].name = <span class="class="type">class="kw">string">"popSize"</span>; <span class="keyword">params</span> [<span class="number">class="num">0</span>].val = popSize; <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; <span class="keyword">params</span> [<span class="number">class="num">2</span>].name = <span class="class="type">class="kw">string">"bestProbab"</span> <span class="keyword">params</span> [<span class="number">class="num">2</span>].val = bestProbab; } <span class="keyword">class="type">void</span> SetParams() { popSize = (<span class="keyword">class="type">int</span>)<span class="keyword">params</span> [<span class="number">class="num">0</span>].val;