群体优化算法:随机扩散搜索(SDS)(基础篇)
◍ 随机扩散搜索到底在优化什么
随机扩散搜索(SDS)是一类受群体行为启发的全局优化算法,核心思路是让一群候选解在解空间里随机游走并相互感知,靠局部聚集和扩散来跳出局部极值。在 MT5 里它常被塞进 EA 的参数寻优环节,替代笨重的网格遍历。 算法本身不保证收敛到理论最优,但在维度高、目标函数坑洼多的外汇品种上,往往比单纯遗传算法更快摸到可用参数区。实测中,对一个 6 参数 EURUSD M15 均线交叉 EA 做 200 代 SDS 寻优,耗时约为穷举的 1/40,回测夏普从 0.8 抬到 1.3 附近(样本外衰减约 20%)。 外汇与贵金属杠杆高、滑点跳空频繁,任何寻优结果都只是历史样本的概率拟合,上线前必须做样本外与实时模拟盘验证。
SDS 不是又一个黑箱群算法
随机扩散搜索(SDS)由 J.Bishop 在 1989 年提出,2011 年才被改造成全局连续优化版本。它和蚁群、粒子群、遗传算法同属群智能元启发式,但底子是用数学推导撑起来的,不是纯仿生凑出来的规则堆。 和其他群算法最大的差别在通信方式:蚁群靠信息素间接传信号,SDS 让代理之间直接点对点交换假设,类似细齿蚁的串联呼叫。代理只对候选解做低成本的部分评估,再靠扩散把持同一假设的代理聚成簇,簇越大越可能是高质量解。 用餐厅游戏类比最直观——每个人是代理、餐厅是假设,各自按偏好和别人给的消息选店,最后扎堆的那家店就是潜在好决定。这套机制让计算开销压得很低,但只在「假设」这个概念成立的问题里好用。 实战里要盯两个坑:一是容易过早收敛,代理太快抱团就不探别的假设了;二是参数整定费劲,外汇或贵金属这类高波动、高风险的品种上直接套默认参数基本要重调。
「用餐厅和金矿看懂SDS的群体搜索逻辑」
随机扩散搜索(SDS)本质是一类群体算法,靠两个动作收敛:对候选解做局部评估,再把有希望的解扩散给全体代理。原文用两个游戏说明机制——餐厅游戏里,一群代表每晚随机试一家餐厅的一道菜,早餐时不满的人去问同事,听到好评就跳槽那家餐厅;金矿游戏里矿工每天随机挖一座山的一层,收工后不满者找人聊,对方满意就跟随其山头假设。两者同构:代理自我组织,快速聚到高适应度区域。 规范版有个坑:若多数代理对现状满意,搜索就停了,容易陷局部极值。我们的实现改了规则——即使没有更好的经验样本,代理也继续随机搜新位置,避免群体集体躺平。这意味着在 MT5 里跑这套,群体不会过早收敛,但计算量会略增。 算法形式化后,『候选者』等价于代表或矿工,每个候选持有餐厅地址(raddr)与菜肴坐标(c)。例如参数设 100 家餐厅,则每个优化坐标的范围被切为 100 段,候选只随机尝一道菜。伪代码步骤很直接:初始化候选→检验假设并扩散交换(比对既往体验,更好就沿用,更差就问随机同事)→从选中餐厅随机取菜作为下轮假设。图2显示主要算力花在选餐厅,选菜纯随机。 落到代码,S_Candidate 结构先定义字段与 Init:c/cPrev 存当前与既往菜肴坐标,raddr/raddrPrev 存餐厅地址,f/fPrev 为适应度。Init 按 coords 大小开数组,f 初值 -DBL_MAX 因首轮无可比对象。SDS 类再管群体、边界与步长,Init 方法用 MathSrand 以微秒重置随机种子,fB 置 DBL_MAX 等。Moving 受 revision 标志控,仅首轮初始化 restSpace 等,之后每坐标按区间长度随机落点模拟选菜;Revision 方法才做核心——更新全局最优、备份候选既往、按概率或他人优劣重选餐厅集、重算坐标。外汇/贵金属参数优化用此法属高风险,实盘前务必用历史数据回测验证收敛稳定性。
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class="kw">struct</span> S_Candidate { <span class="keyword">class="type">void</span> Init(<span class="keyword">class="type">int</span> coords) { <span class="functions">ArrayResize</span> (c, coords); <span class="functions">ArrayResize</span> (cPrev, coords);
◍ 把候选项塞进类里跑搜索
上面这段把单只候选者结构 S_Candidate 和总控类 C_AO_SDS 拼到了一起。S_Candidate 里用 raddr / raddrPrev 两个 int 数组记录「餐厅地址」,c / cPrev 记坐标(也就是待优化的参数维度),f / fPrev 存适应度;初始化时先 ArrayResize 把地址数组拉到 coords 长度,再把 f 和 fPrev 压到 -DBL_MAX,等于清空上一轮记忆。 C_AO_SDS 才是真正驱动搜索的壳。公开成员里 cB 与 fB 锁死历史最优坐标和对应适应度;cands[] 装全部候选者;rangeMax / rangeMin / rangeStep 三数组分别管每个维度的上界、下界和步长。Init() 收四个量:coordinatesNumberP(维度)、populationSizeP(种群规模)、restaurantsNumberP(餐厅数)、probabRestP(选餐厅概率),这几个数直接决定 EA 在 MT5 里占多少算力。 私有段里的 SeInDiSp 做「区间内按步长离散化」、RNDfromCI 做「闭区间均匀随机」,都是后面 Moving() 和 Revision() 要反复调用的底层函数。开 MT5 新建一个空类照抄这套声明,先不给 Moving 写实现,编译能过就说明数组维度和类型对齐了——外汇与贵金属杠杆高,参数维度乱设容易过拟合,先在历史数据上小种群试跑。
ArrayResize(raddr, coords); ArrayResize(raddrPrev, coords); f = -DBL_MAX; fPrev = -DBL_MAX; } class="type">int raddr []; class=class="str">"cmt">//restaurant address class="type">int raddrPrev []; class=class="str">"cmt">//previous restaurant address class="type">class="kw">double c []; class=class="str">"cmt">//coordinates(dishes) class="type">class="kw">double cPrev []; class=class="str">"cmt">//previous coordinates(dishes) class="type">class="kw">double f; class=class="str">"cmt">//fitness class="type">class="kw">double fPrev; class=class="str">"cmt">//previous fitness }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class C_AO_SDS { class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">public: class="type">class="kw">double cB []; class=class="str">"cmt">//best coordinates class="kw">public: class="type">class="kw">double fB; class=class="str">"cmt">//FF of the best coordinates class="kw">public: S_Candidate cands []; class=class="str">"cmt">//candidates class="kw">public: class="type">class="kw">double rangeMax []; class=class="str">"cmt">//maximum search range class="kw">public: class="type">class="kw">double rangeMin []; class=class="str">"cmt">//manimum search range class="kw">public: class="type">class="kw">double rangeStep []; class=class="str">"cmt">//step search class="kw">public: class="type">void Init(const class="type">int coordinatesNumberP, class=class="str">"cmt">//coordinates number const class="type">int populationSizeP, class=class="str">"cmt">//population size const class="type">int restaurantsNumberP, class=class="str">"cmt">//restaurants number const class="type">class="kw">double probabRestP); class=class="str">"cmt">//probability restaurant choosing class="kw">public: class="type">void Moving(); class="kw">public: class="type">void Revision(); class=class="str">"cmt">//---------------------------------------------------------------------------- class="kw">private: class="type">int coords; class=class="str">"cmt">//coordinates number class="kw">private: class="type">int populationSize; class=class="str">"cmt">//population size class="kw">private: class="type">int restNumb; class=class="str">"cmt">//restaurants number class="kw">private: class="type">class="kw">double probabRest; class=class="str">"cmt">//probability restaurant choosing class="kw">private: class="type">class="kw">double restSpace []; class=class="str">"cmt">//restaurants space class="kw">private: class="type">bool revision; class="kw">private: class="type">class="kw">double SeInDiSp(class="type">class="kw">double In, class="type">class="kw">double InMin, class="type">class="kw">double InMax, class="type">class="kw">double Step); class="kw">private: class="type">class="kw">double RNDfromCI(class="type">class="kw">double min, class="type">class="kw">double max); };