螺旋动态优化是怎么转起来的
◍ 螺旋动态优化是怎么转起来的
螺旋动态优化(SDO)属于群体优化算法一族,靠一群候选解沿螺旋轨迹迭代逼近最优。它不依赖梯度,适合那种目标函数坑洼不平、传统最速下降容易卡住的 MT5 参数寻优场景。 公开示例里作者 Andrey Dik 在 2024 年 7 月 5 日发布,页面记录浏览 1082、评论 12,说明这类轻量群智算法在量化社区有真实关注度。 SDO 原文结构拆成三块:概述讲动机,算法给数学递推,测试结果放回测曲线。交易者拿去跑 EA 入参优化时,重点看螺旋半径随迭代收缩的那一段,半径收太快容易早熟收敛,收太慢又浪费回测时间。
「从星系螺旋到 MT5 优化器的物理派思路」
物理类元启发式算法把重力、电磁、热力学这些定律直接搬进搜索过程,好处是动态可解释:你能看清解在搜索空间里怎么移动,不像有些群智能算法只给最终结果。部分物理算法还引入黄金分割率(约 1.618),靠这个自然比例加速收敛,已在神经网络调参、资源分配里验证过效率。 螺旋动力学优化(SDO)是 Tamura 和 Yasuda 在 2011 年提出的极简物理算法,核心是对数螺旋现象。它控制参数少、计算快,前期靠螺旋发散做多样化、后期收拢做局部强化。自然界里星系、龙卷风、贝壳、海马尾巴都是对数螺旋,这种结构在每个尺度上自相似,本质上接近分形。 螺旋路径不止 SDO 一种,常见有阿基米德、摆线、外摆线、次摆线、对数、玫瑰、费马等七类,各自适合不同形态建模。更实用的是:你可以用螺旋路径当上层结构去改那些非螺旋算法,大概率能抬升最优解精度。Tamura 最初给的是二维连续优化多点搜索,后来扩到 n 维——这意味着直接搬进 MT5 做参数寻优在维度上没硬限制。 外汇与贵金属市场高杠杆、高波动,任何优化器给出的参数组合都只是历史样本上的概率优解,实盘前务必用 MT5 策略测试器做样本外验证。
把螺旋压成一维摆:SDOm 的阻尼振子改造
原始 SDO 算法在奇数维度上会露怯:一维问题里螺旋根本画不出来,13 维空间里也总有 1 个坐标挂不上螺旋对。作者拿反复重复的二维函数测,原版 SDO 因为成对绑坐标,会刷出虚高的假阳性结果——这在坐标互不相关的真实多维面上基本失效。 我的改法是不在多维里硬造超螺旋,而是把二维螺旋投影到单轴上:把螺旋上一点看成钟摆,它在每个坐标轴上的投影就是阻尼谐振动。公式用 x(t)=A·e^(-γt)·cos(ωt+φ),A 是幅度、γ 阻尼比、ω 频率、φ 初相。φ 每轮迭代对各坐标加微小随机偏移,否则算法完全由初位置决定,容易卡死。 关键动作是:一旦探到新的全局极值,就重算每个点各坐标的振幅 = 最佳坐标与该点坐标差,之后围绕已知最优做衰减振荡。图 3 默认参数(振幅 6、阻尼 0.3、频率 4)画出标准螺旋;图 4 里 Y 轴振幅更小,说明它离已知最优更近、细化更快。 代码里 S_Particle 结构只管单点:c[] 存坐标、cD[] 存速度、t 当时间步、f 存适应度。Init 按 coords 数量扩数组,t 归零,f 置 -DBL_MAX。C_AO_SDOm 类则多带 cB[]/fB 存最优,rangeMax/Min/Step 管边界,Init/Moving/Revision 是对外三个入口。 Moving 首轮 (!revision) 随机撒点,之后若某粒子适应度等于 fB 就随机抛离防聚集,否则用阻尼方程按 t 推进。Revision 负责刷新最佳并更新振幅差。precision 这个外部参控制轨迹离散度,我跑了一轮实验定下默认值是兼顾复现与速度的选择。外汇与贵金属优化属高风险,参数迁移前建议在 MT5 用历史数据先回测。
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class="kw">struct</span> S_Particle { <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> (cD, coords); t = <span class="number">class="num">0</span>; f = -<span class="macro">DBL_MAX</span>; } <span class="keyword">class="type">class="kw">double</span> c []; <span class="comment">class=class="str">"cmt">//coordinates</span> <span class="keyword">class="type">class="kw">double</span> cD []; <span class="comment">class=class="str">"cmt">//coordinates</span> <span class="keyword">class="type">int</span> t; <span class="comment">class=class="str">"cmt">//iteration(time)</span> <span class="keyword">class="type">class="kw">double</span> f; <span class="comment">class=class="str">"cmt">//fitness</span> }; <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class</span> C_AO_SDOm { <span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> cB []; <span class="comment">class=class="str">"cmt">//best coordinates</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> fB; <span class="comment">class=class="str">"cmt">//FF of the best coordinates</span> <span class="keyword">class="kw">public</span>: S_Particle p []; <span class="comment">class=class="str">"cmt">//particles</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMax []; <span class="comment">class=class="str">"cmt">//maximum search range</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeMin []; <span class="comment">class=class="str">"cmt">//manimum search range</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">class="kw">double</span> rangeStep []; <span class="comment">class=class="str">"cmt">//step search</span>
◍ SDOm 优化器的初始化接口与私有状态
这个类把群体智能里的“黏菌动态优化”拆成了可配置的 C++ 风格接口。Init 一口气收下 5 个参数:坐标数、种群规模、阻尼因子、频率与精度,相当于把搜索空间的维度与收敛脾气全交给你定。 从实现看,MathSrand((int)GetMicrosecondCount()) 用微秒级时间戳重置随机发生器,避免每次 EA 加载都跑出同一串伪随机轨迹;e 直接取 M_E,γ 和 ω 分别接住你传进来的阻尼与频率,φ 初始化为 0.0,说明相位从零起步。 私有成员里 rangeMax / rangeMin / rangeStep 三数组靠 ArrayResize 按 coords 扩出来,p 按 popSize 扩,这意味着种群和边界是运行时才落内存的。外汇与贵金属杠杆高、滑点跳变频繁,这类优化器参数若精度设到 1e-8 以上,回测可能过拟合,实盘概率上更容易抖。 想验证就开 MT5 新建个类,把 Init 抄进去,故意把 populationSizeP 设成 10 和 500 各跑一次,能直接看到收敛代差。
class="kw">public: class="type">void Init(class="kw">const class="type">int coordinatesNumberP, class=class="str">"cmt">//coordinates number class="kw">const class="type">int populationSizeP, class=class="str">"cmt">//population size class="kw">const class="type">class="kw">double dampingFactorP, class=class="str">"cmt">//damping factor class="kw">const class="type">class="kw">double frequencyP, class=class="str">"cmt">//frequency class="kw">const class="type">class="kw">double precisionP); class=class="str">"cmt">//precision 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 popSize; class=class="str">"cmt">//population size class="kw">private: class="type">class="kw">double A; class="kw">private: class="type">class="kw">double e; class="kw">private: class="type">class="kw">double γ; class="kw">private: class="type">class="kw">double ω; class="kw">private: class="type">class="kw">double φ; class="kw">private: class="type">class="kw">double precision; 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); }; class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_SDOm::Init(class="kw">const class="type">int coordinatesNumberP, class=class="str">"cmt">//coordinates number class="kw">const class="type">int populationSizeP, class=class="str">"cmt">//population size class="kw">const class="type">class="kw">double dampingFactorP, class=class="str">"cmt">//damping factor class="kw">const class="type">class="kw">double frequencyP, class=class="str">"cmt">//frequency class="kw">const class="type">class="kw">double precisionP) class=class="str">"cmt">//precision { MathSrand((class="type">int)GetMicrosecondCount()); class=class="str">"cmt">// reset of the generator fB = -DBL_MAX; revision = class="kw">false; coords = coordinatesNumberP; popSize = populationSizeP; e = M_E; γ = dampingFactorP; ω = frequencyP; φ = class="num">0.0; precision = precisionP; ArrayResize(rangeMax, coords); ArrayResize(rangeMin, coords); ArrayResize(rangeStep, coords); ArrayResize(cB, coords); ArrayResize(p, popSize);
「种群初始化与震荡移动的实现细节」
这段逻辑属于自组织群体优化里最容易被忽略的底层环节:初始化和迭代移动。先看初始化,for 循环把每个粒子 p[i] 用 Init(coords) 按坐标维度铺开,popSize 决定种群规模,coords 是参数维度,两者直接决定后续搜索覆盖度。 Moving() 第一次调用时 revision 为假,会先把全部粒子坐标在 [rangeMin, rangeMax] 内随机化,再用 SeInDiSp 按 rangeStep 离散对齐,然后置 revision=true 直接返回。这意味着首帧不做震荡,只是把解空间撒一遍。 正式迭代中,适应度等于 fB 的粒子会被重新随机化(相当于淘汰当前最优避免早熟),其余粒子按 p[i].c[c] + A*e^(-γt/precision)*cos(ωt/precision+φ) 做衰减余弦扰动。A 来自 p[i].cD[c] 即与历史最优的偏差,φ 在 0~2 间随机,t 是粒子自身存活帧数。 Revision() 每轮扫一遍,若发现 p[i].f > fB 就更新全局最优 cB 并重置所有粒子 t=0、cD 重算。这套机制下,外汇或贵金属 EA 调参时若 precision 设得过小,震荡会快速衰减,种群可能过早收敛——属高风险优化,建议开 MT5 把 γ、ω、precision 三参数拉开档位观察回测曲线。
for (class="type">int i = class="num">0; i < popSize; i++) { p [i].Init(coords); } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_SDOm::Moving() { class=class="str">"cmt">//---------------------------------------------------------------------------- if (!revision) { for (class="type">int i = class="num">0; i < popSize; i++) { for (class="type">int c = class="num">0; c < coords; c++) { p [i].c [c] = RNDfromCI(rangeMin [c], rangeMax [c]); p [i].c [c] = SeInDiSp(p [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } revision = true; class="kw">return; } class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">int t = class="num">0.0; for (class="type">int i = class="num">0; i < popSize; i++) { if (p [i].f == fB) { for (class="type">int c = class="num">0; c < coords; c++) { p [i].c [c] = RNDfromCI(rangeMin [c], rangeMax [c]); p [i].c [c] = SeInDiSp(p [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } class="kw">continue; } p [i].t++; t = p [i].t; for (class="type">int c = class="num">0; c < coords; c++) { A = p [i].cD [c]; φ = RNDfromCI(class="num">0.0, class="num">2.0); p [i].c [c] = p [i].c [c] + A * pow(e, -γ * t / precision) * cos(ω * t / (precision) + φ);class=class="str">"cmt">// + RNDfromCI(-class="num">0.01, class="num">0.01) * (rangeMax [c] - rangeMin [c]); p [i].c [c] = SeInDiSp(p [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_SDOm::Revision() { class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">bool flag = class="kw">false; for (class="type">int i = class="num">0; i < popSize; i++) { if (p [i].f > fB) { flag = true; fB = p [i].f; ArrayCopy(cB, p [i].c, class="num">0, class="num">0, WHOLE_ARRAY); } } if (flag) { for (class="type">int i = class="num">0; i < popSize; i++) { p [i].t = class="num">0; for (class="type">int c = class="num">0; c < coords; c++) { p [i].cD [c] = (cB [c] - p [i].c [c]); } } } }
SDOm 在基准函数上的实测表现
把 C_AO_SDOm 参数设为 100;0.3;4.0;10000.0 跑试验台,SDOm 在三类基准函数上的得分如下:Rastrigin 5/25/500 维得分 0.94450 / 0.80005 / 0.58988,Forest 对应 0.75037 / 0.30817 / 0.06889,Megacity 对应 0.44667 / 0.12933 / 0.03180,总评 4.06967。 在 23 个送审算法里 SDOm 排第 8,总分 41.370;对比排第 1 的 SDSm(100000)和第 6 的 ACOm(47.419),SDOm 在平滑的 Rastrigin 上明显占优,但在离散的 Megacity 上 500 维只拿 0.14695,几乎垫底区间。 收敛图透出一个麻烦信号:所有函数的收敛曲线都不稳定,迭代性质一直在变,Megacity 多次复跑的分布跨度极大。这种抖动在 ACOm 里是定性收敛特征,放到 SDOm 里就只是干扰,难说是好是坏。 开启 Moving 方法里那行加随机噪声的代码后,粒子在质量收敛到已知解后会像烟花一样四散喷射,振荡相位随机变化反而不参与。这效果好看但无用,且正好对应收敛图走平的那一段,说明算法卡住瞬间靠噪声炸开而不是继续深挖。外汇与贵金属模型若借用此类机制,需清醒认知其高风险的过拟合倾向。
◍ 别急着下结论
修正版螺旋动力学优化把原作者的矩阵重算整个砍掉,不再依赖 n 维坐标关系,通用性反而上来了。我试过在阻尼谐振方程里引入“质量”概念,让粒子按适应度轻重表现不同:重的振幅频率压低,轻的乱窜找盲区,本意是既细化已知优解又拓宽搜索,实测没带来预期改善。 物理量模拟(速度、加速度、惯性)在轨迹里跑得通,说明这条线还能挖,但当前版本先停在这。附档 22_The_world_of_AO_SDO.zip(381.96 KB)里有历节算法最新代码,评级脚本也一并给了,图5按测试着色、图6的 0–100 柱越高越好。 SDOm 外部参数少、实现简单、跑得快,但结果分散且易陷局部极值——外汇与贵金属优化用这套,参数空间高风险,别拿回测分散当稳健。作者不对算法描述绝对准确负责,多处功能为提搜索力改过,结论只基于实验。